From 2dd223cb7276130c08e41f6d1b07e6a1644c20f5 Mon Sep 17 00:00:00 2001 From: Vahid Sane Date: Sun, 11 Aug 2024 14:20:15 +0000 Subject: [PATCH 1/4] add transaction to save and destroy on parse object --- src/ParseObject.ts | 102 ++++++++++++++++------ src/RESTController.ts | 1 + src/__tests__/ParseObject-test.js | 136 ++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 28 deletions(-) diff --git a/src/ParseObject.ts b/src/ParseObject.ts index 1cab3c1cb..652d75b59 100644 --- a/src/ParseObject.ts +++ b/src/ParseObject.ts @@ -47,6 +47,7 @@ export type SaveOptions = FullOptions & { cascadeSave?: boolean; context?: AttributeMap; batchSize?: number; + transaction?: boolean; }; type FetchOptions = { @@ -1354,6 +1355,14 @@ class ParseObject { } const controller = CoreManager.getObjectController(); const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; + if ( + unsaved && + unsaved.length > 1 && + options.hasOwnProperty('transaction') && + typeof options.transaction === 'boolean' + ) { + saveOptions.transaction = options.transaction; + } return controller.save(unsaved, saveOptions).then(() => { return controller.save(this, saveOptions); }) as Promise as Promise; @@ -1770,6 +1779,14 @@ class ParseObject { if (options.hasOwnProperty('sessionToken')) { destroyOptions.sessionToken = options.sessionToken; } + if (options.hasOwnProperty('transaction') && typeof options.transaction === 'boolean') { + if (options.hasOwnProperty('batchSize')) + throw new ParseError( + ParseError.OTHER_CAUSE, + 'You cannot use both transaction and batchSize options simultaneously.' + ); + destroyOptions.transaction = options.transaction; + } if (options.hasOwnProperty('batchSize') && typeof options.batchSize === 'number') { destroyOptions.batchSize = options.batchSize; } @@ -1805,6 +1822,14 @@ class ParseObject { if (options.hasOwnProperty('sessionToken')) { saveOptions.sessionToken = options.sessionToken; } + if (options.hasOwnProperty('transaction') && typeof options.transaction === 'boolean') { + if (options.hasOwnProperty('batchSize')) + throw new ParseError( + ParseError.OTHER_CAUSE, + 'You cannot use both transaction and batchSize options simultaneously.' + ); + saveOptions.transaction = options.transaction; + } if (options.hasOwnProperty('batchSize') && typeof options.batchSize === 'number') { saveOptions.batchSize = options.batchSize; } @@ -2322,12 +2347,20 @@ const DefaultController = { target: ParseObject | Array, options: RequestOptions ): Promise> { - const batchSize = + if (options && options.batchSize && options.transaction) + throw new ParseError( + ParseError.OTHER_CAUSE, + 'You cannot use both transaction and batchSize options simultaneously.' + ); + + let batchSize = options && options.batchSize ? options.batchSize : CoreManager.get('REQUEST_BATCH_SIZE'); const localDatastore = CoreManager.getLocalDatastore(); const RESTController = CoreManager.getRESTController(); if (Array.isArray(target)) { + if (options && options.transaction && target.length > 1) batchSize = target.length; + if (target.length < 1) { return Promise.resolve([]); } @@ -2348,21 +2381,20 @@ const DefaultController = { let deleteCompleted = Promise.resolve(); const errors = []; batches.forEach(batch => { + const requests = batch.map(obj => { + return { + method: 'DELETE', + path: getServerUrlPath() + 'classes/' + obj.className + '/' + obj._getId(), + body: {}, + }; + }); + const body = + options && options.transaction && requests.length > 1 + ? { requests, transaction: true } + : { requests }; + deleteCompleted = deleteCompleted.then(() => { - return RESTController.request( - 'POST', - 'batch', - { - requests: batch.map(obj => { - return { - method: 'DELETE', - path: getServerUrlPath() + 'classes/' + obj.className + '/' + obj._getId(), - body: {}, - }; - }), - }, - options - ).then(results => { + return RESTController.request('POST', 'batch', body, options).then(results => { for (let i = 0; i < results.length; i++) { if (results[i] && results[i].hasOwnProperty('error')) { const err = new ParseError(results[i].error.code, results[i].error.error); @@ -2402,8 +2434,15 @@ const DefaultController = { target: ParseObject | null | Array, options: RequestOptions ): Promise | ParseFile | undefined> { - const batchSize = + if (options && options.batchSize && options.transaction) + throw new ParseError( + ParseError.OTHER_CAUSE, + 'You cannot use both transaction and batchSize options simultaneously.' + ); + + let batchSize = options && options.batchSize ? options.batchSize : CoreManager.get('REQUEST_BATCH_SIZE'); + const localDatastore = CoreManager.getLocalDatastore(); const mapIdForPin = {}; @@ -2437,6 +2476,15 @@ const DefaultController = { } }); + if (options && options.transaction && pending.length > 1) { + if (pending.some(el => !canBeSerialized(el))) + throw new ParseError( + ParseError.OTHER_CAUSE, + 'Tried to save a transactional batch containing an object with unserializable attributes.' + ); + batchSize = pending.length; + } + return Promise.all(filesSaved).then(() => { let objectError = null; return continueWhile( @@ -2504,18 +2552,16 @@ const DefaultController = { when(batchReady) .then(() => { // Kick off the batch request - return RESTController.request( - 'POST', - 'batch', - { - requests: batch.map(obj => { - const params = obj._getSaveParams(); - params.path = getServerUrlPath() + params.path; - return params; - }), - }, - options - ); + const requests = batch.map(obj => { + const params = obj._getSaveParams(); + params.path = getServerUrlPath() + params.path; + return params; + }); + const body = + options && options.transaction && requests.length > 1 + ? { requests, transaction: true } + : { requests }; + return RESTController.request('POST', 'batch', body, options); }) .then(batchReturned.resolve, error => { batchReturned.reject(new ParseError(ParseError.INCORRECT_TYPE, error.message)); diff --git a/src/RESTController.ts b/src/RESTController.ts index aa5ae8401..c85d58e65 100644 --- a/src/RESTController.ts +++ b/src/RESTController.ts @@ -16,6 +16,7 @@ export type RequestOptions = { context?: any; usePost?: boolean; ignoreEmailVerification?: boolean; + transaction?: boolean; }; export type FullOptions = { diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 412f2eafe..411c46f5f 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2024,6 +2024,142 @@ describe('ParseObject', () => { } }); + it('should fail save with transaction and batchSize option', async () => { + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + + try { + await ParseObject.saveAll([obj1, obj2], { transaction: true, batchSize: 20 }); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe( + 'You cannot use both transaction and batchSize options simultaneously.' + ); + } + }); + + it('should fail destroy with transaction and batchSize option', async () => { + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + + try { + await ParseObject.destroyAll([obj1, obj2], { transaction: true, batchSize: 20 }); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe( + 'You cannot use both transaction and batchSize options simultaneously.' + ); + } + }); + + it('should fail save batch with unserializable attribute and transaction option', async () => { + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + obj1.set('relatedObject', obj2); + + try { + await ParseObject.saveAll([obj1, obj2], { transaction: true }); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe( + 'Tried to save a transactional batch containing an object with unserializable attributes.' + ); + } + }); + + it('should save batch with serializable attribute and transaction option', async () => { + const xhrs = []; + RESTController._setXHR(function () { + const xhr = { + setRequestHeader: jest.fn(), + open: jest.fn(), + send: jest.fn(), + status: 200, + readyState: 4, + }; + xhrs.push(xhr); + return xhr; + }); + + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + obj2.id = 'id2'; + obj1.set('relatedObject', obj2); + + const promise = ParseObject.saveAll([obj1, obj2], { transaction: true }).then( + ([saved1, saved2]) => { + expect(saved1.dirty()).toBe(false); + expect(saved2.dirty()).toBe(false); + expect(saved1.id).toBe('parent'); + expect(saved2.id).toBe('id2'); + } + ); + jest.runAllTicks(); + await flushPromises(); + + expect(xhrs.length).toBe(1); + expect(xhrs[0].open.mock.calls[0]).toEqual(['POST', 'https://api.parse.com/1/batch', true]); + const call = JSON.parse(xhrs[0].send.mock.calls[0]); + expect(call.transaction).toBe(true); + expect(call.requests).toEqual([ + { + method: 'POST', + body: { relatedObject: { __type: 'Pointer', className: 'TestObject', objectId: 'id2' } }, + path: '/1/classes/TestObject', + }, + { method: 'PUT', body: {}, path: '/1/classes/TestObject/id2' }, + ]); + xhrs[0].responseText = JSON.stringify([ + { success: { objectId: 'parent' } }, + { success: { objectId: 'id2' } }, + ]); + xhrs[0].onreadystatechange(); + jest.runAllTicks(); + await flushPromises(); + await promise; + }); + + it('should destroy batch with transaction option', async () => { + const xhrs = []; + RESTController._setXHR(function () { + const xhr = { + setRequestHeader: jest.fn(), + open: jest.fn(), + send: jest.fn(), + status: 200, + readyState: 4, + }; + xhrs.push(xhr); + return xhr; + }); + + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + obj1.id = 'parent'; + obj2.id = 'id2'; + + const promise = ParseObject.saveAll([obj1, obj2], { transaction: true }); + jest.runAllTicks(); + await flushPromises(); + + expect(xhrs.length).toBe(1); + expect(xhrs[0].open.mock.calls[0]).toEqual(['POST', 'https://api.parse.com/1/batch', true]); + const call = JSON.parse(xhrs[0].send.mock.calls[0]); + expect(call.transaction).toBe(true); + expect(call.requests).toEqual([ + { method: 'PUT', body: {}, path: '/1/classes/TestObject/parent' }, + { method: 'PUT', body: {}, path: '/1/classes/TestObject/id2' }, + ]); + xhrs[0].responseText = JSON.stringify([ + { success: { objectId: 'parent' } }, + { success: { objectId: 'id2' } }, + ]); + xhrs[0].onreadystatechange(); + jest.runAllTicks(); + await flushPromises(); + await promise; + }); + it('should fail on invalid date', done => { const obj = new ParseObject('Item'); obj.set('when', new Date(Date.parse(null))); From e803d59ebbf37ae48e17fb3ea928f4d55526e27f Mon Sep 17 00:00:00 2001 From: Vahid Sane Date: Mon, 12 Aug 2024 23:29:15 +0000 Subject: [PATCH 2/4] add support to save children of an object using transaction option --- src/ParseObject.ts | 31 ++++--- src/__tests__/ParseObject-test.js | 141 ++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 13 deletions(-) diff --git a/src/ParseObject.ts b/src/ParseObject.ts index 652d75b59..3bd3b6236 100644 --- a/src/ParseObject.ts +++ b/src/ParseObject.ts @@ -1357,11 +1357,26 @@ class ParseObject { const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; if ( unsaved && - unsaved.length > 1 && - options.hasOwnProperty('transaction') && - typeof options.transaction === 'boolean' + unsaved.length && + options.transaction === true && + unsaved.some(el => el instanceof ParseObject) ) { saveOptions.transaction = options.transaction; + const unsavedFiles: ParseFile[] = []; + const unsavedObjects: ParseObject[] = []; + unsaved.forEach(el => { + if (el instanceof ParseFile) unsavedFiles.push(el); + else unsavedObjects.push(el); + }); + unsavedObjects.push(this); + + const filePromise = unsavedFiles.length + ? controller.save(unsavedFiles, saveOptions) + : Promise.resolve(); + + return filePromise + .then(() => controller.save(unsavedObjects, saveOptions)) + .then((savedOjbects: this[]) => savedOjbects.pop()); } return controller.save(unsaved, saveOptions).then(() => { return controller.save(this, saveOptions); @@ -1780,11 +1795,6 @@ class ParseObject { destroyOptions.sessionToken = options.sessionToken; } if (options.hasOwnProperty('transaction') && typeof options.transaction === 'boolean') { - if (options.hasOwnProperty('batchSize')) - throw new ParseError( - ParseError.OTHER_CAUSE, - 'You cannot use both transaction and batchSize options simultaneously.' - ); destroyOptions.transaction = options.transaction; } if (options.hasOwnProperty('batchSize') && typeof options.batchSize === 'number') { @@ -1823,11 +1833,6 @@ class ParseObject { saveOptions.sessionToken = options.sessionToken; } if (options.hasOwnProperty('transaction') && typeof options.transaction === 'boolean') { - if (options.hasOwnProperty('batchSize')) - throw new ParseError( - ParseError.OTHER_CAUSE, - 'You cannot use both transaction and batchSize options simultaneously.' - ); saveOptions.transaction = options.transaction; } if (options.hasOwnProperty('batchSize') && typeof options.batchSize === 'number') { diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 411c46f5f..eb34baf51 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2067,6 +2067,34 @@ describe('ParseObject', () => { } }); + it('should fail to save object when its children lack IDs using transaction option', async () => { + const xhrs = []; + RESTController._setXHR(function () { + const xhr = { + setRequestHeader: jest.fn(), + open: jest.fn(), + send: jest.fn(), + status: 200, + readyState: 4, + }; + xhrs.push(xhr); + return xhr; + }); + + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + obj1.set('relatedObject', obj2); + + try { + await obj1.save(null, { transaction: true }); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe( + 'Tried to save a transactional batch containing an object with unserializable attributes.' + ); + } + }); + it('should save batch with serializable attribute and transaction option', async () => { const xhrs = []; RESTController._setXHR(function () { @@ -2119,6 +2147,119 @@ describe('ParseObject', () => { await promise; }); + it('should save object along with its children using transaction option', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: [{ success: { objectId: 'id2' } }, { success: { objectId: 'parent' } }], + }, + ]) + ); + + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'request'); + + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + obj2.id = 'id2'; + obj2.set('attribute', true); + + obj1.set('relatedObject', obj2); + + const saved1 = await obj1.save(null, { transaction: true }); + + const saved2 = saved1.get('relatedObject'); + expect(saved1.dirty()).toBe(false); + expect(saved2.dirty()).toBe(false); + expect(saved1.id).toBe('parent'); + expect(saved2.id).toBe('id2'); + + expect(controller.request).toHaveBeenCalledWith( + 'POST', + 'batch', + { + requests: [ + { + method: 'PUT', + body: { attribute: true }, + path: '/1/classes/TestObject/id2', + }, + { + method: 'POST', + body: { + relatedObject: { __type: 'Pointer', className: 'TestObject', objectId: 'id2' }, + }, + path: '/1/classes/TestObject', + }, + ], + transaction: true, + }, + expect.anything() + ); + }); + + it('should save file & object along with its children using transaction option', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: { name: 'mock-name', url: 'mock-url' }, + }, + { + status: 200, + response: [{ success: { objectId: 'id2' } }, { success: { objectId: 'parent' } }], + }, + ]) + ); + + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'request'); + + const file1 = new ParseFile('parse-server-logo', [0, 1, 2, 3]); + const obj1 = new ParseObject('TestObject'); + const obj2 = new ParseObject('TestObject'); + obj2.id = 'id2'; + obj2.set('file', file1); + + obj1.set('relatedObject', obj2); + + const saved1 = await obj1.save(null, { transaction: true }); + + const saved2 = saved1.get('relatedObject'); + expect(saved1.dirty()).toBe(false); + expect(saved2.dirty()).toBe(false); + expect(saved1.id).toBe('parent'); + expect(saved2.id).toBe('id2'); + + const file = saved2.get('file'); + expect(file.name()).toBe('mock-name'); + expect(file.url()).toBe('mock-url'); + + expect(controller.request).toHaveBeenCalledWith( + 'POST', + 'batch', + { + requests: [ + { + method: 'PUT', + body: { file: { __type: 'File', name: 'mock-name', url: 'mock-url' } }, + path: '/1/classes/TestObject/id2', + }, + { + method: 'POST', + body: { + relatedObject: { __type: 'Pointer', className: 'TestObject', objectId: 'id2' }, + }, + path: '/1/classes/TestObject', + }, + ], + transaction: true, + }, + expect.anything() + ); + }); + it('should destroy batch with transaction option', async () => { const xhrs = []; RESTController._setXHR(function () { From e8fc818c51ec4ba79e08bfbab01b9bf4f4deceb5 Mon Sep 17 00:00:00 2001 From: Vahid Sane Date: Mon, 12 Aug 2024 23:52:12 +0000 Subject: [PATCH 3/4] remove try catch from my tests --- src/ParseObject.ts | 16 ++++-- src/__tests__/ParseObject-test.js | 93 ++++++++++++++----------------- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/src/ParseObject.ts b/src/ParseObject.ts index 3bd3b6236..599c94c5d 100644 --- a/src/ParseObject.ts +++ b/src/ParseObject.ts @@ -2440,9 +2440,11 @@ const DefaultController = { options: RequestOptions ): Promise | ParseFile | undefined> { if (options && options.batchSize && options.transaction) - throw new ParseError( - ParseError.OTHER_CAUSE, - 'You cannot use both transaction and batchSize options simultaneously.' + return Promise.reject( + new ParseError( + ParseError.OTHER_CAUSE, + 'You cannot use both transaction and batchSize options simultaneously.' + ) ); let batchSize = @@ -2483,9 +2485,11 @@ const DefaultController = { if (options && options.transaction && pending.length > 1) { if (pending.some(el => !canBeSerialized(el))) - throw new ParseError( - ParseError.OTHER_CAUSE, - 'Tried to save a transactional batch containing an object with unserializable attributes.' + return Promise.reject( + new ParseError( + ParseError.OTHER_CAUSE, + 'Tried to save a transactional batch containing an object with unserializable attributes.' + ) ); batchSize = pending.length; } diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index eb34baf51..0c5ac5ce3 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2016,40 +2016,38 @@ describe('ParseObject', () => { it('should fail saveAll batch cycle', async () => { const obj = new ParseObject('Item'); obj.set('child', obj); - try { - await ParseObject.saveAll([obj]); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe('Tried to save a batch with a cycle.'); - } + + await expect(ParseObject.saveAll([obj])).rejects.toEqual( + expect.objectContaining({ + message: 'Tried to save a batch with a cycle.', + }) + ); }); it('should fail save with transaction and batchSize option', async () => { const obj1 = new ParseObject('TestObject'); const obj2 = new ParseObject('TestObject'); - try { - await ParseObject.saveAll([obj1, obj2], { transaction: true, batchSize: 20 }); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'You cannot use both transaction and batchSize options simultaneously.' - ); - } + await expect( + ParseObject.saveAll([obj1, obj2], { transaction: true, batchSize: 20 }) + ).rejects.toEqual( + expect.objectContaining({ + message: 'You cannot use both transaction and batchSize options simultaneously.', + }) + ); }); it('should fail destroy with transaction and batchSize option', async () => { const obj1 = new ParseObject('TestObject'); const obj2 = new ParseObject('TestObject'); - try { - await ParseObject.destroyAll([obj1, obj2], { transaction: true, batchSize: 20 }); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'You cannot use both transaction and batchSize options simultaneously.' - ); - } + await expect( + ParseObject.destroyAll([obj1, obj2], { transaction: true, batchSize: 20 }) + ).rejects.toEqual( + expect.objectContaining({ + message: 'You cannot use both transaction and batchSize options simultaneously.', + }) + ); }); it('should fail save batch with unserializable attribute and transaction option', async () => { @@ -2057,46 +2055,41 @@ describe('ParseObject', () => { const obj2 = new ParseObject('TestObject'); obj1.set('relatedObject', obj2); - try { - await ParseObject.saveAll([obj1, obj2], { transaction: true }); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Tried to save a transactional batch containing an object with unserializable attributes.' - ); - } + await expect(ParseObject.saveAll([obj1, obj2], { transaction: true })).rejects.toEqual( + expect.objectContaining({ + message: + 'Tried to save a transactional batch containing an object with unserializable attributes.', + }) + ); }); it('should fail to save object when its children lack IDs using transaction option', async () => { - const xhrs = []; - RESTController._setXHR(function () { - const xhr = { - setRequestHeader: jest.fn(), - open: jest.fn(), - send: jest.fn(), - status: 200, - readyState: 4, - }; - xhrs.push(xhr); - return xhr; - }); + RESTController._setXHR(mockXHR([{ status: 200, response: [] }])); const obj1 = new ParseObject('TestObject'); const obj2 = new ParseObject('TestObject'); obj1.set('relatedObject', obj2); - try { - await obj1.save(null, { transaction: true }); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toBe( - 'Tried to save a transactional batch containing an object with unserializable attributes.' - ); - } + await expect(obj1.save(null, { transaction: true })).rejects.toEqual( + expect.objectContaining({ + message: + 'Tried to save a transactional batch containing an object with unserializable attributes.', + }) + ); }); it('should save batch with serializable attribute and transaction option', async () => { const xhrs = []; + + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: [{ success: { objectId: 'parent' } }, { success: { objectId: 'id2' } }], + }, + ]) + ); + RESTController._setXHR(function () { const xhr = { setRequestHeader: jest.fn(), From 2c242d8204ec5f702436a17212c5b5e18fcea438 Mon Sep 17 00:00:00 2001 From: Vahid Sane Date: Tue, 13 Aug 2024 09:50:45 +0000 Subject: [PATCH 4/4] clean XHR mocks for transaction tests --- src/__tests__/ParseObject-test.js | 120 ++++++++++++------------------ 1 file changed, 48 insertions(+), 72 deletions(-) diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 0c5ac5ce3..077c500d0 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2079,8 +2079,6 @@ describe('ParseObject', () => { }); it('should save batch with serializable attribute and transaction option', async () => { - const xhrs = []; - CoreManager.getRESTController()._setXHR( mockXHR([ { @@ -2090,54 +2088,39 @@ describe('ParseObject', () => { ]) ); - RESTController._setXHR(function () { - const xhr = { - setRequestHeader: jest.fn(), - open: jest.fn(), - send: jest.fn(), - status: 200, - readyState: 4, - }; - xhrs.push(xhr); - return xhr; - }); + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'request'); const obj1 = new ParseObject('TestObject'); const obj2 = new ParseObject('TestObject'); obj2.id = 'id2'; obj1.set('relatedObject', obj2); - const promise = ParseObject.saveAll([obj1, obj2], { transaction: true }).then( - ([saved1, saved2]) => { - expect(saved1.dirty()).toBe(false); - expect(saved2.dirty()).toBe(false); - expect(saved1.id).toBe('parent'); - expect(saved2.id).toBe('id2'); - } - ); - jest.runAllTicks(); - await flushPromises(); + const [saved1, saved2] = await ParseObject.saveAll([obj1, obj2], { transaction: true }); - expect(xhrs.length).toBe(1); - expect(xhrs[0].open.mock.calls[0]).toEqual(['POST', 'https://api.parse.com/1/batch', true]); - const call = JSON.parse(xhrs[0].send.mock.calls[0]); - expect(call.transaction).toBe(true); - expect(call.requests).toEqual([ + expect(saved1.dirty()).toBe(false); + expect(saved2.dirty()).toBe(false); + expect(saved1.id).toBe('parent'); + expect(saved2.id).toBe('id2'); + + expect(controller.request).toHaveBeenCalledWith( + 'POST', + 'batch', { - method: 'POST', - body: { relatedObject: { __type: 'Pointer', className: 'TestObject', objectId: 'id2' } }, - path: '/1/classes/TestObject', + requests: [ + { + method: 'POST', + body: { + relatedObject: { __type: 'Pointer', className: 'TestObject', objectId: 'id2' }, + }, + path: '/1/classes/TestObject', + }, + { method: 'PUT', body: {}, path: '/1/classes/TestObject/id2' }, + ], + transaction: true, }, - { method: 'PUT', body: {}, path: '/1/classes/TestObject/id2' }, - ]); - xhrs[0].responseText = JSON.stringify([ - { success: { objectId: 'parent' } }, - { success: { objectId: 'id2' } }, - ]); - xhrs[0].onreadystatechange(); - jest.runAllTicks(); - await flushPromises(); - await promise; + expect.anything() + ); }); it('should save object along with its children using transaction option', async () => { @@ -2254,44 +2237,37 @@ describe('ParseObject', () => { }); it('should destroy batch with transaction option', async () => { - const xhrs = []; - RESTController._setXHR(function () { - const xhr = { - setRequestHeader: jest.fn(), - open: jest.fn(), - send: jest.fn(), - status: 200, - readyState: 4, - }; - xhrs.push(xhr); - return xhr; - }); + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: [{ success: { objectId: 'parent' } }, { success: { objectId: 'id2' } }], + }, + ]) + ); + + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'request'); const obj1 = new ParseObject('TestObject'); const obj2 = new ParseObject('TestObject'); obj1.id = 'parent'; obj2.id = 'id2'; - const promise = ParseObject.saveAll([obj1, obj2], { transaction: true }); - jest.runAllTicks(); - await flushPromises(); + await ParseObject.destroyAll([obj1, obj2], { transaction: true }); - expect(xhrs.length).toBe(1); - expect(xhrs[0].open.mock.calls[0]).toEqual(['POST', 'https://api.parse.com/1/batch', true]); - const call = JSON.parse(xhrs[0].send.mock.calls[0]); - expect(call.transaction).toBe(true); - expect(call.requests).toEqual([ - { method: 'PUT', body: {}, path: '/1/classes/TestObject/parent' }, - { method: 'PUT', body: {}, path: '/1/classes/TestObject/id2' }, - ]); - xhrs[0].responseText = JSON.stringify([ - { success: { objectId: 'parent' } }, - { success: { objectId: 'id2' } }, - ]); - xhrs[0].onreadystatechange(); - jest.runAllTicks(); - await flushPromises(); - await promise; + expect(controller.request).toHaveBeenCalledWith( + 'POST', + 'batch', + { + requests: [ + { method: 'DELETE', body: {}, path: '/1/classes/TestObject/parent' }, + { method: 'DELETE', body: {}, path: '/1/classes/TestObject/id2' }, + ], + transaction: true, + }, + expect.anything() + ); }); it('should fail on invalid date', done => {