/**
* Used by the "CitizenController" to transform HTTP request
* into request for the models in a way that allows for code reuse between POST/PUT requests
* @module Services/CitizenService
* @see module:Controllers/CitizenController
* @todo Maybe dont exports prepare functions themselves
*/
const Boom = require('@hapi/boom')
const CitizenModel = require('../models/citizenModel.js')
const StepModel = require('../models/stepModel.js')
/**
* Retrieves the inputpatterns (from reasoning/interim/input/input_patterns.n3") that match the steps provided in "input"
* Also maps the input into an object thats easyer to use
* @async
* @param {StepModel} stepModel - StepModel to use to retrieve and check all patterns
* @param {any} input Key-Value pairs containing stepname-stepvalue
*
* @returns {Array} - Array containing two elements:
* - An array of objects representing the steps as {name:..., value:...}
* - An array of the patterns (strings) deduced from the stepnames
* @throws - If a provided step doesn't exist
* @todo Maybe dont throw on a bad step name but just not include it
*/
const getStepsAndPatternsFromInput = async (stepModel, input) => {
// get the input pattern for each of the steps
const inputPatterns = await stepModel.getAllInputPatternsAsString()
const patternPromises = []
const steps = []
// loop the provided steps and check if they exist, edit the ones that exist
for (const stepName in input) {
// check if an input pattern exists for the provided step, handle error if it doesn't
if (!inputPatterns.includes(stepName + ' ')) { throw Boom.notFound('The provided step, ' + stepName + ', does not exist.') }
// get the input pattern required to edit if it turns out the user already has data stored for this step
patternPromises.push(stepModel.getInputPatternByStep(stepName))
steps.push({ name: stepName, value: input[stepName] })
}
const patterns = await Promise.all(patternPromises)
return [steps, patterns]
}
/**
* Type returned by {@link module:Services/CitizenService.preparePost}
* and {@link module:Services/CitizenService.preparePut}.
* @typedef {Array} PreparedReturn
* @property {Object} 0 - Key-value pairs step names - step values of steps which need to be added.
* @property {Array} 1 - List of existing quads that need to be edited.
* @property {Array} 2 - New values of the abovementioned quads that need editing
* @property {Array} 3 - List of stored patterns that need to be deleted
* @property {Object} 4 - Lists of steps that have been edited or added.
* @property {Array} 4.edited - List of edited steps
* @property {Array} 4.added - List of added steps
*
* @todo Bad name
*/
/**
* Determines which patterns need to be added, edited or deleted based on the input patterns and currently stored quads.
*
* Appends new values to existing steps if a list of values is supplied,
* otherwise edits steps that are already presesent with a new value
*
* To be used in {@link module:Services/CitizenService.addSteps}.
* @param {Array} steps - An array of objects representing the steps as {name:..., value:...}
* @param {String[]} patterns - Array of pattern objects as returned by {@link StepModel.getInputPatternByStep}
* @param {Array[]} storedQuads - Array of currently stored quads for every pattern
* @returns {PreparedReturn}
*/
exports.preparePost = async (steps, patterns, storedQuads) => {
const response = { edited: [], added: [] }
const stepsToAdd = { }
let quadsToEdit = []
let editValues = []
const patternsToDelete = []
for (let i = 0; i < patterns.length; i++) {
if (i < storedQuads.length && storedQuads[i].length > 0) {
// multiple values already stored so append all new values to the list
if (storedQuads[i].length > 1 ||
// If there need to be multiple values but a single value is already stored
// Append new values as well
(storedQuads[i].length === 1 && Array.isArray(steps[i].value) && steps[i].value.length > 1)) {
stepsToAdd[steps[i].name] = steps[i].value
} else if (storedQuads[i].length === 1) {
// Only a single value stored and a single value given in non array form
// Just edit the existing quad
quadsToEdit = quadsToEdit.concat(storedQuads[i])
editValues = editValues.concat(steps[i].value)
}
response.edited.push(steps[i].name)
} else { // otherwise it doesn't exist yet and we need to add it instead
stepsToAdd[steps[i].name] = steps[i].value
response.added.push(steps[i].name)
}
}
return [stepsToAdd, quadsToEdit, editValues, patternsToDelete, response]
}
/**
* Determines which patterns need to be added, edited or deleted based on the input patterns and currently stored quads.
*
* Overwrites existing steps if a list of values is supplied or if multiple values are already present,
* otherwise edits steps that are already presesent with a new value.
*
* To be used in {@link module:Services/CitizenService.addSteps}.
* @param {Array} steps - An array of objects representing the steps as {name:..., value:...}
* @param {String[]} patterns - Array of pattern objects as returned by {@link StepModel.getInputPatternByStep}
* @param {Array[]} storedQuads - Array of currently stored quads for every pattern
* @returns {PreparedReturn}
*/
exports.preparePut = async (steps, patterns, storedQuads) => {
const response = { edited: [], added: [] }
const stepsToAdd = { }
let quadsToEdit = []
let editValues = []
const patternsToDelete = []
for (let i = 0; i < patterns.length; i++) {
if (i < storedQuads.length && storedQuads[i].length > 0) {
// values are list so delete all found quads
// and recreate the list with the new values
if (storedQuads[i].length > 1 ||
// If there need to be multiple values but a single value is already stored
// Also delete that stored value and send a new list
(storedQuads[i].length === 1 && Array.isArray(steps[i].value) && steps[i].value.length > 1)) {
patternsToDelete.push(patterns[i])
stepsToAdd[steps[i].name] = steps[i].value
} else if (storedQuads[i].length === 1) { // Redundant if condition
quadsToEdit = quadsToEdit.concat(storedQuads[i])
editValues = editValues.concat(steps[i].value)
}
response.edited.push(steps[i].name)
} else { // otherwise it doesn't exist yet and we need to add it instead
stepsToAdd[steps[i].name] = steps[i].value
response.added.push(steps[i].name)
}
}
return [stepsToAdd, quadsToEdit, editValues, patternsToDelete, response]
}
/**
*
* Processes the input according to the type of request.
*
* @param {Object} request - Express request object, used to initialize models
* @param {Object} input Key-Value pairs containing stepname-stepvalue
* @param {Function} prepareFunction - Either {@link module:Services/CitizenService.preparePost}
* or {@link module:Services/CitizenService.preparePut},
* depending on what type of request is required
* @returns {Object} - Object containing a list of which steps where added and which steps were edited
*/
exports.addSteps = async (request, input, prepareFunction) => {
const step = new StepModel(request)
const citizen = new CitizenModel(request, null)
const [steps, patterns] = await getStepsAndPatternsFromInput(step, input)
const storedQuads = await citizen.quadsPerInputPattern(patterns)
const [stepsToAdd, quadsToEdit, editValues, patternsToDelete, response] = await prepareFunction(steps, patterns, storedQuads)
if (patternsToDelete.length > 0) {
const deleteCount = citizen.deleteQuadsByInputPatterns(patternsToDelete)
console.log(`Deleted quads": ${deleteCount}`)
}
if (quadsToEdit.length > 0) {
// edit the info based on the existing quads (oldQuads)
await citizen.editInfoByQuads(quadsToEdit, editValues)
}
if (Object.keys(stepsToAdd).length > 0) {
// Now add the steps that didn't exist yet and need to be added (stepsToAdd)
// turn the JSON request into triples the reasoner can use
// eg: step:addTelephoneNumber :value "123456"
const result = await step.getTriplesFromSteps(stepsToAdd)
// only attempt to add info if there were steps to add, end the response here if there are no additional steps
if (result == null) { return response }
// now use the reasoner to complete the step and add the info for the user
await citizen.addInfoByN3String(result)
}
return response
}