Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ AWS_REGION=eu-west-2
AWS_DEFAULT_REGION=eu-west-2
# LocalStack community edition is sufficient
IMAGE_NAME=localstack/localstack:latest
LAMBDA_TIMEOUT=120
LAMBDA_TIMEOUT=9999999
# Comment the line below and create a new containerised development environment to disable debugging of Lambda functions.
# IMPORTANT - If cloning the remote repository into a container volume, the change must be pushed to a pull request
# branch from which a new containerised development environment is created. If a new containerised development environment
Expand Down
124 changes: 65 additions & 59 deletions lib/functions/processMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,66 @@
const xml2js = require('xml2js')
const moment = require('moment')
const service = require('../helpers/service')
const Message = require('../models/message')
const eventSchema = require('../schemas/processMessageEventSchema')
const aws = require('../helpers/aws')
const { validateXML } = require('xmllint-wasm')
const fs = require('fs')
const path = require('path')
const fs = require('node:fs')
const path = require('node:path')
const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8')
const additionalCapMessageSchema = require('../schemas/additionalCapMessageSchema')
const Message = require('../models/message')

module.exports.processMessage = async (event) => {
try {
// validate the event
await eventSchema.validateAsync(event)

// Add in the references field
const xmlMessage = event.bodyXml.replace('</scope>', '</scope>\n<references></references>')

const xmlResult = await new Promise((resolve, reject) => {
xml2js.parseString(xmlMessage, (err, value) => {
if (err) return reject(err)
resolve(value)
})
})

const results = await Promise.allSettled([
(async () => {
const validationResult = await validate(event.bodyXml, xsdSchema)
if (validationResult.errors?.length > 0) {
throw validationResult.errors
}
})(),
(async () => {
const joiValidation = additionalCapMessageSchema.validate(xmlResult, { abortEarly: false })
if (joiValidation.error) {
throw joiValidation.error.details ?? [joiValidation.error]
}
})()
])

const errors = results.filter(r => r.status === 'rejected').flatMap(r => r.reason)

if (errors.length > 0) {
throw new Error(JSON.stringify(errors))
}

const dbResult = await service.getLastMessage(xmlResult.alert.info[0].area[0].geocode[0].value[0])
// parse the xml
const message = new Message(event.bodyXml)
console.log(`Processing CAP message: ' + ${message.identifier} for ${message.fwisCode}`)

// get Last message
const dbResult = await service.getLastMessage(message.fwisCode)
const lastMessage = (!!dbResult && dbResult.rows.length > 0) ? dbResult.rows[0] : undefined

// If not production set status to test
if (process.env.stage !== 'prd') {
xmlResult.alert.status[0] = 'Test'
message.status = 'Test'
}

updateReferences(lastMessage, xmlResult)
// Add in the references field and update msgtype to Update if references exist and is Alert
const references = getReferences(lastMessage, message.sender)
if (references) {
message.references = references
}

const message = new Message(xmlResult)
// do validation
const results = await Promise.allSettled([
// Validate xml against CAP XSD schema https://eaflood.atlassian.net/browse/NI-95
validateAgainstXsdSchema(message),
// Convert xml to js object for joi extended validation https://eaflood.atlassian.net/browse/NI-113
validateAgainstJoiSchema(message)
])

console.log('Processing CAP message: ' + message.data.identifier + ' for ' + message.data.fwis_code)
// Check for validation failures and throw
const errors = results.filter(r => r.status === 'rejected').flatMap(r => r.reason)
if (errors.length > 0) {
throw new Error(JSON.stringify(errors))
}

await service.putMessage(message.putQuery)
// store the message in database
await service.putMessage(message.putQuery())
console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`)

console.log('Finished processing CAP message: ' + message.data.identifier + ' for ' + message.data.fwis_code)
return {
statusCode: 200,
body: {
message: 'Cap message successfully stored for ' + message.data.fwis_code,
identifier: message.data.identifier,
fwisCode: message.data.fwis_code,
sent: message.data.sent,
expires: message.data.expires,
status: xmlResult.alert.status[0]
message: `Cap message successfully stored for ${message.fwisCode}`,
identifier: message.identifier,
fwisCode: message.fwisCode,
sent: message.sent,
expires: message.expires,
status: message.status
}
}
} catch (err) {
Expand All @@ -84,7 +73,7 @@ module.exports.processMessage = async (event) => {
}
}

async function processFailedMessage (originalError, xmlResult) {
const processFailedMessage = async (originalError, xmlResult) => {
// For backwards compapibility, only send a notification if an AWS SNS topic
// is configured.
if (process.env.CPX_SNS_TOPIC) {
Expand All @@ -109,22 +98,39 @@ async function processFailedMessage (originalError, xmlResult) {
}
}

const validate = (message, schema) => {
return validateXML({
const getReferences = (lastMessage, sender) => {
if (lastMessage && lastMessage.expires > new Date()) {
const newReference = `${sender},${lastMessage.identifier},${moment(lastMessage.sent).utc().format('YYYY-MM-DDTHH:mm:ssZ')}`
return lastMessage.references ? `${lastMessage.references} ${newReference}` : newReference
} else {
return ''
}
}

const validateAgainstXsdSchema = async (message) => {
const validationResult = await validateXML({
xml: [{
fileName: 'message.xml',
contents: message
contents: message.toString()
}],
schema: [schema]
schema: [xsdSchema]
})

if (validationResult.errors?.length > 0) {
throw validationResult.errors
}
}

const updateReferences = (lastMessage, xmlResult) => {
if (lastMessage && lastMessage.expires > new Date()) {
const newReference = `${xmlResult.alert.sender[0]},${lastMessage.identifier},${moment(lastMessage.sent).utc().format('YYYY-MM-DDTHH:mm:ssZ')}`
xmlResult.alert.references = [lastMessage.references ? `${lastMessage.references} ${newReference}` : newReference]
xmlResult.alert.msgType[0] = xmlResult.alert.msgType[0] === 'Alert' ? 'Update' : xmlResult.alert.msgType[0]
} else {
delete xmlResult.alert.references
const validateAgainstJoiSchema = async (message) => {
const jsMessage = await new Promise((resolve, reject) => {
xml2js.parseString(message.toString(), (err, value) => {
if (err) return reject(err)
return resolve(value)
})
})

const joiValidation = additionalCapMessageSchema.validate(jsMessage, { abortEarly: false })
if (joiValidation.error) {
throw joiValidation.error.details ?? [joiValidation.error]
}
}
113 changes: 87 additions & 26 deletions lib/models/message.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,98 @@
'use strict'

const xml2js = require('xml2js')
const xmldom = require('@xmldom/xmldom')
const xmlFormat = require('xml-formatter')
const { Sql } = require('sql-ts')
const sql = new Sql('postgres')
const messages = sql.define({
name: 'messages',
columns: ['identifier', 'msg_type', 'references', 'alert', 'fwis_code', 'expires', 'sent', 'created']
})
const xmlBuilder = new xml2js.Builder({
headless: true,
cdata: true
})

function Message (xmlMessage) {
const message = {
identifier: xmlMessage.alert.identifier[0],
msg_type: xmlMessage.alert.msgType[0],
references: xmlMessage.alert.references ? xmlMessage.alert.references[0] : '',
alert: xmlBuilder.buildObject(xmlMessage).replace(/&#xD;/g, ''),
fwis_code: xmlMessage.alert.info[0].area[0].geocode[0].value[0],
expires: xmlMessage.alert.info[0].expires[0],
sent: xmlMessage.alert.sent[0],
created: new Date().toISOString()
}

Object.defineProperties(this, {
data: {
value: message
},
putQuery: {
value: messages.insert(message).toQuery()
class Message {
constructor (xmlString) {
this.doc = new xmldom.DOMParser().parseFromString(xmlString, 'text/xml')
}

get fwisCode () {
return this.getFirstElement('geocode').getElementsByTagName('value')[0].textContent
}

get identifier () {
return this.getFirstElement('identifier').textContent
}

get sender () {
return this.getFirstElement('sender').textContent
}

get msgType () {
return this.getFirstElement('msgType').textContent
}

set msgType (value) {
this.getFirstElement('msgType').textContent = value
}

get references () {
return this.getFirstElement('references') ? this.getFirstElement('references').textContent : ''
}

set references (value) {
if (value) {
if (this.references) {
this.getFirstElement('references').textContent = value
} else {
this.addElement('scope', 'references', value)
}
if (this.msgType === 'Alert') {
this.msgType = 'Update'
}
}
}

get status () {
return this.getFirstElement('status').textContent
}

set status (value) {
this.getFirstElement('status').textContent = value
}

get expires () {
return this.getFirstElement('expires').textContent
}

get sent () {
return this.getFirstElement('sent').textContent
}

getFirstElement (tagName) {
return this.doc.getElementsByTagName(tagName)[0]
}

addElement (parentTag, elTag, elValue) {
const parentEl = this.doc.getElementsByTagName(parentTag)[0]
const newEl = this.doc.createElement(elTag)
newEl.textContent = elValue
return parentEl.parentNode.insertBefore(newEl, parentEl.nextSibling)
}

toString () {
return xmlFormat(new xmldom.XMLSerializer().serializeToString(this.doc), { indentation: ' ', collapseContent: true })
}

putQuery () {
const message = {
identifier: this.identifier,
msg_type: this.msgType,
references: this.references,
alert: this.toString(),
fwis_code: this.fwisCode,
expires: this.expires,
sent: this.sent,
created: new Date().toISOString()
}
})
return messages.insert(message).toQuery()
}
}

module.exports = Message
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
"license": "OGL",
"dependencies": {
"@aws-sdk/client-sns": "^3.873.0",
"@xmldom/xmldom": "^0.8.11",
"feed": "5.1.0",
"joi": "^18.0.1",
"moment": "^2.30.1",
"pg": "8.16.3",
"sql-ts": "7.1.0",
"xml-formatter": "^3.6.7",
"xml2js": "0.6.2",
"xmllint-wasm": "^5.0.0"
},
Expand Down
Loading