Tireo/pg-patch

View on GitHub
lib/file-scanner.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

const fsp = require('./fs-promise');
const q = require('q');

const common = require('./common');
const patchData = require('./patch-data');

const actions = common.action;
const patchFileTemplateMode = common.patchFileTemplateMode;

let FileScanner = function (config) {
    config = common.determineValue(config, {});

    this.actionUpdate = common.determineValue(config.actionUpdate, 'up');
    this.actionRollback = common.determineValue(config.actionRollback, 'rb');
    this.patchFileTemplate = common.determineValue(config.patchFileTemplate, '^patch-$VERSION-$ACTION(?:-$DESCRIPTION)?\\.(?:sql|js)$');
    this.patchDir = common.determineValue(config.patchDir, 'pg-patch');

    this.patchFileTemplateMode = patchFileTemplateMode.INVALID;
};

let createNumericalRegexGetFn = function (group) {
    return function (filename) {
        let match = this.createPatchFileRegex(group).exec(filename);
        /* istanbul ignore else */
        if (match) {
            match = match[1];
            /* istanbul ignore else */
            if (match) {
                return parseInt(match, 10);
            }
        }

        return common.determineValue(match, null);
    };
};

FileScanner.prototype = {
    msg: common.msgHandler,
    createEmptyPatchDataObject: function () {
        return new patchData();
    },
    getPatchFileProperties: function (fName) { //TODO: improve handling of invalid templateMode?
        let version = null, action = null, source = null, target = null, diff = null;

        if (this.patchFileTemplateMode === patchFileTemplateMode.AV) {
            version = this.getPatchFileVersion(fName);
            action = this.getPatchFileAction(fName);
        } else /* istanbul ignore else */ if (this.patchFileTemplateMode === patchFileTemplateMode.ST) {
            source = this.getPatchFileSource(fName);
            target = this.getPatchFileTarget(fName);

            diff = target - source;
            if (diff * diff === 1) {
                if (diff > 0) {
                    action = actions.UPDATE;
                    version = target;
                } else {
                    action = actions.ROLLBACK;
                    version = source;
                }
            }
        }

        return {
            action: action,
            version: version,
            description: common.determineValue(this.getPatchFileDescription(fName), null)
        };
    },
    scanDirectoryForPatchFiles: function (dirPath, rData) {
        dirPath = common.determineValue(dirPath, []);
        rData = common.determineValue(rData, this.createEmptyPatchDataObject());

        let currentSubDir = dirPath.join('/');
        let currentFullDir = `${this.patchDir ? this.patchDir : '.'}${currentSubDir ? '/' + currentSubDir : ''}`;

        this.msg('DIR_SCAN:START', currentFullDir);

        return fsp.readDir(currentFullDir).then(files => {
            let dirPromises = [];
            files.forEach(fName => {

                let pathPromise = fsp.lstat(`${currentFullDir}/${fName}`).then(lstat => {
                    if (lstat.isFile() && this.isPatchFile(fName)) {
                        this.msg('PATCH_FILE:FOUND', fName);

                        let patchFileProps = this.getPatchFileProperties(fName);

                        if (patchFileProps.action && patchFileProps.version) {
                            rData.addData({
                                dir: currentFullDir,
                                name: fName,
                                type: 'FILE',
                                description: patchFileProps.description,
                                action: patchFileProps.action,
                                version: patchFileProps.version
                            });
                        }
                        return true;
                    } else if (lstat.isDirectory()) {
                        return this.scanDirectoryForPatchFiles(dirPath.concat([fName]), rData);
                        //dirPromises.push(this.scanDirectoryForPatchFiles(dirPath.concat([fName]), rData));
                    }
                });

                dirPromises.push(pathPromise);
            });

            return q.all(dirPromises).then(function () {
                return rData;
            });
        });
    },
    validatePatchFileTemplate: function () {
        let template = this.patchFileTemplate;
        let found = {
            action: /\$ACTION/g.test(template),
            version: /\$VERSION/g.test(template),
            source: /\$SOURCE/g.test(template),
            target: /\$TARGET/g.test(template)
        };

        if ((found.action || found.version) && (found.source || found.target)) {
            return q.reject("Invalid patch file template: action/version cannot be mixed with source/target");
        } else if (found.action && found.version) {
            return q(this.patchFileTemplateMode = patchFileTemplateMode.AV);
        } else if (found.source && found.target) {
            return q(this.patchFileTemplateMode = patchFileTemplateMode.ST);
        } else {
            return q.reject("Invalid patch file template: you need to supply (action AND version) OR (source AND target)");
        }
    },
    createPatchFileRegexGroup: function (key, capturing) {
        let groupSubstitution = {
            version: '\\d+',
            action: `${this.actionUpdate}|${this.actionRollback}`,
            source: `\\d+`,
            target: `\\d+`,
            description: `[0-9a-zA-Z\-\_]+`
        }[key];
        return `(${capturing ? '' : '?:'}${groupSubstitution})`;
    },
    createPatchFileRegex: function (key) {
        let versionTemplatePart, actionTemplatePart, sourceTemplatePart, targetTemplatePart;

        let descriptionTemplatePart = this.createPatchFileRegexGroup('description', key === 'description');
        if (this.patchFileTemplateMode === patchFileTemplateMode.AV) {
            versionTemplatePart = this.createPatchFileRegexGroup('version', key === 'version');
            actionTemplatePart = this.createPatchFileRegexGroup('action', key === 'action');
        } else /* istanbul ignore else */ if (this.patchFileTemplateMode === patchFileTemplateMode.ST) {
            sourceTemplatePart = this.createPatchFileRegexGroup('source', key === 'source');
            targetTemplatePart = this.createPatchFileRegexGroup('target', key === 'target');
        }

        let regexString = this.patchFileTemplate.replace(/\$DESCRIPTION/g, descriptionTemplatePart);

        if (this.patchFileTemplateMode === patchFileTemplateMode.AV) {
            regexString = regexString
                .replace(/\$VERSION/g, versionTemplatePart)
                .replace(/\$ACTION/g, actionTemplatePart);
        } else /* istanbul ignore else */ if (this.patchFileTemplateMode === patchFileTemplateMode.ST) {
            regexString = regexString
                .replace(/\$SOURCE/g, sourceTemplatePart)
                .replace(/\$TARGET/g, targetTemplatePart);
        }

        return new RegExp(regexString, 'ig');
    },
    getPatchFileAction: function (filename) {
        let match = this.createPatchFileRegex('action').exec(filename);

        /* istanbul ignore else */
        if (!match) {
            return null;
        }
        match = match[1];

        if (match === this.actionUpdate) {
            return actions.UPDATE;
        } else /* istanbul ignore else */ if (match === this.actionRollback) {
            return actions.ROLLBACK;
        }
    },
    getPatchFileVersion: createNumericalRegexGetFn('version'),
    getPatchFileSource: createNumericalRegexGetFn('source'),
    getPatchFileTarget: createNumericalRegexGetFn('target'),
    getPatchFileDescription: function (filename) {
        let match = this.createPatchFileRegex('description').exec(filename);
        return match ? common.determineValue(match[1], null) : null;
    },
    isPatchFile: function (filename) {
        return this.createPatchFileRegex().test(filename);
    }
};

module.exports = FileScanner;