diff --git a/lib-es5/api.js b/lib-es5/api.js index 0741a8d7..b2bf9548 100644 --- a/lib-es5/api.js +++ b/lib-es5/api.js @@ -618,4 +618,169 @@ exports.update_resources_access_mode_by_ids = function update_resources_access_m var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; return updateResourcesAccessMode(access_mode, "public_ids[]", ids, callback, options); +}; + +/** + * Creates a new metadata field definition + * + * @see https://cloudinary.com/documentation/admin_api#create_a_metadata_field + * + * @param {Object} field The field to add + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.add_metadata_field = function add_metadata_field(field, callback) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + var params = only(field, "external_id", "type", "label", "mandatory", "default_value", "validation", "datasource"); + options.content_type = "json"; + return call_api("post", ["metadata_fields"], params, callback, options); +}; + +/** + * Returns a list of all metadata field definitions + * + * @see https://cloudinary.com/documentation/admin_api#get_metadata_fields + * + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.list_metadata_fields = function list_metadata_fields(callback) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + return call_api("get", ["metadata_fields"], {}, callback, options); +}; + +/** + * Deletes a metadata field definition. + * + * The field should no longer be considered a valid candidate for all other endpoints + * + * @see https://cloudinary.com/documentation/admin_api#delete_a_metadata_field_by_external_id + * + * @param {String} field_external_id The external id of the field to delete + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.delete_metadata_field = function delete_metadata_field(field_external_id, callback) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + return call_api("delete", ["metadata_fields", field_external_id], {}, callback, options); +}; + +/** + * Get a metadata field by external id + * + * @see https://cloudinary.com/documentation/admin_api#get_a_metadata_field_by_external_id + * + * @param {String} external_id The ID of the metadata field to retrieve + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.metadata_field_by_field_id = function metadata_field_by_field_id(external_id, callback) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + return call_api("get", ["metadata_fields", external_id], {}, callback, options); +}; + +/** + * Updates a metadata field by external id + * + * Updates a metadata field definition (partially, no need to pass the entire object) passed as JSON data. + * See {@link https://cloudinary.com/documentation/admin_api#generic_structure_of_a_metadata_field Generic structure of a metadata field} for details. + * + * @see https://cloudinary.com/documentation/admin_api#update_a_metadata_field_by_external_id + * + * @param {String} external_id The ID of the metadata field to update + * @param {Object} field Updated values of metadata field + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.update_metadata_field = function update_metadata_field(external_id, field, callback) { + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + var params = only(field, "external_id", "type", "label", "mandatory", "default_value", "validation", "datasource"); + options.content_type = "json"; + return call_api("put", ["metadata_fields", external_id], params, callback, options); +}; + +/** + * Updates a metadata field datasource + * + * Updates the datasource of a supported field type (currently only enum and set), passed as JSON data. The + * update is partial: datasource entries with an existing external_id will be updated and entries with new + * external_id’s (or without external_id’s) will be appended. + * + * @see https://cloudinary.com/documentation/admin_api#update_a_metadata_field_datasource + * + * @param {String} field_external_id The ID of the field to update + * @param {Object} entries_external_id Updated values for datasource + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.update_metadata_field_datasource = function update_metadata_field_datasource(field_external_id, entries_external_id, callback) { + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + var params = only(entries_external_id, "values"); + options.content_type = "json"; + return call_api("put", ["metadata_fields", field_external_id, "datasource"], params, callback, options); +}; + +/** + * Deletes entries in a metadata field datasource + * + * Deletes (blocks) the datasource entries for a specified metadata field definition. Sets the state of the + * entries to inactive. This is a soft delete, the entries still exist under the hood and can be activated again + * with the restore datasource entries method. + * + * @see https://cloudinary.com/documentation/admin_api#delete_entries_in_a_metadata_field_datasource + * + * @param {String} field_external_id The ID of the metadata field + * @param {Array} entries_external_id An array of IDs of datasource entries to delete + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.delete_datasource_entries = function delete_datasource_entries(field_external_id, entries_external_id, callback) { + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + options.content_type = "json"; + var params = { external_ids: entries_external_id }; + return call_api("delete", ["metadata_fields", field_external_id, "datasource"], params, callback, options); +}; + +/** + * Restores entries in a metadata field datasource + * + * Restores (unblocks) any previously deleted datasource entries for a specified metadata field definition. + * Sets the state of the entries to active. + * + * @see https://cloudinary.com/documentation/admin_api#restore_entries_in_a_metadata_field_datasource + * + * @param {String} field_external_id The ID of the metadata field + * @param {Array} entries_external_id An array of IDs of datasource entries to delete + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.restore_metadata_field_datasource = function restore_metadata_field_datasource(field_external_id, entries_external_id, callback) { + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + options.content_type = "json"; + var params = { external_ids: entries_external_id }; + return call_api("post", ["metadata_fields", field_external_id, "datasource_restore"], params, callback, options); }; \ No newline at end of file diff --git a/lib-es5/uploader.js b/lib-es5/uploader.js index c0282859..77277ddb 100644 --- a/lib-es5/uploader.js +++ b/lib-es5/uploader.js @@ -654,4 +654,28 @@ exports.unsigned_image_upload_tag = function unsigned_image_upload_tag(field, up unsigned: true, upload_preset: upload_preset })); +}; + +/** + * Populates metadata fields with the given values. Existing values will be overwritten. + * + * @param {Object} metadata A list of custom metadata fields (by external_id) and the values to assign to each + * @param {Array} public_ids The public IDs of the resources to update + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.update_metadata = function update_metadata(metadata, public_ids, callback) { + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + return call_api("metadata", callback, options, function () { + var params = { + metadata: utils.encode_context(metadata), + public_ids: utils.build_array(public_ids), + timestamp: utils.timestamp(), + type: options.type + }; + return [params]; + }); }; \ No newline at end of file diff --git a/lib-es5/utils/index.js b/lib-es5/utils/index.js index f3a65b90..e20e96b7 100644 --- a/lib-es5/utils/index.js +++ b/lib-es5/utils/index.js @@ -705,6 +705,9 @@ function updateable_resource_params(options) { if (options.context != null) { params.context = utils.encode_context(options.context); } + if (options.metadata != null) { + params.metadata = utils.encode_context(options.metadata); + } if (options.custom_coordinates != null) { params.custom_coordinates = utils.encode_double_array(options.custom_coordinates); } diff --git a/lib-es5/v2/api.js b/lib-es5/v2/api.js index e4798954..450cf5ce 100644 --- a/lib-es5/v2/api.js +++ b/lib-es5/v2/api.js @@ -51,5 +51,13 @@ v1_adapters(exports, api, { update_resources_access_mode_by_tag: 2, update_resources_access_mode_by_ids: 2, search: 1, - delete_derived_by_transformation: 2 + delete_derived_by_transformation: 2, + add_metadata_field: 1, + list_metadata_fields: 1, + delete_metadata_field: 1, + metadata_field_by_field_id: 1, + update_metadata_field: 2, + update_metadata_field_datasource: 2, + delete_datasource_entries: 2, + restore_metadata_field_datasource: 2 }); \ No newline at end of file diff --git a/lib-es5/v2/uploader.js b/lib-es5/v2/uploader.js index 4e49f3bb..844e1a09 100644 --- a/lib-es5/v2/uploader.js +++ b/lib-es5/v2/uploader.js @@ -26,7 +26,8 @@ v1_adapters(exports, uploader, { remove_all_context: 1, replace_tag: 2, create_archive: 0, - create_zip: 0 + create_zip: 0, + update_metadata: 2 }); exports.direct_upload = uploader.direct_upload; diff --git a/lib/api.js b/lib/api.js index 13d17904..278d402b 100644 --- a/lib/api.js +++ b/lib/api.js @@ -498,3 +498,152 @@ exports.update_resources_access_mode_by_ids = function update_resources_access_m ) { return updateResourcesAccessMode(access_mode, "public_ids[]", ids, callback, options); }; + +/** + * Creates a new metadata field definition + * + * @see https://cloudinary.com/documentation/admin_api#create_a_metadata_field + * + * @param {Object} field The field to add + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.add_metadata_field = function add_metadata_field(field, callback, options = {}) { + const params = only(field, "external_id", "type", "label", "mandatory", "default_value", "validation", "datasource"); + options.content_type = "json"; + return call_api("post", ["metadata_fields"], params, callback, options); +}; + +/** + * Returns a list of all metadata field definitions + * + * @see https://cloudinary.com/documentation/admin_api#get_metadata_fields + * + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.list_metadata_fields = function list_metadata_fields(callback, options = {}) { + return call_api("get", ["metadata_fields"], {}, callback, options); +}; + +/** + * Deletes a metadata field definition. + * + * The field should no longer be considered a valid candidate for all other endpoints + * + * @see https://cloudinary.com/documentation/admin_api#delete_a_metadata_field_by_external_id + * + * @param {String} field_external_id The external id of the field to delete + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.delete_metadata_field = function delete_metadata_field(field_external_id, callback, options = {}) { + return call_api("delete", ["metadata_fields", field_external_id], {}, callback, options); +}; + +/** + * Get a metadata field by external id + * + * @see https://cloudinary.com/documentation/admin_api#get_a_metadata_field_by_external_id + * + * @param {String} external_id The ID of the metadata field to retrieve + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.metadata_field_by_field_id = function metadata_field_by_field_id(external_id, callback, options = {}) { + return call_api("get", ["metadata_fields", external_id], {}, callback, options); +}; + +/** + * Updates a metadata field by external id + * + * Updates a metadata field definition (partially, no need to pass the entire object) passed as JSON data. + * See {@link https://cloudinary.com/documentation/admin_api#generic_structure_of_a_metadata_field Generic structure of a metadata field} for details. + * + * @see https://cloudinary.com/documentation/admin_api#update_a_metadata_field_by_external_id + * + * @param {String} external_id The ID of the metadata field to update + * @param {Object} field Updated values of metadata field + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.update_metadata_field = function update_metadata_field(external_id, field, callback, options = {}) { + const params = only(field, "external_id", "type", "label", "mandatory", "default_value", "validation", "datasource"); + options.content_type = "json"; + return call_api("put", ["metadata_fields", external_id], params, callback, options); +}; + +/** + * Updates a metadata field datasource + * + * Updates the datasource of a supported field type (currently only enum and set), passed as JSON data. The + * update is partial: datasource entries with an existing external_id will be updated and entries with new + * external_id’s (or without external_id’s) will be appended. + * + * @see https://cloudinary.com/documentation/admin_api#update_a_metadata_field_datasource + * + * @param {String} field_external_id The ID of the field to update + * @param {Object} entries_external_id Updated values for datasource + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.update_metadata_field_datasource = function update_metadata_field_datasource(field_external_id, entries_external_id, callback, options = {}) { + const params = only(entries_external_id, "values"); + options.content_type = "json"; + return call_api("put", ["metadata_fields", field_external_id, "datasource"], params, callback, options); +}; + +/** + * Deletes entries in a metadata field datasource + * + * Deletes (blocks) the datasource entries for a specified metadata field definition. Sets the state of the + * entries to inactive. This is a soft delete, the entries still exist under the hood and can be activated again + * with the restore datasource entries method. + * + * @see https://cloudinary.com/documentation/admin_api#delete_entries_in_a_metadata_field_datasource + * + * @param {String} field_external_id The ID of the metadata field + * @param {Array} entries_external_id An array of IDs of datasource entries to delete + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.delete_datasource_entries = function delete_datasource_entries(field_external_id, entries_external_id, callback, options = {}) { + options.content_type = "json"; + const params = { external_ids: entries_external_id }; + return call_api("delete", ["metadata_fields", field_external_id, "datasource"], params, callback, options); +}; + +/** + * Restores entries in a metadata field datasource + * + * Restores (unblocks) any previously deleted datasource entries for a specified metadata field definition. + * Sets the state of the entries to active. + * + * @see https://cloudinary.com/documentation/admin_api#restore_entries_in_a_metadata_field_datasource + * + * @param {String} field_external_id The ID of the metadata field + * @param {Array} entries_external_id An array of IDs of datasource entries to delete + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.restore_metadata_field_datasource = function restore_metadata_field_datasource(field_external_id, entries_external_id, callback, options = {}) { + options.content_type = "json"; + const params = { external_ids: entries_external_id }; + return call_api("post", ["metadata_fields", field_external_id, "datasource_restore"], params, callback, options); +}; diff --git a/lib/uploader.js b/lib/uploader.js index ad45a7f6..0719b363 100644 --- a/lib/uploader.js +++ b/lib/uploader.js @@ -555,3 +555,26 @@ exports.unsigned_image_upload_tag = function unsigned_image_upload_tag(field, up upload_preset: upload_preset, })); }; + + +/** + * Populates metadata fields with the given values. Existing values will be overwritten. + * + * @param {Object} metadata A list of custom metadata fields (by external_id) and the values to assign to each + * @param {Array} public_ids The public IDs of the resources to update + * @param {Function} callback Callback function + * @param {Object} options Configuration options + * + * @return {Object} + */ +exports.update_metadata = function update_metadata(metadata, public_ids, callback, options = {}) { + return call_api("metadata", callback, options, function () { + let params = { + metadata: utils.encode_context(metadata), + public_ids: utils.build_array(public_ids), + timestamp: utils.timestamp(), + type: options.type, + }; + return [params]; + }); +}; diff --git a/lib/utils/index.js b/lib/utils/index.js index 42be1e00..de044c2c 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -692,6 +692,9 @@ function updateable_resource_params(options, params = {}) { if (options.context != null) { params.context = utils.encode_context(options.context); } + if (options.metadata != null) { + params.metadata = utils.encode_context(options.metadata); + } if (options.custom_coordinates != null) { params.custom_coordinates = utils.encode_double_array(options.custom_coordinates); } diff --git a/lib/v2/api.js b/lib/v2/api.js index 16cd05cb..4e5f16da 100644 --- a/lib/v2/api.js +++ b/lib/v2/api.js @@ -50,4 +50,12 @@ v1_adapters(exports, api, { update_resources_access_mode_by_ids: 2, search: 1, delete_derived_by_transformation: 2, + add_metadata_field: 1, + list_metadata_fields: 1, + delete_metadata_field: 1, + metadata_field_by_field_id: 1, + update_metadata_field: 2, + update_metadata_field_datasource: 2, + delete_datasource_entries: 2, + restore_metadata_field_datasource: 2, }); diff --git a/lib/v2/uploader.js b/lib/v2/uploader.js index 738db374..84386b69 100644 --- a/lib/v2/uploader.js +++ b/lib/v2/uploader.js @@ -25,6 +25,7 @@ v1_adapters(exports, uploader, { replace_tag: 2, create_archive: 0, create_zip: 0, + update_metadata: 2, }); exports.direct_upload = uploader.direct_upload; diff --git a/test/api_spec.js b/test/api_spec.js index 205096a3..080f6f87 100644 --- a/test/api_spec.js +++ b/test/api_spec.js @@ -11,13 +11,12 @@ const Q = require('q'); const cloudinary = require("../cloudinary"); const helper = require("./spechelper"); -const { merge } = cloudinary.utils; const sharedExamples = helper.sharedExamples; const itBehavesLike = helper.itBehavesLike; const TEST_TAG = helper.TEST_TAG; -const IMAGE_FILE = helper.IMAGE_FILE; const UPLOAD_TAGS = helper.UPLOAD_TAGS; const uploadImage = helper.uploadImage; +const TEST_ID = helper.TEST_ID; const SUFFIX = helper.SUFFIX; const PUBLIC_ID_PREFIX = "npm_api_test"; const PUBLIC_ID = PUBLIC_ID_PREFIX + SUFFIX; @@ -45,6 +44,9 @@ const EXPLICIT_TRANSFORMATION2 = { crop: "scale", overlay: `text:Arial_60:${TEST_TAG}`, }; +const METADATA_EXTERNAL_ID_UPLOAD = "metadata_upload_" + TEST_ID; +const METADATA_EXTERNAL_ID_UPDATE = "metadata_uploader_update_" + TEST_ID; +const METADATA_EXTERNAL_ID_EXPLICIT = "metadata_explicit_" + TEST_ID; sharedExamples("a list with a cursor", function (testFunc, ...args) { specify(":max_results", function () { @@ -128,34 +130,30 @@ describe("api", function () { before(function () { this.timeout(helper.TIMEOUT_LONG); return Q.allSettled([ - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID, - tags: UPLOAD_TAGS, - context: "key=value", - eager: [EXPLICIT_TRANSFORMATION], - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID_2, - tags: UPLOAD_TAGS, - context: "key=value", - eager: [EXPLICIT_TRANSFORMATION], - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID_5, - tags: UPLOAD_TAGS, - context: `${contextKey}=test`, - eager: [EXPLICIT_TRANSFORMATION], - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID_6, - tags: UPLOAD_TAGS, - context: `${contextKey}=alt-test`, - eager: [EXPLICIT_TRANSFORMATION], - }), + uploadImage({ + public_id: PUBLIC_ID, + tags: UPLOAD_TAGS, + context: "key=value", + eager: [EXPLICIT_TRANSFORMATION], + }), + uploadImage({ + public_id: PUBLIC_ID_2, + tags: UPLOAD_TAGS, + context: "key=value", + eager: [EXPLICIT_TRANSFORMATION], + }), + uploadImage({ + public_id: PUBLIC_ID_5, + tags: UPLOAD_TAGS, + context: `${contextKey}=test`, + eager: [EXPLICIT_TRANSFORMATION], + }), + uploadImage({ + public_id: PUBLIC_ID_6, + tags: UPLOAD_TAGS, + context: `${contextKey}=alt-test`, + eager: [EXPLICIT_TRANSFORMATION], + }), ]).finally(function () {}); }); after(function () { @@ -188,7 +186,7 @@ describe("api", function () { var publicId; this.timeout(helper.TIMEOUT_MEDIUM); publicId = ''; - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ tags: UPLOAD_TAGS, }).then(function (result) { publicId = result.public_id; @@ -201,7 +199,7 @@ describe("api", function () { }); it("should allow listing resources by type", function () { this.timeout(helper.TIMEOUT_MEDIUM); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ tags: UPLOAD_TAGS, }).then( ({ public_id }) => cloudinary.v2.api.resources({ type: "upload" }) @@ -294,7 +292,7 @@ describe("api", function () { }); it("should allow get resource metadata", function () { this.timeout(helper.TIMEOUT_LONG); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ tags: UPLOAD_TAGS, eager: [EXPLICIT_TRANSFORMATION], }).then(({ public_id }) => cloudinary.v2.api.resource(public_id) @@ -321,7 +319,7 @@ describe("api", function () { describe("delete", function () { it("should allow deleting derived resource", function () { this.timeout(helper.TIMEOUT_MEDIUM); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ tags: UPLOAD_TAGS, eager: [ { @@ -349,25 +347,21 @@ describe("api", function () { it("should allow deleting derived resources by transformations", function () { this.timeout(helper.TIMEOUT_LONG); return Q.all([ - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID_1, - tags: UPLOAD_TAGS, - eager: [EXPLICIT_TRANSFORMATION], - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID_2, - tags: UPLOAD_TAGS, - eager: [EXPLICIT_TRANSFORMATION2], - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: PUBLIC_ID_3, - tags: UPLOAD_TAGS, - eager: [EXPLICIT_TRANSFORMATION, - EXPLICIT_TRANSFORMATION2], - }), + uploadImage({ + public_id: PUBLIC_ID_1, + tags: UPLOAD_TAGS, + eager: [EXPLICIT_TRANSFORMATION], + }), + uploadImage({ + public_id: PUBLIC_ID_2, + tags: UPLOAD_TAGS, + eager: [EXPLICIT_TRANSFORMATION2], + }), + uploadImage({ + public_id: PUBLIC_ID_3, + tags: UPLOAD_TAGS, + eager: [EXPLICIT_TRANSFORMATION, EXPLICIT_TRANSFORMATION2], + }), ]).then(() => cloudinary.v2.api.delete_derived_by_transformation( [PUBLIC_ID_1, PUBLIC_ID_3], [EXPLICIT_TRANSFORMATION, EXPLICIT_TRANSFORMATION2] )).then( @@ -385,7 +379,7 @@ describe("api", function () { }); it("should allow deleting resources", function () { this.timeout(helper.TIMEOUT_MEDIUM); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ public_id: PUBLIC_ID_3, tags: UPLOAD_TAGS, }).then( @@ -406,7 +400,7 @@ describe("api", function () { itBehavesLike("accepts next_cursor", cloudinary.v2.api.delete_resources_by_prefix, "prefix_foobar"); return it("should allow deleting resources by prefix", function () { this.timeout(helper.TIMEOUT_MEDIUM); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ public_id: "api_test_by_prefix", tags: UPLOAD_TAGS, }).then( @@ -429,7 +423,7 @@ describe("api", function () { itBehavesLike("accepts next_cursor", cloudinary.v2.api.delete_resources_by_prefix, deleteTestTag); it("should allow deleting resources by tags", function () { this.timeout(helper.TIMEOUT_MEDIUM); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ public_id: PUBLIC_ID_4, tags: UPLOAD_TAGS.concat([deleteTestTag]) }).then( @@ -715,7 +709,7 @@ describe("api", function () { }); it("should support changing moderation status with notification-url", function () { this.timeout(helper.TIMEOUT_LONG); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ moderation: "manual", }).then(upload_result => cloudinary.v2.api.update(upload_result.public_id, { moderation_status: "approved", @@ -730,7 +724,7 @@ describe("api", function () { }); it("should support setting manual moderation status", function () { this.timeout(helper.TIMEOUT_LONG); - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ moderation: "manual", }).then(upload_result => cloudinary.v2.api.update(upload_result.public_id, { moderation_status: "approved", @@ -807,6 +801,85 @@ describe("api", function () { }); }); }); + describe("structured metadata fields", function () { + this.timeout(helper.TIMEOUT_LONG); + const METADATA_VALUE = "123456"; + before(function () { + return Q.allSettled( + [ + cloudinary.v2.api.add_metadata_field({ + external_id: METADATA_EXTERNAL_ID_UPDATE, + label: "subject", + type: "string", + }), + cloudinary.v2.api.add_metadata_field({ + external_id: METADATA_EXTERNAL_ID_UPLOAD, + label: "input", + type: "string", + }), + cloudinary.v2.api.add_metadata_field({ + external_id: METADATA_EXTERNAL_ID_EXPLICIT, + label: "field", + type: "string", + }), + ] + ).finally(function () {}); + }); + after(function () { + return Q.allSettled( + [ + cloudinary.v2.api.delete_metadata_field(METADATA_EXTERNAL_ID_UPDATE), + cloudinary.v2.api.delete_metadata_field(METADATA_EXTERNAL_ID_UPLOAD), + cloudinary.v2.api.delete_metadata_field(METADATA_EXTERNAL_ID_EXPLICIT), + ] + ).finally(function () {}); + }); + it("should be updatable with uploader.update_metadata", function () { + let publicId; + return uploadImage({ + tags: [TEST_TAG], + }) + .then((result) => { + publicId = result.public_id; + return cloudinary.v2.uploader.update_metadata({ [METADATA_EXTERNAL_ID_UPDATE]: METADATA_VALUE }, [publicId]); + }) + .then((result) => { + expect(result).not.to.be.empty(); + expect(result.public_ids[0]).to.eql(publicId); + return cloudinary.v2.api.resource(publicId); + }) + .then((result) => { + expect(result.metadata[METADATA_EXTERNAL_ID_UPDATE]).to.eql(METADATA_VALUE); + }); + }); + it("should be supported when uploading a resource with metadata", function () { + return uploadImage({ + tags: [TEST_TAG], + metadata: { [METADATA_EXTERNAL_ID_UPLOAD]: METADATA_VALUE }, + }).then((result) => { + expect(result).not.to.be.empty(); + return cloudinary.v2.api.resource(result.public_id); + }).then((result) => { + expect(result.metadata[METADATA_EXTERNAL_ID_UPLOAD]).to.eql(METADATA_VALUE); + }); + }); + it("should be supported when calling explicit with metadata", function () { + return uploadImage({ + tags: [TEST_TAG], + }).then((result) => { + return cloudinary.v2.uploader.explicit(result.public_id, { + type: "upload", + tags: [TEST_TAG], + metadata: { [METADATA_EXTERNAL_ID_EXPLICIT]: METADATA_VALUE }, + }); + }).then(function (result) { + expect(result).not.to.be.empty(); + return cloudinary.v2.api.resource(result.public_id); + }).then((result) => { + expect(result.metadata[METADATA_EXTERNAL_ID_EXPLICIT]).to.eql(METADATA_VALUE); + }); + }); + }); it("should support listing by moderation kind and value", function () { itBehavesLike("a list with a cursor", cloudinary.v2.api.resources_by_moderation, "manual", "approved"); return helper.mockPromise((xhr, write, request) => ["approved", "pending", "rejected"].forEach((stat) => { @@ -830,31 +903,26 @@ describe("api", function () { it("should list folders in cloudinary", function () { this.timeout(helper.TIMEOUT_LONG); return Q.all([ - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: 'test_folder1/item', - tags: UPLOAD_TAGS, - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: 'test_folder2/item', - tags: UPLOAD_TAGS, - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: 'test_folder2/item', - tags: UPLOAD_TAGS, - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: 'test_folder1/test_subfolder1/item', - tags: UPLOAD_TAGS, - }), - cloudinary.v2.uploader.upload(IMAGE_FILE, - { - public_id: 'test_folder1/test_subfolder2/item', - tags: UPLOAD_TAGS, - }), + uploadImage({ + public_id: 'test_folder1/item', + tags: UPLOAD_TAGS, + }), + uploadImage({ + public_id: 'test_folder2/item', + tags: UPLOAD_TAGS, + }), + uploadImage({ + public_id: 'test_folder2/item', + tags: UPLOAD_TAGS, + }), + uploadImage({ + public_id: 'test_folder1/test_subfolder1/item', + tags: UPLOAD_TAGS, + }), + uploadImage({ + public_id: 'test_folder1/test_subfolder2/item', + tags: UPLOAD_TAGS, + }), ]).then(function (results) { return Q.all([cloudinary.v2.api.root_folders(), cloudinary.v2.api.sub_folders('test_folder1')]); }).then(function (results) { @@ -885,7 +953,7 @@ describe("api", function () { this.timeout(helper.TIMEOUT_MEDIUM); const folderPath= "test_folder/delete_folder/"+TEST_TAG; before(function(){ - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ folder: folderPath, tags: UPLOAD_TAGS }).delay(2 * 1000).then(function() { @@ -910,7 +978,7 @@ describe("api", function () { this.timeout(helper.TIMEOUT_MEDIUM); const publicId = "api_test_restore" + SUFFIX; - before(() => cloudinary.v2.uploader.upload(IMAGE_FILE, { + before(() => uploadImage({ public_id: publicId, backup: true, tags: UPLOAD_TAGS, @@ -981,7 +1049,7 @@ describe("api", function () { idsToDelete = []; beforeEach(function () { publishTestTag = TEST_TAG + i++; - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ type: "authenticated", tags: UPLOAD_TAGS.concat([publishTestTag]) }).then((result) => { @@ -1046,7 +1114,7 @@ describe("api", function () { access_mode_tag = ''; beforeEach(function () { access_mode_tag = TEST_TAG + "access_mode" + i++; - return cloudinary.v2.uploader.upload(IMAGE_FILE, { + return uploadImage({ access_mode: "authenticated", tags: UPLOAD_TAGS.concat([access_mode_tag]), }).then((result) => { diff --git a/test/spechelper.js b/test/spechelper.js index 5eb43a6a..953f3cf8 100644 --- a/test/spechelper.js +++ b/test/spechelper.js @@ -11,7 +11,7 @@ const https = require('https'); const cloudinary = require("../cloudinary"); const { utils, config, Cache } = cloudinary; -const { isEmpty } = utils; +const { isEmpty, includes } = utils; const libPath = Number(process.versions.node.split('.')[0]) < 8 ? 'lib-es5' : 'lib'; const FileKeyValueStorage = require(`../${libPath}/cache/FileKeyValueStorage`); @@ -26,7 +26,8 @@ exports.TIMEOUT_LONG = 50000; exports.SUFFIX = process.env.TRAVIS_JOB_ID || Math.floor(Math.random() * 999999); exports.SDK_TAG = "SDK_TEST"; // identifies resources created by all SDKs tests exports.TEST_TAG_PREFIX = "cloudinary_npm_test"; // identifies resources created by this SDK's tests -exports.TEST_TAG = exports.TEST_TAG_PREFIX + "_" + exports.SUFFIX; // identifies resources created in the current test run +exports.TEST_TAG = exports.TEST_TAG_PREFIX + "_" + exports.SUFFIX; // identifies resources created in the current test run with a unique tag +exports.TEST_ID = exports.TEST_TAG; // identifies resources created in the current test run with a unique id exports.UPLOAD_TAGS = [exports.TEST_TAG, exports.TEST_TAG_PREFIX, exports.SDK_TAG]; exports.IMAGE_FILE = "test/resources/logo.png"; exports.LARGE_RAW_FILE = "test/resources/TheCompleteWorksOfShakespeare.mobi"; @@ -91,6 +92,60 @@ expect.Assertion.prototype.beServedByCloudinary = function (done) { return this; }; +/** + * Asserts that a given object is a metadata field. + * Optionally tests the values in the metadata field for equality + * + * @returns {expect.Assertion} + */ +expect.Assertion.prototype.beAMetadataField = function () { + let metadataField, expectedValues; + if (Array.isArray(this.obj)) { + [metadataField, expectedValues] = this.obj; + } else { + metadataField = this.obj; + } + // Check that all mandatory keys exist + const mandatoryKeys = ['type', 'external_id', 'label', 'mandatory', 'default_value', 'validation']; + mandatoryKeys.forEach((key) => { + this.assert(key in metadataField, function () { + return `expected metadata field to contain mandatory field: ${key}`; + }, function () { + return `expected metadata field not to contain a ${key} field`; + }); + }); + + // If type is enum or set test it + if (includes(['enum', 'set'], metadataField.type)) { + this.assert('datasource' in metadataField, function () { + return `expected metadata field of type ${metadataField.type} to contain a datasource field`; + }, function () { + return `expected metadata field of type ${metadataField.type} not to contain a datasource field`; + }); + } + + // Make sure type is acceptable + const acceptableTypes = ['string', 'integer', 'date', 'enum', 'set']; + this.assert(includes(acceptableTypes, metadataField.type), function () { + return `expected metadata field type to be one of ${acceptableTypes.join(', ')}. Unknown field type ${metadataField.type} received`; + }, function () { + return `expected metadata field not to be of a certain type`; + }); + + // Verify object values + if (expectedValues) { + Object.entries(expectedValues).forEach(([key, value]) => { + this.assert(metadataField[key] === value, function () { + return `expected metadata field's ${key} to equal ${value} but got ${metadataField[key]} instead`; + }, function () { + return `expected metadata field's ${key} not to equal ${value}`; + }); + }); + } + + return this; +}; + const allExamples = {}; function sharedExamples(name, examples) { diff --git a/test/structured_metadata_spec.js b/test/structured_metadata_spec.js new file mode 100644 index 00000000..e1c6e901 --- /dev/null +++ b/test/structured_metadata_spec.js @@ -0,0 +1,357 @@ +const expect = require("expect.js"); +const Q = require('q'); +const cloudinary = require("../cloudinary"); +const helper = require("./spechelper"); + +const TEST_ID = helper.TEST_ID; +const TEST_TAG = helper.TEST_TAG; +const UPLOAD_TAGS = helper.UPLOAD_TAGS; +const uploadImage = helper.uploadImage; +const EXTERNAL_ID_CREATE = 'metadata_create_' + TEST_ID; +const EXTERNAL_ID_CREATE_2 = 'metadata_create_2_' + TEST_ID; +const EXTERNAL_ID_DATE_VALIDATION = 'metadata_validate_date_' + TEST_ID; +const EXTERNAL_ID_DATE_VALIDATION_2 = 'metadata_validate_date_2_' + TEST_ID; +const EXTERNAL_ID_GET_LIST = 'metadata_list_' + TEST_ID; +const EXTERNAL_ID_GET_FIELD = 'metadata_get_by_id_' + TEST_ID; +const EXTERNAL_ID_UPDATE_BY_ID = 'metadata_update_by_id_' + TEST_ID; +const EXTERNAL_ID_DELETE = 'metadata_delete_' + TEST_ID; +const EXTERNAL_ID_UPDATE_DATASOURCE = 'metadata_datasource_update_' + TEST_ID; +const EXTERNAL_ID_DELETE_DATASOURCE_ENTRIES = 'metadata_delete_datasource_entries_' + TEST_ID; +const EXTERNAL_ID_RESTORE_DATASOURCE_ENTRIES = 'metadata_restore_datasource_entries_' + TEST_ID; +const EXTERNAL_ID_UPDATE = 'metadata_update_' + TEST_ID; +const PUBLIC_ID_UPLOAD = "metadata_upload_" + TEST_ID; +const LABEL_INT_1 = 'metadata_label_1_' + TEST_ID; +const LABEL_INT_2 = 'metadata_label_2_' + TEST_ID; +const LABEL_INT_3 = 'metadata_label_3_' + TEST_ID; +const LABEL_INT_4 = 'metadata_label_4_' + TEST_ID; +const LABEL_SET_1 = 'metadata_set_1_' + TEST_ID; +const LABEL_SET_2 = 'metadata_set_2_' + TEST_ID; +const LABEL_SET_3 = 'metadata_set_3_' + TEST_ID; +const LABEL_STRING_1 = 'metadata_string_1_' + TEST_ID; +const LABEL_STRING_2 = 'metadata_string_2_' + TEST_ID; +const LABEL_STRING_3 = 'metadata_string_3_' + TEST_ID; +const LABEL_DATE = 'metadata_date_' + TEST_ID; + +const api = cloudinary.v2.api; + +describe("structured metadata api", function () { + this.timeout(helper.TIMEOUT_MEDIUM); + + before(function () { + return Q.allSettled( + [ + api.add_metadata_field({ + external_id: EXTERNAL_ID_GET_LIST, + label: LABEL_INT_1, + type: "integer", + default_value: 10, + }), + api.add_metadata_field({ + external_id: EXTERNAL_ID_GET_FIELD, + label: LABEL_INT_2, + type: "integer", + default_value: 1, + }), + api.add_metadata_field({ + external_id: EXTERNAL_ID_UPDATE_BY_ID, + label: LABEL_INT_3, + type: "integer", + default_value: 1, + }), + api.add_metadata_field({ + external_id: EXTERNAL_ID_DELETE, + label: LABEL_INT_4, + type: "integer", + default_value: 6, + }), + api.add_metadata_field({ + external_id: EXTERNAL_ID_UPDATE_DATASOURCE, + label: LABEL_SET_1, + type: "set", + datasource: { + values: [ + { external_id: "color_1", value: "red" }, + { external_id: "color_2", value: "blue" }, + ], + }, + }), + api.add_metadata_field({ + external_id: EXTERNAL_ID_UPDATE, + label: LABEL_STRING_1, + type: "string", + }), + uploadImage({ + public_id: PUBLIC_ID_UPLOAD, + tags: UPLOAD_TAGS, + }), + ] + ).finally(function () {}); + }); + + after(function () { + // Delete all metadata fields created during the test + return Q.allSettled( + [ + EXTERNAL_ID_CREATE, + EXTERNAL_ID_CREATE_2, + EXTERNAL_ID_DATE_VALIDATION, + EXTERNAL_ID_DATE_VALIDATION_2, + EXTERNAL_ID_GET_LIST, + EXTERNAL_ID_GET_FIELD, + EXTERNAL_ID_UPDATE_BY_ID, + EXTERNAL_ID_DELETE, + EXTERNAL_ID_UPDATE_DATASOURCE, + EXTERNAL_ID_DELETE_DATASOURCE_ENTRIES, + EXTERNAL_ID_RESTORE_DATASOURCE_ENTRIES, + EXTERNAL_ID_UPDATE, + ].map(field => api.delete_metadata_field(field)) + ).then(function () { + return api.delete_resources_by_tag(TEST_TAG); + }).finally(function () {}); + }); + + describe("add metadata field", function () { + it("should create metadata", function () { + const metadataFields = [ + { + external_id: EXTERNAL_ID_CREATE, + label: LABEL_STRING_2, + type: "string", + default_value: "blue", + }, { + external_id: EXTERNAL_ID_CREATE_2, + label: LABEL_STRING_3, + type: "string", + }, + ]; + return Q.all(metadataFields.map(field => api.add_metadata_field(field))) + .then((results) => { + expect([results[0], metadataFields[0]]).to.beAMetadataField(); + expect([results[1], metadataFields[1]]).to.beAMetadataField(); + return Q.all( + [ + api.metadata_field_by_field_id(EXTERNAL_ID_CREATE), + api.metadata_field_by_field_id(EXTERNAL_ID_CREATE_2), + ] + ); + }) + .then((results) => { + expect([results[0], metadataFields[0]]).to.beAMetadataField(); + expect([results[1], metadataFields[1]]).to.beAMetadataField(); + }); + }); + + describe("date_field_validation", function () { + const maxValidDate = '2000-01-01'; + const minValidDate = '1950-01-01'; + const validDate = '1980-04-20'; + const invalidDate = '1940-01-20'; + const validMetadata = { + external_id: EXTERNAL_ID_DATE_VALIDATION, + label: LABEL_DATE, + type: "date", + mandatory: true, + default_value: validDate, + validation: { + type: "and", + rules: [ + { + type: "greater_than", + value: minValidDate, + }, { + type: "less_than", + value: maxValidDate, + }, + ], + }, + }; + const invalidMetadata = { + ...validMetadata, + external_id: EXTERNAL_ID_DATE_VALIDATION_2, + default_value: invalidDate, + }; + + it("should create date field when default value validation passes", function () { + return api.add_metadata_field(validMetadata) + .then((result) => { + expect(result).to.beAMetadataField(); + return api.metadata_field_by_field_id(EXTERNAL_ID_DATE_VALIDATION); + }) + .then((result) => { + expect(result).to.beAMetadataField(); + expect(result.default_value).to.eql(validMetadata.default_value); + }); + }); + + it("should not create date field with illegal default value", function () { + return api.add_metadata_field(invalidMetadata).then(() => { + expect().fail(); + }).catch((res) => { + expect(res.error).not.to.be(void 0); + expect(res.error.message).to.contain("default_value is invalid"); + }); + }); + }); + }); + + describe("list_metadata_fields", function () { + it("should return all metadata field definitions", function () { + return api.list_metadata_fields() + .then((result) => { + expect(result).not.to.be.empty(); + expect(result.metadata_fields).not.to.be.empty(); + expect(result.metadata_fields).to.be.an("array"); + result.metadata_fields.forEach((field) => { + expect(field).to.beAMetadataField(); + }); + }); + }); + }); + + describe("metadata_field_by_field_id", function () { + it("should return metadata field by external id", function () { + return api.metadata_field_by_field_id(EXTERNAL_ID_GET_FIELD) + .then((result) => { + expect([result, { external_id: EXTERNAL_ID_GET_FIELD }]).to.beAMetadataField(); + }); + }); + }); + + describe("update_metadata_field", function () { + it("should update metadata field by external id", function () { + const metadataChanges = { + default_value: 10, + }; + return api.update_metadata_field(EXTERNAL_ID_UPDATE_BY_ID, metadataChanges) + .then((result) => { + expect([result, metadataChanges]).to.beAMetadataField(); + return api.metadata_field_by_field_id(EXTERNAL_ID_UPDATE_BY_ID); + }) + .then((result) => { + expect([result, metadataChanges]).to.beAMetadataField(); + }); + }); + }); + + describe("delete_metadata_field", function () { + it("should delete metadata field by external id", function () { + return api.delete_metadata_field(EXTERNAL_ID_DELETE) + .then((result) => { + expect(result).not.to.be.empty(); + expect(result.message).to.eql("ok"); + return api.metadata_field_by_field_id(EXTERNAL_ID_DELETE); + }) + .catch(({ error }) => { + expect(error).not.to.be(void 0); + expect(error.http_code).to.eql(404); + expect(error.message).to.contain(`External ID ${EXTERNAL_ID_DELETE} doesn't exist`); + }); + }); + }); + + describe("update_metadata_field_datasource", function () { + it("should update metadata field datasource by external id", function () { + const datasource_changes = { + values: [ + { external_id: "color_1", value: "brown" }, + { external_id: "color_2", value: "black" }, + ], + }; + return api.update_metadata_field_datasource(EXTERNAL_ID_UPDATE_DATASOURCE, datasource_changes) + .then((result) => { + expect(result).not.to.be.empty(); + return api.metadata_field_by_field_id(EXTERNAL_ID_UPDATE_DATASOURCE); + }) + .then((result) => { + expect(result).to.beAMetadataField(); + result.datasource.values.forEach((item) => { + const old_value = datasource_changes.values.find(val => val.external_id === item.external_id).value; + expect(item.value).to.eql(old_value); + }); + }); + }); + }); + + describe("delete_datasource_entries", function () { + it("should delete entries in metadata field datasource", function () { + const metadata = { + external_id: EXTERNAL_ID_DELETE_DATASOURCE_ENTRIES, + label: LABEL_SET_3, + type: "set", + datasource: { + values: [ + { + external_id: "size_1", + value: "big", + }, + { + external_id: "size_2", + value: "small", + }, + ], + }, + }; + const external_ids_for_deletion = [metadata.datasource.values[0].external_id]; + return api.add_metadata_field(metadata) + .then(() => api.delete_datasource_entries(EXTERNAL_ID_DELETE_DATASOURCE_ENTRIES, external_ids_for_deletion)) + .then((result) => { + expect(result).not.to.be.empty(); + expect(result.values.length).to.eql(1); + expect(result.values[0].external_id).to.eql(metadata.datasource.values[1].external_id); + }); + }); + }); + + describe("restore_metadata_field_datasource", function () { + it("should restore a deleted entry in a metadata field datasource", function () { + const metadata = { + external_id: EXTERNAL_ID_RESTORE_DATASOURCE_ENTRIES, + label: LABEL_SET_2, + type: "set", + datasource: { + values: [ + { + external_id: "size_1", + value: "big", + }, + { + external_id: "size_2", + value: "small", + }, + ], + }, + }; + const DELETED_ENTRY = [metadata.datasource.values[0].external_id]; + return api.add_metadata_field(metadata) + .then(() => api.delete_datasource_entries(EXTERNAL_ID_RESTORE_DATASOURCE_ENTRIES, DELETED_ENTRY)) + .then((result) => { + expect(result).not.to.be.empty(); + expect(result.values.length).to.eql(1); + expect(result.values[0].external_id).to.eql(metadata.datasource.values[1].external_id); + }) + .then(() => api.restore_metadata_field_datasource(EXTERNAL_ID_RESTORE_DATASOURCE_ENTRIES, DELETED_ENTRY)) + .then((result) => { + expect(result).not.to.be.empty(); + expect(result.values.length).to.eql(2); + expect(result.values[0].external_id).to.eql(metadata.datasource.values[0].external_id); + expect(result.values[1].external_id).to.eql(metadata.datasource.values[1].external_id); + }); + }); + }); + + describe("api.update", function () { + const METADATA_VALUE = "123456"; + it("should update metadata", function () { + return api.update(PUBLIC_ID_UPLOAD, { + metadata: { [EXTERNAL_ID_UPDATE]: METADATA_VALUE }, + }) + .then((result) => { + expect(result).not.to.be.empty(); + return api.resource(PUBLIC_ID_UPLOAD); + }) + .then((result) => { + expect(result.metadata[EXTERNAL_ID_UPDATE]).to.eql(METADATA_VALUE); + }); + }); + }); +}); diff --git a/test/uploader_spec.js b/test/uploader_spec.js index bc4f74db..4c1299dd 100644 --- a/test/uploader_spec.js +++ b/test/uploader_spec.js @@ -251,7 +251,7 @@ describe("uploader", function () { }); }); }); - describe("text images", function() { + describe("text images", function () { it("should successfully generate text image", function () { return cloudinary.v2.uploader.text("hello world", { tags: UPLOAD_TAGS, @@ -261,13 +261,13 @@ describe("uploader", function () { }); }); var mocked = helper.mockTest(); - it("should pass text image parameters to server", function() { + it("should pass text image parameters to server", function () { cloudinary.v2.uploader.text("hello word", - { - font_family: "Arial", - font_size: 12, - font_weight: "black" - }); + { + font_family: "Arial", + font_size: 12, + font_weight: "black", + }); sinon.assert.calledWith(mocked.write, sinon.match(helper.uploadParamMatcher("font_family", "Arial"))); sinon.assert.calledWith(mocked.write, sinon.match(helper.uploadParamMatcher("font_size", "12"))); sinon.assert.calledWith(mocked.write, sinon.match(helper.uploadParamMatcher("font_weight", "black")));