models/baseModel.js


const FileService = require('../services/n3FileService.js')

// include our config file
const config = require('../config.js')

// File system stuff
const fs = require('fs')

// RDF stuff
const N3 = require('n3')
const {
  DataFactory
} = N3
const {
  namedNode
} = DataFactory

const {
  quad
} = DataFactory

/**
* The baseModel contains reusable code that can be extended to other Models
* @class BaseModel
* @todo Factor out code only used on startup for fixing links that has no business being in the extended classes.
*/
class BaseModel {
  /**
   * @constructor
   * @param {Object} req - HTTP request, used to generate the correct links from the URL of the request
   */
  constructor (req) {
    if (req.fullUrl == null) {
      this.url = req.protocol + '://' + req.get('host') + config.serverOptions.routesPrefix
    } else {
      this.url = req.fullUrl
    }

    this.dataFolder = 'data'

    /**
     * @member {Object} - Key - value pairs containing default prefixes we always need
     */

    this.prefixes = {
      rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
      rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
      http: 'http//www.w3.org/2011/http#>',
      hydra: 'http://www.w3.org/ns/hydra/context.jsonld',
      oslo: 'http://oslo.example.org#',
      agent: 'http://fast.example.org/agent#',
      ldp: 'http://www.w3.org/ns/ldp#',
      dcterms: 'http://purl.org/dc/terms/',
      ' ': 'http://example.org#',
      ex: this.url + '/example-vocab#',
      state: this.url + '/states#',
      step: this.url + '/steps#',
      shape: this.url + '/shapes#'
    }

    this.fileService = new FileService()
    this.store = new N3.Store()
    /**
     * @member {Object} - The prefixes which depend on the server's current URL and thus always need to point to the right address.
     * @todo move this out, only used on startup
     */
    this.replacementPrefixes = {
      state: this.url + '/states#',
      step: this.url + '/steps#',
      shape: this.url + '/shapes#'
    }
    /** @member {RegExp} - Regular expression to match prefixes that need to be adjusted to the server's url
     *
     * Only used once at startup in app.js
     *  @todo move this out
     */
    this.prefixRegex = null
    /** @member {RegExp} - Regular expression to match 'subpathlinks' that need to be adjusted to the server's url
     *
     * Only used once at startup in app.js
     *  @todo move this out
     */
    this.subpathlinkRegex = null
  }

  /**
   * Replaces all "@prefix..." and "ex:sublinkpath..." lines in the supplied file
   *  with the correct links as these links depend on the hostname of the server
   *
   * @param {string} file - name of the file whose contents we will be replacing
   * @todo - replacing the sublinkpath links won't work if there are references to links not pointing to a 'plan'
   *       - pull this out of baseModel as it's only used once on startup but never in any of the derived classes
   */
  replacePrefixesInFile (file) {
    // Only construct the regexes once and only if this function is called as to not slow down other classes extending BaseModel
    if (this.prefixRegex === null) {
      // regex we will use to replace only the prefixes that depend on where it's hosted
      this.prefixRegex = new RegExp(
        Object.keys(this.replacementPrefixes)
          .map(key => '(@prefix|PREFIX) ' + key + ': <.+> *\\.?')
          .join('|'),
        'g')
    }
    if (this.subpathlinkRegex === null) {
      // TODO:
      this.subpathlinkRegex = new RegExp(/ex:subpathlink.+?<(.+)\/plan/g)
    }

    const planUrl = 'ex:subpathlink <' + this.url + '/plan'
    fs.readFile(file, 'utf8', (err, data) => {
      if (err) {
        return console.log(err)
      }
      var result = data.replace(this.prefixRegex, (match, p1, p2, p3, offset, string) => {
        // check which prefix was found, return the replacement (which depends on which prefix was found)
        for (const prefix in this.replacementPrefixes) {
          if (match.includes(prefix)) {
            return '@prefix ' + prefix + ': <' + this.replacementPrefixes[prefix] + '>.'
          }
        }
      })
      // only replaces the matched part so we only need to replace that part
      result = result.replace(this.subpathlinkRegex, (match, p1, p2, p3, offset, string) => {
        return planUrl
      })

      // write the changes back to the file
      fs.writeFile(file, result, 'utf8', (err) => {
        if (err) return console.log(err)
      })
    })
  }

  /**
  * gets all of the items from this model and converts to jsonLD
  * @param {Object} req - Request, currently unused
  * @returns {Object} - Json representation of data
  */
  async getAll (req) {
    // define the fileService locally for this function, otherwise it can't be called in asynchronous calls
    const fileService = this.fileService

    // let currentURL = req.protocol + "://" + req.get('host') + req.originalUrl;
    const currentURL = this.url
    // let baseURL = req.protocol + "://" + req.get('host');

    let quads = []
    // define additional quads we want to add
    const myQuad = quad(
      namedNode(currentURL),
      namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
      namedNode('http://www.w3.org/ns/ldp#Container')
    )
    quads.push(myQuad)

    // file associated with the model
    const file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName

    // read quads from file associated with this model
    const quadsFile = await fileService.fileToQuads(file)
    quads = quads.concat(quadsFile)

    // turn our quads into an N3 string with the prefixes added
    const quadstring = await fileService.quadsToString(quads, this.prefixes)
    if (Object.prototype.hasOwnProperty.call(req.headers, 'accept') && req.headers.accept === 'text/plain') {
      return quadstring
    } else {
      return fileService.stringToJSONLD(quadstring)
    }
  }
}

module.exports = BaseModel