Source: document.js

const extend = require('extend');
const qs = require('querystring');
const join = require('./deps/utils/join');
const Base = require('./base');
const constants = require('./deps/constants');

/**
 * The `Document` class wraps a document.
 *
 * **Cannot directly be instantiated**
 */
class Document extends Base {
  /**
   * Creates a Document.
   * @param {object} doc - The initial document object. This Document object will be extended with doc properties.
   * @param {object} opts - The configuration options.
   * @param {string} opts.nuxeo - The {@link Nuxeo} object linked to this `Document` object.
   * @param {object} opts.repository - The {@link Repository} object linked to this `Document` object.
   */
  constructor(doc, opts) {
    super(opts);
    this._nuxeo = opts.nuxeo;
    this._repository = opts.repository || this._nuxeo.repository(doc.repository, opts);
    this.properties = {};
    this._dirtyProperties = {};
    extend(true, this, doc);
  }

  /**
   * Sets document properties.
   * @param {object} properties - The properties to set.
   * @returns {Document}
   *
   * @example
   * doc.set({
   *   'dc:title': 'new title',
   *   'dc:description': 'new description',
   * });
   */
  set(properties) {
    this._dirtyProperties = extend(true, {}, this._dirtyProperties, properties);
    return this;
  }

  /**
   * Gets a document property.
   * @param {string} propertyName - The property name, such as 'dc:title', 'file:filename', ...
   * @returns {Document}
   */
  get(propertyName) {
    return this._dirtyProperties[propertyName] || this.properties[propertyName];
  }

  /**
   * Saves the document. It updates only the 'dirty properties' set through the {@link Document#set} method.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the updated document.
   */
  save(opts = {}) {
    const options = this._computeOptions(opts);
    return this._repository.update({
      'entity-type': 'document',
      uid: this.uid,
      properties: this._dirtyProperties,
    }, options);
  }

  /**
   * Returns whether this document is folderish or not.
   * @returns {Boolean} true if this document is folderish, false otherwise.
   */
  isFolder() {
    return this.hasFacet('Folderish');
  }

  /**
   * Returns whether this document has the input facet or not.
   * @returns {Boolean} true if this document has the facet, false otherwise.
   */
  hasFacet(facet) {
    return this.facets.indexOf(facet) !== -1;
  }

  /**
   * Returns whether this document is a collection or not.
   * @returns {Boolean} true if this document is a collection, false otherwise.
   */
  isCollection() {
    return this.hasFacet('Collection');
  }

  /**
   * Returns whether this document can be added to a collection or not.
   * @returns {Boolean} true if this document can be added to a collection, false otherwise.
   */
  isCollectable() {
    return !this.hasFacet('NotCollectionMember');
  }

  /**
   * Fetch a Blob from this document.
   * @param {string} [xpath=blobholder:0] - The Blob xpath. Default to the main blob 'blobholder:0'.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the response.
   */
  fetchBlob(xpath = 'blobholder:0', opts = {}) {
    let options = opts;
    let blobXPath = xpath;
    if (typeof xpath === 'object') {
      options = xpath;
      blobXPath = 'blobholder:0';
    }
    options = this._computeOptions(options);
    const path = join('id', this.uid, '@blob', blobXPath);
    return this._nuxeo.request(path).get(options);
  }

  /**
   * Moves this document.
   * @param {string} dst - The destination folder.
   * @param {string} [name] - The destination name, can be null.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the moved document.
   */
  move(dst, name = null, opts = {}) {
    const options = this._computeOptions(opts);
    options.repository = this._repository;
    return this._nuxeo.operation('Document.Move')
      .input(this.uid)
      .params({
        name,
        target: dst,
      })
      .execute(options);
  }

  /**
   * Follows a given life cycle transition.
   * @param {string} transitionName - The life cycle transition to follow.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the updated document.
   */
  followTransition(transitionName, opts = {}) {
    const options = this._computeOptions(opts);
    options.repository = this._repository;
    return this._nuxeo.operation('Document.FollowLifecycleTransition')
      .input(this.uid)
      .params({
        value: transitionName,
      })
      .execute(options);
  }

  /**
   * Converts a Blob from this document.
   * @param {object} convertOpts - Configuration options for the conversion.
                                   At least one of the 'converter', 'type' or 'format' option must be defined.
   * @param {string} [convertOpts.xpath=blobholder:0] - The Blob xpath. Default to the main blob 'blobholder:0'.
   * @param {string} convertOpts.converter - Named converter to use.
   * @param {string} convertOpts.type - The destination mime type, such as 'application/pdf'.
   * @param {string} convertOpts.format - The destination format, such as 'pdf'.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the response.
   */
  convert(convertOpts, opts = {}) {
    const options = this._computeOptions(opts);
    const xpath = convertOpts.xpath || 'blobholder:0';
    const path = join('id', this.uid, '@blob', xpath, '@convert');
    return this._nuxeo.request(path)
      .queryParams({
        converter: convertOpts.converter,
        type: convertOpts.type,
        format: convertOpts.format,
      })
      .get(options);
  }

  /**
   * Schedule a conversion of the Blob from this document.
   * @param {object} convertOpts - Configuration options for the conversion.
                                   At least one of the 'converter', 'type' or 'format' option must be defined.
   * @param {string} [convertOpts.xpath=blobholder:0] - The Blob xpath. Default to the main blob 'blobholder:0'.
   * @param {string} convertOpts.converter - Named converter to use.
   * @param {string} convertOpts.type - The destination mime type, such as 'application/pdf'.
   * @param {string} convertOpts.format - The destination format, such as 'pdf'.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the response.
   */
  scheduleConversion(convertOpts, opts = {}) {
    const params = {
      async: true,
      converter: convertOpts.converter,
      type: convertOpts.type,
      format: convertOpts.format,
    };
    opts.body = qs.stringify(params);
    const options = this._computeOptions(opts);
    options.headers['Content-Type'] = 'multipart/form-data';
    const xpath = convertOpts.xpath || 'blobholder:0';
    const path = join('id', this.uid, '@blob', xpath, '@convert');
    return this._nuxeo.request(path)
      .post(options);
  }

  /**
   * Starts a workflow on this document given a workflow model name.
   * @param {string} workflowModelName - The workflow model name.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the started `Workflow` object.
   */
  startWorkflow(workflowModelName, opts = {}) {
    opts.body = {
      workflowModelName,
      'entity-type': 'workflow',
    };
    const options = this._computeOptions(opts);
    const path = join('id', this.uid, '@workflow');
    options.documentId = this.uid;
    return this._nuxeo.request(path)
      .post(options);
  }

  /**
   * Fetches the started workflows on this document.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the started workflows.
   */
  fetchWorkflows(opts = {}) {
    const options = this._computeOptions(opts);
    const path = join('id', this.uid, '@workflow');
    options.documentId = this.uid;
    return this._nuxeo.request(path)
      .get(options);
  }

  /**
   * Fetches the renditions list of this document.
   *
   * Only available on Nuxeo version LTS 2016 or later.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the rendition definitions.
   */
  fetchRenditions(opts = {}) {
    const { Promise } = this._nuxeo;
    if (this.contextParameters && this.contextParameters.renditions) {
      return Promise.resolve(this.contextParameters.renditions);
    }

    const options = this._computeOptions(opts);
    options.enrichers = { document: ['renditions'] };
    return this._repository
      .fetch(this.uid, options)
      .then((doc) => {
        if (!this.contextParameters) {
          this.contextParameters = {};
        }
        this.contextParameters.renditions = doc.contextParameters.renditions;
        return this.contextParameters.renditions;
      });
  }

  /**
   * Fetch a rendition from this document.
   * @param {string} name - The rendition name.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the response.
   */
  fetchRendition(name, opts = {}) {
    const options = this._computeOptions(opts);
    const path = join('id', this.uid, '@rendition', name);
    return this._nuxeo.request(path)
      .get(options);
  }

  /**
   * Fetches the ACLs list of this document.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the ACLs.
   */
  fetchACLs(opts = {}) {
    const { Promise } = this._nuxeo;
    if (this.contextParameters && this.contextParameters.acls) {
      return Promise.resolve(this.contextParameters.acls);
    }

    const options = this._computeOptions(opts);
    options.enrichers = { document: [constants.enricher.document.ACLS] };
    return this._repository
      .fetch(this.uid, options)
      .then((doc) => {
        if (!this.contextParameters) {
          this.contextParameters = {};
        }
        this.contextParameters.acls = doc.contextParameters.acls;
        return this.contextParameters.acls;
      });
  }

  /**
   * Checks if the user has a given permission. It only works for now for 'Write', 'Read' and 'Everything' permission.
   * This method may call the server to compute the available permissions (using the 'permissions' enricher)
   * if not already present.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with true or false.
   */
  hasPermission(name, opts = {}) {
    const { Promise } = this._nuxeo;
    if (this.contextParameters && this.contextParameters.permissions) {
      return Promise.resolve(this.contextParameters.permissions.indexOf(name) !== -1);
    }

    const options = this._computeOptions(opts);
    options.enrichers = { document: [constants.enricher.document.PERMISSIONS] };
    return this._repository
      .fetch(this.uid, options)
      .then((doc) => {
        if (!this.contextParameters) {
          this.contextParameters = {};
        }
        this.contextParameters.permissions = doc.contextParameters.permissions;
        return this.contextParameters.permissions.indexOf(name) !== -1;
      });
  }

  /**
   * Adds a new permission.
   * @param {object} params - The params needed to add a new permission.
   * @param {string} params.permission - The permission string to set, such as 'Write', 'Read', ...
   * @param {string} params.username - The target username. `username` or `email` must be set.
   * @param {string} params.email - The target email. `username` or `email` must be set.
   * @param {string} [params.acl] - The ACL name where to add the new permission.
   * @param {string} [params.begin] - Optional begin date.
   * @param {string} [params.end] - Optional end date.
   * @param {string} [params.blockInheritance] - Whether to block the permissions inheritance or not
   *                                             before adding the new permission.
   * @param {string} [params.notify] - Optional flag to notify the user of the new permission.
   * @param {string} [params.comment] - Optional comment used for the user notification.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the updated document.
   */
  addPermission(params, opts = {}) {
    const options = this._computeOptions(opts);
    options.repository = this._repository;
    return this._nuxeo.operation('Document.AddPermission')
      .input(this.uid)
      .params(params)
      .execute(options);
  }

  /**
   * Removes a permission given its id, or all permissions for a given user.
   * @param {object} params - The params needed to remove a permission.
   * @param {string} params.id - The permission id. `id` or `user` must be set.
   * @param {string} params.user - The user to rem. `id` or `user` must be set.
   * @param {string} [params.acl] - The ACL name where to add the new permission.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the updated document.
   */
  removePermission(params, opts = {}) {
    const options = this._computeOptions(opts);
    options.repository = this._repository;
    return this._nuxeo.operation('Document.RemovePermission')
      .input(this.uid)
      .params(params)
      .execute(options);
  }

  /**
   * Fetches the lock status of the document.
   * @example
   * // if the doc is locked
   * doc.fetchLockStatus()
   *   .then(function(status) {
   *     // status.lockOwner === 'Administrator'
   *     // status.lockCreated === '2011-10-23T12:00:00.00Z'
   *   });
   * @example
   * // if the doc is not locked
   * doc.fetchLockStatus()
   *   .then(function(status) {
   *     // status.lockOwner === undefined
   *     // status.lockCreated === undefined
   *   });
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with true or false.
   */
  fetchLockStatus(opts = {}) {
    const options = this._computeOptions(opts);
    options.fetchProperties = { document: ['lock'] };
    return this._repository
      .fetch(this.uid, options)
      .then((doc) => {
        return {
          lockOwner: doc.lockOwner,
          lockCreated: doc.lockCreated,
        };
      });
  }

  /**
   * Locks the document.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the updated document.
   */
  lock(opts = {}) {
    const options = this._computeOptions(opts);
    options.repository = this._repository;
    return this._nuxeo.operation('Document.Lock')
      .input(this.uid)
      .execute(options);
  }

  /**
   * Unlocks the document.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with the updated document.
   */
  unlock(opts = {}) {
    const options = this._computeOptions(opts);
    options.repository = this._repository;
    return this._nuxeo.operation('Document.Unlock')
      .input(this.uid)
      .execute(options);
  }

  /**
   * Fetches the audit of the document.
   * @param {object} [queryOpts] - Parameters for the audit query.
   * @param {Array} [queryOpts.eventId] - List of event ids to filter.
   * @param {Array} [queryOpts.category] - List of categories to filter
   * @param {Array} [queryOpts.principalName] - List of principal names to filter.
   * @param {object} [queryOpts.startEventDate] - Start date.
   * @param {object} [queryParams.endEventDate] - End date
   * @param {number} [queryOpts.pageSize=0] - The number of results per page.
   * @param {number} [queryOpts.currentPageIndex=0] - The current page index.
   * @param {number} [queryOpts.maxResults] - The expected max results.
   * @param {string} [queryOpts.sortBy] - The sort by info.
   * @param {string} [queryOpts.sortOrder] - The sort order info.
   * @param {object} [opts] - Options overriding the ones from this object.
   * @returns {Promise} A promise object resolved with audit entries.
   */
  fetchAudit(queryOpts = {}, opts = {}) {
    const options = this._computeOptions(opts);
    const path = join('id', this.uid, '@audit');
    return this._nuxeo.request(path)
      .queryParams(queryOpts)
      .get(options);
  }
}

module.exports = Document;