plugins/repository/aptrust.js

const { AppSetting } = require('../../core/app_setting');
const { Context } = require('../../core/context');
const { RemoteRepository } = require('../../core/remote_repository');
const path = require('path');
const { RepositoryBase } = require('./repository_base');
const request = require('request');
const Templates = require('../../ui/common/templates');
const { Util } = require('../../core/util');

/**
 * APTrustClient provides methods for querying an APTrust repository
 * that conform to the DART repository interface.
 *
 * This repository plugin provides two reports: one listing recently
 * ingested objects, and one listing recently updated tasks. In APTrust
 * these tasks (also called WorkItems) describe the status of pending
 * ingest requests as well as other types of requests. The ingest
 * WorkItems are of most interest to depositors, since they show a bag's
 * progress through ingest pipeline.
 *
 * @param {RemoteRepository} remoteRepository - The repository to connect to.
 */
class APTrustClient extends RepositoryBase {

    constructor(remoteRepository) {
        super(remoteRepository);
        let setting = AppSetting.find('name', 'Institution Domain');
        if (setting) {
            this.institutionDomain = setting.value;
        }
        /**
         * The Pharos URL to query for a list of recently ingested objects.
         *
         * @type {string}
         * @private
         */
        this.objectsUrl = `${this.repo.url}/member-api/v2/objects/?page=1&per_page=50&sort=date&state=A`
        /**
         * The Pharos URL to query for a list of currently active and recently
         * active WorkItems.
         *
         * @type {string}
         * @private
         */
        this.itemsUrl = `${this.repo.url}/member-api/v2/items/?page=1&per_page=50&sort=date`
        /**
         * This is the path to the Handlebars template used to format results
         * from the object query. (Recently ingested items.)
         *
         * @type {string}
         * @private
         */
        this.objectsTemplate = Templates.compile(path.join(__dirname, 'aptrust', 'objects.html'));
        /**
         * This is the path to the Handlebars template used to format results
         * from the WorkItems query.
         *
         * @type {string}
         * @private
         */
        this.itemsTemplate = Templates.compile(path.join(__dirname, 'aptrust', 'work_items.html'));
    }

    /**
     * Returns a {@link PluginDefinition} object describing this plugin.
     *
     * @returns {PluginDefinition}
     */
    static description() {
        return {
            id: 'c5a6b7db-5a5f-4ca5-a8f8-31b2e60c84bd',
            name: 'APTrustClient',
            description: 'APTrust repository client. This allows DART to talk to the APTrust demo and/or production repository.',
            version: '0.1',
            readsFormats: [],
            writesFormats: [],
            implementsProtocols: [],
            talksToRepository: ['aptrust'],
            setsUp: []
        };
    }

    /**
     * This returns a list of objects describing what reports this
     * module provides. The DART dashboard queries this list to see
     * what method calls this plugin makes available. Each object in
     * the list this function returns has three properties.
     *
     * title - This is the title of the report. The dashboard will
     * display this title as is at the top of the report.
     *
     * description - A description of the report.
     *
     * method - A function to call to get the contents of the report.
     * The function takes no parameters and should a promis that
     * ultimately returns HTML. The dashboard will display the HTML
     * when the promise is resolved.
     *
     * @type {Array<object>}
     */
    provides() {
        let aptrust = this;
        return [
            {
                title: `Ingested Objects (${this.repo.name})`,
                description: 'Recently ingested objects.',
                method: () => { return aptrust.recentIngests() }
            },
            {
                title: `Work Items  (${this.repo.name})`,
                description: 'A list of tasks.',
                method: () => { return aptrust.recentWorkItems() }
            }
        ];
    }


    /**
     * This fetches a list of recently ingested objects from Pharos,
     * which is APTrust's REST API. After retrieving the data, this
     * function formats the list into an HTML table.
     *
     * This function returns a promise. The promise resolves to the
     * HTML, which DART will display in its Dashboard.
     *
     * @returns {Promise}
     */
    recentIngests() {
        let aptrust = this;
        return this._doRequest(this.objectsUrl, (data) => {
            data.results.forEach((item) => {
                item.url = `${aptrust.repo.url}/objects/${encodeURIComponent(item.identifier)}`;
                item.escapedTitle = item.title.replace(/"/g, "'");
                item.displayDate = item.updated_at.split('T')[0];
            });
            return aptrust.objectsTemplate({ objects: data.results })
        });
    }

    /**
     * This returns a list of Pharos Work Items, which describe pending
     * ingest requests and other tasks. Items uploaded for ingest that have
     * not yet been processed will be in this list.
     *
     * This function returns a promise. The promise resolves to the
     * HTML, which DART will display in its Dashboard.
     *
     * @returns {Promise}
     */
    recentWorkItems() {
        let aptrust = this;
        return this._doRequest(this.itemsUrl, (data) => {
            data.results.forEach((item) => {
                item.url = `${aptrust.repo.url}/items/${item.id}`;
                item.escaped_note = item.note.replace(/"/g, "'");
            });
            return aptrust.itemsTemplate({ items: data.results })
        });
    }

    /**
     * This creates an HTTP(S) request and returns a promise.
     *
     * @param {string} url - The URL to fetch. For this module, all requests
     * will be GET requests.
     *
     * @param {formatter} function - The function to format the data, if it
     * is successfully retrieved. The formatter function should take a single
     * paramater, an object, which is constructed from the parsed JSON data
     * in the response body fetched from url.
     *
     * @returns {Promise}
     */
    _doRequest(url, formatter) {
        let aptrust = this;
        return new Promise(function(resolve, reject) {
            aptrust._request(url, function(data) {
                resolve(formatter(data));
            }, function(error) {
                reject(error);
            });
        });
    }


    /**
     * This returns true if the RemoteRepository object has enough info to
     * attempt a connection. For APTrust, we require url, userId, and apiToken.
     * Other repositories may require different data in their RemoteRepository
     * object.
     *
     * @returns {boolean}
     */
    hasRequiredConnectionInfo() {
        return !Util.isEmpty(this.repo.url) && !Util.isEmpty(this.repo.userId) && !Util.isEmpty(this.repo.apiToken);
    }

    /**
     * Returns the HTTP request headers our client will need to send when
     * connecting to Pharos.
     *
     * @returns {object}
     */
    _getHeaders() {
        return {
            'User-Agent': `DART ${Context.dartReleaseNumber()} / Node.js request`,
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'X-Pharos-API-User': this.repo.getValue('userId'),
            'X-Pharos-API-Key': this.repo.getValue('apiToken')
        }
    }

    /**
     * This sends a GET request to url, calling the onSuccess callback
     * if it gets a 200 response, and the onError callback for all other
     * responses.
     *
     * Other repository plugins may need to support PUT, POST, and HEAD
     * requests, and may need more robust handling for different response
     * status codes.
     *
     * For APTrust, we're hitting only two endpoints, using only GET,
     * and we know that any non-200 response means something is wrong.
     *
     * Because we're using the request library from
     * https://github.com/request/request, the onSuccess and onError
     * functions take params (error, response, body), which are an
     * Error object, a Response object, and the body of the HTTP
     * response (which should be JSON).
     *
     * @param {string} url - The URL to get.
     *
     * @param {function} onSuccess - A function to handle successful
     * responses. Takes params (error, response, body).
     *
     * @param {function} onError - A function to handle errors.
     * Takes params (error, response, body).
     */
    _request(url, onSuccess, onError) {
        let opts = {
            url: url,
            method: 'GET',
            headers: this._getHeaders()
        }
        Context.logger.info(`Requesting ${url}`);
        request(opts, (err, res, body) => {
            if (err) {
                Context.logger.error(`Error from ${url}:`);
                Context.logger.error(err);
                onError(err, res, body);
            }
            if (res.statusCode == 200) {
                onSuccess(JSON.parse(body));
            } else {
                let errMsg = res.headers.status;
                try {
                    let data = JSON.parse(body);
                    errMsg += ` - ${data.error}`
                } catch (ex) {}
                Context.logger.error(`Unexpected response from ${url}: ${errMsg}`);
                onError(errMsg);
            }
        });
    }

}

module.exports = APTrustClient;