refactoring to a better nodejs template and adding

better filtering using sample data
This commit is contained in:
2023-06-30 16:28:52 -05:00
parent a141e18120
commit fa15cce2e1
19 changed files with 22879 additions and 251 deletions

20
src/api/routes/config.js Normal file
View File

@@ -0,0 +1,20 @@
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
const config = {
"auth:": {
username: process.env.EO_API_USERNAME,
password: process.env.EO_API_PASSWORD ? "*******" : undefined,
scope: process.env.EO_API_SCOPE,
client_id: process.env.EO_API_CLIENT_ID,
client_secret: process.env.EO_API_SECRET ? "*******" : undefined,
},
endpoints: {
token: process.env.EO_API_ACCESS_TOKEN_URL,
udg: process.env.EO_API_UDG_URL,
},
};
res.send(config);
});
export default router;

11
src/api/routes/index.js Normal file
View File

@@ -0,0 +1,11 @@
import { Router } from "express";
import config from "./config.js";
import interactionsFlows from "./interactions-flow.js";
import udg from "./unified-data-gateway.js";
const router = Router();
router.use("/config", config);
router.use("/interactions-flow", interactionsFlows);
router.use("/unified-data-gateway", udg);
export default router;

View File

@@ -0,0 +1,157 @@
import { Router } from "express";
import sampleFlow from "../../utils/sampleFlow.js";
import sampleUDG from "../../utils/sampleUDGresponse.js";
import { logger } from "../../utils/index.js";
const router = Router();
router.get("/", (req, res) => {
const filter = req.query.filter;
// nodes
var channels = [];
var queues = [];
var outcomes = [];
// links
var channel_queue_links = {};
var channel_outcome_links = {};
var queue_outcome_links = {};
sampleUDG.data.findContactsCompletedBetween.edges.forEach((value) => {
const hasChannel = value.node.interaction.__typename ? true : false;
const hasQueue = value.node.queue ? true : false;
const hasOutcome = value.node.outcome ? true : false;
// NODES
if (
hasChannel &&
channels.indexOf(value.node.interaction.__typename) == -1
) {
channels.push(value.node.interaction.__typename);
}
if (hasQueue) {
const queue = JSON.stringify(value.node.queue);
if (queues.indexOf(queue) == -1) {
queues.push(queue);
}
if (hasChannel) {
const key =
value.node.interaction.__typename + JSON.stringify(value.node.queue);
if (channel_queue_links[key]) {
channel_queue_links[key].value++;
} else {
channel_queue_links[key] = {
source: value.node.interaction.__typename,
target: JSON.stringify(value.node.queue),
value: 1,
};
}
}
}
if (hasOutcome) {
value.node.outcome.edges.forEach((element) => {
var outcome = element.node.text;
if (outcomes.indexOf(outcome) == -1) {
outcomes.push(outcome);
}
if (hasQueue) {
var key = outcome+JSON.stringify(value.node.queue);
if (queue_outcome_links[key]) {
queue_outcome_links[key].value++;
} else {
queue_outcome_links[key] = {
source: JSON.stringify(value.node.queue),
target: outcome,
value: 1,
};
}
}
if (hasChannel) {
key = outcome+value.node.interaction.__typename;
if (channel_outcome_links[key]) {
channel_outcome_links[key].value++;
} else {
channel_outcome_links[key] = {
source: value.node.interaction.__typename,
target: outcome,
value: 1,
};
}
}
});
}
});
logger.debug(`channels: ${JSON.stringify(channels)}`);
logger.debug(`queues: ${JSON.stringify(queues)}`);
logger.debug(`channel_queue_links: ${JSON.stringify(channel_queue_links)}`);
logger.debug(`outcomes: ${JSON.stringify(outcomes)}`);
if (req.query.useSampleData) {
const data = sampleFlow(filter);
res.send(data);
return;
}
var data = {
nodes: [],
links: [],
};
if (filter.includes("Channel")) {
channels.forEach((value) => {
data.nodes.push({
name: value,
category: "Channel",
});
});
}
if (filter.includes("Queue")) {
queues.forEach((value) => {
data.nodes.push({
name: value,
category: "Queue",
});
});
}
if (filter.includes("Outcome")) {
outcomes.forEach((value) => {
data.nodes.push({
name: value,
category: "Outcome",
});
});
}
if (filter.includes("Channel") && filter.includes("Queue")) {
Object.values(channel_queue_links).forEach((element) => {
data.links.push(element);
});
}
if (filter.includes("Queue") && filter.includes("Outcome")) {
Object.values(queue_outcome_links).forEach((element) => {
data.links.push(element);
});
}
if (filter.includes("Channel") && filter.includes("Outcome") && !filter.includes("Queue")) {
Object.values(channel_outcome_links).forEach((element) => {
data.links.push(element);
});
}
res.send(data);
});
export default router;

View File

@@ -0,0 +1,253 @@
import { Router } from "express";
import axios from "axios";
import { URLSearchParams } from "url";
import { inspect } from "util";
const router = Router();
router.get("/", (req, res) => {
// token in session -> get user data and send it back to the vue app
if (req.session.token) {
query(req.query.referenceId);
}
// no token -> send nothing
else {
const params = new URLSearchParams({
grant_type: "password",
username: process.env.EO_API_USERNAME,
password: process.env.EO_API_PASSWORD,
scope: process.env.EO_API_SCOPE,
client_id: process.env.EO_API_CLIENT_ID,
client_secret: process.env.EO_API_SECRET,
});
axios
.post(process.env.EO_API_ACCESS_TOKEN_URL, params.toString(), {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then((result) => {
// save token to session
req.session.token = result.data.access_token;
console.log(result);
query(req.query.referenceId);
})
.catch((err) => {
sendError(err, res);
});
}
function query(referenceId) {
console.log("Executing Query");
const query = `query ($startTime: DateTime, $endTime: DateTime) {
findContactsCompletedBetween(startTime: $startTime, endTime: $endTime, filter: {interactionTypes : EMAIL}) {
totalCount
edges {
node {
systemId
startTime
endTime
direction
handledBy {
username
firstName
lastName
nickname
orgScope
}
activeDuration
notes {
totalCount
edges {
node {
text
}
}
}
interaction {
systemId
locale
__typename
... on Email {
messageId
threadId
sentDate
receivedDate
subject
fromAddress
toAddresses
ccAddresses
bccAddresses
detectedLanguage
mailboxName
attachmentCount
isDuplicate
}
}
outcome {
totalCount
edges {
node {
text
isActive
isVisible
}
}
}
customer {
totalCount
edges {
node {
ref
firstName
lastName
}
}
}
queue {
name
orgScope
}
}
}
}
}`;
const startTime = new Date(
new Date().setFullYear(new Date().getFullYear() - 2)
);
const endTime = new Date(new Date().setHours(new Date().getHours() - 1));
axios
.post(
process.env.EO_API_UDG_URL,
JSON.stringify({
query,
variables: { startTime, endTime },
}),
{
headers: {
Authorization: `OIDC_id_token ${req.session.token}`,
"Content-Type": "application/json",
},
}
)
.then((result) => {
const contacts = result.data.data.findContactsCompletedBetween.edges;
// Log error to console
if (result.data.errors && result.data.errors.length > 0) {
result.data.errors.forEach(function (error) {
console.log("ERROR: Errors in results - " + error.message);
});
// TODO: Should keep errors for filteredContacts
result.data.errors = [];
}
const filteredContacts = [];
contacts.forEach(function (contact) {
if (contact.node.interaction.__typename === "Email") {
// threadId if Reference in Subject line will do
if (
(contact.node.interaction.threadId === referenceId) |
contact.node.interaction.subject.includes(
`<< Ref:${referenceId} >>`
)
) {
filteredContacts.push(contact);
}
}
});
result.data.data.findContactsCompletedBetween.edges = filteredContacts;
result.data.data.findContactsCompletedBetween.totalCount =
filteredContacts.length;
// Summary Values
const summary = {};
summary.totalCount = filteredContacts.length;
summary.totalInboundCount = 0;
summary.totalInboundActiveSeconds = 0;
if (summary.totalCount > 0) {
summary.firstContactReceivedDate = new Date(
filteredContacts[0].node.interaction.receivedDate
);
filteredContacts.forEach(function (contact) {
if (contact.node.direction === "INBOUND") {
summary.totalInboundCount++;
if (!summary.firstInboundContactStartDate) {
summary.firstInboundContactStartDate = new Date(
contact.node.startTime
);
summary.firstContactReceivedDate = new Date(
contact.node.interaction.receivedDate
);
}
summary.totalInboundActiveSeconds += contact.node.activeDuration;
}
});
// TODO: Because of overlapping contacts, we may need to calculate max instead of last.
summary.lastContactEndTime = new Date(
filteredContacts[filteredContacts.length - 1].node.endTime
);
summary.totalHTHours =
(summary.lastContactEndTime.getTime() -
summary.firstContactReceivedDate.getTime()) /
(1000 * 3600);
summary.activeHTMinutes =
(summary.lastContactEndTime.getTime() -
summary.firstInboundContactStartDate.getTime()) /
(1000 * 60);
}
result.data.data.summary = summary;
if (result.data) {
console.log(
inspect(summary, {
showHidden: false,
depth: null,
colors: true,
})
);
res.send(result.data);
}
// expired token -> send nothing
else {
req.session.destroy();
res.send({});
}
})
.catch((err) => {
// bin the token on error
req.session.destroy();
sendError(err, res);
});
}
});
function sendError(err, res) {
console.error(err);
let errStatus = 500;
if (err.response) errStatus = err.response.status;
res.status(errStatus).send({
errors: [
{
status: errStatus,
title: err.code,
detail: err.message,
},
],
});
}
export default router;

17
src/app.js Normal file
View File

@@ -0,0 +1,17 @@
import express from "express";
import config from "./config/index.js";
import loader from "./loaders/index.js";
const app = express();
loader(app);
app.listen(config.port, (err) => {
if (err) {
console.log(err);
return process.exit(1);
}
console.log(`Server is running on ${config.port}`);
});
export default app;

38
src/config/index.js Normal file
View File

@@ -0,0 +1,38 @@
import dotenv from 'dotenv';
// Set the NODE_ENV to 'development' by default
process.env.NODE_ENV = process.env.NODE_ENV || "development";
const envFound = dotenv.config();
if (envFound.error) {
// This error should crash whole process
throw new Error("⚠️ Couldn't find .env file ⚠️");
}
export default {
/**
* Your favorite port
*/
port: parseInt(process.env.PORT, 10),
/**
* Your secret sauce
*/
jwtSecret: process.env.JWT_SECRET,
jwtAlgorithm: process.env.JWT_ALGO,
/**
* Used by winston logger
*/
logs: {
level: process.env.LOG_LEVEL || "silly",
},
/**
* API configs
*/
api: {
prefix: "/api",
},
};

47
src/index.js Normal file
View File

@@ -0,0 +1,47 @@
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const session = require("express-session");
// dotenv
require("dotenv").config();
const app = express();
// Use our middlewares
app.use(cors({ origin: true, credentials: true }));
app.use(morgan("common"));
app.use(express.json());
app.use(
session({
secret: "1234567890", // don't use this secret in prod :)
resave: false,
saveUninitialized: false,
cookie: {
secure: "auto",
httpOnly: true,
maxAge: 3600000,
},
})
);
// Provide a default port
const port = process.env.SERVER_PORT || 3000;
// Listen to server
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
// ...
// Main Page
app.get("/", (req, res) => {
res.send({
message: "Engagement Orchestration Services",
});
});
// ...
app.use("/config", require("./routes/config"));
app.use("/unified-data-gateway", require("./routes/unified-data-gateway"));
app.use("/interactions-flow", require("./routes/interactions-flow"));

99
src/loaders/express.js Normal file
View File

@@ -0,0 +1,99 @@
import express from "express";
import cors from "cors";
import compression from "compression";
import morgan from "morgan";
import helmet from "helmet";
import config from "./../config/index.js";
import routes from "./../api/routes/index.js";
import { logger } from "../utils/index.js";
// import { rateLimiter } from '../api/middlewares/index.js';
import bodyParser from "body-parser";
import session from "express-session";
export default (app) => {
logger.info("Loading Express ...");
process.on("uncaughtException", async (error) => {
logger.error(error);
});
process.on("unhandledRejection", async (ex) => {
logger.error(ex);
});
if (!config.jwtSecret) {
logger.crit("Jwtprivatekey is not defined");
process.exit(1);
}
app.enable("trust proxy");
// app.use(cors());
app.use(cors({ origin: true, credentials: true }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(morgan("dev"));
app.use(helmet());
app.use(compression());
app.use(express.static("public"));
app.disable("x-powered-by");
app.disable("etag");
app.use(
session({
secret: "1234567890", // don't use this secret in prod :)
resave: false,
saveUninitialized: false,
cookie: {
secure: "auto",
httpOnly: true,
maxAge: 3600000,
},
})
);
// app.use(rateLimiter);
logger.info(`Mounting routes on ${config.api.prefix}`);
app.use(config.api.prefix, routes);
app.get("/", (_req, res) => {
return res
.status(200)
.json({
resultMessage: {
en: "Project is successfully working...",
},
resultCode: "00004",
})
.end();
});
app.use((req, res, next) => {
// res.header("Access-Control-Allow-Origin", "*");
// res.header(
// "Access-Control-Allow-Headers",
// "Origin, X-Requested-With, Content-Type, Accept, Authorization"
// );
// res.header("Content-Security-Policy-Report-Only", "default-src: https:");
// if (req.method === "OPTIONS") {
// res.header("Access-Control-Allow-Methods", "PUT POST PATCH DELETE GET");
// return res.status(200).json({});
// }
next();
});
app.use((_req, _res, next) => {
const error = new Error("Endpoint could not find!");
error.status = 404;
next(error);
});
app.use((error, req, res) => {
res.status(error.status || 500);
logger.error(error.message);
return res.json({
resultMessage: {
en: error.message,
tr: error.message,
},
});
});
};

5
src/loaders/index.js Normal file
View File

@@ -0,0 +1,5 @@
import expressLoader from "./express.js";
export default async (app) => {
expressLoader(app);
};

1
src/utils/index.js Normal file
View File

@@ -0,0 +1 @@
export { default as logger } from "./logger.js";

32
src/utils/logger.js Normal file
View File

@@ -0,0 +1,32 @@
import winston from "winston";
import config from "../config/index.js";
const transports = [];
if (process.env.NODE_ENV !== "development") {
transports.push(new winston.transports.Console());
} else {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat()
),
})
);
}
const LoggerInstance = winston.createLogger({
level: config.logs.level,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
transports,
});
export default LoggerInstance;

207
src/utils/sampleFlow.js Normal file
View File

@@ -0,0 +1,207 @@
import { logger } from "../utils/index.js";
export default function generate(filter) {
filter = filter ? filter : "Channel, Sub-Channel, Queue, Outcome";
logger.info(`Generating sample data for ${filter}`);
var data = {
nodes: [],
links: [],
};
const channels = ["Email", "Messaging", "Legacy Live Chat"];
const subChannels = ["Live Chat", "Facebook Messenger", "Twitter DM", "WhatsApp","Other"];
const queues = ["General Enquiries", "Complaints", "Default"];
const outcomes = ["No need for response", "Case Updated", "Escalated to Manager"];
if (filter.includes("Channel")) {
channels.forEach((value) => {
data.nodes.push({
name: value,
category: "Channel",
});
});
}
if (filter.includes("Sub-Channel")) {
subChannels.forEach((value) => {
data.nodes.push({
name: value,
category: "Sub-Channel",
});
});
}
if (filter.includes("Queue")) {
queues.forEach((value) => {
data.nodes.push({
name: value,
category: "Queue",
});
});
}
if (filter.includes("Outcome")) {
outcomes.forEach((value) => {
data.nodes.push({
name: value,
category: "Outcome",
});
});
}
data.links.push(
{
source: "Email",
target: "Default",
value: 342,
},
{
source: "Messaging",
target: "Live Chat",
value: 232,
},
{
source: "Messaging",
target: "Facebook Messenger",
value: 623,
},
{
source: "Messaging",
target: "Twitter DM",
value: 434,
},
{
source: "Messaging",
target: "WhatsApp",
value: 1243,
},
{
source: "Messaging",
target: "Other",
value: 150,
},
{
source: "Live Chat",
target: "Default",
value: 132,
},
{
source: "Live Chat",
target: "Complaints",
value: 90,
},
{
source: "Live Chat",
target: "General Enquires",
value: 42,
},
{
source: "WhatsApp",
target: "Default",
value: 343,
},
{
source: "WhatsApp",
target: "Complaints",
value: 300,
},
{
source: "WhatsApp",
target: "General Enquires",
value: 523,
},
{
source: "Facebook Messenger",
target: "Default",
value: 143,
},
{
source: "Facebook Messenger",
target: "Complaints",
value: 200,
},
{
source: "Facebook Messenger",
target: "General Enquires",
value: 323,
},
{
source: "Twitter DM",
target: "Default",
value: 143,
},
{
source: "Twitter DM",
target: "Complaints",
value: 50,
},
{
source: "Twitter DM",
target: "General Enquires",
value: 223,
},
{
source: "General Enquires",
target: "Case Closed",
value: 421,
},
{
source: "General Enquires",
target: "Completed",
value: 612,
},
{
source: "General Enquires",
target: "Escalated to Manager",
value: 23,
},
{
source: "General Enquires",
target: "No need for response",
value: 241,
},
{
source: "Complaints",
target: "Case Closed",
value: 21,
},
{
source: "Complaints",
target: "Completed",
value: 12,
},
{
source: "Complaints",
target: "Escalated to Manager",
value: 3,
},
{
source: "Complaints",
target: "No need for response",
value: 41,
},
{
source: "Default",
target: "Completed",
value: 41,
},
{
source: "Default",
target: "Case Updated",
value: 410,
},
{
source: "General Enquires",
target: "Case Updated",
value: 50,
},
{
source: "Complaints",
target: "Case Updated",
value: 410,
}
);
return data;
}

21746
src/utils/sampleUDGresponse.js Normal file

File diff suppressed because it is too large Load Diff