models/citizenModel.js

// initialize error handler
const Boom = require('@hapi/boom')

const ReasoningService = require('../services/reasoning.js')
const BaseModel = require('./baseModel.js')

// RDF stuff
const N3 = require('n3')
const { DataFactory } = N3
const { namedNode, literal, defaultGraph, quad } = DataFactory

// for creating path names to files
const path = require('path')
const fs = require('fs')
const assert = require('assert').strict

/**
 * Class to operate on citizen data
 * @extends BaseModel
 */
class CitizenModel extends BaseModel {
  constructor (req, solidService) {
    super(req)
    this.solidService = solidService
    if (this.solidService !== null) {
      this.solidService.uploadProfileToPersonalStorage(req.body)
    }
    this.extendedFolder = '../reasoning/profile'
    this.fileName = 'profileInfo.n3'
    this.fullFilePath = path.join(this.dataFolder, this.extendedFolder, this.fileName)
  }

  /**
   * Returns a promise resolving to all the information available about a specific step
   * Invokes the reasoner to achieve this
   * @async
   * @param {string} stepName - name of the step (without "step:" prefix)
   * @returns {Promise<string>} - Promise resolving to N3-string containing all the information
   */
  async getInfoByStep (stepName) {
    if (stepName.includes(' ')) { return Promise.reject(Boom.badRequest('Step cannot contain any whitespaces.'), null) }

    let myQuad = null
    // use full URI or step label depending on what was supplied
    if (stepName.includes('http://')) {
      // create quad needed for reasoning query
      // eg: step:showWasteCollection ex:displayInput true.
      myQuad = quad(
        namedNode(stepName),
        namedNode('http://example.org/ns/example#' + 'displayInput'),
        literal(true),
        defaultGraph()
      )
    } else {
      myQuad = quad(
        namedNode(this.prefixes.step + stepName),
        namedNode('http://example.org/ns/example#' + 'displayInput'),
        literal(true),
        defaultGraph()
      )
    }

    const quads = [myQuad]

    const prefixes = { step: this.prefixes.step, ' ': this.prefixes[' '] }

    // convert the quad to a file to be used by reasoner
    const tempFile = await this.fileService.quadsToTempFile(quads, prefixes)

    const inference = {
      query: 'reasoning/show/query_used.n3',
      data: [
        'reasoning/profile/knowledge.n3',
        'reasoning/profile/personalInfo.n3',
        'reasoning/show/getInfo.n3',
        'reasoning/interim/show/query-patterns.n3 ',
        'reasoning/interim/steps/component-level-steps.n3',
        'reasoning/interim/steps/container-level-steps.n3',
        'reasoning/interim/steps/journey-level-steps.n3',
        'reasoning/help-functions/built-ins.n3',
        tempFile
      ]
    }
    if (this.solidService !== null) {
      this.solidService.injectUserStorageLocation(inference)
    } else {
      inference.data.push(this.fullFilePath)
    }
    // Here comes the all the DAG invocation
    const result = await ReasoningService.eyePromise(inference)
    fs.unlink(tempFile, (err) => {
      console.log(`Unlinking ${tempFile}`)
      if (err) throw err
    })
    if (this.solidService !== null) {
      this.solidService.removeUserData()
    }
    return result
  }

  /**
   * Add new information on a user
   * @async
   * @param {string} n3string - N3-string to be added to the users profile file
   * @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
   */
  async addInfoByN3String (n3string) {
    // define the file we want to append to
    // var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;
    // let file = path.join(this.dataFolder, this.extendedFolder,this.fileName);

    // create the file if it doesn't exist
    // const handle = await this.fileService.newFile(this.fullFilePath);
    return this.fileService.appendN3stringToFile(this.fullFilePath, n3string)
    // .then(() => handle.close());
  }

  /**
   * Edit info of a user
   * @param {Quad[]} quads - old quads that should change
   * @param {Array} values - new values to apply to old quad
   * @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
   */
  async editInfoByQuads (quads, values) {
    // make sure array format was supplied
    if (!Array.isArray(quads)) {
      console.log("Passing scalar quad to 'citizenModel::editInfoByQuads'")
      quads = [quads]
    }
    if (!Array.isArray(values)) {
      console.log("Passing scalar value to 'citizenModel::editInfoByQuads'")
      values = [values]
    }

    // file that holds all user info
    // var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;

    // get existing quads and prefixes from file
    const fileQuads = await this.fileService.fileToQuads(this.fullFilePath)
    const prefixes = await this.fileService.prefixesFromFile(this.fullFilePath)

    // store the user's quads, so we can easily manipulate them
    const store = new N3.Store(fileQuads)

    for (const i in quads) {
      // remove the old quad from the store
      store.removeQuad(quads[i])

      // change the quad to match the new value
      quads[i].object = literal(values[i])
    }

    // retrieve the quads from the store after removal of old quads
    let storeQuads = store.getQuads()
    // add new quads to the storedQuads
    storeQuads = storeQuads.concat(quads)

    // write changes to file
    return this.fileService.writeQuadsToFile(this.fullFilePath, storeQuads, prefixes)
  }

  /**
   * Edit info of a user
   * Replaces old quad with new quad with new value
   * and persists this to disk
   * @async
   * @param {Quad} quad - the old quad we want to edit
   * @param {} value -  new value for the object field of the quad
   * @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
   */
  async editInfoByQuad (quad, value) {
    return this.editInfoByQuads([quad], value)
  }

  /**
  * Retrieves all stored quads that match the supplied patterns
  * @async
  * @param {string[]} inputPattern - N3 pattern used to match stored quads
  * @return {Array} - An array of Quads for every input pattern (this is an array of arrays) or an empty array if no patterns were supplied
  */
  async quadsPerInputPattern (patterns) {
    if (patterns === null || patterns.length === 0) {
      return []
    }

    try {
      const fileQuads = await this.fileService.fileToQuads(this.fullFilePath)
      // store the user's stored quads
      const store = new N3.Store(fileQuads)
      const foundPerPattern = []

      for (const inputPattern of patterns) {
        if (inputPattern === null) {
          continue
        }
        const inputQuads = this.fileService.stringToQuads(inputPattern.prefixes + '\r\n' + inputPattern.triples + '.')

        let foundQuads = []

        for (const quad of inputQuads) {
          foundQuads = foundQuads.concat(store.getQuads(quad.subject, quad.predicate, null))
        }

        foundPerPattern.push(foundQuads)
      }

      return foundPerPattern
    } catch (err) {
      console.error(err)
      return []
    }
  }

  /**
  * Retrieves all stored quads that match the pattern
  * @param {String} inputPattern - N3 pattern used to match stored quads
  * @return {Array} - The found quads or an empty array if no patterns were found
  */
  getAllQuadsByInputPattern (inputPattern) {
    const found = this.quadsPerInputPattern([inputPattern])
    // Were only providing a single pattern so the list of quadlists should have at most 1 element
    // If no elements are found just return an empty list because found[0] might not exist
    assert.strictEqual(found.length <= 1, true, 'More quadlists returned then input patterns provided')
    if (found.length === 1) { return found[0] }

    return []
  }

  /**
   * Delete all stored quads matching the input patterns templates provided
   * @async
   * @param {Array} - Array of patterns to remove
   * @returns {void}
   * @todo Use removeMatches instead of manually retrieving quads by quadsPerInputPattern and deleting them
   */
  async deleteQuadsByInputPatterns (inputPatterns) {
    if (inputPatterns === null) {
      return 0
    }
    // make sure array format was supplied
    if (!Array.isArray(inputPatterns)) {
      inputPatterns = [inputPatterns]
      console.warn('Passing scalar value to citizenModel::deleteQuadsByInputPatterns')
    }

    // get all of the quads we stored for the user, get prefixes too to keep files clean
    const fileQuads = await this.fileService.fileToQuads(this.fullFilePath)
    const prefixes = await this.fileService.prefixesFromFile(this.fullFilePath)

    // initialize an N3 store with the user's stored quads
    const store = new N3.Store(fileQuads)

    const quads = this.quadsPerInputPattern(inputPatterns)

    const flatQuads = quads.flat()

    store.removeQuads(flatQuads)

    // get the remaining quads from the store
    const remainingQuads = store.getQuads()

    /* TODO: Check if this is equivalent to the above
    // initialize an N3 store with the user's stored quads
    const store = new N3.Store(fileQuads);
    for(const pattern of inputPatterns){
      store.removeMatches(pattern);
    }
    const remainingQuads = store.getQuads();
    */

    // write changes to file
    try {
      await this.fileService.writeQuadsToFile(this.fullFilePath, remainingQuads, prefixes)
    } catch (e) {
      console.error('deleteQuadsByInputPatterns: error writing quads back to file')
    }
  }

  /*
  * Delete all info on user,
  * ! currently clears the entire storage file
  */
  /**
   * Delete all info on user,
   * ! currently clears the entire storage file
   * @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
   */
  async deleteAllInfo () {
    // define the file we want to append to
    // var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;

    return this.fileService.deleteContent(this.fullFilePath)
  }
}

module.exports = CitizenModel

/*
* Delete a filled in input pattern by using the input pattern template
*/
/* async deleteQuadsByInputPattern(inputPattern)
  {
    return deleteQuadsByInputPatterns([inputPattern]);

    // get the quad endpoint quad for this pattern if it exists
    const quad = await this.getQuadByInputPattern(inputPattern);
    if (quad == null){
      console.log('No data to delete');
      return new Promise((resolve) => {
        resolve()
      }); //next(Boom.badRequest('No existing info to delete.'), null)
    }
    // file that holds all user info
    //var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;

    // get all of the quads we stored for the user, get prefixes too to keep files clean
    const fileQuads = await this.fileService.fileToQuads(this.fullFilePath);
    const prefixes = await this.fileService.prefixesFromFile(this.fullFilePath);

    // initialize an N3 store with stored quads
    const store = new N3.Store(fileQuads);

    // use this quad to work way back to find the other stored quads for this pattern
    //TODO: why overwrite?
    let quads = store.getQuads(null, null, quad.subject);
    quads = store.getQuads(quad.subject, null, null);

    // remove the quads we found from the store
    store.removeQuads(quads);

    // get the remaining quads from the store
    const storeQuads = store.getQuads();

    // write changes to file
    return this.fileService.writeQuadsToFile(this.fullFilePath, storeQuads, prefixes);

  }
  */

/*
  * Find if a filled in input pattern already exists for each of the patterns in inputPatterns,
  * send back the endpoint quads (which hold the info) for each of the patterns.
  */
/*
  async getQuadsByInputPatterns(inputPatterns) {
    if (!Array.isArray(inputPatterns))
      inputPatterns = [inputPatterns];

    let quadPromises = [];
    for(let pattern of inputPatterns){
      quadPromises.push(this.getQuadByInputPattern(pattern));
    }
    //await Promise.all(inputPatterns.map(pattern => this.getQuadByInputPattern(pattern)));
    let quads = await Promise.all(quadPromises);
    return quads;
  }
  */

/*
  * Find if a filled in input pattern already exists,
  * send back the stored endpoint quad (which holds the info) for this pattern.
  * Returns null if there doesn't exist a stored quad
  * or the inputPattern is null (i.e. it doesn't specify a variable to store)
  */

/*
  async getQuadByInputPattern(inputPattern) {
    if (inputPattern === null) {
      return null;
    }
    // file that holds all user info
    //var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;

    // get all of the quads
    const fileQuads = await this.fileService.fileToQuads(this.fullFilePath);
    const inputQuads = this.fileService.stringToQuads(inputPattern.prefixes + '\r\n' + inputPattern.triples + '.');
    let replaceQuad = this.fileService.stringToQuads(inputPattern.prefixes + '\r\n' + inputPattern.replaceVar)[0];

    // store the user's stored quads
    const store = new N3.Store(fileQuads);

    // store the input pattern quads
    const inputPatternStore = new N3.Store(inputQuads);

    // use the input patterns store to search the quad containing the info on the step
    replaceQuad = inputPatternStore.getQuads(null, null, replaceQuad.object)[0];

    const keepPredicate = replaceQuad.predicate;
    inputPatternStore.removeQuad(replaceQuad);

    replaceQuad = inputPatternStore.getQuads(replaceQuad.subject, null, null)[0];
    let storedQuad = null;
    if (replaceQuad == null) {
      replaceQuad = inputPatternStore.getQuads(null, null, null)[0];
      if (replaceQuad != null) {
        storedQuad = store.getQuads(null, replaceQuad.predicate, null)[0];
      }
      if (storedQuad != null) {
        storedQuad = store.getQuads(storedQuad.object, keepPredicate, null)[0];
      }
      if (storedQuad == null) {
        storedQuad = store.getQuads(null, keepPredicate, null)[0];
      }

    } else {
      if (replaceQuad != null) {
        storedQuad = store.getQuads(null, null, replaceQuad.object)[0];
      }
      if (storedQuad != null) {
        storedQuad = store.getQuads(storedQuad.subject, keepPredicate, null)[0];
      }
    }
    // will return null if user doesn't have anything stored for this pattern
    return storedQuad;

  }
  */