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;