core/workflow.js

const { BagItProfile } = require('../bagit/bagit_profile');
const { Context } = require('./context');
const { PersistentObject } = require('./persistent_object');
const { PluginManager } = require('../plugins/plugin_manager');
const { StorageService } = require('./storage_service');
const { Util } = require('./util');

/**
 * A workflow defines the set up steps that compose a job. These
 * steps may include packaging, validation, and uploading
 * to one or more storage services.
 *
 * If the packaging format is BagIt, the Workflow should include
 * a BagIt profile as well.
 *
 * A workflow allows users to define a repeatable set of actions,
 * and to run jobs based on those pre-defined actions.
 *
 * For info on how to create a {@link Job} from a {@link Workflow},
 * see {@link Workflow.fromJob}.
 *
 * For info on how a {@link Workflow} becomes a {@link Job},
 * see {@link JobParams#toJob}.
 *
 * @param {object} opts
 *
 * @param {string} opts.name - The name to assign to this Workflow.
 *
 * @param {string} [opts.description] - An optional description of
 * this workflow.
 *
 * @param {string} [opts.packageFormat] - The package format, if this
 * workflow includes a packaging step. Options include "None", "BagIt",
 * or the UUID of any format/write plugin.
 *
 * @param {string} [opts.packagePluginId] - The id of the plugin that
 * will write the package to disk, if there is a packaging step.
 *
 * @param {string} [opts.bagItProfileId] - The id of the BagIt profile
 * to be used in the packaging step, if there is a packaging step. If
 * this is specified, the package will be a bag that conforms to this
 * profile.
 *
 * @param {Array<string>} [opts.storageServiceIds] - A list of
 * {@link StorageService} ids. The workflow will upload files to these
 * storage services.
 *
 */
class Workflow extends PersistentObject {
    constructor(opts = {}) {
        opts.required = ["name"];
        super(opts);
        /**
         * The name of this workflow.
         *
         * @type {string}
         */
        this.name = opts.name;
        /**
         * A description of this workflow.
         *
         * @type {string}
         */
        this.description = opts.description;
        /**
         * The package format, if this workflow includes a
         * packaging step. Options include "None", "BagIt",
         * or the UUID of any format/write plugin.
         *
         * @type {string}
         */
        this.packageFormat = opts.packageFormat;
        /**
         * The id of the plugin that will write the
         * package to disk, if there is a packaging step.
         *
         * @type {string}
         */
        this.packagePluginId = opts.packagePluginId;
        /**
         * The id of the BagIt profile to be used in the
         * packaging step, if there is a packaging step. If
         * this is specified, the package will be a bag that
         * conforms to this profile.
         *
         * @type {string}
         */
        this.bagItProfileId = opts.bagItProfileId;
        /**
         * A list of {@link StorageService} ids. The workflow
         * will upload files to these storage services.
         *
         * @type {string}
         */
        this.storageServiceIds = opts.storageServiceIds || [];
    }

    /**
     * This returns the plugin class that provides the packaging
     * for this workflow. Note that this returns a class and not
     * an instance of an object.
     *
     * @returns {function}
     */
    packagePlugin() {
        return PluginManager.findById(this.packagePluginId);
    }

    /**
     * Returns the name of the plugin that will be used to package
     * data in this workflow.
     *
     * @returns {string}
     */
    packagePluginName() {
        let name = null;
        let plugin = this.packagePlugin();
        if (plugin) {
            name = plugin.description().name;
        }
        return name;
    }

    /**
     * This returns the BagItProfile the workflow will use to generate
     * bags.
     *
     * @returns {BagItProfile}
     */
    bagItProfile() {
        return BagItProfile.find(this.bagItProfileId);
    }

    /**
     * Returns the name of the BagItProfile that will be used to
     * create bags in this workflow (if there is one).
     *
     * @returns {string}
     */
    bagItProfileName() {
        let name = null;
        let profile = this.bagItProfile();
        if (profile) {
            name = profile.name;
        }
        return name;
    }

    /**
     * Returns an array of {@link StorageService} objects. The workflow
     * will upload files to each of these services.
     *
     * @returns {Array<StorageService>}
     */
    storageServices() {
        let services = [];
        for (let id of this.storageServiceIds) {
            services.push(StorageService.find(id));
        }
        return services;
    }

    /**
     * Returns the names of the {@link StorageService}s to which this
     * workflow will upload files.
     *
     * @returns {Array<string>}
     */
    storageServiceNames() {
        let names = [];
        for(let service of this.storageServices()) {
            names.push(service.name);
        }
        return names;
    }

    /**
     * Checks to see if this workflow has a unique non-empty name.
     * Returns true if so, false otherwise.
     *
     * @returns {boolean}
     */
    validate() {
        super.validate();
        let wf = Workflow.firstMatching('name', this.name);
        if (wf && wf.id != this.id) {
            this.errors["name"] = Context.y18n.__(
                "%s must be unique", "name"
            );
        }
        let duplicate = this.findDuplicate();
        if (duplicate) {
            this.errors["storageServiceIds"] = Context.y18n.__(
                "This workflow duplicates '%s'. Either delete this workflow, or change its package format, bagit profile, or upload targets.", duplicate.name
            );
        }
        return Object.keys(this.errors).length == 0;
    }

    /**
     * Returns the first Workflow that has the same packaging format and
     * upload targets as this Workflow. Returns null if there is no match.
     *
     * @returns {Workflow}
     */
    findDuplicate() {
        let self = this;
        let opts = { orderBy: 'name', sortDirection: 'asc', limit: 1, offset: 0 };
        let duplicate = null;
        let filter = function(wf) {
            return (wf.id != self.id &&
                    wf.packageFormat == self.packageFormat &&
                    wf.packagePluginId == self.packagePluginId &&
                    wf.bagItProfileId == self.bagItProfileId &&
                    Util.arrayContentsMatch(wf.storageServiceIds,
                                            self.storageServiceIds,
                                            false)
                   );
        }
        let matches = Workflow.list(filter, opts);
        if (matches && matches.length > 0) {
            duplicate = matches[0];
        }
        return duplicate;
    }

    /**
     * Returns a JSON version of this workflow suitable for use in
     * dart-runner. Note the exported JSON contains full representations
     * of the BagIt profile and storage services, instead of just containing
     * their IDs. This makes the JSON workflow self-contained and able to
     * be run on any dart-runner installation.
     *
     * @returns {string}
     */
    exportJson() {
        let workflow = this;
        let data = {
            id: workflow.id,
            name: workflow.name,
            description: workflow.description,
            packageFormat: workflow.packageFormat,
            packagePluginId: workflow.packagePluginId,
            packagePluginName: workflow.packagePluginName(),
            bagItProfile: workflow.bagItProfile(),
            storageServices: workflow.storageServices()
        }
        return JSON.stringify(data, null, 2)
    }

    /**
     * Given a {@link Job} object, this returns a Workflow that has
     * the same pattern as the Job.
     *
     * For info on how a {@link Workflow} becomes a {@link Job},
     * see {@link JobParams#toJob}.
     *
     * @param {Job}
     *
     * @returns {Workflow}
     */
    static fromJob(job) {
        let profileId = job.bagItProfile ? job.bagItProfile.id : null;
        let packageFormat = job.packageOp ? job.packageOp.packageFormat : 'None';
        let pluginId = job.packageOp ? job.packageOp.pluginId : null;
        let ssids = job.uploadOps ? job.uploadOps.map(op => op.storageServiceId) : [];
        return new Workflow({
            name: '',
            description: '',
            packageFormat: packageFormat,
            packagePluginId: pluginId,
            bagItProfileId: profileId,
            storageServiceIds: ssids
        });
    }

    /**
     * This converts a generic object into an Workflow
     * object. this is useful when loading objects from JSON.
     *
     * @param {object} data - An object you want to convert to
     * a Workflow.
     *
     * @returns {Workflow}
     *
     */
    static inflateFrom(data) {
        let setting = new Workflow();
        Object.assign(setting, data);
        return setting;
    }

}

module.exports.Workflow = Workflow;