services/citizenService.js

/**
 * 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
}