diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 39992c1f2..b5c738a75 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2045,4 +2045,60 @@ describe('Parse Object', () => { expect(obj.get('string')).toBeDefined(); expect(obj.get('string')).toBeInstanceOf(String); }); + + it('allowCustomObjectId', async () => { + await reconfigureServer({ allowCustomObjectId: true }); + Parse.allowCustomObjectId = true; + const customId = `${Date.now()}`; + const object = new Parse.Object('TestObject'); + try { + await object.save(); + fail(); + } catch (error) { + expect(error.message).toBe('objectId must not be empty, null or undefined'); + } + object.id = customId; + object.set('foo', 'bar'); + await object.save(); + expect(object.id).toBe(customId); + + const query = new Parse.Query('TestObject'); + const result = await query.get(customId); + expect(result.get('foo')).toBe('bar'); + expect(result.id).toBe(customId); + + result.set('foo', 'baz'); + await result.save(); + + const afterSave = await query.get(customId); + expect(afterSave.get('foo')).toBe('baz'); + Parse.allowCustomObjectId = false; + }); + + it('allowCustomObjectId saveAll', async () => { + await reconfigureServer({ allowCustomObjectId: true }); + Parse.allowCustomObjectId = true; + const customId1 = `${Date.now()}`; + const customId2 = `${Date.now()}`; + const obj1 = new TestObject({ foo: 'bar' }); + const obj2 = new TestObject({ foo: 'baz' }); + try { + await Parse.Object.saveAll([obj1, obj2]); + fail(); + } catch (error) { + expect(error.message).toBe('objectId must not be empty, null or undefined'); + } + obj1.id = customId1; + obj2.id = customId2; + await Parse.Object.saveAll([obj1, obj2]); + expect(obj1.id).toBe(customId1); + expect(obj2.id).toBe(customId2); + + const query = new Parse.Query(TestObject); + const results = await query.find(); + results.forEach(result => { + expect([customId1, customId2].includes(result.id)); + }); + Parse.allowCustomObjectId = false; + }); }); diff --git a/src/CoreManager.js b/src/CoreManager.js index b6a43bfa6..188970e5c 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.js @@ -200,6 +200,7 @@ const config: Config & { [key: string]: mixed } = { FORCE_REVOCABLE_SESSION: false, ENCRYPTED_USER: false, IDEMPOTENCY: false, + ALLOW_CUSTOM_OBJECT_ID: false, }; function requireMethods(name: string, methods: Array, controller: any) { diff --git a/src/Parse.js b/src/Parse.js index 3d8af96d4..e4b351923 100644 --- a/src/Parse.js +++ b/src/Parse.js @@ -199,6 +199,17 @@ const Parse = { get idempotency() { return CoreManager.get('IDEMPOTENCY'); }, + + /** + * @member {boolean} Parse.allowCustomObjectId + * @static + */ + set allowCustomObjectId(value) { + CoreManager.set('ALLOW_CUSTOM_OBJECT_ID', value); + }, + get allowCustomObjectId() { + return CoreManager.get('ALLOW_CUSTOM_OBJECT_ID'); + }, }; Parse.ACL = require('./ParseACL').default; diff --git a/src/ParseObject.js b/src/ParseObject.js index 2885c8e7f..191a57842 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -324,10 +324,18 @@ class ParseObject { } _getSaveParams(): SaveParams { - const method = this.id ? 'PUT' : 'POST'; + let method = this.id ? 'PUT' : 'POST'; const body = this._getSaveJSON(); let path = 'classes/' + this.className; - if (this.id) { + if (CoreManager.get('ALLOW_CUSTOM_OBJECT_ID')) { + if (!this.createdAt) { + method = 'POST'; + body.objectId = this.id; + } else { + method = 'PUT'; + path += '/' + this.id; + } + } else if (this.id) { path += '/' + this.id; } else if (this.className === '_User') { path = 'users'; @@ -2353,6 +2361,7 @@ const DefaultController = { const RESTController = CoreManager.getRESTController(); const stateController = CoreManager.getObjectStateController(); + const allowCustomObjectId = CoreManager.get('ALLOW_CUSTOM_OBJECT_ID'); options = options || {}; options.returnStatus = options.returnStatus || true; @@ -2375,6 +2384,12 @@ const DefaultController = { if (el instanceof ParseFile) { filesSaved.push(el.save(options)); } else if (el instanceof ParseObject) { + if (allowCustomObjectId && !el.id) { + throw new ParseError( + ParseError.MISSING_OBJECT_ID, + 'objectId must not be empty, null or undefined' + ); + } pending.push(el); } }); @@ -2468,6 +2483,12 @@ const DefaultController = { }); }); } else if (target instanceof ParseObject) { + if (allowCustomObjectId && !target.id) { + throw new ParseError( + ParseError.MISSING_OBJECT_ID, + 'objectId must not be empty, null or undefined' + ); + } // generate _localId in case if cascadeSave=false target._getId(); const localId = target._localId; diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index afe8ebd17..d0dd5ab9d 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -161,6 +161,13 @@ describe('Parse module', () => { CoreManager.set('REQUEST_BATCH_SIZE', 20); }); + it('can set allowCustomObjectId', () => { + expect(Parse.allowCustomObjectId).toBe(false); + Parse.allowCustomObjectId = true; + expect(CoreManager.get('ALLOW_CUSTOM_OBJECT_ID')).toBe(true); + Parse.allowCustomObjectId = false; + }); + it('getServerHealth', () => { const controller = { request: jest.fn(), diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 62b6a25a5..976b0bf12 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -3770,4 +3770,40 @@ describe('ParseObject pin', () => { done(); }); }); + + it('can allowCustomObjectId', async done => { + CoreManager.set('ALLOW_CUSTOM_OBJECT_ID', true); + const o = new ParseObject('Person'); + let params = o._getSaveParams(); + expect(params).toEqual({ + method: 'POST', + body: { objectId: undefined }, + path: 'classes/Person', + }); + try { + await o.save(); + done.fail(); + } catch (error) { + expect(error.message).toBe('objectId must not be empty, null or undefined'); + } + try { + await ParseObject.saveAll([o]); + done.fail(); + } catch (error) { + expect(error.message).toBe('objectId must not be empty, null or undefined'); + } + o._finishFetch({ + objectId: 'CUSTOM_ID', + createdAt: { __type: 'Date', iso: new Date().toISOString() }, + updatedAt: { __type: 'Date', iso: new Date().toISOString() }, + }); + params = o._getSaveParams(); + expect(params).toEqual({ + method: 'PUT', + body: {}, + path: 'classes/Person/CUSTOM_ID', + }); + CoreManager.set('ALLOW_CUSTOM_OBJECT_ID', false); + done(); + }); });