diff --git a/integration/test/ParseFileTest.js b/integration/test/ParseFileTest.js index 191daf8ba..1064d0a48 100644 --- a/integration/test/ParseFileTest.js +++ b/integration/test/ParseFileTest.js @@ -6,7 +6,7 @@ const Parse = require('../../node'); describe('Parse.File', () => { beforeEach((done) => { - Parse.initialize('integration'); + Parse.initialize('integration', null, 'notsosecret'); Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse'); Parse.Storage._clear(); clear().then(done).catch(done.fail); @@ -91,4 +91,28 @@ describe('Parse.File', () => { data = await file.getData(); assert.equal(data, 'ParseA=='); }); + + it('can delete file', async () => { + const parseLogo = 'https://raw.githubusercontent.com/parse-community/parse-server/master/.github/parse-server-logo.png'; + const file = new Parse.File('parse-server-logo', { uri: parseLogo }); + await file.save(); + const data = await file.getData(); + + const deletedFile = await file.destroy(); + const deletedData = await file.getData(); + assert.equal(file, deletedFile); + assert.notEqual(data, deletedData); + }); + + it('can handle delete file error', async () => { + const parseLogo = 'https://raw.githubusercontent.com/parse-community/parse-server/master/.github/parse-server-logo.png'; + const file = new Parse.File('parse-server-logo', { uri: parseLogo }); + try { + await file.destroy(); + assert.equal(false, true); + } catch (e) { + assert.equal(e.code, Parse.Error.FILE_DELETE_ERROR); + assert.equal(e.message, 'Could not delete file.'); + } + }); }); diff --git a/src/ParseFile.js b/src/ParseFile.js index e82fc1f9f..60b481cbc 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -271,6 +271,24 @@ class ParseFile { this._requestTask = null; } + /** + * Deletes the file from the Parse cloud. + * In Cloud Code and Node only with Master Key + * + * @return {Promise} Promise that is resolved when the delete finishes. + */ + destroy() { + if (!this._name) { + throw new Error('Cannot delete an unsaved ParseFile.'); + } + const controller = CoreManager.getFileController(); + return controller.deleteFile(this._name).then(() => { + this._data = null; + this._requestTask = null; + return this; + }); + } + toJSON(): { name: ?string, url: ?string } { return { __type: 'File', @@ -423,9 +441,29 @@ const DefaultController = { }); }, + deleteFile: function(name) { + const headers = { + 'X-Parse-Application-ID': CoreManager.get('APPLICATION_ID'), + 'X-Parse-Master-Key': CoreManager.get('MASTER_KEY'), + }; + let url = CoreManager.get('SERVER_URL'); + if (url[url.length - 1] !== '/') { + url += '/'; + } + url += 'files/' + name; + return CoreManager.getRESTController().ajax('DELETE', url, '', headers).catch(response => { + // TODO: return JSON object in server + if (!response || response === 'SyntaxError: Unexpected end of JSON input') { + return Promise.resolve(); + } else { + return CoreManager.getRESTController().handleError(response); + } + }); + }, + _setXHR(xhr: any) { XHR = xhr; - } + }, }; CoreManager.setFileController(DefaultController); diff --git a/src/ParseObject.js b/src/ParseObject.js index 4bfc9f11b..2f81cff78 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -2116,17 +2116,17 @@ const DefaultController = { return Promise.resolve(results); }); } else { - const RESTController = CoreManager.getRESTController(); - const params = {}; - if (options && options.include) { - params.include = options.include.join(); - } if (!target.id) { return Promise.reject(new ParseError( ParseError.MISSING_OBJECT_ID, 'Object does not have an ID' )); } + const RESTController = CoreManager.getRESTController(); + const params = {}; + if (options && options.include) { + params.include = options.include.join(); + } return RESTController.request( 'GET', 'classes/' + target.className + '/' + target._getId(), diff --git a/src/RESTController.js b/src/RESTController.js index 4543398b7..392c8d4ce 100644 --- a/src/RESTController.js +++ b/src/RESTController.js @@ -270,31 +270,32 @@ const RESTController = { return response; } }); - }).catch(function(response: { responseText: string }) { - // Transform the error into an instance of ParseError by trying to parse - // the error string as JSON - let error; - if (response && response.responseText) { - try { - const errorJSON = JSON.parse(response.responseText); - error = new ParseError(errorJSON.code, errorJSON.error); - } catch (e) { - // If we fail to parse the error text, that's okay. - error = new ParseError( - ParseError.INVALID_JSON, - 'Received an error with invalid JSON from Parse: ' + - response.responseText - ); - } - } else { + }).catch(RESTController.handleError); + }, + + handleError(response) { + // Transform the error into an instance of ParseError by trying to parse + // the error string as JSON + let error; + if (response && response.responseText) { + try { + const errorJSON = JSON.parse(response.responseText); + error = new ParseError(errorJSON.code, errorJSON.error); + } catch (e) { + // If we fail to parse the error text, that's okay. error = new ParseError( - ParseError.CONNECTION_FAILED, - 'XMLHttpRequest failed: ' + JSON.stringify(response) + ParseError.INVALID_JSON, + 'Received an error with invalid JSON from Parse: ' + + response.responseText ); } - - return Promise.reject(error); - }); + } else { + error = new ParseError( + ParseError.CONNECTION_FAILED, + 'XMLHttpRequest failed: ' + JSON.stringify(response) + ); + } + return Promise.reject(error); }, _setXHR(xhr: any) { diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index 9ff972d6f..9811ffb73 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -11,6 +11,7 @@ jest.autoMockOff(); jest.mock('http'); jest.mock('https'); +const ParseError = require('../ParseError').default; const ParseFile = require('../ParseFile').default; const CoreManager = require('../CoreManager'); const EventEmitter = require('../EventEmitter'); @@ -613,4 +614,55 @@ describe('FileController', () => { expect(f.url()).toBe('https://files.parsetfss.com/a//api.parse.com/1/files/parse.txt'); }); }); + + it('should throw error if file deleted without name', async (done) => { + const file = new ParseFile('', [1, 2, 3]); + try { + await file.destroy(); + } catch (e) { + expect(e.message).toBe('Cannot delete an unsaved ParseFile.'); + done(); + } + }); + + it('should delete file', async () => { + const file = new ParseFile('filename', [1, 2, 3]); + const ajax = jest.fn().mockResolvedValueOnce({ foo: 'bar' }); + CoreManager.setRESTController({ ajax, request: () => {} }); + const result = await file.destroy(); + expect(result).toEqual(file); + expect(ajax).toHaveBeenCalledWith('DELETE', 'https://api.parse.com/1/files/filename', '', { + "X-Parse-Application-ID": null, + "X-Parse-Master-Key": null, + }); + }); + + it('should handle delete file error', async () => { + const file = new ParseFile('filename', [1, 2, 3]); + const ajax = jest.fn().mockResolvedValueOnce(Promise.reject(new ParseError(403, 'Cannot delete file.'))); + const handleError = jest.fn(); + CoreManager.setRESTController({ ajax, request: () => {}, handleError }); + const result = await file.destroy(); + expect(result).toEqual(file); + expect(ajax).toHaveBeenCalledWith('DELETE', 'https://api.parse.com/1/files/filename', '', { + "X-Parse-Application-ID": null, + "X-Parse-Master-Key": null, + }); + expect(handleError).toHaveBeenCalled(); + }); + + it('should handle delete file error invalid server response', async () => { + const file = new ParseFile('filename', [1, 2, 3]); + const response = null; + const ajax = jest.fn().mockResolvedValueOnce(Promise.reject(response)); + const handleError = jest.fn(); + CoreManager.setRESTController({ ajax, request: () => {}, handleError }); + const result = await file.destroy(); + expect(result).toEqual(file); + expect(ajax).toHaveBeenCalledWith('DELETE', 'https://api.parse.com/1/files/filename', '', { + "X-Parse-Application-ID": null, + "X-Parse-Master-Key": null, + }); + expect(handleError).not.toHaveBeenCalled(); + }); });