Skip to content
102 changes: 74 additions & 28 deletions src/ParseObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
cascadeSave?: boolean;
context?: AttributeMap;
batchSize?: number;
transaction?: boolean;
};

type FetchOptions = {
Expand Down Expand Up @@ -1354,6 +1355,14 @@
}
const controller = CoreManager.getObjectController();
const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null;
if (
unsaved &&
unsaved.length > 1 &&
options.hasOwnProperty('transaction') &&
typeof options.transaction === 'boolean'

Check warning on line 1362 in src/ParseObject.ts

View check run for this annotation

Codecov / codecov/patch

src/ParseObject.ts#L1361-L1362

Added lines #L1361 - L1362 were not covered by tests
) {
saveOptions.transaction = options.transaction;

Check warning on line 1364 in src/ParseObject.ts

View check run for this annotation

Codecov / codecov/patch

src/ParseObject.ts#L1364

Added line #L1364 was not covered by tests
}
return controller.save(unsaved, saveOptions).then(() => {
return controller.save(this, saveOptions);
}) as Promise<ParseObject> as Promise<this>;
Expand Down Expand Up @@ -1770,6 +1779,14 @@
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;

Check warning on line 1788 in src/ParseObject.ts

View check run for this annotation

Codecov / codecov/patch

src/ParseObject.ts#L1788

Added line #L1788 was not covered by tests
}
if (options.hasOwnProperty('batchSize') && typeof options.batchSize === 'number') {
destroyOptions.batchSize = options.batchSize;
}
Expand Down Expand Up @@ -1805,6 +1822,14 @@
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;
}
Expand Down Expand Up @@ -2322,12 +2347,20 @@
target: ParseObject | Array<ParseObject>,
options: RequestOptions
): Promise<ParseObject | Array<ParseObject>> {
const batchSize =
if (options && options.batchSize && options.transaction)
throw new ParseError(

Check warning on line 2351 in src/ParseObject.ts

View check run for this annotation

Codecov / codecov/patch

src/ParseObject.ts#L2351

Added line #L2351 was not covered by tests
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([]);
}
Expand All @@ -2348,21 +2381,20 @@
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 }

Check warning on line 2393 in src/ParseObject.ts

View check run for this annotation

Codecov / codecov/patch

src/ParseObject.ts#L2393

Added line #L2393 was not covered by tests
: { 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);
Expand Down Expand Up @@ -2402,8 +2434,15 @@
target: ParseObject | null | Array<ParseObject | ParseFile>,
options: RequestOptions
): Promise<ParseObject | Array<ParseObject> | ParseFile | undefined> {
const batchSize =
if (options && options.batchSize && options.transaction)
throw new ParseError(

Check warning on line 2438 in src/ParseObject.ts

View check run for this annotation

Codecov / codecov/patch

src/ParseObject.ts#L2438

Added line #L2438 was not covered by tests
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 = {};

Expand Down Expand Up @@ -2437,6 +2476,15 @@
}
});

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(
Expand Down Expand Up @@ -2504,18 +2552,16 @@
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));
Expand Down
1 change: 1 addition & 0 deletions src/RESTController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type RequestOptions = {
context?: any;
usePost?: boolean;
ignoreEmailVerification?: boolean;
transaction?: boolean;
};

export type FullOptions = {
Expand Down
136 changes: 136 additions & 0 deletions src/__tests__/ParseObject-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
Loading