@@ -2,6 +2,7 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController')
22 . ParseServerRESTController ;
33const ParseServer = require ( '../lib/ParseServer' ) . default ;
44const Parse = require ( 'parse/node' ) . Parse ;
5+ const TestUtils = require ( '../lib/TestUtils' ) ;
56
67let RESTController ;
78
@@ -40,7 +41,7 @@ describe('ParseServerRESTController', () => {
4041 ) ;
4142 } ) ;
4243
43- it ( 'should handle a POST batch' , done => {
44+ it ( 'should handle a POST batch without transaction ' , done => {
4445 RESTController . request ( 'POST' , 'batch' , {
4546 requests : [
4647 {
@@ -69,6 +70,272 @@ describe('ParseServerRESTController', () => {
6970 ) ;
7071 } ) ;
7172
73+ it ( 'should handle a POST batch with transaction=false' , done => {
74+ RESTController . request ( 'POST' , 'batch' , {
75+ requests : [
76+ {
77+ method : 'GET' ,
78+ path : '/classes/MyObject' ,
79+ } ,
80+ {
81+ method : 'POST' ,
82+ path : '/classes/MyObject' ,
83+ body : { key : 'value' } ,
84+ } ,
85+ {
86+ method : 'GET' ,
87+ path : '/classes/MyObject' ,
88+ } ,
89+ ] ,
90+ transaction : false ,
91+ } ) . then (
92+ res => {
93+ expect ( res . length ) . toBe ( 3 ) ;
94+ done ( ) ;
95+ } ,
96+ err => {
97+ jfail ( err ) ;
98+ done ( ) ;
99+ }
100+ ) ;
101+ } ) ;
102+
103+ if (
104+ ( process . env . MONGODB_VERSION === '4.0.4' &&
105+ process . env . MONGODB_TOPOLOGY === 'replicaset' &&
106+ process . env . MONGODB_STORAGE_ENGINE === 'wiredTiger' ) ||
107+ process . env . PARSE_SERVER_TEST_DB === 'postgres'
108+ ) {
109+ describe ( 'transactions' , ( ) => {
110+ beforeAll ( async ( ) => {
111+ if (
112+ process . env . MONGODB_VERSION === '4.0.4' &&
113+ process . env . MONGODB_TOPOLOGY === 'replicaset' &&
114+ process . env . MONGODB_STORAGE_ENGINE === 'wiredTiger'
115+ ) {
116+ await reconfigureServer ( {
117+ databaseAdapter : undefined ,
118+ databaseURI :
119+ 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset' ,
120+ } ) ;
121+ }
122+ } ) ;
123+
124+ beforeEach ( async ( ) => {
125+ await TestUtils . destroyAllDataPermanently ( true ) ;
126+ } ) ;
127+
128+ it ( 'should handle a batch request with transaction = true' , done => {
129+ const myObject = new Parse . Object ( 'MyObject' ) ; // This is important because transaction only works on pre-existing collections
130+ myObject
131+ . save ( )
132+ . then ( ( ) => {
133+ return myObject . destroy ( ) ;
134+ } )
135+ . then ( ( ) => {
136+ spyOn ( databaseAdapter , 'createObject' ) . and . callThrough ( ) ;
137+
138+ RESTController . request ( 'POST' , 'batch' , {
139+ requests : [
140+ {
141+ method : 'POST' ,
142+ path : '/1/classes/MyObject' ,
143+ body : { key : 'value1' } ,
144+ } ,
145+ {
146+ method : 'POST' ,
147+ path : '/1/classes/MyObject' ,
148+ body : { key : 'value2' } ,
149+ } ,
150+ ] ,
151+ transaction : true ,
152+ } ) . then ( response => {
153+ expect ( response . length ) . toEqual ( 2 ) ;
154+ expect ( response [ 0 ] . success . objectId ) . toBeDefined ( ) ;
155+ expect ( response [ 0 ] . success . createdAt ) . toBeDefined ( ) ;
156+ expect ( response [ 1 ] . success . objectId ) . toBeDefined ( ) ;
157+ expect ( response [ 1 ] . success . createdAt ) . toBeDefined ( ) ;
158+ const query = new Parse . Query ( 'MyObject' ) ;
159+ query . find ( ) . then ( results => {
160+ expect ( databaseAdapter . createObject . calls . count ( ) ) . toBe ( 2 ) ;
161+ expect ( databaseAdapter . createObject . calls . argsFor ( 0 ) [ 3 ] ) . toBe (
162+ databaseAdapter . createObject . calls . argsFor ( 1 ) [ 3 ]
163+ ) ;
164+ expect ( results . map ( result => result . get ( 'key' ) ) . sort ( ) ) . toEqual (
165+ [ 'value1' , 'value2' ]
166+ ) ;
167+ done ( ) ;
168+ } ) ;
169+ } ) ;
170+ } ) ;
171+ } ) ;
172+
173+ it ( 'should not save anything when one operation fails in a transaction' , done => {
174+ const myObject = new Parse . Object ( 'MyObject' ) ; // This is important because transaction only works on pre-existing collections
175+ myObject
176+ . save ( )
177+ . then ( ( ) => {
178+ return myObject . destroy ( ) ;
179+ } )
180+ . then ( ( ) => {
181+ RESTController . request ( 'POST' , 'batch' , {
182+ requests : [
183+ {
184+ method : 'POST' ,
185+ path : '/1/classes/MyObject' ,
186+ body : { key : 'value1' } ,
187+ } ,
188+ {
189+ method : 'POST' ,
190+ path : '/1/classes/MyObject' ,
191+ body : { key : 10 } ,
192+ } ,
193+ ] ,
194+ transaction : true ,
195+ } ) . catch ( error => {
196+ expect ( error . message ) . toBeDefined ( ) ;
197+ const query = new Parse . Query ( 'MyObject' ) ;
198+ query . find ( ) . then ( results => {
199+ expect ( results . length ) . toBe ( 0 ) ;
200+ done ( ) ;
201+ } ) ;
202+ } ) ;
203+ } ) ;
204+ } ) ;
205+
206+ it ( 'should generate separate session for each call' , async ( ) => {
207+ const myObject = new Parse . Object ( 'MyObject' ) ; // This is important because transaction only works on pre-existing collections
208+ await myObject . save ( ) ;
209+ await myObject . destroy ( ) ;
210+
211+ const myObject2 = new Parse . Object ( 'MyObject2' ) ; // This is important because transaction only works on pre-existing collections
212+ await myObject2 . save ( ) ;
213+ await myObject2 . destroy ( ) ;
214+
215+ spyOn ( databaseAdapter , 'createObject' ) . and . callThrough ( ) ;
216+
217+ let myObjectCalls = 0 ;
218+ Parse . Cloud . beforeSave ( 'MyObject' , async ( ) => {
219+ myObjectCalls ++ ;
220+ if ( myObjectCalls === 2 ) {
221+ try {
222+ await RESTController . request ( 'POST' , 'batch' , {
223+ requests : [
224+ {
225+ method : 'POST' ,
226+ path : '/1/classes/MyObject2' ,
227+ body : { key : 'value1' } ,
228+ } ,
229+ {
230+ method : 'POST' ,
231+ path : '/1/classes/MyObject2' ,
232+ body : { key : 10 } ,
233+ } ,
234+ ] ,
235+ transaction : true ,
236+ } ) ;
237+ fail ( 'should fail' ) ;
238+ } catch ( e ) {
239+ expect ( e ) . toBeDefined ( ) ;
240+ }
241+ }
242+ } ) ;
243+
244+ const response = await RESTController . request ( 'POST' , 'batch' , {
245+ requests : [
246+ {
247+ method : 'POST' ,
248+ path : '/1/classes/MyObject' ,
249+ body : { key : 'value1' } ,
250+ } ,
251+ {
252+ method : 'POST' ,
253+ path : '/1/classes/MyObject' ,
254+ body : { key : 'value2' } ,
255+ } ,
256+ ] ,
257+ transaction : true ,
258+ } ) ;
259+
260+ expect ( response . length ) . toEqual ( 2 ) ;
261+ expect ( response [ 0 ] . success . objectId ) . toBeDefined ( ) ;
262+ expect ( response [ 0 ] . success . createdAt ) . toBeDefined ( ) ;
263+ expect ( response [ 1 ] . success . objectId ) . toBeDefined ( ) ;
264+ expect ( response [ 1 ] . success . createdAt ) . toBeDefined ( ) ;
265+
266+ await RESTController . request ( 'POST' , 'batch' , {
267+ requests : [
268+ {
269+ method : 'POST' ,
270+ path : '/1/classes/MyObject3' ,
271+ body : { key : 'value1' } ,
272+ } ,
273+ {
274+ method : 'POST' ,
275+ path : '/1/classes/MyObject3' ,
276+ body : { key : 'value2' } ,
277+ } ,
278+ ] ,
279+ } ) ;
280+
281+ const query = new Parse . Query ( 'MyObject' ) ;
282+ const results = await query . find ( ) ;
283+ expect ( results . map ( result => result . get ( 'key' ) ) . sort ( ) ) . toEqual ( [
284+ 'value1' ,
285+ 'value2' ,
286+ ] ) ;
287+
288+ const query2 = new Parse . Query ( 'MyObject2' ) ;
289+ const results2 = await query2 . find ( ) ;
290+ expect ( results2 . length ) . toEqual ( 0 ) ;
291+
292+ const query3 = new Parse . Query ( 'MyObject3' ) ;
293+ const results3 = await query3 . find ( ) ;
294+ expect ( results3 . map ( result => result . get ( 'key' ) ) . sort ( ) ) . toEqual ( [
295+ 'value1' ,
296+ 'value2' ,
297+ ] ) ;
298+
299+ expect ( databaseAdapter . createObject . calls . count ( ) ) . toBe ( 5 ) ;
300+ let transactionalSession ;
301+ let transactionalSession2 ;
302+ let myObjectDBCalls = 0 ;
303+ let myObject2DBCalls = 0 ;
304+ let myObject3DBCalls = 0 ;
305+ for ( let i = 0 ; i < 5 ; i ++ ) {
306+ const args = databaseAdapter . createObject . calls . argsFor ( i ) ;
307+ switch ( args [ 0 ] ) {
308+ case 'MyObject' :
309+ myObjectDBCalls ++ ;
310+ if ( ! transactionalSession ) {
311+ transactionalSession = args [ 3 ] ;
312+ } else {
313+ expect ( transactionalSession ) . toBe ( args [ 3 ] ) ;
314+ }
315+ if ( transactionalSession2 ) {
316+ expect ( transactionalSession2 ) . not . toBe ( args [ 3 ] ) ;
317+ }
318+ break ;
319+ case 'MyObject2' :
320+ myObject2DBCalls ++ ;
321+ transactionalSession2 = args [ 3 ] ;
322+ if ( transactionalSession ) {
323+ expect ( transactionalSession ) . not . toBe ( args [ 3 ] ) ;
324+ }
325+ break ;
326+ case 'MyObject3' :
327+ myObject3DBCalls ++ ;
328+ expect ( args [ 3 ] ) . toEqual ( null ) ;
329+ break ;
330+ }
331+ }
332+ expect ( myObjectDBCalls ) . toEqual ( 2 ) ;
333+ expect ( myObject2DBCalls ) . toEqual ( 1 ) ;
334+ expect ( myObject3DBCalls ) . toEqual ( 2 ) ;
335+ } ) ;
336+ } ) ;
337+ }
338+
72339 it ( 'should handle a POST request' , done => {
73340 RESTController . request ( 'POST' , '/classes/MyObject' , { key : 'value' } )
74341 . then ( ( ) => {
0 commit comments