Gottwik/Enduro

View on GitHub
libs/flat_db/flat.js

Summary

Maintainability
A
1 hr
Test Coverage
// * ———————————————————————————————————————————————————————— * //
// *     flatdb custom built for enduro.js
// *     handles cms data storage
// * ———————————————————————————————————————————————————————— * //
const flat = function () {}

// * vendor dependencies
const Promise = require('bluebird')
const fs = require('fs')
const require_from_string = require('require-from-string')
const decode = require('urldecode')
const stringify_object = require('stringify-object')
const path = require('path')
const _ = require('lodash')

// * enduro dependencies
const flat_helpers = require(enduro.enduro_path + '/libs/flat_db/flat_helpers')
const log_clusters = require(enduro.enduro_path + '/libs/log_clusters/log_clusters')
const brick_processors = require(enduro.enduro_path + '/libs/bricks/brick_processors')

// * ———————————————————————————————————————————————————————— * //
// *     Save cms file
// *    @param {String} filename - Path to file without extension, relative to /cms folder
// *    @param {Object} contents - Content to be saved
// *    @return {Promise} - Promise with no content. Resolve if saved successfully, reject otherwise
// * ———————————————————————————————————————————————————————— * //
flat.prototype.save = function (filename, contents) {
    const self = this

    return new Promise(function (resolve, reject) {
        // TODO: maybe the file could be backed up somewhere before overwriting
        contents = contents || {}

        // url decode filename
        filename = decode(filename)

        const fullpath_to_cms_file = self.get_full_path_to_flat_object(filename)

        const flatObj = require_from_string('module.exports = ' + JSON.stringify(contents))

        // add meta data (only if meta is enabled - currently juicebox)
        if (enduro.config.meta_context_enabled) {
            flat_helpers.add_meta_context(flatObj)
        }

        // formats js file so it can be edited by hand later
        const prettyString = '(' + stringify_object(flatObj, {indent: '    ', singleQuotes: true}) + ')'

        // save cms file
        flat_helpers.ensure_directory_existence(fullpath_to_cms_file)
            .then(() => {
                fs.writeFile(fullpath_to_cms_file, prettyString, function (err) {
                    if (err) {
                        reject()
                    }
                    resolve()
                })
            })
    })
}

// * ———————————————————————————————————————————————————————— * //
// *     Load cms file
// *    @param {string} filename - Path to file without extension, relative to /cms folder
// *    @param {bool} is_full_absolute_path - if true, filename is handled as absolute path
// *    @return {Promise} - Promise returning an object
// * ———————————————————————————————————————————————————————— * //
flat.prototype.load = function (filename, is_full_absolute_path) {
    const self = this

    return new Promise(function (resolve, reject) {

        // url decode filename
        filename = decode(filename)

        let fullpath_to_cms_file
        if (is_full_absolute_path) {
            fullpath_to_cms_file = filename
        } else {
            fullpath_to_cms_file = self.get_full_path_to_flat_object(filename)
        }

        // check if file exists. return empty object if not
        if (!flat_helpers.file_exists_sync(fullpath_to_cms_file)) {
            resolve({})
        } else {
            fs.readFile(fullpath_to_cms_file, function (err, raw_context_data) {
                if (err) {
                    console.log(err)
                    reject()
                }

                // strip whitespace
                raw_context_data = raw_context_data.toString().trim()

                // check if file is empty. return empty object if so
                if (raw_context_data == '') {
                    return resolve({})
                }

                // convert the string-based javascript into an object
                let context = {}
                try {
                    context = require_from_string('module.exports = ' + raw_context_data)
                } catch (e) {
                    console.log(e)
                    log_clusters.log('malformed_context_file', filename)
                }

                // brick_processors enable bricks (plugins) to manipulate the context just before
                // page rendering happens
                return brick_processors.process('cms_context_processor', context)
                    .then((proccessed_context) => {
                        resolve(proccessed_context)
                    })

            })
        }
    })
}

// * ———————————————————————————————————————————————————————— * //
// *     Get full path of a cms file
// *    @param {string} flat_object_path - path to file without extension, relative to flat root folder
// *    @return {string} - peturns full server path to specified file
// * ———————————————————————————————————————————————————————— * //
flat.prototype.get_full_path_to_flat_object = (filename) => {
    return path.join(enduro.project_path, 'cms', filename + '.js')
}

// * ———————————————————————————————————————————————————————— * //
// *     get cms flat_object_path from a full path
// *    @param {string} full_path - absolute, server-root-related path to the file
// *    @return {string} - returns file name relative to /cms folder
// * ———————————————————————————————————————————————————————— * //
flat.prototype.get_cms_filename_from_fullpath = (full_path) => {
    return full_path.match(/(?:\/|\\)cms(?:\/|\\)(.*)\..*/)[1]
}

// * ———————————————————————————————————————————————————————— * //
// *     checks if specified file exists
// *    @param {string} flat_object_path - path to file without extension, relative to flat root folder
// *    @return {boolean} - returns true if specified file exists
// * ———————————————————————————————————————————————————————— * //
flat.prototype.flat_object_exists = function (flat_object_path) {
    const self = this
    return flat_helpers.file_exists_sync(self.get_full_path_to_flat_object(flat_object_path))
}

// * ———————————————————————————————————————————————————————— * //
// *     updates flat object with new context
// *    merges array instead of replacing them
// *    @param {string} flat_object_path - path to file without extension, relative to flat root folder
// *    @param {object} context_to_update - object to be merged with current context
// *    @return {object} - returns merged object
// * ———————————————————————————————————————————————————————— * //
flat.prototype.upsert = function (flat_object_path, context_to_update) {
    const self = this

    return self.load(flat_object_path)
        .then((current_context) => {
            const merged_context = _.mergeWith(current_context, context_to_update, function (objValue, srcValue) {
                if (Array.isArray(objValue) && Array.isArray(srcValue)) {
                    return _.union(objValue, srcValue)
                }
            })
            return self.save(flat_object_path, merged_context)
        })
}

// * ———————————————————————————————————————————————————————— * //
// *     checks filename and returns if it defines a generator file or not
// *    @param {string} filename - path to file without extension, relative to flat folder
// *    @return {bool} - returns true if filename belongs to a generator
// * ———————————————————————————————————————————————————————— * //
flat.prototype.is_generator = function (flat_object_path) {
    return flat_object_path.split('/')[0] == 'generators'
}

// * ———————————————————————————————————————————————————————— * //
// *     returns a relative http url from flat_object_path
// *    for example: `generators/blog/blog_entry` will result in `blog/blog_entry`
// *    @param {string} flat_object_path - path to file without extension, relative to flat folder
// *    @return {string} - returns relative url that will serve this flat object
// * ———————————————————————————————————————————————————————— * //
flat.prototype.url_from_filename = function (flat_object_path) {
    if (flat_object_path == 'index') {
        return ''
    }

    if (this.is_generator(flat_object_path)) {
        const temp_path = flat_object_path.split('/').slice(1)
        return temp_path.join('/')
    }

    return flat_object_path
}

// * ———————————————————————————————————————————————————————— * //
// *     returns a relative path to the file actually being served when flat object is requested
// *    for example: `generators/blog/blog_entry` will result in `blog/blog_entry/index.html`
// *    @param {string} flat_object_path - path to file without extension, relative to flat folder
// *    @return {string} - returns relative url to the file
// * ———————————————————————————————————————————————————————— * //
flat.prototype.filepath_from_filename = function (flat_object_path) {
    if (flat_object_path == 'index') {
        return 'index'
    }

    if (this.is_generator(flat_object_path)) {
        let temp_path = flat_object_path.split('/').slice(1)
        temp_path.push('index')
        return path.join(...temp_path)
    }

    return path.join(flat_object_path, 'index')
}

// * ———————————————————————————————————————————————————————— * //
// *     returns true if flat_object is directly linked to a accessible, served http page
// *    currently global flat objects have no page associated with them
// *    @param {string} flat_object_path - path to file without extension, relative to flat folder
// *    @return {bool}
// * ———————————————————————————————————————————————————————— * //
flat.prototype.has_page_associated = function (flat_object_path) {
    const first_route_part = flat_object_path.split('/')[0].toLowerCase()

    // global flat objects does not have a page associated with them
    if (first_route_part == 'global') {
        return false
    }

    return true
}

// * ———————————————————————————————————————————————————————— * //
// *     makes a decision whether this content file is deletable
// *    currently only generator flat objects are deletable
// *    @param {string} filename - path to file without extension, relative to flat folder
// *    @return {bool} - returns true if particular content file is deletable
// * ———————————————————————————————————————————————————————— * //
flat.prototype.is_deletable = function (filename) {
    return this.is_generator(filename)
}

module.exports = new flat()