Demo Project: AddressDB synchronizing address details using Node.js
Overview
In this tutorial we will be building a simple guest address database which is synchronized with SIHOT using the SIHOT@360° API.
The demo application will be using minimal dependencies for demo purposes. It is not for production workloads!
For this demo we will:
- capture the GUEST notifications
- get latest guest details from SIHOT
- update our address database
What You Will Need
- Internet access
- Basic knowledge of Node.js
- Access to SIHOT to set up a push notification
- Access to a 360° service such as public WSDL
- Node.js (v12.16.3++)
Additionally, SIHOT will need to be configured to notify based on address details. For details refer to notification configuration.
The Tutorial
In this session we will be implementing a small address database that contains all guest addresses and uses the guest email address as unique identifier for the guest profile. To keep the dependencies simple we will be building a pure Node.js solution thus why using NeDB a pure Node.js.
Obviously to store all guest profiles an initial synchronsization would be necessary which we skip for the purpose of this tutorial as we focus on the continuous integration and the possibilities SIHOT@360° provides for doing this.
SIHOT@360° uses an asynchronous approach, therefore the following sequence will need to be implemented.
Step - 1 setting up express with a SOAP application
- express: the back and web application framework
- express-xml-bodyparser: an express middleware for xml workloads
- nedb-promises: NeDB is a pure Node.js database with an MongoDB like API
- winston: a popular logging library
- comander: a great utility for parsing CLI options
- dotenv: a lightweight npm package that automatically loads environment variables from a .env file into the process
Initialize the project and load dependencies.
npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (tutorial2) AddressDB
Sorry, name can no longer contain capital letters.
package name: (tutorial2) addressdb
version: (1.0.0)
description: A sample CRM integration
entry point: (index.js) server.js
test command:
git repository:
keywords: 360° push
author:
license: (ISC)
About to write to D:\src\unofficial\node\tutorial2\package.json:
{
"name": "addressdb",
"version": "1.0.0",
"description": "A sample CRM integration",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"360°",
"push"
],
"author": "",
"license": "ISC"
}
Is this OK? (yes)
After the application is initialized let's add the soap dependency.
npm install soap --save
npm install dotenv --save
npm install express --save
npm install express-xml-bodyparser --save
npm install commander --save
npm install winston --save
npm install winston-daily-rotate-file --save
npm install nedb-promises --save
Step - 2 Setting up express with a SOAP application
Setting up the server using express and commander to handle the core application.
For this use the following code....
Note: a full explanation of the below is beyond this tutorial if you are new to node.js / express please refer to other resources such as Express.js Tutorial - javaTpoint.
index.js (main application)
// AddressDB
const { createServer } = require('http');
let commander = require("commander");
const logger = require ('./utils/logger');
const express = require('express');
let PORT = 8080
let IP = "0.0.0.0"
commander.option("-p, --port [directory name]", "listening port of the fileserver (default:8080)")
commander.option("-i, --ip [IP]", "Bind to a specific IP. If not set 0.0.0.0")
.parse(process.argv);
if (commander.opts().port) {
PORT = commander.opts().port
}
if (commander.opts().ip) {
IP = commander.opts().ip
}
const app = require ("./app.js");
const server = createServer(app);
server.listen(PORT, IP, function () {
logger.info(`Listening on http://${IP}:${PORT}`);
});
app.js
To handle the incoming SOAP requests we create a small express handler including the xml bodyparser middleware
'use strict';
require('dotenv').config()
const express = require('express');
const xmlparser = require('express-xml-bodyparser');
const stripNS = require('xml2js').processors.stripPrefix;
const service = require('./service');
const logger = require ('./utils/logger');
const app = express();
app.use(xmlparser({
explicitArray: false,
normalize: true,
normalizeTags: false,
trim: true,
tagNameProcessors: [stripNS],
firstCharLowerCase: true
}));
app.post("/notify", async function (req, res) {
try {
res.setHeader('content-type', 'text/xml');
const rc =service.getS_GUEST_PUSH_V001Response();
res.send(rc);
service.processGuestNotification(req.body)
} catch (e) {
logger.error('app.post("/notify )"' + e)
res.sendStatus(500);
}
})
module.exports = app;
Step - 3 Implementing the SIHOT service
In this step we will implement a service that will handle all SIHOT communications and dataset and uses a datastore.js to store data. For the SOAP communication we will be using again the "soap" library from the Getting Started using Node.js.
getS_GUEST_PUSH_V001Response (Acknowledgement)
First of all, we need the acknowledgement for the PUSH notification when accepting the notification call. For this we define a function that delivers the XML content as expected.
Note: How to obtain a securityID and what it is used for refer to the previous tutorial Getting Started using Node.js.
function getS_GUEST_PUSH_V001Response() {
const rc = `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:envgen="http://soapenvelopegenerator.eduardocastro.info/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soapenv:Header />
<soapenv:Body>
<S_GUEST_PUSH_V001Response xmlns="PushNotifications">
<S_GUEST_PUSH_V001Result>
<Success>true</Success>
<ErrorMsg></ErrorMsg>
</S_GUEST_PUSH_V001Result>
</S_GUEST_PUSH_V001Response>
</soapenv:Body>
</soapenv:Envelope>`
return rc;
}
processGuestNotification
After this we implement the function to manage the required sequence.
This function will
- Receive a S_GUEST_PUSH_V001
- Request the details for the guest modified from SIHOT
- Validates the guest type, from SIHOT since we only want to build an address database of natural people
- Store the guest profile
- Confirm/report an error to SIHOT
async function processGuestNotification(guestNotification) {
logger.info("received notification for " + guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK.name1 + " (" + guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['GUEST-OBJID'] + " )");
let guest = await getGuestDetails(guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['GUEST-OBJID']);
if (guest.type === "1" || guest.type === "0") {
logger.info("storing guest");
try {
await datastore.store(guest);
}
catch (e) {
logger.error("can not store guest");
logger.error(e);
setErrorAutoTask(guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['TASK-OBJID'], "no need to store profile, not the correct guest type.");
return;
}
}
else {
logger.info("no need to store profile, not the correct guest type.")
}
confirmAutoTask(guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['TASK-OBJID'])
}
getGuestDetails
To retrieve the guest details the function checks if the securityID is still valid and builds a S_GUEST_GET_V002Request request to obtain all relevant information of the geust profile. After this only the relevant fields are extracted and stored into a smaller dataset.
async function getGuestDetails(objid) {
// request using SOAP
const now = new Date()
if (tokenValidUntil <= now) {
logger.info(tokenValidUntil + " now it was " + now)
logger.info("getting new securityID .. ")
await getSecurityID();
}
const S_GUEST_GET_V002Request = {
Authentication: {
SecurityID: securityID
},
GUEST: {
"GUEST-OBJID": objid
}
}
const S_GUEST_GET_V002Response = await client.S_GUEST_GET_V002Async(S_GUEST_GET_V002Request);
if (S_GUEST_GET_V002Response.length <= 0) {
logger.error("Response invalid")
}
if (S_GUEST_GET_V002Response[0].Result.Success !== "true") {
logger.error("Method call failed")
logger.error(S_GUEST_GET_V002Response[0].Result.ErrorMsg)
logger.error(S_GUEST_GET_V002Response[0].Result['MSG-LIST'])
}
if (S_GUEST_GET_V002Response[0].GUEST === undefined) {
logger.error("Response invalid not GUEST block found")
}
const guest = S_GUEST_GET_V002Response[0].GUEST[0]
const rc = {
type: guest.guestType,
lastName: guest.lastName,
firstName: guest.firstName,
street: guest.street,
postCode: guest.postcode,
city: guest.city,
email: guest.email
}
return rc;
}
confirmAutoTask / setErrorAutoTask
Additionally to the above two helper functions are created to confirm or error reporting back to SIHOT.
async function confirmAutoTask(taskID){
try {
const S_AUTOTASK_CONFIRMATION_V001Request = {
Authentication: {
SecurityID: securityID
},
AutoTaskConfirm: {
"AUTOTASK-OBJID": taskID
}
}
const S_AUTOTASK_CONFIRMATION_V001Response = await client.S_AUTOTASK_CONFIRMATION_V001Async(S_AUTOTASK_CONFIRMATION_V001Request);
if (S_AUTOTASK_CONFIRMATION_V001Response.length <= 0) {
logger.error("Could not confirm task")
}
if (S_AUTOTASK_CONFIRMATION_V001Response[0].Result.Success !== "true") {
logger.error("Could not confirm task")
logger.error(S_AUTOTASK_CONFIRMATION_V001Response[0].Result.ErrorMsg)
logger.error(S_AUTOTASK_CONFIRMATION_V001Response[0].Result['MSG-LIST'])
}
}
catch (e) {
logger.error("can not confirm task");
logger.error(e);
}
}
async function setErrorAutoTask(taskID, message){
logger.error (`Sending Error to SIHOT for Task: ${taskID} with ${message}` );
try {
const S_AUTOTASK_SETERROR_V001Request = {
Authentication: {
SecurityID: securityID
},
AutoTaskSetError: {
"AUTOTASK-OBJID": taskID,
ErrorMsg: message,
SysMessage: 0
}
}
const S_AUTOTASK_SETERROR_V001Response = await client.S_AUTOTASK_SETERROR_V001Async(S_AUTOTASK_SETERROR_V001Request);
if (S_AUTOTASK_SETERROR_V001Response.length <= 0) {
logger.error("Could not setError task")
}
if (S_AUTOTASK_SETERROR_V001Response[0].Result.Success !== "true") {
logger.error("Could not setError task")
logger.error(S_AUTOTASK_SETERROR_V001Response[0].Result.ErrorMsg)
logger.error(S_AUTOTASK_SETERROR_V001Response[0].Result['MSG-LIST'])
}
}
catch (e) {
logger.error("can not setError task");
logger.error(e);
}
}
service.js (complete source)
For reference this is the complete source of service.js
'use strict'
require('dotenv').config()
const soap = require('soap');
const logger = require('./utils/logger');
const datastore = require('./datastore');
const localwsdl = "./SihotServices01.xml";
const wsEndPoint = process.env.WSURL;
// local variabels to store the securityID as well as the invalidation timestamp
let securityID = ""
let tokenValidUntil = new Date()
let client = null;
async function init() {
try {
logger.info("init SOAP Client")
client = await soap.createClientAsync(localwsdl);
client.setEndpoint(wsEndPoint) //- overwrite the SOAP service endpoint address
}
catch (e) {
logger.error("Initialization of SOAP Client failed.")
logger.error(e)
}
}
// build the AuenticateRequest
const AuthenticateRequest = {
AuthenticationInfos: {
"user": process.env.SIHOT_USER,
"password": process.env.SIHOT_PASSWORD,
"hotel": process.env.SIHOT_HOTEL
}
};
async function getSecurityID() {
if (client == null) {
try {
await init();
}
catch (e) {
logger.error("Initialisation failed");
logger.error(e)
}
}
try {
const AuthenticateResponse = await client.AuthenticateAsync(AuthenticateRequest);
securityID = AuthenticateResponse[0].Authentication[0].SecurityID;
tokenValidUntil = new Date();
// calculate the datetime when the token will be invalidated
tokenValidUntil.setTime(tokenValidUntil.getTime() + (parseInt(AuthenticateResponse[0].Authentication[0].DurationInSec) * 1000))
logger.info("New security received: " + securityID)
logger.info("New security valid until: " + tokenValidUntil.toLocaleString())
}
catch (e) {
logger.error("Authentification failed");
logger.error(e)
}
}
function getS_GUEST_PUSH_V001Response() {
const rc = `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:envgen="http://soapenvelopegenerator.eduardocastro.info/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soapenv:Header />
<soapenv:Body>
<S_GUEST_PUSH_V001Response xmlns="PushNotifications">
<S_GUEST_PUSH_V001Result>
<Success>true</Success>
<ErrorMsg></ErrorMsg>
</S_GUEST_PUSH_V001Result>
</S_GUEST_PUSH_V001Response>
</soapenv:Body>
</soapenv:Envelope>`
return rc;
}
/**
* processes the [S_GUEST_PUSH_V001]()
* - get Details
* - stores guests and updates using *datastore*
* - confirms the processing
*/
async function processGuestNotification(guestNotification) {
logger.info("received notification for " + guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK.name1 + " (" + guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['GUEST-OBJID'] + " )");
let guest = await getGuestDetails(guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['GUEST-OBJID']);
if (guest.type === "1" || guest.type === "0") {
logger.info("storing guest");
try {
await datastore.store(guest);
}
catch (e) {
logger.error("can not store guest");
logger.error(e);
setErrorAutoTask(guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['TASK-OBJID'], "no need to store profile, not the correct guest type.");
return;
}
}
else {
logger.info("no need to store profile, not the correct guest type.")
}
confirmAutoTask(guestNotification.Envelope.Body.S_GUEST_PUSH_V001.AUTOTASK['TASK-OBJID'])
}
/**
* Get the guest details using [S_GUEST_GET_V002](https://partner.sihot.com/360/S_GUEST_GET_V002/)
* and parses required fields into a JSON stucture
*/
async function getGuestDetails(objid) {
// request using SOAP
const now = new Date()
if (tokenValidUntil <= now) {
logger.info(tokenValidUntil + " now it was " + now)
logger.info("getting new securityID .. ")
await getSecurityID();
}
const S_GUEST_GET_V002Request = {
Authentication: {
SecurityID: securityID
},
GUEST: {
"GUEST-OBJID": objid
}
}
const S_GUEST_GET_V002Response = await client.S_GUEST_GET_V002Async(S_GUEST_GET_V002Request);
if (S_GUEST_GET_V002Response.length <= 0) {
logger.error("Response invalid")
}
if (S_GUEST_GET_V002Response[0].Result.Success !== "true") {
logger.error("Method call failed")
logger.error(S_GUEST_GET_V002Response[0].Result.ErrorMsg)
logger.error(S_GUEST_GET_V002Response[0].Result['MSG-LIST'])
}
if (S_GUEST_GET_V002Response[0].GUEST === undefined) {
logger.error("Response invalid not GUEST block found")
}
const guest = S_GUEST_GET_V002Response[0].GUEST[0]
const rc = {
type: guest.guestType,
lastName: guest.lastName,
firstName: guest.firstName,
street: guest.street,
postCode: guest.postcode,
city: guest.city,
email: guest.email
}
return rc;
}
async function confirmAutoTask(taskID){
try {
const S_AUTOTASK_CONFIRMATION_V001Request = {
Authentication: {
SecurityID: securityID
},
AutoTaskConfirm: {
"AUTOTASK-OBJID": taskID
}
}
const S_AUTOTASK_CONFIRMATION_V001Response = await client.S_AUTOTASK_CONFIRMATION_V001Async(S_AUTOTASK_CONFIRMATION_V001Request);
if (S_AUTOTASK_CONFIRMATION_V001Response.length <= 0) {
logger.error("Could not confirm task")
}
if (S_AUTOTASK_CONFIRMATION_V001Response[0].Result.Success !== "true") {
logger.error("Could not confirm task")
logger.error(S_AUTOTASK_CONFIRMATION_V001Response[0].Result.ErrorMsg)
logger.error(S_AUTOTASK_CONFIRMATION_V001Response[0].Result['MSG-LIST'])
}
}
catch (e) {
logger.error("can not confirm task");
logger.error(e);
}
}
async function setErrorAutoTask(taskID, message){
logger.error (`Sending Error to SIHOT for Task: ${taskID} with ${message}` );
try {
const S_AUTOTASK_SETERROR_V001Request = {
Authentication: {
SecurityID: securityID
},
AutoTaskSetError: {
"AUTOTASK-OBJID": taskID,
ErrorMsg: message,
SysMessage: 0
}
}
const S_AUTOTASK_SETERROR_V001Response = await client.S_AUTOTASK_SETERROR_V001Async(S_AUTOTASK_SETERROR_V001Request);
if (S_AUTOTASK_SETERROR_V001Response.length <= 0) {
logger.error("Could not setError task")
}
if (S_AUTOTASK_SETERROR_V001Response[0].Result.Success !== "true") {
logger.error("Could not setError task")
logger.error(S_AUTOTASK_SETERROR_V001Response[0].Result.ErrorMsg)
logger.error(S_AUTOTASK_SETERROR_V001Response[0].Result['MSG-LIST'])
}
}
catch (e) {
logger.error("can not setError task");
logger.error(e);
}
}
module.exports = { processGuestNotification: processGuestNotification, getS_GUEST_PUSH_V001Response: getS_GUEST_PUSH_V001Response };
Step - 4 Implementing the datastore
'use strict'
const Datastore = require('nedb-promises')
const logger = require('./utils/logger');
let datastore = Datastore.create({ filename: './database', autoload: true });
datastore.ensureIndex({ fieldName: 'email' , "unique":true });
async function store(data) {
const guest = await datastore.findOne({ email: data.email })
if (guest) {
logger.info("update guest... " + guest.email)
const rc = await datastore.update(guest, data)
}
else {
// since we use email as a unique key we want to ensure it is available
if ( data.email )
{
logger.info("insert new guest... " + data.email)
const rc = await datastore.insert(data)
}
}
}
module.exports = { store: store }
What is next?
Download of the Source
A complete working example can be downloaded at: addressdb.zip
To install the projetc run:
npm install
Please ensure that you enter username, password and hotel of the .env configuration file!
run as node index.js