From 3fd7e3cc68c8841c279d9df45abf065474f4977b Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 23 Feb 2021 17:39:27 -0600 Subject: [PATCH 1/2] feat: Allow saving with custom objectId --- integration/test/ParseObjectTest.js | 56 +++++++++++++++++++++++++++++ src/CoreManager.js | 1 + src/Parse.js | 11 ++++++ src/ParseObject.js | 26 ++++++++++++-- src/__tests__/Parse-test.js | 7 ++++ src/__tests__/ParseObject-test.js | 36 +++++++++++++++++++ 6 files changed, 135 insertions(+), 2 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 45304dfc8..864573d44 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2055,4 +2055,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 41f9c87da..c340a9796 100644 --- a/src/Parse.js +++ b/src/Parse.js @@ -189,6 +189,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 6ca88dba8..7cbd6bbf2 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -324,10 +324,19 @@ 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'; + path += ''; + body.objectId = this.id; + } else { + method = 'PUT'; + path += '/' + this.id; + } + } else if (this.id) { path += '/' + this.id; } else if (this.className === '_User') { path = 'users'; @@ -2362,6 +2371,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; @@ -2384,6 +2394,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); } }); @@ -2477,6 +2493,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 9e6d54865..b6db4f241 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('_request', () => { const controller = { request: jest.fn(), diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 2dbdf3535..c549f14fa 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -3780,4 +3780,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(); + }); }); From ef55fb41b14d4f79f85faa04e15a91750bbb788d Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 23 Feb 2021 19:00:01 -0600 Subject: [PATCH 2/2] clean up --- src/ParseObject.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 487fd2cde..191a57842 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -330,7 +330,6 @@ class ParseObject { if (CoreManager.get('ALLOW_CUSTOM_OBJECT_ID')) { if (!this.createdAt) { method = 'POST'; - path += ''; body.objectId = this.id; } else { method = 'PUT';