Home Reference Source

src/client.js

/** * Imports ***/
import ChrisInstance from './chrisinstance';
import Collection from './cj';
import Request from './request';
import { FeedList, PublicFeedList, Feed } from './feed';
import { PluginAdminList, PluginAdmin } from './admin';
import { ComputeResourceList, ComputeResource } from './computeresource';
import { PluginMetaList, PluginMeta } from './pluginmeta';
import { PluginList, Plugin } from './plugin';
import {
  AllPluginInstanceList,
  PluginInstanceList,
  PluginInstance,
  PluginInstanceSplitList,
  PluginInstanceSplit,
} from './plugininstance';
import { AllWorkflowList, WorkflowList, Workflow } from './workflow';
import { PipelineList, Pipeline, PipelineSourceFileList, PipelineSourceFile } from './pipeline';
import { TagList, Tag, Tagging } from './tag';
import { UserFileList, UserFile } from './userfile';
import { PACSFileList, PACSSeriesList, PACSFile , PACSSeries } from './pacsfile';
import { FileBrowserFolderList, FileBrowserFolder } from './filebrowser';
import { DownloadTokenList, DownloadToken } from './downloadtoken';
import { GroupList, Group } from './group';
import User from './user';

/**
 * API client object.
 */
export default class Client {
  /**
   * Constructor
   *
   * @param {string} url - url of the ChRIS service
   * @param {Object} [auth=null] - authentication object
   * @param {string} [auth.token] - authentication token
   */
  constructor(url, auth = null) {
    /** @type {string} */
    this.url = url;

    /** @type {Object} */
    this.auth = auth;

    /* Urls of the high level API resources */
    this.feedsUrl = this.url;
    this.publicFeedsUrl = '';
    this.chrisInstanceUrl = '';
    this.computeResourcesUrl = '';
    this.pluginMetasUrl = '';
    this.pluginsUrl = '';
    this.pluginInstancesUrl = '';
    this.pipelinesUrl = '';
    this.workflowsUrl = '';
    this.tagsUrl = '';
    this.pipelineSourceFilesUrl = '';
    this.userFilesUrl = '';
    this.pacsFilesUrl = '';
    this.pacsSeriesUrl = '';
    this.fileBrowserUrl = '';
    this.downloadTokensUrl = '';
    this.groupsUrl = '';
    this.userUrl = '';
    this.adminUrl = '';
  }

  /**
   * Set the urls of the high level API resources.
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise} - JS Promise
   */
  setUrls(timeout = 30000) {
    return this.getFeeds(null, timeout);
  }

  /**
   * Get the ChRIS instance resource object.
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<ChrisInstance>} - JS Promise, resolves to a ``ChrisInstance`` object
   */
  getChrisInstance(timeout = 30000) {
    return this._fetchRes('chrisInstanceUrl', ChrisInstance, null, timeout);
  }

  /**
   * Get a paginated list of currently authenticated user's feeds
   * from the REST API given query search parameters. If no search parameters
   * then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match feed id exactly with this number
   * @param {number} [searchParams.min_id] - match feed id gte this number
   * @param {number} [searchParams.max_id] - match feed id lte this number
   * @param {string} [searchParams.name] - match feed name containing this string
   * @param {string} [searchParams.name_exact] - match feed name exactly with this string
   * @param {string} [searchParams.name_startswith] - match feed name starting with this string
   * @param {string} [searchParams.files_fname_icontains] - match the feeds that have files containing
   * all the substrings from the queried string (which in turn represents a white-space-separated list
   * of query strings) case insensitive anywhere in their fname.
   * @param {number} [searchParams.min_creation_date] - match feed creation date gte this date
   * @param {number} [searchParams.max_creation_date] - match feed creation date lte this date
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<FeedList>} - JS Promise, resolves to a ``FeedList`` object
   */
  getFeeds(searchParams = null, timeout = 30000) {
    const feedList = new FeedList(this.feedsUrl, this.auth);

    return feedList.get(searchParams, timeout).then((feedList) => {
      const coll = feedList.collection;
      const getUrl = Collection.getLinkRelationUrls;

      this.chrisInstanceUrl = this.chrisInstanceUrl || getUrl(coll, 'chrisinstance')[0];
      this.publicFeedsUrl = this.publicFeedsUrl || getUrl(coll, 'public_feeds')[0];
      this.computeResourcesUrl = this.computeResourcesUrl || getUrl(coll, 'compute_resources')[0];
      this.pluginMetasUrl = this.pluginMetasUrl || getUrl(coll, 'plugin_metas')[0];
      this.pluginsUrl = this.pluginsUrl || getUrl(coll, 'plugins')[0];
      this.pluginInstancesUrl = this.pluginInstancesUrl || getUrl(coll, 'plugin_instances')[0];
      this.pipelinesUrl = this.pipelinesUrl || getUrl(coll, 'pipelines')[0];
      this.workflowsUrl = this.workflowsUrl || getUrl(coll, 'workflows')[0];
      this.tagsUrl = this.tagsUrl || getUrl(coll, 'tags')[0];
      this.pipelineSourceFilesUrl = this.pipelineSourceFilesUrl || getUrl(coll, 'pipelinesourcefiles')[0];
      this.userFilesUrl = this.userFilesUrl || getUrl(coll, 'userfiles')[0];
      this.pacsFilesUrl = this.pacsFilesUrl || getUrl(coll, 'pacsfiles')[0];
      this.pacsSeriesUrl = this.pacsSeriesUrl || getUrl(coll, 'pacsseries')[0];
      this.fileBrowserUrl = this.fileBrowserUrl || getUrl(coll, 'filebrowser')[0];

      if (!this.downloadTokensUrl) {
        this.downloadTokensUrl = getUrl(coll, 'download_tokens');
        this.downloadTokensUrl = this.downloadTokensUrl.length ? this.downloadTokensUrl[0] : '';
      }
      if (!this.groupsUrl) {
        this.groupsUrl = getUrl(coll, 'groups');
        this.groupsUrl = this.groupsUrl.length ? this.groupsUrl[0] : '';
      }
      if (!this.userUrl) {
        this.userUrl = getUrl(coll, 'user');
        this.userUrl = this.userUrl.length ? this.userUrl[0] : '';
      }
      if (!this.adminUrl) {
        this.adminUrl = getUrl(coll, 'admin');
        this.adminUrl = this.adminUrl.length ? this.adminUrl[0] : '';
      }

      return feedList;
    });
  }

  /**
   * Get a paginated list of public feeds from the REST API given query search parameters. 
   * If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match feed id exactly with this number
   * @param {number} [searchParams.min_id] - match feed id gte this number
   * @param {number} [searchParams.max_id] - match feed id lte this number
   * @param {string} [searchParams.name] - match feed name containing this string
   * @param {string} [searchParams.name_exact] - match feed name exactly with this string
   * @param {string} [searchParams.name_startswith] - match feed name starting with this string
   * @param {string} [searchParams.files_fname_icontains] - match the feeds that have files containing
   * all the substrings from the queried string (which in turn represents a white-space-separated list
   * of query strings) case insensitive anywhere in their fname.
   * @param {number} [searchParams.min_creation_date] - match feed creation date gte this date
   * @param {number} [searchParams.max_creation_date] - match feed creation date lte this date
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PublicFeedList>} - JS Promise, resolves to a ``PublicFeedList`` object
   */
  getPublicFeeds(searchParams = null, timeout = 30000) {
    return this._fetchRes('publicFeedsUrl', PublicFeedList, searchParams, timeout);
  }

  /**
   * Get a feed resource object given its id.
   *
   * @param {number} id - feed id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Feed|null>} - JS Promise, resolves to a ``Feed`` object or ``null``
   */
  getFeed(id, timeout = 30000) {
    return this.getFeeds({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Get a paginated list of compute resources from the REST API given query
   * search parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match file id exactly with this number
   * @param {string} [searchParams.name] - match compute resource's name containing this string
   * @param {string} [searchParams.name_exact] - match compute resource's name exactly with this string
   * @param {string} [searchParams.description] - match compute resource's description containing this string
   * @param {string} [searchParams.plugin_id] - match plugin id exactly with this string for all the
   * compute resources associated with the plugin
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<ComputeResourceList>} - JS Promise, resolves to a ``ComputeResourceList`` object
   */
  getComputeResources(searchParams = null, timeout = 30000) {
    return this._fetchRes('computeResourcesUrl', ComputeResourceList, searchParams, timeout);
  }

  /**
   * Get a compute resource object given its id.
   *
   * @param {number} id - compute resource id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<ComputeResource|null>} - JS Promise, resolves to a ``ComputeResource`` object or ``null``
   */
  getComputeResource(id, timeout = 30000) {
    return this.getComputeResources({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Get a paginated list of plugin metas from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match plugin meta id exactly with this number
   * @param {string} [searchParams.name] - match plugin meta name containing this string
   * @param {string} [searchParams.name_exact] - match plugin meta name exactly with this string
   * @param {string} [searchParams.title] - match plugin meta title containing this string
   * @param {string} [searchParams.category] - match plugin meta category exactly with this string
   * @param {string} [searchParams.type] - match plugin meta type exactly with this string
   * @param {string} [searchParams.authors] - match plugin meta authors containing this string
   * @param {number} [searchParams.min_creation_date] - match plugin meta creation date gte this date
   * @param {number} [searchParams.max_creation_date] - match plugin meta creation date lte this date
   * @param {string} [searchParams.name_title_category] - match plugin meta name, title or
   * category containing this string
   * @param {string} [searchParams.name_authors_category] - match plugin meta name, authors or
   * category containing this string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginMetaList>} - JS Promise, resolves to a ``PluginMetaList`` object
   */
  getPluginMetas(searchParams = null, timeout = 30000) {
    return this._fetchRes('pluginMetasUrl', PluginMetaList, searchParams, timeout);
  }

  /**
   * Get a plugin meta resource object given its id.
   *
   * @param {number} id - plugin meta id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginMeta|null>} - JS Promise, resolves to a ``PluginMeta`` object or ``null``
   */
  getPluginMeta(id, timeout = 30000) {
    return this.getPluginMetas({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Get a paginated list of plugins from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match plugin id exactly with this number
   * @param {string} [searchParams.name] - match plugin name containing this string
   * @param {string} [searchParams.name_exact] - match plugin name exactly with this string
   * @param {string} [searchParams.version] - match plugin version exactly with this string
   * @param {string} [searchParams.dock_image] - match plugin docker image exactly with this string
   * @param {string} [searchParams.type] - match plugin type exactly with this string
   * @param {string} [searchParams.category] - match plugin category containing this string
   * @param {string} [searchParams.title] - match plugin title containing this string
   * @param {string} [searchParams.description] - match plugin description containing this string
   * @param {string} [searchParams.min_creation_date] - match plugin creation date gte this date
   * @param {string} [searchParams.max_creation_date] - match plugin creation date lte this date
   * @param {string} [searchParams.name_title_category] - match plugin name, title or
   * category containing this string
   * @param {number} [searchParams.compute_resource_id] - match plugin's compute resource id exactly
   * with this number
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginList>} - JS Promise, resolves to a ``PluginList`` object
   */
  getPlugins(searchParams = null, timeout = 30000) {
    return this._fetchRes('pluginsUrl', PluginList, searchParams, timeout);
  }

  /**
   * Get a plugin resource object given its id.
   *
   * @param {number} id - plugin id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Plugin|null>} - JS Promise, resolves to a ``Plugin`` object or ``null``
   */
  getPlugin(id, timeout = 30000) {
    return this.getPlugins({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Upload a plugin representation file and create a new plugin admin resource through the REST API.
   *
   * @param {Object} data - request JSON data object
   * @param {string} data.compute_names - string representing a comma-separated
   * list of names of already registered compute resources
   * @param {Object} pluginFileObj - custom file object
   * @param {Object} pluginFileObj.fname - plugin's file blob
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginAdmin>} - JS Promise, resolves to a ``PluginAdmin`` object
   */
  adminUploadPlugin(data, pluginFileObj, timeout = 30000) {
    const createRes = () => {
      const res = new PluginAdminList(this.adminUrl, this.auth);
      return res.post(data, pluginFileObj, timeout).then(res => res.getItems()[0]);
    };
    return this.adminUrl ? createRes() : this.setUrls().then(() => createRes());
  }

  /**
   * Get a paginated list of plugin instances from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match plugin instance id exactly with this number
   * @param {number} [searchParams.root_id] - match root plugin instance's id exactly with this number
   * @param {number} [searchParams.previous_id] - match previous plugin instance's id exactly with this number
   * @param {string} [searchParams.title] - match plugin instance title containing this string
   * @param {string} [searchParams.status] - match plugin instance execution status exactly with this string
   * @param {string} [searchParams.owner_username] - match plugin instances's owner username exactly with this string
   * @param {number} [searchParams.feed_id] - match associated feed's id exactly with this number
   * @param {number} [searchParams.workflow_id] - match associated workflows's id exactly with this number
   * @param {number} [searchParams.plugin_id] - match associated plugin's id exactly with this number
   * @param {number} [searchParams.plugin_name] - match associated plugin's name containing this string
   * @param {number} [searchParams.plugin_name_exact] - match associated plugin's name exact with this string
   * @param {number} [searchParams.plugin_version] - match associated plugin's verion exactly with this string
   * @param {string} [searchParams.min_start_date] - match plugin instance's start date gte this date
   * @param {string} [searchParams.max_start_date] - match plugin instance's start date lte this date
   * @param {string} [searchParams.min_end_date] - match plugin instance's end date gte this date
   * @param {string} [searchParams.max_end_date] - match plugin instance's end date lte this date
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<AllPluginInstanceList>} - JS Promise, resolves to ``AllPluginInstanceList`` object
   */
  getPluginInstances(searchParams = null, timeout = 30000) {
    return this._fetchRes('pluginInstancesUrl', AllPluginInstanceList, searchParams, timeout);
  }

  /**
   * Get a plugin instance resource object given its id.
   *
   * @param {number} id - plugin instance id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginInstance|null>} - JS Promise, resolves to a ``PluginInstance`` object or ``null``
   */
  getPluginInstance(id, timeout = 30000) {
    return this.getPluginInstances({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Create a new plugin instance resource through the REST API.
   *
   * @param {number} pluginId - plugin id
   * @param {Object} data - request data object which is plugin-specific
   * @param {number} data.previous_id=null - id of the previous plugin instance
   * @param {string} [data.title] - title
   * @param {string} [data.compute_resource_name] - remote compute resource name
   * @param {string} [data.cpu_limit] - cpu limit
   * @param {string} [data.memory_limit] - memory limit
   * @param {string} [data.number_of_workers] - number of workers
   * @param {string} [data.gpu_limit] - gpu limit
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginInstance>} - JS Promise, resolves to a ``PluginInstance`` object
   */
  createPluginInstance(pluginId, data, timeout = 30000) {
    return this.getPlugin(pluginId, timeout)
      .then((plg) => {
        const instancesUrl = Collection.getLinkRelationUrls(plg.collection.items[0], 'instances');
        const plgInstList = new PluginInstanceList(instancesUrl[0], this.auth);
        return plgInstList.post(data, timeout);
      })
      .then(plgInstList => plgInstList.getItems()[0]);
  }

  /**
   * Create a new plugin instance split resource through the REST API.
   *
   * @param {number} pluginInstanceId - plugin instance id
   * @param {string} [filter=''] - comma-separated list of regular expressions
   * @param {string} [cr_name=''] - remote compute resource name
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PluginInstanceSplit>} - JS Promise, resolves to a ``PluginInstanceSplit`` object
   */
  createPluginInstanceSplit(pluginInstanceId, filter = '', cr_name = '', timeout = 30000) {
    return this.getPluginInstance(pluginInstanceId, timeout)
      .then((plgInst) => {
        const splitsUrl = Collection.getLinkRelationUrls(plgInst.collection.items[0], 'splits');
        const plgInstSplitList = new PluginInstanceSplitList(splitsUrl[0], this.auth);
        let data = { filter: filter };
        if (cr_name) {
          data = { filter: filter, compute_resource_name: cr_name };
        }
        return plgInstSplitList.post(data, timeout);
      })
      .then(plgInstSplitList => plgInstSplitList.getItems()[0]);
  }

  /**
   * Get a paginated list of pipelines from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match plugin id exactly with this number
   * @param {string} [searchParams.name] - match plugin name containing this string
   * @param {string} [searchParams.owner_username] - match pipeline's owner username exactly with this string
   * @param {string} [searchParams.category] - match plugin category containing this string
   * @param {string} [searchParams.description] - match plugin description containing this string
   * @param {string} [searchParams.authors] - match plugin authors containing this string
   * @param {string} [searchParams.min_creation_date] - match plugin creation date gte this date
   * @param {string} [searchParams.max_creation_date] - match plugin creation date lte this date
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PipelineList>} - JS Promise, resolves to a ``PipelineList`` object
   */
  getPipelines(searchParams = null, timeout = 30000) {
    return this._fetchRes('pipelinesUrl', PipelineList, searchParams, timeout);
  }

  /**
   * Get a pipeline resource object given its id.
   *
   * @param {number} id - pipeline id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Pipeline|null>} - JS Promise, resolves to a ``Pipeline`` object or ``null``
   */
  getPipeline(id, timeout = 30000) {
    return this.getPipelines({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Create a new pipeline resource through the REST API.
   *
   * @param {Object} data - request data object
   * @param {string} data.name - pipeline name
   * @param {string} [data.authors] - pipeline authors
   * @param {string} [data.category] - pipeline category
   * @param {string} [data.description] - pipeline description
   * @param {boolean} [data.locked=true] - pipeline status
   * @param {string} [data.plugin_tree] - JSON string containing a plugin tree list
   * @param {number} [data.plugin_inst_id] - plugin instance id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Pipeline>} - JS Promise, resolves to a ``Pipeline`` object
   */
  createPipeline(data, timeout = 30000) {
    const createRes = () => {
      const res = new PipelineList(this.pipelinesUrl, this.auth);
      return res.post(data, timeout).then(res => res.getItems()[0]);
    };
    return this.pipelinesUrl ? createRes() : this.setUrls().then(() => createRes());
  }

  /**
   * Get a paginated list of workflows from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match workflow id exactly with this number
   * @param {string} [searchParams.title] - match workflow title containing this string
   * @param {string} [searchParams.owner_username] - match workflow's owner username exactly with this string
   * @param {string} [searchParams.pipeline_name] - match associated pipeline name containing this string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<AllWorkflowList>} - JS Promise, resolves to ``AllWorkflowList`` object
   */
  getWorkflows(searchParams = null, timeout = 30000) {
    return this._fetchRes('workflowsUrl', AllWorkflowList, searchParams, timeout);
  }

  /**
   * Get a workflow resource object given its id.
   *
   * @param {number} id - workflow id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Workflow>|null} - JS Promise, resolves to a ``Workflow`` object or ``null``
   */
  getWorkflow(id, timeout = 30000) {
    return this.getWorkflows({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Helper method to create the ``nodes_info`` field required by ``createWorkflow`` method's
   * ``data`` argument to create a workflow from a pipeline's default parameters data array
   * tipically returned by ``Pipeline.getDefaultParameters().data``.
   *
   * @param {Object[]} pipelineDefaultParameters - array of objects with the default parameters
   * as returned by ``Pipeline.getDefaultParameters().data``
   * @param {boolean} [includeAllDefaults=false] - if set to `true`` then non-null parameters are also included
   * in the result
   *
   * @return {Object[]} - array of workflow node objects
   */
   computeWorkflowNodesInfo(pipelineDefaultParameters, includeAllDefaults=false) {
     const pipings = {};

     for (let defaultParam of pipelineDefaultParameters) {
       let pipingId = defaultParam.plugin_piping_id;

       if ( !(pipingId in pipings) ) {
         pipings[pipingId] = {
           piping_id: pipingId,
           previous_piping_id: defaultParam.previous_plugin_piping_id,
           compute_resource_name: 'host',
           title: defaultParam.plugin_piping_title,
           plugin_parameter_defaults: []
         };
       }

       if ( includeAllDefaults || defaultParam.value === null ) {
         pipings[pipingId].plugin_parameter_defaults.push({
           name: defaultParam.param_name,
           default: defaultParam.value
         });
       }
     }

    const nodesInfo = [];
    for (let pipingId in pipings) {
      if ( pipings[pipingId].plugin_parameter_defaults.length === 0 ) {
        delete pipings[pipingId].plugin_parameter_defaults;
      }
      nodesInfo.push(pipings[pipingId]);
    }
    return nodesInfo;
  }

  /**
   * Create a new workflow resource through the REST API.
   *
   * @param {number} pipelineId - pipeline id
   * @param {Object} data - request data object
   * @param {number} data.previous_plugin_inst_id - previous plugin instance id
   * @param {string} data.nodes_info - pipeline-specific JSON string encoding a list of objects.
   * Each object is a workflow node containing a ``piping_id``, ``compute_resource_name``,
   * ``title`` and a list of objects called ``plugin_parameter_defaults``. Each object in
   * this list has ``name`` and ``default`` properties.
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Workflow>} - JS Promise, resolves to a ``Workflow`` object
   */
  createWorkflow(pipelineId, data, timeout = 30000) {
    return this.getPipeline(pipelineId, timeout)
      .then(pipeline => {
        const workflowsUrl = Collection.getLinkRelationUrls(
          pipeline.collection.items[0],
          'workflows'
        );
        const workflowList = new WorkflowList(workflowsUrl[0], this.auth);
        return workflowList.post(data, timeout);
      })
      .then(workflowList => workflowList.getItems()[0]);
  }

  /**
   * Get a paginated list of tags from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match tag id exactly with this number
   * @param {string} [searchParams.name] - match tag name containing this string
   * @param {string} [searchParams.owner_username] - match tag's owner username exactly with this string
   * @param {string} [searchParams.color] - match plugin color containing this string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<TagList>} - JS Promise, resolves to a ``TagList`` object
   */
  getTags(searchParams = null, timeout = 30000) {
    return this._fetchRes('tagsUrl', TagList, searchParams, timeout);
  }

  /**
   * Get a tag resource object given its id.
   *
   * @param {number} id - tag id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Tag|null>} - JS Promise, resolves to a ``Tag`` object or ``null``
   */
  getTag(id, timeout = 30000) {
    return this.getTags({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Create a new tag resource through the REST API.
   *
   * @param {Object} data - request data object
   * @param {string} data.color - tag color
   * @param {string} [data.name=''] - tag name
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Tag>} - JS Promise, resolves to a ``Tag`` object
   */
  createTag(data, timeout = 30000) {
    const createRes = () => {
      const res = new TagList(this.tagsUrl, this.auth);
      return res.post(data, timeout).then(res => res.getItems()[0]);
    };
    return this.tagsUrl ? createRes() : this.setUrls().then(() => createRes());
  }

    /**
   * Get a paginated list of pipeline source files from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match file id exactly with this number
   * @param {string} [searchParams.fname] - match file's path starting with this string
   * @param {string} [searchParams.fname_exact] - match file's path exactly with this string
   * @param {string} [searchParams.fname_icontains] - match file's path containing this string
   * @param {string} [searchParams.uploader_username] - match file's uploader username exactly with this string
   * @param {string} [searchParams.min_creation_date] - match file's creation_date greater than this date string
   * @param {string} [searchParams.max_creation_date] - match file's creation_date lesser than this date string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PipelineSourceFileList>} - JS Promise, resolves to a ``PipelineSourceFileList`` object
   */
    getPipelineSourceFiles(searchParams = null, timeout = 30000) {
      return this._fetchRes('pipelineSourceFilesUrl', PipelineSourceFileList, searchParams, timeout);
    }
  
    /**
     * Get a pipeline source file resource object given its id.
     *
     * @param {number} id - pipeline source file id
     * @param {number} [timeout=30000] - request timeout
     *
     * @return {Promise<PipelineSourceFile|null>} - JS Promise, resolves to a ``PipelineSourceFile`` object or ``null`
     */
    getPipelineSourceFile(id, timeout = 30000) {
      return this.getPipelineSourceFiles({ id: id }, timeout).then(listRes => listRes.getItem(id));
    }
  
    /**
     * Upload a pipeline source file and create a new pipeline source file resource through the REST API.
     * In addition, this creates a new pipeline resource based on the source of the uploaded file.
     *
     * @param {Object} data - request data object
     * @param {string} data.type - pipeline source file type
     * @param {Object} uploadFileObj - custom file object
     * @param {Object} uploadFileObj.fname - file blob
     * @param {number} [timeout=30000] - request timeout
     *
     * @return {Promise<PipelineSourceFile>} - JS Promise, resolves to a ``PipelineSourceFile`` object
     */
    uploadPipelineSourceFile(data, uploadFileObj, timeout = 30000) {
      const createRes = () => {
        const res = new PipelineSourceFileList(this.pipelineSourceFilesUrl, this.auth);
        return res.post(data, uploadFileObj, timeout).then(res => res.getItems()[0]);
      };
      return this.pipelineSourceFilesUrl ? createRes() : this.setUrls().then(() => createRes());
    }
  
  /**
   * Get a paginated list of user files from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match file id exactly with this number
   * @param {string} [searchParams.fname] - match file's path starting with this string
   * @param {string} [searchParams.fname_exact] - match file's path exactly with this string
   * @param {string} [searchParams.fname_icontains] - match file's path containing this string
   * @param {string|number} [searchParams.fname_nslashes] - match file's path containing this number of slashes
   * @param {string} [searchParams.owner_username] - match file's owner username exactly with this string
   * @param {string} [searchParams.min_creation_date] - match file's creation_date greater than this date string
   * @param {string} [searchParams.max_creation_date] - match file's creation_date lesser than this date string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<UserFileList>} - JS Promise, resolves to a ``UserFileList`` object
   */
  getUserFiles(searchParams = null, timeout = 30000) {
    return this._fetchRes('userFilesUrl', UserFileList, searchParams, timeout);
  }

  /**
   * Get a user file resource object given its id.
   *
   * @param {number} id - user file id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<UserFile|null>} - JS Promise, resolves to a ``UserFile`` object or ``null``
   */
  getUserFile(id, timeout = 30000) {
    return this.getUserFiles({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Upload a file and create a new user file resource through the REST API.
   *
   * @param {Object} data - request data object
   * @param {string} data.upload_path - absolute path including file name where the file
   * will be uploaded on the storage service
   * @param {Object} uploadFileObj - custom file object
   * @param {Object} uploadFileObj.fname - file blob
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<UserFile>} - JS Promise, resolves to a ``UserFile`` object
   */
  uploadFile(data, uploadFileObj, timeout = 30000) {
    const createRes = () => {
      const res = new UserFileList(this.userFilesUrl, this.auth);
      return res.post(data, uploadFileObj, timeout).then(res => res.getItems()[0]);
    };
    return this.userFilesUrl ? createRes() : this.setUrls().then(() => createRes());
  }

  /**
   * Get a paginated list of PACS files from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match file id exactly with this number
   * @param {string} [searchParams.fname] - match file's path starting with this string
   * @param {string} [searchParams.fname_exact] - match file's path exactly with this string
   * @param {string} [searchParams.fname_icontains] - match file's path containing this string
   * @param {string} [searchParams.fname_icontains_topdir_unique] - match file's path containing all the substrings
   * from the queried string (which in turn represents a white-space-separated list of query strings) case
   * insensitive anywhere in their fname. But only one file is returned per toplevel directory under
   * SERVICES/PACS/pacs_name. This is useful to efficiently determine the top level directories containing a file
   * that matches the query.
   * @param {string} [searchParams.min_creation_date] - match file's creation_date greater than this date string
   * @param {string} [searchParams.max_creation_date] - match file's creation_date lesser than this date string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PACSFileList>} - JS Promise, resolves to a ``PACSFileList`` object
   */
  getPACSFiles(searchParams = null, timeout = 30000) {
    return this._fetchRes('pacsFilesUrl', PACSFileList, searchParams, timeout);
  }

  /**
   * Get a PACS file resource object given its id.
   *
   * @param {number} id - PACS file id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PACSFile|null>} - JS Promise, resolves to a ``PACSFile`` object or ``null`
   */
  getPACSFile(id, timeout = 30000) {
    return this.getPACSFiles({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Get a paginated list of PACS series from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match file id exactly with this number
   * @param {string} [searchParams.PatientID] - match file's PatientID exactly with this string
   * @param {string} [searchParams.PatientName] - match file's PatientName containing this string
   * @param {string} [searchParams.PatientSex] - match file's PatientSex exactly with this string
   * @param {number} [searchParams.PatientAge] - match file's PatientAge exactly with this number
   * @param {number} [searchParams.min_PatientAge] - match file's PatientAge greater than this number
   * @param {number} [searchParams.max_PatientAge] - match file's PatientAge lesser than this number
   * @param {string} [searchParams.PatientBirthDate] - match file's PatientBirthDate exactly with this date string
   * @param {string} [searchParams.StudyDate] - match file's StudyDate exactly with this date string
   * @param {string} [searchParams.AccessionNumber] - match file's AccessionNumber exactly with this string
   * @param {string} [searchParams.ProtocolName] - match file's ProtocolName exactly with this string
   * @param {string} [searchParams.StudyInstanceUID] - match file's StudyInstanceUID exactly with this string
   * @param {string} [searchParams.StudyDescription] - match file's StudyDescription containing this string
   * @param {string} [searchParams.SeriesInstanceUID] - match file's SeriesInstanceUID exactly with this string
   * @param {string} [searchParams.SeriesDescription] - match file's SeriesDescription containing this string
   * @param {string} [searchParams.min_creation_date] - match file's creation_date greater than this date string
   * @param {string} [searchParams.max_creation_date] - match file's creation_date lesser than this date string
   * @param {string} [searchParams.pacs_identifier] - match file's PACS exactly with this string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PACSSeriesList>} - JS Promise, resolves to a ``PACSSeriesList`` object
   */
  getPACSSeriesList(searchParams = null, timeout = 30000) {
    return this._fetchRes('pacsSeriesUrl', PACSSeriesList, searchParams, timeout);
  }

  /**
   * Get a PACS series resource object given its id.
   *
   * @param {number} id - PACS series id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<PACSSeries|null>} - JS Promise, resolves to a ``PACSSeries`` object or ``null``
   */
  getPACSSeries(id, timeout = 30000) {
    return this.getPACSSeriesList({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }  

  /**
   * Get a list with the matching file browser folder (the returned list only has at most one element) 
   * from the REST API given query search parameters. If no search parameters then get a list with the 
   * default root folder.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match folder id exactly with this number
   * @param {string} [searchParams.path] - match folder's path exactly with this string
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<FileBrowserFolderList>} - JS Promise, resolves to a ``FileBrowserFolderList`` object
   */
  getFileBrowserFolders(searchParams = null, timeout = 30000) {
    return this._fetchRes('fileBrowserUrl', FileBrowserFolderList, searchParams, timeout);
  }

  /**
   * Get a file browser folder resource object given its id.
   *
   * @param {number} id - file browser folder id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<FileBrowserFolder|null>} - JS Promise, resolves to a ``FileBrowserFolder`` object or ``null``
   */
  getFileBrowserFolder(id, timeout = 30000) {
    return this.getFileBrowserFolders({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Get a file browser folder resource object given its path.
   *
   * @param {string} [path=''] - file browser folder path
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<FileBrowserFolder|null>} - JS Promise, resolves to a ``FileBrowserFolder`` object or ``null``
   */
   getFileBrowserFolderByPath(path = '', timeout = 30000) {
    return this.getFileBrowserFolders({ path: path }, timeout).then(listRes => {
      const items = listRes.getItems();
      return items.length ? items[0] : null;
    });
  } 

  /**
   * Create a new file browser folder resource through the REST API.
   *
   * @param {Object} data - request data object
   * @param {string} data.path - folder path
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<FileBrowserFolder>} - JS Promise, resolves to a ``FileBrowserFolder`` object
   */
  createFileBrowserFolder(data, timeout = 30000) {
    const createRes = () => {
      const res = new FileBrowserFolderList(this.fileBrowserUrl, this.auth);
      return res.post(data, timeout).then(res => res.getItems()[0]);
    };
    return this.fileBrowserUrl ? createRes() : this.setUrls().then(() => createRes());
  }

  /**
   * Get a paginated list of file download tokens for the authenticated user from the REST API 
   * given query search parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match file download token id exactly with this number
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<DownloadTokenList>} - JS Promise, resolves to a ``DownloadTokenList`` object
   */
  getDownloadTokens(searchParams = null, timeout = 30000) {
    return this._fetchRes('downloadTokensUrl', DownloadTokenList, searchParams, timeout);
  }

  /**
   * Get a download token resource object given its id.
   *
   * @param {number} id - file download token id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<DownloadToken|null>} - JS Promise, resolves to a ``DownloadToken`` object or ``null``
   */
  getDownloadToken(id, timeout = 30000) {
    return this.getDownloadTokens({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Create a new file download token resource through the REST API.
   *
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<DownloadToken>} - JS Promise, resolves to a ``DownloadToken`` object
   */
  createDownloadToken(timeout = 30000) {
    const createRes = () => {
      const res = new DownloadTokenList(this.downloadTokensUrl, this.auth);
      return res.post(timeout).then(res => res.getItems()[0]);
    };
    return this.downloadTokensUrl ? createRes() : this.setUrls().then(() => createRes());
  }  

  /**
   * Get a paginated list of groups from the REST API given query search
   * parameters. If no search parameters then get the default first page.
   *
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [searchParams.limit] - page limit
   * @param {number} [searchParams.offset] - page offset
   * @param {number} [searchParams.id] - match group id exactly with this number
   * @param {string} [searchParams.name] - match group name exactly with this string
   * @param {string} [searchParams.name_icontains] - match group name containing this string 
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<GroupList>} - JS Promise, resolves to a ``GroupList`` object
   */
   getGroups(searchParams = null, timeout = 30000) {
    return this._fetchRes('groupsUrl', GroupList, searchParams, timeout);
  }

  /**
   * Get a group resource object given its id.
   *
   * @param {number} id - group id
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Group|null>} - JS Promise, resolves to a ``Group`` object or ``null``
   */
  getGroup(id, timeout = 30000) {
    return this.getGroups({ id: id }, timeout).then(listRes => listRes.getItem(id));
  }

  /**
   * Create a new group resource through the REST API.
   *
   * @param {Object} data - request data object
   * @param {string} data.name - group name
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<Group>} - JS Promise, resolves to a ``Group`` object
   */
  adminCreateGroup(data, timeout = 30000) {
    const createRes = () => {
      const res = new GroupList(this.groupsUrl, this.auth);
      return res.post(data, timeout).then(res => res.getItems()[0]);
    };
    return this.groupsUrl ? createRes() : this.setUrls().then(() => createRes());
  }

  /**
   * Get a user resource object for the currently authenticated user.
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<User>} - JS Promise, resolves to a ``User`` object
   */
  getUser(timeout = 30000) {
    return this._fetchRes('userUrl', User, null, timeout);
  }

  /**
   * Create a new user account.
   *
   * @param {string} usersUrl - url of the user accounts service
   * @param {string} username - username
   * @param {string} password - password
   * @param {string} email - user email
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<User>} - JS Promise, resolves to a ``User`` object
   */
  static createUser(usersUrl, username, password, email, timeout = 30000) {
    const req = new Request(undefined, 'application/vnd.collection+json', timeout);
    const userData = {
      template: {
        data: [
          { name: 'username', value: username },
          { name: 'password', value: password },
          { name: 'email', value: email },
        ],
      },
    };
    return req.post(usersUrl, userData).then(resp => {
      const coll = resp.data.collection;
      const userUrl = coll.items[0].href;
      const auth = { username: username, password: password };
      const user = new User(userUrl, auth);
      user.collection = coll;
      return user;
    });
  }

  /**
   * Fetch a user's login authorization token from the REST API.
   * @param {string} authUrl - url of the authorization service
   * @param {string} username - username
   * @param {string} password - password
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise<string>} - JS Promise, resolves to a ``string`` value
   */
  static getAuthToken(authUrl, username, password, timeout = 30000) {
    const req = new Request(undefined, 'application/json', timeout);
    const authData = {
      username: username,
      password: password,
    };
    return req.post(authUrl, authData).then(resp => resp.data.token);
  }

  /**
   * Helper method to run an asynchronous task defined by a task generator function.
   *
   * @param {function*()} taskGenerator - generator function
   */
  static runAsyncTask(taskGenerator) {
    Request.runAsyncTask(taskGenerator);
  }

  /**
   * Internal method to fetch a high level resource through the REST API.
   *
   * @param {string} resUrlProp -  property of the `this` object containing the url of the resource
   * @param {string} ResClass - resource class
   * @param {Object} [searchParams=null] - search parameters object
   * @param {number} [timeout=30000] - request timeout
   *
   * @return {Promise} - JS Promise
   */
  _fetchRes(resUrlProp, ResClass, searchParams = null, timeout = 30000) {
    const getRes = () => {
      const res = new ResClass(this[resUrlProp], this.auth);

      return 'searchParams' in res ? res.get(searchParams, timeout) : res.get(timeout);
    };
    return this[resUrlProp] ? getRes() : this.setUrls().then(() => getRes());
  }
}