Skip to content

Commit 8222855

Browse files
committed
Batch transaction boilerplate
1 parent 2e0940c commit 8222855

File tree

6 files changed

+219
-23
lines changed

6 files changed

+219
-23
lines changed

spec/batch.spec.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const batch = require('../lib/batch');
2+
const request = require('../lib/request');
23

34
const originalURL = '/parse/batch';
45
const serverURL = 'http://localhost:1234/parse';
@@ -7,6 +8,13 @@ const serverURLNaked = 'http://localhost:1234/';
78
const publicServerURL = 'http://domain.com/parse';
89
const publicServerURLNaked = 'http://domain.com/';
910

11+
const headers = {
12+
'Content-Type': 'application/json',
13+
'X-Parse-Application-Id': 'test',
14+
'X-Parse-REST-API-Key': 'rest',
15+
'X-Parse-Installation-Id': 'yolo',
16+
};
17+
1018
describe('batch', () => {
1119
it('should return the proper url', () => {
1220
const internalURL = batch.makeBatchRoutingPathFunction(originalURL)(
@@ -59,4 +67,114 @@ describe('batch', () => {
5967

6068
expect(internalURL).toEqual('/classes/Object');
6169
});
70+
71+
it('should handle a batch request without transaction', done => {
72+
request({
73+
method: 'POST',
74+
headers: headers,
75+
url: 'http://localhost:8378/1/batch',
76+
body: JSON.stringify({
77+
requests: [
78+
{
79+
method: 'POST',
80+
path: '/1/classes/MyObject',
81+
body: { key: 'value1' },
82+
},
83+
{
84+
method: 'POST',
85+
path: '/1/classes/MyObject',
86+
body: { key: 'value2' },
87+
},
88+
],
89+
}),
90+
}).then(response => {
91+
expect(response.data.length).toEqual(2);
92+
expect(response.data[0].success.objectId).toBeDefined();
93+
expect(response.data[0].success.createdAt).toBeDefined();
94+
expect(response.data[1].success.objectId).toBeDefined();
95+
expect(response.data[1].success.createdAt).toBeDefined();
96+
const query = new Parse.Query('MyObject');
97+
query.find().then(results => {
98+
expect(results.map(result => result.get('key')).sort()).toEqual([
99+
'value1',
100+
'value2',
101+
]);
102+
done();
103+
});
104+
});
105+
});
106+
107+
it('should handle a batch request with transaction = false', done => {
108+
request({
109+
method: 'POST',
110+
headers: headers,
111+
url: 'http://localhost:8378/1/batch',
112+
body: JSON.stringify({
113+
requests: [
114+
{
115+
method: 'POST',
116+
path: '/1/classes/MyObject',
117+
body: { key: 'value1' },
118+
},
119+
{
120+
method: 'POST',
121+
path: '/1/classes/MyObject',
122+
body: { key: 'value2' },
123+
},
124+
],
125+
transaction: false,
126+
}),
127+
}).then(response => {
128+
expect(response.data.length).toEqual(2);
129+
expect(response.data[0].success.objectId).toBeDefined();
130+
expect(response.data[0].success.createdAt).toBeDefined();
131+
expect(response.data[1].success.objectId).toBeDefined();
132+
expect(response.data[1].success.createdAt).toBeDefined();
133+
const query = new Parse.Query('MyObject');
134+
query.find().then(results => {
135+
expect(results.map(result => result.get('key')).sort()).toEqual([
136+
'value1',
137+
'value2',
138+
]);
139+
done();
140+
});
141+
});
142+
});
143+
144+
it('should handle a batch request with transaction = true', done => {
145+
request({
146+
method: 'POST',
147+
headers: headers,
148+
url: 'http://localhost:8378/1/batch',
149+
body: JSON.stringify({
150+
requests: [
151+
{
152+
method: 'POST',
153+
path: '/1/classes/MyObject',
154+
body: { key: 'value1' },
155+
},
156+
{
157+
method: 'POST',
158+
path: '/1/classes/MyObject',
159+
body: { key: 'value2' },
160+
},
161+
],
162+
transaction: true,
163+
}),
164+
}).then(response => {
165+
expect(response.data.length).toEqual(2);
166+
expect(response.data[0].success.objectId).toBeDefined();
167+
expect(response.data[0].success.createdAt).toBeDefined();
168+
expect(response.data[1].success.objectId).toBeDefined();
169+
expect(response.data[1].success.createdAt).toBeDefined();
170+
const query = new Parse.Query('MyObject');
171+
query.find().then(results => {
172+
expect(results.map(result => result.get('key')).sort()).toEqual([
173+
'value1',
174+
'value2',
175+
]);
176+
done();
177+
});
178+
});
179+
});
62180
});

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,24 @@ export class MongoStorageAdapter implements StorageAdapter {
10481048
})
10491049
.catch(err => this.handleError(err));
10501050
}
1051+
1052+
createTransactionalSession(): Promise<any> {
1053+
const transactionalSection = this.client.startSession();
1054+
transactionalSection.startTransaction();
1055+
return Promise.resolve(transactionalSection);
1056+
}
1057+
1058+
commitTransactionalSession(transactionalSection): Promise<void> {
1059+
return transactionalSection.commitTransaction().then(() => {
1060+
transactionalSection.endSession();
1061+
});
1062+
}
1063+
1064+
abortTransactionalSession(transactionalSection): Promise<void> {
1065+
return transactionalSection.abortTransaction().then(() => {
1066+
transactionalSection.endSession();
1067+
});
1068+
}
10511069
}
10521070

10531071
export default MongoStorageAdapter;

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2330,6 +2330,18 @@ export class PostgresStorageAdapter implements StorageAdapter {
23302330
updateEstimatedCount(className: string) {
23312331
return this._client.none('ANALYZE $1:name', [className]);
23322332
}
2333+
2334+
createTransactionalSession(): Promise<any> {
2335+
return Promise.resolve();
2336+
}
2337+
2338+
commitTransactionalSession(): Promise<void> {
2339+
return Promise.resolve();
2340+
}
2341+
2342+
abortTransactionalSession(): Promise<void> {
2343+
return Promise.resolve();
2344+
}
23332345
}
23342346
23352347
function convertPolygonToSQL(polygon) {

src/Adapters/Storage/StorageAdapter.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,7 @@ export interface StorageAdapter {
114114
fields: any,
115115
conn: ?any
116116
): Promise<void>;
117+
createTransactionalSession(): Promise<any>;
118+
commitTransactionalSession(transactionalSession: string): Promise<void>;
119+
abortTransactionalSession(transactionalSession: string): Promise<void>;
117120
}

src/Controllers/DatabaseController.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const validateQuery = (
119119
*/
120120
Object.keys(query).forEach(key => {
121121
const noCollisions = !query.$or.some(subq =>
122-
subq.hasOwnProperty(key)
122+
Object.hasOwnProperty.call(subq, key)
123123
);
124124
let hasNears = false;
125125
if (query[key] != null && typeof query[key] == 'object') {
@@ -411,6 +411,7 @@ class DatabaseController {
411411
schemaCache: any;
412412
schemaPromise: ?Promise<SchemaController.SchemaController>;
413413
skipMongoDBServer13732Workaround: boolean;
414+
transactionalSession: ?any;
414415

415416
constructor(
416417
adapter: StorageAdapter,
@@ -1531,6 +1532,18 @@ class DatabaseController {
15311532
return protectedKeys;
15321533
}
15331534

1535+
createTransactionalSession() {
1536+
return this.adapter.createTransactionalSession();
1537+
}
1538+
1539+
commitTransactionalSession(transactionalSession) {
1540+
return this.adapter.commitTransactionalSession(transactionalSession);
1541+
}
1542+
1543+
abortTransactionalSession(transactionalSession) {
1544+
return this.adapter.abortTransactionalSession(transactionalSession);
1545+
}
1546+
15341547
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
15351548
// have a Parse app without it having a _User collection.
15361549
performInitialization() {

src/batch.js

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,30 +83,62 @@ function handleBatch(router, req) {
8383
req.config.publicServerURL
8484
);
8585

86-
const promises = req.body.requests.map(restRequest => {
87-
const routablePath = makeRoutablePath(restRequest.path);
88-
// Construct a request that we can send to a handler
89-
const request = {
90-
body: restRequest.body,
91-
config: req.config,
92-
auth: req.auth,
93-
info: req.info,
94-
};
86+
let initialPromise = Promise.resolve();
87+
if (req.body.transaction === true) {
88+
initialPromise = req.config.database.createTransactionalSession();
89+
}
9590

96-
return router
97-
.tryRouteRequest(restRequest.method, routablePath, request)
98-
.then(
99-
response => {
100-
return { success: response.response };
101-
},
102-
error => {
103-
return { error: { code: error.code, error: error.message } };
104-
}
105-
);
106-
});
91+
return initialPromise.then(transactionalSession => {
92+
const promises = req.body.requests.map(restRequest => {
93+
const routablePath = makeRoutablePath(restRequest.path);
94+
// Construct a request that we can send to a handler
10795

108-
return Promise.all(promises).then(results => {
109-
return { response: results };
96+
if (transactionalSession) {
97+
req.config.database.transactionalSession = transactionalSession;
98+
}
99+
100+
const request = {
101+
body: restRequest.body,
102+
config: req.config,
103+
auth: req.auth,
104+
info: req.info,
105+
};
106+
107+
return router
108+
.tryRouteRequest(restRequest.method, routablePath, request)
109+
.then(
110+
response => {
111+
return { success: response.response };
112+
},
113+
error => {
114+
return { error: { code: error.code, error: error.message } };
115+
}
116+
);
117+
});
118+
119+
return Promise.all(promises)
120+
.then(results => {
121+
if (transactionalSession) {
122+
return req.config.database
123+
.commitTransactionalSession(transactionalSession)
124+
.then(() => {
125+
return { response: results };
126+
});
127+
} else {
128+
return { response: results };
129+
}
130+
})
131+
.catch(error => {
132+
if (transactionalSession) {
133+
return req.config.database
134+
.abortTransactionalSession(transactionalSession)
135+
.then(() => {
136+
throw error;
137+
});
138+
} else {
139+
throw error;
140+
}
141+
});
110142
});
111143
}
112144

0 commit comments

Comments
 (0)