Source: managers/metadata.ts

/**
 * @fileoverview Manager for the Box Metadata Resource
 */

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

import BoxClient from '../box-client';
import urlPath from '../util/url-path';
const merge = require('merge-options');

// -----------------------------------------------------------------------------
// Typedefs
// -----------------------------------------------------------------------------

/**
 * Valid metadata field types
 * @readonly
 * @enum {MetadataFieldType}
 */
enum MetadataFieldType {
  STRING = 'string',
  ENUM = 'enum',
  NUMBER = 'float',
  DATE = 'date',
  MULTI_SELECT = 'multiSelect',
}

/**
 * Metadata enum option
 * @typedef {Object} MetadataEnumOption
 * @property {string} key The option value
 */
type MetadataEnumOption = {
  key: string;
};

/**
 * Field definition for a metadata template
 * @typedef {Object} MetadataTemplateField
 * @property {MetadataFieldType} type The type of the field
 * @property {string} key The programmatic name of the field
 * @property {string} displayName The display name of the field
 * @property {boolean} hidden Whether this field is hidden in the UI for the user and can only be set through the API instead
 * @property {MetadataEnumOption[]} [options] For enum fields, the options
 */
type MetadataTemplateField = {
  type: MetadataFieldType;
  key: string;
  displayName: string;
  hidden: boolean;
  options?: MetadataEnumOption[];
};

// -----------------------------------------------------------------------------
// Private
// -----------------------------------------------------------------------------
const PROPERTIES_TEMPLATE = 'properties',
  BASE_PATH = '/metadata_templates',
  SCHEMA_SUBRESOURCE = 'schema',
  ENTERPRISE_SCOPE = 'enterprise',
  GLOBAL_SCOPE = 'global',
  CASCADE_POLICIES_PATH = '/metadata_cascade_policies',
  QUERY_PATH = '/metadata_queries/execute_read';

// -----------------------------------------------------------------------------
// Public
// -----------------------------------------------------------------------------

/**
 * Simple manager for interacting with all metadata endpoints and actions.
 *
 * @constructor
 * @param {BoxClient} client - The Box API Client that is responsible for making calls to the API
 * @returns {void}
 */
class Metadata {
  client: BoxClient;

  templates!: Record<string, any>;
  scopes!: Record<string, any>;
  cascadeResolution!: Record<string, any>;
  fieldTypes!: typeof MetadataFieldType;

  constructor(client: BoxClient) {
    this.client = client;
  }

  /**
   * Retrieve the schema definition for a metadata template
   *
   * API Endpoint: '/metadata_templates/:scope/:template'
   * Method: GET
   *
   * @param {string} scope - The scope of the template, e.g. "enterprise"
   * @param {string} template - The template to retrieve
   * @param {Function} [callback] - Called with the template schema if successful
   * @returns {Promise<Object>} A promise resolving to the template schema
   */
  getTemplateSchema(scope: string, template: string, callback?: Function) {
    var apiPath = urlPath(BASE_PATH, scope, template, SCHEMA_SUBRESOURCE);
    return this.client.wrapWithDefaultHandler(this.client.get)(
      apiPath,
      null,
      callback
    );
  }

  /**
   * Retrieve the schema definition for a metadata template by ID
   *
   * API Endpoint: '/metadata_templates/:id'
   * Method: GET
   *
   * @param {string} templateID - The ID of the template to retrieve
   * @param {Function} [callback] - Called with the template schema if successful
   * @returns {Promise<Object>} A promise resolving to the template schema
   */
  getTemplateByID(templateID: string, callback?: Function) {
    var apiPath = urlPath(BASE_PATH, templateID);
    return this.client.wrapWithDefaultHandler(this.client.get)(
      apiPath,
      null,
      callback
    );
  }

  /**
   * Get all templates in a given scope
   *
   * API Endpoint: '/metadata_templates/:scope'
   * Method: GET
   *
   * @param {string} scope - The scope to retrieve templates for
   * @param {Function} [callback] - Called with an array of templates when successful
   * @returns {Promise<Object>} A promise resolving to the collection of templates
   */
  getTemplates(scope: string, callback?: Function) {
    var apiPath = urlPath(BASE_PATH, scope);
    return this.client.wrapWithDefaultHandler(this.client.get)(
      apiPath,
      null,
      callback
    );
  }

  /**
   * Create a new metadata template
   *
   * API Endpoint: '/metadata_templates/schema',
   * Method: POST
   *
   * @param {string} templateName - The name of the metadata template
   * @param {MetadataTemplateField[]} fields - A list of fields for the template
   * @param {Object} [options] - Optional parameters, can be left null in many cases
   * @param {string} [options.templateKey] - The programmatic key for the template
   * @param {boolean} [options.hidden] - Whether the template should be hidden in the UI
   * @param {string} [options.scope=enterprise] - The scope for the template, only 'enterprise' is supported for now
   * @param {boolean} [options.copyInstanceOnItemCopy] - Whether to include the metadata when a file or folder is copied
   * @param {Function} [callback] - Passed the template if successful, error otherwise
   * @returns {Promise<Object>} A promise resolving to the created template
   */
  createTemplate(
    templateName: string,
    fields: MetadataTemplateField[],
    options?: {
      templateKey?: string;
      hidden?: boolean;
      scope?: string;
      copyInstanceOnItemCopy?: boolean;
    },
    callback?: Function
  ) {
    var apiPath = urlPath(BASE_PATH, SCHEMA_SUBRESOURCE),
      params = {
        body: {
          scope: ENTERPRISE_SCOPE,
          displayName: templateName,
          fields,
        },
      };

    Object.assign(params.body, options);

    return this.client.wrapWithDefaultHandler(this.client.post)(
      apiPath,
      params,
      callback
    );
  }

  /**
   * Update a metadata template via one or more non-breaking operations.  Each
   * operation is a an object descrbing one change to the template or its
   * fields.
   *
   * API Endpoint: '/metadata_templates/:scope/:template/schema'
   * Method: PUT
   *
   * @param {string} scope - The scope of the template to modify
   * @param {string} template - The template to modify
   * @param {Object[]} operations - The operations to perform
   * @param {Function} [callback] - Passed the updated template if successful, error otherwise
   * @returns {Promise<Object>} A promise resolving to the updated template
   * @see {@link https://developer.box.com/en/reference/put-metadata-templates-id-id-schema/}
   */
  updateTemplate(
    scope: string,
    template: string,
    operations: Record<string, any>[],
    callback?: Function
  ) {
    var apiPath = urlPath(BASE_PATH, scope, template, SCHEMA_SUBRESOURCE),
      params = {
        body: operations,
      };

    return this.client.wrapWithDefaultHandler(this.client.put)(
      apiPath,
      params,
      callback
    );
  }

  /**
   * Delete a metadata template from an enterprise.
   *
   * API Endpoint: '/metadata_templates/:scope/:template/schema'
   * Method: DELETE
   *
   * @param {string} scope - The scope of the template to delete
   * @param {string} template - The template to delete
   * @param {Function} [callback] - Passed empty response body if successful, err otherwise
   * @returns {Promise<void>} A promise resolving to nothing
   * @see {@link https://developer.box.com/en/reference/delete-metadata-templates-id-id-schema/}
   */
  deleteTemplate(scope: string, template: string, callback?: Function) {
    var apiPath = urlPath(BASE_PATH, scope, template, SCHEMA_SUBRESOURCE);
    return this.client.wrapWithDefaultHandler(this.client.del)(
      apiPath,
      null,
      callback
    );
  }

  /**
   * Get the cascade policies associated with a given folder.
   *
   * API Endpoint: '/metadata_cascade_policies'
   * Method: GET
   *
   * @param {string} folderID The ID of the folder to get cascade policies for
   * @param {Object} [options] Optional parameters
   * @param {string} [options.owner_enterprise_id] ID of the enterprise to get policies for
   * @param {Function} [callback] Passed the collection of policies if successful
   * @returns {Promise<Object>} Promise resolving to the collection of policies
   */
  getCascadePolicies(
    folderID: string,
    options?: {
      owner_enterprise_id?: string;
    },
    callback?: Function
  ) {
    var apiPath = urlPath(CASCADE_POLICIES_PATH),
      params = {
        qs: Object.assign({ folder_id: folderID }, options),
      };

    return this.client.wrapWithDefaultHandler(this.client.get)(
      apiPath,
      params,
      callback
    );
  }

  /**
   * Get a metadata cascade policy object by ID
   *
   * API Endpoint: '/metadata_cascade_policies/:policyID'
   * Method: GET
   *
   * @param {string} policyID The ID of the policy to retrieve
   * @param {Function} [callback] Passed the cascade policy if successful
   * @returns {Promise<Object>} Promise resolving to the cascade policy
   */
  getCascadePolicy(policyID: string, callback?: Function) {
    var apiPath = urlPath(CASCADE_POLICIES_PATH, policyID);

    return this.client.wrapWithDefaultHandler(this.client.get)(
      apiPath,
      null,
      callback
    );
  }

  /**
   * Add a new cascade policy to a folder/metadata template, causing the
   * metadata template to be applied to all items and subfolders inside the
   * folder.
   *
   * API Endpoint: '/metadata_cascade_policies'
   * Method: POST
   *
   * @param {string} scope Metadata template scope for the template to cascade
   * @param {string} templateKey Metadata template key for the template to cascade
   * @param {string} folderID The ID of the folder to cascade over
   * @param {Function} [callback] Passed the cascade policy if successful
   * @returns {Promise<Object>} Promise resolving to the cascade policy
   */
  createCascadePolicy(
    scope: string,
    templateKey: string,
    folderID: string,
    callback?: Function
  ) {
    var apiPath = urlPath(CASCADE_POLICIES_PATH),
      params = {
        body: {
          folder_id: folderID,
          scope,
          templateKey,
        },
      };

    return this.client.wrapWithDefaultHandler(this.client.post)(
      apiPath,
      params,
      callback
    );
  }

  /**
   * Delete the metadata cascade policy with the given ID
   *
   * API Endpoint: '/metadata_cascade_policies/:policyID'
   * Method: DELETE
   *
   * @param {string} policyID The ID of the policy to delete
   * @param {Function} [callback] Passed nothing if successful
   * @returns {Promise<void>} Promise resolving to nothing
   */
  deleteCascadePolicy(policyID: string, callback?: Function) {
    var apiPath = urlPath(CASCADE_POLICIES_PATH, policyID);
    return this.client.wrapWithDefaultHandler(this.client.del)(
      apiPath,
      null,
      callback
    );
  }

  /**
   * If a policy already exists on a folder, this will apply that policy to all existing files and
   * sub-folders within the target folder.
   *
   * API Endpoint: '/metadata_cascade_policies/:policyID/apply'
   * Method: POST
   *
   * @param {string} policyID The ID of the policy to delete
   * @param {string} resolutionMethod How to resolve conflicts, either "none" or "overwrite"
   * @param {Function} [callback] Passed nothing if successful
   * @returns {Promise<void>} Promise resolving to nothing
   */
  forceApplyCascadePolicy(
    policyID: string,
    resolutionMethod: string,
    callback?: Function
  ) {
    var apiPath = urlPath(CASCADE_POLICIES_PATH, policyID, 'apply'),
      params = {
        body: {
          conflict_resolution: resolutionMethod,
        },
      };

    return this.client.wrapWithDefaultHandler(this.client.post)(
      apiPath,
      params,
      callback
    );
  }

  /**
   * Query Box items by their metadata.
   *
   * API Endpoint: '/metadata_queries/execute_read'
   * Method: POST
   *
   * @param {string} from - The template used in the query. Must be in the form scope.templateKey
   * @param {string} ancestorFolderId - The folder_id to which to restrain the query
   * @param {Object} [options] - Optional parameters
   * @param {string} [options.query] - The logical expression of the query
   * @param {Object} [options.query_params] - Required if query present. The arguments for the query
   * @param {Object} [options.order_by] - The field_key(s) to order on and the corresponding direction(s)
   * @param {Array} [options.fields] - An array of fields to return
   * @param {int} [options.limit=100] - The number of results to return for a single request
   * @param {string} [options.marker] - Pagination marker
   * @param {Function} [callback] - Passed a collection of items and their associated metadata
   * @returns {Promise<void>} Promise resolving to a collection of items and their associated metadata
   */
  query(
    from: string,
    ancestorFolderId: string,
    options?: {
      query?: string;
      query_params?: Record<string, any>;
      order_by?: Record<string, any>;
      fields?: string[];
      limit?: number;
      marker?: string;
    },
    callback?: Function
  ) {
    var body = {
      from,
      ancestor_folder_id: ancestorFolderId,
    };

    var params = {
      body: merge(body, options || {}),
    };

    return this.client.wrapWithDefaultHandler(this.client.post)(
      QUERY_PATH,
      params,
      callback
    );
  }
}

Metadata.prototype.templates = {
  PROPERTIES: PROPERTIES_TEMPLATE,
};

Metadata.prototype.scopes = {
  ENTERPRISE: ENTERPRISE_SCOPE,
  GLOBAL: GLOBAL_SCOPE,
};

Metadata.prototype.cascadeResolution = Object.freeze({
  PRESERVE_EXISTING: 'none',
  OVERWRITE: 'overwrite',
});

/**
 * Valid metadata field types
 * @readonly
 * @enum {MetadataFieldType}
 */
Metadata.prototype.fieldTypes = MetadataFieldType;

export = Metadata;