Skip to content

feat: Add transaction to save and destroy on Parse.Object #2265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 14, 2024
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