diff --git a/docs/metadata.md b/docs/metadata.md index 9d9c1a0f..7959a3a0 100644 --- a/docs/metadata.md +++ b/docs/metadata.md @@ -32,13 +32,11 @@ in a flexible way, without pre-defined template structure. - [Update Metadata Template](#update-metadata-template) - [Get Enterprise Metadata Templates](#get-enterprise-metadata-templates) - [Delete Metadata Template](#delete-metadata-template) -- [Add Metadata to a File](#add-metadata-to-a-file) +- [Set Metadata on a File](#set-metadata-on-a-file) - [Get Metadata on a File](#get-metadata-on-a-file) -- [Update Metadata on a File](#update-metadata-on-a-file) - [Remove Metadata from a File](#remove-metadata-from-a-file) -- [Add Metadata to a Folder](#add-metadata-to-a-folder) +- [Set Metadata on a Folder](#set-metadata-on-a-folder) - [Get Metadata on a Folder](#get-metadata-on-a-folder) -- [Update Metadata on a Folder](#update-metadata-on-a-folder) - [Remove Metadata from a Folder](#remove-metadata-from-a-folder) - [Create Cascade Policy](#create-cascade-policy) - [Get Cascade Policy](#get-cascade-policy) @@ -329,13 +327,52 @@ method with the template scope and template name. client.metadata.deleteTemplate('enterprise', 'testtemplate', callback); ``` -Add Metadata to a File +Set Metadata on a File ---------------------- -Metadata can be created on a file by calling -[`files.addMetadata(fileID, scope, template, metadata, callback)`](http://opensource.box.com/box-node-sdk/jsdoc/Files.html#addMetadata) +To set metadata on a file, call [`files.setMetadata(fileID, scope, template, metadata, callback)`][set-metadata] +with the scope and template key of the metadata template, as well as an `Object` containing the metadata keys +and values to set. + +> __Note:__ This method will unconditionally apply the provided metadata, overwriting existing metadata +> for the keys provided. To specifically create or update metadata, see the `addMetadata()` and `updateMetadata()` +> methods below. + +```js +var metadataValues = { + audience: "internal", + documentType: "Q1 plans", + competitiveDocument: "no", + status: "active", + author: "Jones", + currentState: "proposal" +}; +client.files.setMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", metadataValues) + .then(metadata => { + /* metadata -> { + audience: 'internal', + documentType: 'Q1 plans', + competitiveDocument: 'no', + status: 'active', + author: 'Jones', + currentState: 'proposal', + '$type': 'marketingCollateral-d086c908-2498-4d3e-8a1f-01e82bfc2abe', + '$parent': 'file_11111', + '$id': '2094c584-68e1-475c-a581-534a4609594e', + '$version': 0, + '$typeVersion': 0, + '$template': 'marketingCollateral', + '$scope': 'enterprise_12345' } + */ + }); +``` + +To add new metadata to a file, call [`files.addMetadata(fileID, scope, template, metadata, callback)`][add-metadata] with a metadata template and an object of key/value pairs to add as metadata. +> __Note:__: This method will only succeed if the provided metadata template is not current applied to the file, +> otherwise it will fail with a Conflict error. + ```js var metadataValues = { audience: "internal", @@ -365,6 +402,52 @@ client.files.addMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingC }); ``` +Update a file's existing metadata by calling +[`files.updateMetadata(fileID, scope, template, patch, callback)`][update-metadata] +with an array of [JSON Patch](http://jsonpatch.com/) formatted operations. + +> __Note:__ This method will only succeed if the provided metadata template has already been applied to +> the file; if the file does not have existing metadata, this method will fail with a Not Found error. +> This is useful in cases where you know the file will already have metadata applied, since it will +> save an API call compared to `setMetadata()`. + +```js +var updates = [ + { op: 'test', path: '/competitiveDocument', value: 'no' }, + { op: 'remove', path: '/competitiveDocument' }, + { op: 'test', path: '/status', value: 'active' }, + { op: 'replace', path: '/status', value: 'inactive' }, + { op: 'test', path: '/author', value: 'Jones' }, + { op: 'copy', from: '/author', path: '/editor' }, + { op: 'test', path: '/currentState', value: 'proposal' }, + { op: 'move', from: '/currentState', path: '/previousState' }, + { op: 'add', path: '/currentState', value: 'reviewed' } +]; +client.files.updateMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", updates) + .then(metadata => { + /* metadata -> { + audience: 'internal', + documentType: 'Q1 plans', + status: 'inactive', + author: 'Jones', + '$type': 'marketingCollateral-d086c908-2498-4d3e-8a1f-01e82bfc2abe', + '$parent': 'file_11111', + '$id': '2094c584-68e1-475c-a581-534a4609594e', + '$version': 1, + '$typeVersion': 0, + editor: 'Jones', + previousState: 'proposal', + currentState: 'reviewed', + '$template': 'marketingCollateral', + '$scope': 'enterprise_12345' } + */ + }); +``` + +[set-metadata]: http://opensource.box.com/box-node-sdk/jsdoc/Files.html#setMetadata +[add-metadata]: http://opensource.box.com/box-node-sdk/jsdoc/Files.html#addMetadata +[update-metadata]: http://opensource.box.com/box-node-sdk/jsdoc/Files.html#updateMetadata + Get Metadata on a File ---------------------- @@ -432,66 +515,66 @@ client.files.getAllMetadata('11111') }); ``` -Update Metadata on a File -------------------------- +Remove Metadata from a File +--------------------------- -Update a file's metadata by calling -[`files.updateMetadata(fileID, scope, template, patch, callback)`](http://opensource.box.com/box-node-sdk/jsdoc/Files.html#updateMetadata) -with an array of [JSON Patch](http://jsonpatch.com/) formatted operations. +A metadata template can be removed from a file by calling +[`files.deleteMetadata(fileID, scope, template, callback)`](http://opensource.box.com/box-node-sdk/jsdoc/Files.html#deleteMetadata). ```js -var updates = [ - { op: 'test', path: '/competitiveDocument', value: 'no' }, - { op: 'remove', path: '/competitiveDocument' }, - { op: 'test', path: '/status', value: 'active' }, - { op: 'replace', path: '/status', value: 'inactive' }, - { op: 'test', path: '/author', value: 'Jones' }, - { op: 'copy', from: '/author', path: '/editor' }, - { op: 'test', path: '/currentState', value: 'proposal' }, - { op: 'move', from: '/currentState', path: '/previousState' }, - { op: 'add', path: '/currentState', value: 'reviewed' } -]; -client.files.updateMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", updates) +client.files.deleteMetadata('67890', client.metadata.scopes.GLOBAL, client.metadata.templates.PROPERTIES) + .then(() => { + // removal succeeded — no value returned + });; +``` + +Set Metadata on a Folder +------------------------ + +To set metadata on a folder, call [`folders.setMetadata(folderID, scope, template, metadata, callback)`][set-metadata] +with the scope and template key of the metadata template, as well as an `Object` containing the metadata keys +and values to set. + +> __Note:__ This method will unconditionally apply the provided metadata, overwriting existing metadata +> for the keys provided. To specifically create or update metadata, see the `addMetadata()` and `updateMetadata()` +> methods below. + +```js +var metadataValues = { + audience: "internal", + documentType: "Q1 plans", + competitiveDocument: "no", + status: "active", + author: "Jones", + currentState: "proposal" +}; +client.folders.setMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", metadataValues) .then(metadata => { /* metadata -> { audience: 'internal', documentType: 'Q1 plans', - status: 'inactive', + competitiveDocument: 'no', + status: 'active', author: 'Jones', + currentState: 'proposal', '$type': 'marketingCollateral-d086c908-2498-4d3e-8a1f-01e82bfc2abe', - '$parent': 'file_11111', + '$parent': 'folder_11111', '$id': '2094c584-68e1-475c-a581-534a4609594e', - '$version': 1, + '$version': 0, '$typeVersion': 0, - editor: 'Jones', - previousState: 'proposal', - currentState: 'reviewed', '$template': 'marketingCollateral', '$scope': 'enterprise_12345' } */ }); ``` -Remove Metadata from a File ---------------------------- - -A metadata template can be removed from a file by calling -[`files.deleteMetadata(fileID, scope, template, callback)`](http://opensource.box.com/box-node-sdk/jsdoc/Files.html#deleteMetadata). - -```js -client.files.deleteMetadata('67890', client.metadata.scopes.GLOBAL, client.metadata.templates.PROPERTIES) - .then(() => { - // removal succeeded — no value returned - });; -``` - -Add Metadata to a Folder ------------------------- - -Metadata can be created on a folder by calling -[`folders.addMetadata(folderID, scope, template, metadata, callback)`](http://opensource.box.com/box-node-sdk/jsdoc/Folders.html#addMetadata) +To add new metadata to a folder, call +[`folders.addMetadata(folderID, scope, template, metadata, callback)`][add-metadata] with a metadata template and an object of key/value pairs to add as metadata. +> __Note:__: This method will only succeed if the provided metadata template is not current applied to the folder, +> otherwise it will fail with a Conflict error. + ```js var metadataValues = { audience: "internal", @@ -501,7 +584,7 @@ var metadataValues = { author: "Jones", currentState: "proposal" }; -client.folders.addMetadata('11111', client.metadata.scopes.ENTERPRISE, 'marketingCollateral', metadataValues); +client.folders.addMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", metadataValues) .then(metadata => { /* metadata -> { audience: 'internal', @@ -521,6 +604,52 @@ client.folders.addMetadata('11111', client.metadata.scopes.ENTERPRISE, 'marketin }); ``` +Update a folder's existing metadata by calling +[`folders.updateMetadata(fileID, scope, template, patch, callback)`][update-metadata] +with an array of [JSON Patch](http://jsonpatch.com/) formatted operations. + +> __Note:__ This method will only succeed if the provided metadata template has already been applied to +> the folder; if the folder does not have existing metadata, this method will fail with a Not Found error. +> This is useful in cases where you know the folder will already have metadata applied, since it will +> save an API call compared to `setMetadata()`. + +```js +var updates = [ + { op: 'test', path: '/competitiveDocument', value: 'no' }, + { op: 'remove', path: '/competitiveDocument' }, + { op: 'test', path: '/status', value: 'active' }, + { op: 'replace', path: '/status', value: 'inactive' }, + { op: 'test', path: '/author', value: 'Jones' }, + { op: 'copy', from: '/author', path: '/editor' }, + { op: 'test', path: '/currentState', value: 'proposal' }, + { op: 'move', from: '/currentState', path: '/previousState' }, + { op: 'add', path: '/currentState', value: 'reviewed' } +]; +client.folders.updateMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", updates) + .then(metadata => { + /* metadata -> { + audience: 'internal', + documentType: 'Q1 plans', + status: 'inactive', + author: 'Jones', + '$type': 'marketingCollateral-d086c908-2498-4d3e-8a1f-01e82bfc2abe', + '$parent': 'folder_11111', + '$id': '2094c584-68e1-475c-a581-534a4609594e', + '$version': 1, + '$typeVersion': 0, + editor: 'Jones', + previousState: 'proposal', + currentState: 'reviewed', + '$template': 'marketingCollateral', + '$scope': 'enterprise_12345' } + */ + }); +``` + +[set-metadata]: http://opensource.box.com/box-node-sdk/jsdoc/Folders.html#setMetadata +[add-metadata]: http://opensource.box.com/box-node-sdk/jsdoc/Folders.html#addMetadata +[update-metadata]: http://opensource.box.com/box-node-sdk/jsdoc/Folders.html#updateMetadata + Get Metadata on a Folder ------------------------ @@ -588,46 +717,6 @@ client.folders.getAllMetadata('11111') }); ``` -Update Metadata on a Folder ---------------------------- - -Update a folder's metadata by calling -[`folders.updateMetadata(folderID, scope, template, patch, callback)`](http://opensource.box.com/box-node-sdk/jsdoc/Folders.html#updateMetadata) -with an array of [JSON Patch](http://jsonpatch.com/) formatted operations. - -```js -var updates = [ - { op: 'test', path: '/competitiveDocument', value: 'no' }, - { op: 'remove', path: '/competitiveDocument' }, - { op: 'test', path: '/status', value: 'active' }, - { op: 'replace', path: '/status', value: 'inactive' }, - { op: 'test', path: '/author', value: 'Jones' }, - { op: 'copy', from: '/author', path: '/editor' }, - { op: 'test', path: '/currentState', value: 'proposal' }, - { op: 'move', from: '/currentState', path: '/previousState' }, - { op: 'add', path: '/currentState', value: 'reviewed' } -]; -client.folders.updateMetadata('11111', client.metadata.scopes.ENTERPRISE, "marketingCollateral", updates) - .then(metadata => { - /* metadata -> { - audience: 'internal', - documentType: 'Q1 plans', - status: 'inactive', - author: 'Jones', - '$type': 'marketingCollateral-d086c908-2498-4d3e-8a1f-01e82bfc2abe', - '$parent': 'folder_11111', - '$id': '2094c584-68e1-475c-a581-534a4609594e', - '$version': 1, - '$typeVersion': 0, - editor: 'Jones', - previousState: 'proposal', - currentState: 'reviewed', - '$template': 'marketingCollateral', - '$scope': 'enterprise_12345' } - */ - }); -``` - Remove Metadata from a Folder ----------------------------- diff --git a/lib/managers/files.js b/lib/managers/files.js index aa2a41bc..90a896c2 100644 --- a/lib/managers/files.js +++ b/lib/managers/files.js @@ -743,6 +743,37 @@ Files.prototype.updateMetadata = function(fileID, scope, template, patch, callba return this.client.wrapWithDefaultHandler(this.client.put)(apiPath, params, callback); }; +/** + * Sets metadata on a file, overwriting any metadata that exists for the provided keys. + * + * @param {string} fileID - The file to set metadata on + * @param {string} scope - The scope of the metadata template + * @param {string} template - The key of the metadata template + * @param {Object} metadata - The metadata to set + * @param {Function} [callback] - Called with updated metadata if successful + * @returns {Promise} A promise resolving to the updated metadata + */ +Files.prototype.setMetadata = function(fileID, scope, template, metadata, callback) { + + return this.addMetadata(fileID, scope, template, metadata) + .catch(err => { + + if (err.statusCode !== 409) { + throw err; + } + + // Metadata already exists on the file; update instead + var updates = Object.keys(metadata).map(key => ({ + op: 'add', + path: `/${key}`, + value: metadata[key], + })); + + return this.updateMetadata(fileID, scope, template, updates); + }) + .asCallback(callback); +}; + /** * Deletes a metadata template from a file. * diff --git a/lib/managers/folders.js b/lib/managers/folders.js index 9c3725b4..9c13d7b3 100644 --- a/lib/managers/folders.js +++ b/lib/managers/folders.js @@ -368,6 +368,37 @@ Folders.prototype.updateMetadata = function(folderID, scope, template, patch, ca return this.client.wrapWithDefaultHandler(this.client.put)(apiPath, params, callback); }; +/** + * Sets metadata on a folder, overwriting any metadata that exists for the provided keys. + * + * @param {string} folderID - The folder to set metadata on + * @param {string} scope - The scope of the metadata template + * @param {string} template - The key of the metadata template + * @param {Object} metadata - The metadata to set + * @param {Function} [callback] - Called with updated metadata if successful + * @returns {Promise} A promise resolving to the updated metadata + */ +Folders.prototype.setMetadata = function(folderID, scope, template, metadata, callback) { + + return this.addMetadata(folderID, scope, template, metadata) + .catch(err => { + + if (err.statusCode !== 409) { + throw err; + } + + // Metadata already exists on the file; update instead + var updates = Object.keys(metadata).map(key => ({ + op: 'add', + path: `/${key}`, + value: metadata[key], + })); + + return this.updateMetadata(folderID, scope, template, updates); + }) + .asCallback(callback); +}; + /** * Deletes a metadata template from a folder. * diff --git a/tests/endpoint-test.js b/tests/endpoint-test.js index 4a3c212b..138b0e73 100644 --- a/tests/endpoint-test.js +++ b/tests/endpoint-test.js @@ -1818,6 +1818,80 @@ describe('Endpoint', function() { }); }); + describe('setMetadata()', function() { + + var fileID = '11111', + scope = 'enterprise', + template = 'testTemplate', + fixture = getFixture('files/post_files_id_metadata_scope_template_201'); + + var metadataValues = { + testEnum: 'foo' + }; + + var metadataUpdate = [ + { + op: 'add', + path: '/testEnum', + value: 'foo', + } + ]; + + it('should try POST call to create metadata instance', function() { + + apiMock.post(`/2.0/files/${fileID}/metadata/${scope}/${template}`, metadataValues) + .matchHeader('Authorization', function(authHeader) { + assert.equal(authHeader, `Bearer ${TEST_ACCESS_TOKEN}`); + return true; + }) + .matchHeader('User-Agent', function(uaHeader) { + assert.include(uaHeader, 'Box Node.js SDK v'); + return true; + }) + .reply(201, fixture); + + return basicClient.files.setMetadata(fileID, scope, template, metadataValues) + .then(metadata => assert.deepEqual(metadata, JSON.parse(fixture))); + }); + + it('should make PUT call to update metadata instance when instance exists', function() { + + apiMock.post(`/2.0/files/${fileID}/metadata/${scope}/${template}`, metadataValues) + .matchHeader('Authorization', function(authHeader) { + assert.equal(authHeader, `Bearer ${TEST_ACCESS_TOKEN}`); + return true; + }) + .matchHeader('User-Agent', function(uaHeader) { + assert.include(uaHeader, 'Box Node.js SDK v'); + return true; + }) + .reply(409) + .put(`/2.0/files/${fileID}/metadata/${scope}/${template}`, metadataUpdate) + .matchHeader('Authorization', function(authHeader) { + assert.equal(authHeader, `Bearer ${TEST_ACCESS_TOKEN}`); + return true; + }) + .matchHeader('User-Agent', function(uaHeader) { + assert.include(uaHeader, 'Box Node.js SDK v'); + return true; + }) + .reply(200, fixture); + + return basicClient.files.setMetadata(fileID, scope, template, metadataValues) + .then(metadata => assert.deepEqual(metadata, JSON.parse(fixture))); + }); + + it('should produce error when creation operation fails with non-conflict error', function() { + + apiMock.post(`/2.0/files/${fileID}/metadata/${scope}/${template}`, metadataValues) + .reply(400); + + return basicClient.files.setMetadata(fileID, scope, template, metadataValues) + .then(() => assert.fail('Expected method to fail')) + .catch(err => assert.propertyVal(err, 'statusCode', 400)); + }); + }); + describe('getTrashedFile()', function() { it('should make GET call to retrieve information about file in trash', function() { @@ -2296,6 +2370,80 @@ describe('Endpoint', function() { }); }); + describe('setMetadata()', function() { + + var folderID = '11111', + scope = 'enterprise', + template = 'testTemplate', + fixture = getFixture('folders/post_folders_id_metadata_scope_template_201'); + + var metadataValues = { + testEnum: 'foo' + }; + + var metadataUpdate = [ + { + op: 'add', + path: '/testEnum', + value: 'foo', + } + ]; + + it('should try POST call to create metadata instance', function() { + + apiMock.post(`/2.0/folders/${folderID}/metadata/${scope}/${template}`, metadataValues) + .matchHeader('Authorization', function(authHeader) { + assert.equal(authHeader, `Bearer ${TEST_ACCESS_TOKEN}`); + return true; + }) + .matchHeader('User-Agent', function(uaHeader) { + assert.include(uaHeader, 'Box Node.js SDK v'); + return true; + }) + .reply(201, fixture); + + return basicClient.folders.setMetadata(folderID, scope, template, metadataValues) + .then(metadata => assert.deepEqual(metadata, JSON.parse(fixture))); + }); + + it('should make PUT call to update metadata instance when instance exists', function() { + + apiMock.post(`/2.0/folders/${folderID}/metadata/${scope}/${template}`, metadataValues) + .matchHeader('Authorization', function(authHeader) { + assert.equal(authHeader, `Bearer ${TEST_ACCESS_TOKEN}`); + return true; + }) + .matchHeader('User-Agent', function(uaHeader) { + assert.include(uaHeader, 'Box Node.js SDK v'); + return true; + }) + .reply(409) + .put(`/2.0/folders/${folderID}/metadata/${scope}/${template}`, metadataUpdate) + .matchHeader('Authorization', function(authHeader) { + assert.equal(authHeader, `Bearer ${TEST_ACCESS_TOKEN}`); + return true; + }) + .matchHeader('User-Agent', function(uaHeader) { + assert.include(uaHeader, 'Box Node.js SDK v'); + return true; + }) + .reply(200, fixture); + + return basicClient.folders.setMetadata(folderID, scope, template, metadataValues) + .then(metadata => assert.deepEqual(metadata, JSON.parse(fixture))); + }); + + it('should produce error when creation operation fails with non-conflict error', function() { + + apiMock.post(`/2.0/folders/${folderID}/metadata/${scope}/${template}`, metadataValues) + .reply(400); + + return basicClient.folders.setMetadata(folderID, scope, template, metadataValues) + .then(() => assert.fail('Expected method to fail')) + .catch(err => assert.propertyVal(err, 'statusCode', 400)); + }); + }); + describe('getCollaborations()', function() { it('should make GET call to retrieve folder collaborations', function() {