From 616d6f3b69ebf9c625163382c412445ffdf37309 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Thu, 8 Aug 2019 11:03:21 -0400 Subject: [PATCH 01/19] added console.log --- src/ParseObject.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index 7270227f4..c2482432b 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -2143,6 +2143,7 @@ const DefaultController = { }, save(target: ParseObject | Array, options: RequestOptions) { + console.log('saving object!!'); const batchSize = (options && options.batchSize) ? options.batchSize : DEFAULT_BATCH_SIZE; const localDatastore = CoreManager.getLocalDatastore(); const mapIdForPin = {}; From 4e693dd61d0a54258f5c74e01c76be72ee544de7 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Thu, 8 Aug 2019 17:46:50 -0400 Subject: [PATCH 02/19] added _batchCount and _batchIndex properties --- src/ParseObject.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index c2482432b..974bd31bd 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -309,6 +309,8 @@ class ParseObject { for (attr in pending[0]) { json[attr] = pending[0][attr].toJSON(); } + json._batchIndex = this._batchIndex || 0; + json._batchCount = this._batchCount || 1; return json; } @@ -2143,7 +2145,6 @@ const DefaultController = { }, save(target: ParseObject | Array, options: RequestOptions) { - console.log('saving object!!'); const batchSize = (options && options.batchSize) ? options.batchSize : DEFAULT_BATCH_SIZE; const localDatastore = CoreManager.getLocalDatastore(); const mapIdForPin = {}; @@ -2161,6 +2162,8 @@ const DefaultController = { let unsaved = target.concat(); for (let i = 0; i < target.length; i++) { if (target[i] instanceof ParseObject) { + target[i]._batchIndex = i; + target[i]._batchCount = target.length; unsaved = unsaved.concat(unsavedChildren(target[i], true)); } } From 50aa5a8befc7c7594eff4f65165c8ecf56c8a3a3 Mon Sep 17 00:00:00 2001 From: Steve Date: Thu, 8 Aug 2019 21:40:44 -0400 Subject: [PATCH 03/19] test batch index and count --- src/__tests__/ParseObject-test.js | 55 ++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 8b7429866..11ff45428 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -348,7 +348,7 @@ describe('ParseObject', () => { expect(o.dirtyKeys()).toEqual(['name']); expect(o.dirty()).toBe(true); expect(o.dirty('name')).toBe(true); - expect(o._getSaveJSON()).toEqual({ name: 'Will' }); + expect(o._getSaveJSON()).toEqual({ name: 'Will', _batchCount: 1, _batchIndex: 0 }); // set multiple fields at once o.set({ name: 'William', behavior: 'formal' }); @@ -443,11 +443,19 @@ describe('ParseObject', () => { expect(o.attributes).toEqual({ age: 1 }); expect(o.op('age') instanceof IncrementOp).toBe(true); expect(o.dirtyKeys()).toEqual(['age']); - expect(o._getSaveJSON()).toEqual({ age: { __op: 'Increment', amount: 1 } }); + expect(o._getSaveJSON()).toEqual({ + _batchCount: 1, + _batchIndex: 0, + age: { __op: 'Increment', amount: 1 } + }); o.increment('age', 4); expect(o.attributes).toEqual({ age: 5 }); - expect(o._getSaveJSON()).toEqual({ age: { __op: 'Increment', amount: 5 } }); + expect(o._getSaveJSON()).toEqual({ + _batchCount: 1, + _batchIndex: 0, + age: { __op: 'Increment', amount: 5 } + }); expect(o.increment.bind(o, 'age', 'four')).toThrow( 'Cannot increment by a non-numeric amount.' @@ -462,7 +470,11 @@ describe('ParseObject', () => { o.set('age', 30); o.increment('age'); expect(o.attributes).toEqual({ age: 31 }); - expect(o._getSaveJSON()).toEqual({ age: 31 }); + expect(o._getSaveJSON()).toEqual({ + _batchCount: 1, + _batchIndex: 0, + age: 31 + }); const o2 = new ParseObject('Person'); o2._finishFetch({ @@ -499,6 +511,8 @@ describe('ParseObject', () => { expect(o.dirtyKeys()).toEqual(['otherField', 'objectField.number', 'objectField']); expect(o._getSaveJSON()).toEqual({ 'objectField.number': 20, + _batchCount: 1, + _batchIndex: 0, otherField: { hello: 'world' }, }); }); @@ -510,7 +524,10 @@ describe('ParseObject', () => { expect(o.attributes).toEqual({}); expect(o.op('objectField.number') instanceof SetOp).toBe(false); expect(o.dirtyKeys()).toEqual([]); - expect(o._getSaveJSON()).toEqual({}); + expect(o._getSaveJSON()).toEqual({ + _batchCount: 1, + _batchIndex: 0 + }); }); it('can add elements to an array field', () => { @@ -650,6 +667,8 @@ describe('ParseObject', () => { expect(o.dirty()).toBe(true); expect(o.dirtyKeys()).toEqual(['obj']); expect(o._getSaveJSON()).toEqual({ + _batchCount: 1, + _batchIndex: 0, obj: { a: 12, b: 21 @@ -1435,6 +1454,8 @@ describe('ParseObject', () => { method: 'POST', path: '/1/classes/Item', body: { + _batchCount: 2, + _batchIndex: 0, child: { __type: 'Pointer', className: 'Item', @@ -1495,7 +1516,10 @@ describe('ParseObject', () => { [{ method: 'POST', path: '/1/classes/Item', - body: { } + body: { + _batchCount: 1, + _batchIndex: 0, + } }] ); xhrs[0].responseText = JSON.stringify([ { success: { objectId: 'grandchild' } } ]); @@ -1512,6 +1536,8 @@ describe('ParseObject', () => { method: 'POST', path: '/1/classes/Item', body: { + _batchCount: 1, + _batchIndex: 0, child: { __type: 'Pointer', className: 'Item', @@ -1534,6 +1560,8 @@ describe('ParseObject', () => { method: 'POST', path: '/1/classes/Item', body: { + _batchCount: 1, + _batchIndex: 0, child: { __type: 'Pointer', className: 'Item', @@ -1644,7 +1672,10 @@ describe('ParseObject', () => { expect(JSON.parse(xhr.send.mock.calls[0]).requests[0]).toEqual({ method: 'POST', path: '/1/classes/Person', - body: {} + body: { + _batchCount: 5, + _batchIndex: 0, + } }); done(); }); @@ -2238,7 +2269,10 @@ describe('ObjectController', () => { objectId: 'b123', items: [{ __type: 'Pointer', objectId: 'i222', className: 'Item' }] }); - expect(brand._getSaveJSON()).toEqual({}); + expect(brand._getSaveJSON()).toEqual({ + _batchCount: 1, + _batchIndex: 0, + }); const items = brand.get('items'); items.push(new ParseObject('Item')); brand.set('items', items); @@ -2395,7 +2429,10 @@ describe('ParseObject (unique instance mode)', () => { expect(JSON.parse(xhr.send.mock.calls[0]).requests[0]).toEqual({ method: 'POST', path: '/1/classes/Person', - body: {} + body: { + _batchCount: 5, + _batchIndex: 0, + } }); }); jest.runAllTicks(); From 4275c29214c17db976717dd823783905ad186b5b Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Mon, 23 Dec 2019 18:03:53 -0500 Subject: [PATCH 04/19] added hint --- src/ParseQuery.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ParseQuery.js b/src/ParseQuery.js index de8fbc5b1..3d0468b75 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -232,7 +232,6 @@ class ParseQuery { _queriesLocalDatastore: boolean; _localDatastorePinName: any; _extraOptions: { [key: string]: mixed }; - /** * @param {(String|Parse.Object)} objectClass An instance of a subclass of Parse.Object, or a Parse className string. */ @@ -729,7 +728,6 @@ class ParseQuery { */ aggregate(pipeline: mixed, options?: FullOptions): Promise> { options = options || {}; - const aggregateOptions = {}; aggregateOptions.useMasterKey = true; @@ -742,8 +740,7 @@ class ParseQuery { throw new Error('Invalid pipeline must be Array or Object'); } - const params = { pipeline }; - + const params = { pipeline, hint: this._hint }; return controller.aggregate( this.className, params, @@ -910,6 +907,13 @@ class ParseQuery { }); } + hint(value: mixed): ParseQuery { + if (typeof value === 'undefined') { + delete this._hint; + } + this._hint = value; + } + /** Query Conditions **/ /** From 7d488b8575662d1ea7279e24ee8004c4304a07cf Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Tue, 7 Jan 2020 16:26:31 -0500 Subject: [PATCH 05/19] added hint to ParseQuery --- src/ParseQuery.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ParseQuery.js b/src/ParseQuery.js index 3d0468b75..c194520ba 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -232,6 +232,7 @@ class ParseQuery { _queriesLocalDatastore: boolean; _localDatastorePinName: any; _extraOptions: { [key: string]: mixed }; + _hint: mixed; /** * @param {(String|Parse.Object)} objectClass An instance of a subclass of Parse.Object, or a Parse className string. */ @@ -269,6 +270,7 @@ class ParseQuery { this._queriesLocalDatastore = false; this._localDatastorePinName = null; this._extraOptions = {}; + this._hint = null; } /** @@ -431,6 +433,9 @@ class ParseQuery { if (this._subqueryReadPreference) { params.subqueryReadPreference = this._subqueryReadPreference; } + if (this._hint) { + params.hint = this._hint; + } for (const key in this._extraOptions) { params[key] = this._extraOptions[key]; } @@ -504,9 +509,13 @@ class ParseQuery { this._subqueryReadPreference = json.subqueryReadPreference; } + if (json.hint) { + this._hint = json.hint; + } + for (const key in json) { if (json.hasOwnProperty(key)) { - if (["where", "include", "keys", "count", "limit", "skip", "order", "readPreference", "includeReadPreference", "subqueryReadPreference"].indexOf(key) === -1) { + if (["where", "include", "keys", "count", "limit", "skip", "order", "readPreference", "includeReadPreference", "subqueryReadPreference", "hint"].indexOf(key) === -1) { this._extraOptions[key] = json[key]; } } @@ -703,7 +712,8 @@ class ParseQuery { const controller = CoreManager.getQueryController(); const params = { distinct: key, - where: this._where + where: this._where, + hint: this._hint, }; return controller.aggregate( @@ -855,7 +865,7 @@ class ParseQuery { return s; }); } - + query._hint = this._hint; query._where = {}; for (const attr in this._where) { const val = this._where[attr]; @@ -1718,7 +1728,6 @@ class ParseQuery { const DefaultController = { find(className: string, params: QueryJSON, options: RequestOptions): Promise> { const RESTController = CoreManager.getRESTController(); - return RESTController.request( 'GET', 'classes/' + className, From cd612b9dbe53d3b9da2eefef84221cd8f62fd192 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Tue, 7 Jan 2020 16:52:46 -0500 Subject: [PATCH 06/19] fixed failed tests --- src/ParseQuery.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ParseQuery.js b/src/ParseQuery.js index c194520ba..c52ea453c 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -713,7 +713,7 @@ class ParseQuery { const params = { distinct: key, where: this._where, - hint: this._hint, + hint: this._hint ? this._hint : undefined, }; return controller.aggregate( @@ -750,7 +750,10 @@ class ParseQuery { throw new Error('Invalid pipeline must be Array or Object'); } - const params = { pipeline, hint: this._hint }; + const params = { + pipeline, + hint: this._hint ? this._hint : undefined, + }; return controller.aggregate( this.className, params, From 7efb326f98659fb5e44cc24cc0352fd2a5020e0b Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Tue, 7 Jan 2020 17:16:27 -0500 Subject: [PATCH 07/19] removed _batchIndex and _batchCount --- src/ParseObject.js | 4 --- src/__tests__/ParseObject-test.js | 43 +++++-------------------------- 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 974bd31bd..7270227f4 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -309,8 +309,6 @@ class ParseObject { for (attr in pending[0]) { json[attr] = pending[0][attr].toJSON(); } - json._batchIndex = this._batchIndex || 0; - json._batchCount = this._batchCount || 1; return json; } @@ -2162,8 +2160,6 @@ const DefaultController = { let unsaved = target.concat(); for (let i = 0; i < target.length; i++) { if (target[i] instanceof ParseObject) { - target[i]._batchIndex = i; - target[i]._batchCount = target.length; unsaved = unsaved.concat(unsavedChildren(target[i], true)); } } diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 11ff45428..1e10ad487 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -348,7 +348,7 @@ describe('ParseObject', () => { expect(o.dirtyKeys()).toEqual(['name']); expect(o.dirty()).toBe(true); expect(o.dirty('name')).toBe(true); - expect(o._getSaveJSON()).toEqual({ name: 'Will', _batchCount: 1, _batchIndex: 0 }); + expect(o._getSaveJSON()).toEqual({ name: 'Will' }); // set multiple fields at once o.set({ name: 'William', behavior: 'formal' }); @@ -444,16 +444,12 @@ describe('ParseObject', () => { expect(o.op('age') instanceof IncrementOp).toBe(true); expect(o.dirtyKeys()).toEqual(['age']); expect(o._getSaveJSON()).toEqual({ - _batchCount: 1, - _batchIndex: 0, age: { __op: 'Increment', amount: 1 } }); o.increment('age', 4); expect(o.attributes).toEqual({ age: 5 }); expect(o._getSaveJSON()).toEqual({ - _batchCount: 1, - _batchIndex: 0, age: { __op: 'Increment', amount: 5 } }); @@ -471,8 +467,6 @@ describe('ParseObject', () => { o.increment('age'); expect(o.attributes).toEqual({ age: 31 }); expect(o._getSaveJSON()).toEqual({ - _batchCount: 1, - _batchIndex: 0, age: 31 }); @@ -511,8 +505,6 @@ describe('ParseObject', () => { expect(o.dirtyKeys()).toEqual(['otherField', 'objectField.number', 'objectField']); expect(o._getSaveJSON()).toEqual({ 'objectField.number': 20, - _batchCount: 1, - _batchIndex: 0, otherField: { hello: 'world' }, }); }); @@ -524,10 +516,7 @@ describe('ParseObject', () => { expect(o.attributes).toEqual({}); expect(o.op('objectField.number') instanceof SetOp).toBe(false); expect(o.dirtyKeys()).toEqual([]); - expect(o._getSaveJSON()).toEqual({ - _batchCount: 1, - _batchIndex: 0 - }); + expect(o._getSaveJSON()).toEqual({}); }); it('can add elements to an array field', () => { @@ -667,8 +656,6 @@ describe('ParseObject', () => { expect(o.dirty()).toBe(true); expect(o.dirtyKeys()).toEqual(['obj']); expect(o._getSaveJSON()).toEqual({ - _batchCount: 1, - _batchIndex: 0, obj: { a: 12, b: 21 @@ -1454,8 +1441,6 @@ describe('ParseObject', () => { method: 'POST', path: '/1/classes/Item', body: { - _batchCount: 2, - _batchIndex: 0, child: { __type: 'Pointer', className: 'Item', @@ -1516,10 +1501,7 @@ describe('ParseObject', () => { [{ method: 'POST', path: '/1/classes/Item', - body: { - _batchCount: 1, - _batchIndex: 0, - } + body: {} }] ); xhrs[0].responseText = JSON.stringify([ { success: { objectId: 'grandchild' } } ]); @@ -1536,8 +1518,6 @@ describe('ParseObject', () => { method: 'POST', path: '/1/classes/Item', body: { - _batchCount: 1, - _batchIndex: 0, child: { __type: 'Pointer', className: 'Item', @@ -1560,8 +1540,6 @@ describe('ParseObject', () => { method: 'POST', path: '/1/classes/Item', body: { - _batchCount: 1, - _batchIndex: 0, child: { __type: 'Pointer', className: 'Item', @@ -1672,10 +1650,7 @@ describe('ParseObject', () => { expect(JSON.parse(xhr.send.mock.calls[0]).requests[0]).toEqual({ method: 'POST', path: '/1/classes/Person', - body: { - _batchCount: 5, - _batchIndex: 0, - } + body: {} }); done(); }); @@ -2269,10 +2244,7 @@ describe('ObjectController', () => { objectId: 'b123', items: [{ __type: 'Pointer', objectId: 'i222', className: 'Item' }] }); - expect(brand._getSaveJSON()).toEqual({ - _batchCount: 1, - _batchIndex: 0, - }); + expect(brand._getSaveJSON()).toEqual({}); const items = brand.get('items'); items.push(new ParseObject('Item')); brand.set('items', items); @@ -2429,10 +2401,7 @@ describe('ParseObject (unique instance mode)', () => { expect(JSON.parse(xhr.send.mock.calls[0]).requests[0]).toEqual({ method: 'POST', path: '/1/classes/Person', - body: { - _batchCount: 5, - _batchIndex: 0, - } + body: {} }); }); jest.runAllTicks(); From 773601c0372e3bcacd25900625d8273585580032 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 7 Jan 2020 22:10:41 -0500 Subject: [PATCH 08/19] added documentation and support for chaining --- src/ParseQuery.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ParseQuery.js b/src/ParseQuery.js index f3ae92b61..c848ccae3 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -932,11 +932,18 @@ class ParseQuery { }); } + /** + * Adds a hint to force index selection. (https://docs.mongodb.com/manual/reference/operator/meta/hint/) + * + * @param {Mixed} value String or Object of index that should be used when executing query + * @return {Promise} A promise that is resolved with the query completes. + */ hint(value: mixed): ParseQuery { if (typeof value === 'undefined') { delete this._hint; } this._hint = value; + return this; } /** From 1827459a902942b67c074a1d16c25841bb956350 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 7 Jan 2020 22:36:41 -0500 Subject: [PATCH 09/19] added documentation and tests --- src/ParseQuery.js | 7 +- src/__tests__/ParseQuery-test.js | 149 +++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/src/ParseQuery.js b/src/ParseQuery.js index c848ccae3..aa36a3492 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -272,7 +272,6 @@ class ParseQuery { this._queriesLocalDatastore = false; this._localDatastorePinName = null; this._extraOptions = {}; - this._hint = null; this._xhrRequest = { task: null, onchange: () => {}, @@ -723,7 +722,7 @@ class ParseQuery { const params = { distinct: key, where: this._where, - hint: this._hint ? this._hint : undefined, + hint: this._hint, }; return controller.aggregate( this.className, @@ -763,7 +762,7 @@ class ParseQuery { const params = { pipeline, - hint: this._hint ? this._hint : undefined, + hint: this._hint, }; return controller.aggregate( this.className, @@ -936,7 +935,7 @@ class ParseQuery { * Adds a hint to force index selection. (https://docs.mongodb.com/manual/reference/operator/meta/hint/) * * @param {Mixed} value String or Object of index that should be used when executing query - * @return {Promise} A promise that is resolved with the query completes. + * @return {Parse.Query} Returns the query, so you can chain this call. */ hint(value: mixed): ParseQuery { if (typeof value === 'undefined') { diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 0e7807a7e..43748dd1b 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -919,6 +919,14 @@ describe('ParseQuery', () => { }); }); + it('can set hint value', () => { + const q = new ParseQuery('Item'); + q.hint('_id_'); + expect(q.toJSON()).toEqual({ + where: {}, + hint: '_id_', + }); + }); it('can generate queries that include full data for pointers', () => { const q = new ParseQuery('Item'); @@ -1006,6 +1014,19 @@ describe('ParseQuery', () => { expect(q2._extraOptions.randomOption).toBe('test'); }); + it('can use hint', () => { + const q = new ParseQuery('Item'); + q.hint('_id_'); + const json = q.toJSON(); + expect(json).toEqual({ + where: {}, + hint: '_id_', + }); + const q2 = new ParseQuery('Item'); + q2.withJSON(json); + expect(q2._hint).toBe('_id_'); + }); + it('can specify certain fields to send back', () => { const q = new ParseQuery('Item'); q.select('size'); @@ -1568,6 +1589,74 @@ describe('ParseQuery', () => { }); }); + + it('can pass options to each() with hint', (done) => { + CoreManager.setQueryController({ + aggregate() {}, + find(className, params, options) { + expect(className).toBe('Item'); + expect(params).toEqual({ + limit: 100, + order: 'objectId', + keys: 'size,name', + where: { + size: { + $in: ['small', 'medium'] + }, + valid: true + }, + hint: '_id_', + }); + expect(options.useMasterKey).toEqual(true); + expect(options.sessionToken).toEqual('1234'); + return Promise.resolve({ + results: [ + { objectId: 'I55', size: 'medium', name: 'Product 55' }, + { objectId: 'I89', size: 'small', name: 'Product 89' }, + { objectId: 'I91', size: 'small', name: 'Product 91' }, + ] + }); + } + }); + + const q = new ParseQuery('Item'); + q.containedIn('size', ['small', 'medium']); + q.equalTo('valid', true); + q.select('size', 'name'); + q.hint('_id_'); + let calls = 0; + + q.each(() => { + calls++; + }, { + useMasterKey: true, + sessionToken: '1234' + }).then(() => { + expect(calls).toBe(3); + done(); + }); + }); + + it('should add hint as string', () => { + const q = new ParseQuery('Item'); + q.hint('_id_'); + expect(q._hint).toBe('_id_'); + }); + + it('should set hint as object', () => { + const q = new ParseQuery('Item'); + q.hint({ _id: 1 }); + expect(q._hint).toStrictEqual({ _id: 1 }); + }); + + it('should delete hint when set as undefined', () => { + const q = new ParseQuery('Item'); + q.hint('_id_'); + expect(q._hint).toBe('_id_'); + q.hint(); + expect(q._hint).toBeUndefined(); + }); + it('can iterate over results with map()', async () => { CoreManager.setQueryController({ aggregate() {}, @@ -1997,6 +2086,37 @@ describe('ParseQuery', () => { }); }); + it('can pass options to a distinct query with hint', (done) => { + CoreManager.setQueryController({ + find() {}, + aggregate(className, params, options) { + expect(className).toBe('Item'); + expect(params).toEqual({ + distinct: 'size', + where: { + size: 'small' + }, + hint: '_id_', + }); + expect(options.useMasterKey).toEqual(true); + expect(options.sessionToken).toEqual('1234'); + expect(options.requestTask).toBeDefined(); + return Promise.resolve({ + results: ['L'] + }); + } + }); + + + const q = new ParseQuery('Item'); + q.equalTo('size', 'small').hint('_id_').distinct('size', { + sessionToken: '1234' + }).then((results) => { + expect(results[0]).toBe('L'); + done(); + }); + }); + it('can issue an aggregate query with array pipeline', (done) => { const pipeline = [ { group: { objectId: '$name' } } @@ -2102,6 +2222,35 @@ describe('ParseQuery', () => { }); }); + it('can pass options to an aggregate query with hint', (done) => { + const pipeline = [ + { group: { objectId: '$name' } } + ]; + CoreManager.setQueryController({ + find() {}, + aggregate(className, params, options) { + expect(className).toBe('Item'); + expect(params).toEqual({ + pipeline: [{ group: { objectId: '$name' } }], + hint: '_id_', + }); + expect(options.useMasterKey).toEqual(true); + expect(options.sessionToken).toEqual('1234'); + return Promise.resolve({ + results: [] + }); + } + }); + + const q = new ParseQuery('Item'); + q.hint('_id_').aggregate(pipeline, { + sessionToken: '1234' + }).then((results) => { + expect(results).toEqual([]); + done(); + }); + }); + it('can cancel query', async () => { const mockRequestTask = { abort: () => {}, From 81088cd915ac0be973d3d965b69782d3029629c4 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Fri, 17 Jan 2020 10:09:47 -0500 Subject: [PATCH 10/19] added support for metadata and tags --- src/ParseFile.js | 56 +++++++++++++++++++++- src/__tests__/ParseFile-test.js | 84 +++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/ParseFile.js b/src/ParseFile.js index e82fc1f9f..28afe3f6c 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -71,6 +71,8 @@ class ParseFile { _previousSave: ?Promise; _data: ?string; _requestTask: ?any; + _metadata: ?Object; + _tags: ?Object; /** * @param name {String} The file's name. This will be prefixed by a unique @@ -99,11 +101,15 @@ class ParseFile { * @param type {String} Optional Content-Type header to use for the file. If * this is omitted, the content type will be inferred from the name's * extension. + * @param metadata {Object} Optional key value pairs to be stored with file object (S3 Only) + * @param tags {Object} Optional key value pairs to be stored with file object (S3 Only) */ - constructor(name: string, data?: FileData, type?: string) { + constructor(name: string, data?: FileData, type?: string, metadata?: Object, tags?: Object) { const specifiedType = type || ''; this._name = name; + this._metadata = metadata || {}; + this._tags = tags || {}; if (data !== undefined) { if (Array.isArray(data)) { @@ -217,6 +223,8 @@ class ParseFile { save(options?: FullOptions) { options = options || {}; options.requestTask = (task) => this._requestTask = task; + options.metadata = this._metadata; + options.tags = this._tags; const controller = CoreManager.getFileController(); if (!this._previousSave) { @@ -292,6 +300,52 @@ class ParseFile { ); } + /** + * Sets metadata to be saved with file object. + * @param {Object} metadata Key value pairs to be stored with file object + */ + setMetadata(metadata: Object) { + if (metadata && typeof metadata === 'object') { + Object.keys(metadata).forEach((key) => { + this.addMetadata(key, metadata[key]); + }); + } + } + + /** + * Sets metadata to be saved with file object. + * @param {String} key + * @param {String} value + */ + addMetadata(key: String, value: String) { + if (typeof key === 'string' && typeof value === 'string') { + this._metadata[key] = value; + } + } + + /** + * Sets tags to be saved with file object. + * @param {Object} tags Key value pairs to be stored with file object + */ + setTags(tags: Object) { + if (tags && typeof tags === 'object') { + Object.keys(tags).forEach((key) => { + this.addTag(key, tags[key]); + }); + } + } + + /** + * Sets tags to be saved with file object. + * @param {String} key + * @param {String} value + */ + addTag(key: String, value: String) { + if (typeof key === 'string' && typeof value === 'string') { + this._tags[key] = value; + } + } + static fromJSON(obj): ParseFile { if (obj.__type !== 'File') { throw new TypeError('JSON object does not represent a ParseFile'); diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index 9ff972d6f..c47ecbbf7 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -258,6 +258,90 @@ describe('ParseFile', () => { file.cancel(); expect(mockRequestTask.abort).toHaveBeenCalledTimes(1); }); + + it('should save file with metadata and tag options', async () => { + const fileController = { + saveFile: jest.fn().mockResolvedValue({}), + saveBase64: () => {}, + download: () => {}, + }; + CoreManager.setFileController(fileController); + const file = new ParseFile('donald_duck.txt', new File(['Parse'], 'donald_duck.txt')); + file.addMetadata('foo', 'bar'); + file.addTag('bar', 'foo'); + await file.save(); + expect(fileController.saveFile).toHaveBeenCalledWith( + 'donald_duck.txt', + { + file: expect.any(File), + format: 'file', + type: '' + }, + { + metadata: { foo: 'bar' }, + tags: { bar: 'foo' }, + requestTask: expect.any(Function), + }, + ); + }); + + it('should create new ParseFile with metadata and tags', () => { + const metadata = { foo: 'bar' }; + const tags = { bar: 'foo' }; + const file = new ParseFile('parse.txt', [61, 170, 236, 120], '', metadata, tags); + expect(file._source.base64).toBe('ParseA=='); + expect(file._source.type).toBe(''); + expect(file._metadata).toBe(metadata); + expect(file._tags).toBe(tags); + }); + + it('should set metadata', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.setMetadata({ foo: 'bar' }); + expect(file._metadata).toEqual({ foo: 'bar' }); + }); + + it('should set metadata key', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.addMetadata('foo', 'bar'); + expect(file._metadata).toEqual({ foo: 'bar' }); + }); + + it('should not set metadata if value is not a string', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.addMetadata('foo', 10); + expect(file._metadata).toEqual({}); + }); + + it('should not set metadata if key is not a string', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.addMetadata(10, ''); + expect(file._metadata).toEqual({}); + }); + + it('should set tags', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.setTags({ foo: 'bar' }); + expect(file._tags).toEqual({ foo: 'bar' }); + }); + + it('should set tag key', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.addTag('foo', 'bar'); + expect(file._tags).toEqual({ foo: 'bar' }); + }); + + it('should not set tag if value is not a string', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.addTag('foo', 10); + expect(file._tags).toEqual({}); + }); + + it('should not set tag if key is not a string', () => { + const file = new ParseFile('parse.txt', [61, 170, 236, 120]); + file.addTag(10, 'bar'); + expect(file._tags).toEqual({}); + }); }); describe('FileController', () => { From 5a44266e0f6b6cbab35d04db6ebb032ddc4503a7 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Fri, 17 Jan 2020 10:11:48 -0500 Subject: [PATCH 11/19] added more docs --- src/ParseFile.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ParseFile.js b/src/ParseFile.js index 28afe3f6c..caa162a40 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -301,7 +301,7 @@ class ParseFile { } /** - * Sets metadata to be saved with file object. + * Sets metadata to be saved with file object. Overwrites existing metadata * @param {Object} metadata Key value pairs to be stored with file object */ setMetadata(metadata: Object) { @@ -313,7 +313,7 @@ class ParseFile { } /** - * Sets metadata to be saved with file object. + * Sets metadata to be saved with file object. Adds to existing metadata * @param {String} key * @param {String} value */ @@ -324,7 +324,7 @@ class ParseFile { } /** - * Sets tags to be saved with file object. + * Sets tags to be saved with file object. Overwrites existing tags * @param {Object} tags Key value pairs to be stored with file object */ setTags(tags: Object) { @@ -336,7 +336,7 @@ class ParseFile { } /** - * Sets tags to be saved with file object. + * Sets tags to be saved with file object. Adds to existing tags * @param {String} key * @param {String} value */ From 47e462e9460e60d2e6c7a803b61a875aba32a665 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Fri, 17 Jan 2020 15:59:35 -0500 Subject: [PATCH 12/19] removed validation for values --- src/ParseFile.js | 10 +++++----- src/__tests__/ParseFile-test.js | 12 ------------ 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/ParseFile.js b/src/ParseFile.js index caa162a40..5ebcab531 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -315,10 +315,10 @@ class ParseFile { /** * Sets metadata to be saved with file object. Adds to existing metadata * @param {String} key - * @param {String} value + * @param {Mixed} value */ - addMetadata(key: String, value: String) { - if (typeof key === 'string' && typeof value === 'string') { + addMetadata(key: String, value: any) { + if (typeof key === 'string') { this._metadata[key] = value; } } @@ -338,10 +338,10 @@ class ParseFile { /** * Sets tags to be saved with file object. Adds to existing tags * @param {String} key - * @param {String} value + * @param {Mixed} value */ addTag(key: String, value: String) { - if (typeof key === 'string' && typeof value === 'string') { + if (typeof key === 'string') { this._tags[key] = value; } } diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index c47ecbbf7..4b9d5b7dc 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -307,12 +307,6 @@ describe('ParseFile', () => { expect(file._metadata).toEqual({ foo: 'bar' }); }); - it('should not set metadata if value is not a string', () => { - const file = new ParseFile('parse.txt', [61, 170, 236, 120]); - file.addMetadata('foo', 10); - expect(file._metadata).toEqual({}); - }); - it('should not set metadata if key is not a string', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.addMetadata(10, ''); @@ -331,12 +325,6 @@ describe('ParseFile', () => { expect(file._tags).toEqual({ foo: 'bar' }); }); - it('should not set tag if value is not a string', () => { - const file = new ParseFile('parse.txt', [61, 170, 236, 120]); - file.addTag('foo', 10); - expect(file._tags).toEqual({}); - }); - it('should not set tag if key is not a string', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.addTag(10, 'bar'); From da5fa5bdbb1598571d27490da4691ff0236528dd Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 18 Jan 2020 12:30:32 -0500 Subject: [PATCH 13/19] added docs for beforeSaveFile and afterSaveFile --- src/CloudCode.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/CloudCode.js b/src/CloudCode.js index 6730e8749..4399d162c 100644 --- a/src/CloudCode.js +++ b/src/CloudCode.js @@ -101,6 +101,96 @@ * @param {Function} func The function to run before a save. This function should take two parameters a {@link Parse.Cloud.TriggerRequest} and a {@link Parse.Cloud.BeforeSaveResponse}. */ + +/** + * + * Registers an before save file function. A new Parse.File can be returned to override the file that gets saved. + * If you want to replace the rquesting Parse.File with a Parse.File that is already saved, simply return the already saved Parse.File. + * You can also add metadata and tags to the file that will be stored via whatever file storage solution you're using (currently available in AWS S3 only) + * + * **Available in Cloud Code only.** + * + * Example: adding metadata and tags + * ``` + * Parse.Cloud.beforeSaveFile(({ file, user }) => { + * file.addMetadata('foo', 'bar'); + * file.addTag('createdBy', user.id); + * }); + * + * ``` + * + * Example: replacing file with an already saved file + * + * ``` + * Parse.Cloud.beforeSaveFile(({ file, user }) => { + * return user.get('avatar'); + * }); + * + * ``` + * + * Example: replacing file with a different file + * + * ``` + * Parse.Cloud.beforeSaveFile(({ file, user }) => { + * const metadata = { foo: 'bar' }; + * const tags = { createdBy: user.id }; + * const newFile = new Parse.File(file.name(), , 'text/plain', { metadata, tags }); + * return newFile; + * }); + * + * ``` + * + * @method beforeSaveFile + * @name Parse.Cloud.beforeSaveFile + * @param {Function} func The function to run before a file saves. This function should take one parameter, a {@link Parse.Cloud.FileTriggerRequest}. + */ + + +/** + * + * Registers an after save file function. + * + * **Available in Cloud Code only.** + * + * Example: creating a new object that references this file in a separate collection + * ``` + * Parse.Cloud.afterSaveFile(async ({ file, user }) => { + * const fileObject = new Parse.Object('FileObject'); + * fileObject.set('metadata', file.getMetadata()); + * fileObject.set('tags', file.getTags()); + * fileObject.set('name', file.name()); + * fileObject.set('createdBy', user); + * await fileObject.save({ sessionToken: user.getSessionToken() }); + * }); + * + * ``` + * + * Example: replacing file with an already saved file + * + * ``` + * Parse.Cloud.beforeSaveFile(({ file, user }) => { + * return user.get('avatar'); + * }); + * + * ``` + * + * Example: replacing file with a different file + * + * ``` + * Parse.Cloud.beforeSaveFile(({ file, user }) => { + * const metadata = { foo: 'bar' }; + * const tags = { createdBy: user.id }; + * const newFile = new Parse.File(file.name(), , 'text/plain', { metadata, tags }); + * return newFile; + * }); + * + * ``` + * + * @method beforeSaveFile + * @name Parse.Cloud.beforeSaveFile + * @param {Function} func The function to run before a file saves. This function should take one parameter, a {@link Parse.Cloud.FileTriggerRequest}. + */ + /** * Makes an HTTP Request. * @@ -152,7 +242,20 @@ * @property {Parse.Object} original If set, the object, as currently stored. */ + /** + * @typedef Parse.Cloud.FileTriggerRequest + * @property {String} installationId If set, the installationId triggering the request. + * @property {Boolean} master If true, means the master key was used. + * @property {Parse.User} user If set, the user that made the request. + * @property {Parse.File} file The file triggering the hook. + * @property {String} ip The IP address of the client making the request. + * @property {Object} headers The original HTTP headers for the request. + * @property {String} triggerName The name of the trigger (`beforeSaveFile`, `afterSaveFile`, ...) + * @property {Object} log The current logger inside Parse Server. + */ + + /** * @typedef Parse.Cloud.FunctionRequest * @property {String} installationId If set, the installationId triggering the request. * @property {Boolean} master If true, means the master key was used. From 312175d02c2ce6683697e431fc3bc93ff772b5e9 Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 18 Jan 2020 12:33:43 -0500 Subject: [PATCH 14/19] updated docs --- src/CloudCode.js | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/CloudCode.js b/src/CloudCode.js index 4399d162c..4baba53e3 100644 --- a/src/CloudCode.js +++ b/src/CloudCode.js @@ -163,32 +163,9 @@ * await fileObject.save({ sessionToken: user.getSessionToken() }); * }); * - * ``` - * - * Example: replacing file with an already saved file - * - * ``` - * Parse.Cloud.beforeSaveFile(({ file, user }) => { - * return user.get('avatar'); - * }); - * - * ``` - * - * Example: replacing file with a different file - * - * ``` - * Parse.Cloud.beforeSaveFile(({ file, user }) => { - * const metadata = { foo: 'bar' }; - * const tags = { createdBy: user.id }; - * const newFile = new Parse.File(file.name(), , 'text/plain', { metadata, tags }); - * return newFile; - * }); - * - * ``` - * - * @method beforeSaveFile - * @name Parse.Cloud.beforeSaveFile - * @param {Function} func The function to run before a file saves. This function should take one parameter, a {@link Parse.Cloud.FileTriggerRequest}. + * @method afterSaveFile + * @name Parse.Cloud.afterSaveFile + * @param {Function} func The function to run after a file saves. This function should take one parameter, a {@link Parse.Cloud.FileTriggerRequest}. */ /** From 23106c498c6810bf60df86e8ace64bf0e324ddf3 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 18 Jan 2020 20:14:52 -0600 Subject: [PATCH 15/19] add getters --- src/CloudCode.js | 13 +++++-------- src/ParseFile.js | 17 +++++++++++++++++ src/__tests__/ParseFile-test.js | 16 ++++++++-------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/CloudCode.js b/src/CloudCode.js index 4baba53e3..b51d206f6 100644 --- a/src/CloudCode.js +++ b/src/CloudCode.js @@ -101,16 +101,15 @@ * @param {Function} func The function to run before a save. This function should take two parameters a {@link Parse.Cloud.TriggerRequest} and a {@link Parse.Cloud.BeforeSaveResponse}. */ - /** * * Registers an before save file function. A new Parse.File can be returned to override the file that gets saved. * If you want to replace the rquesting Parse.File with a Parse.File that is already saved, simply return the already saved Parse.File. - * You can also add metadata and tags to the file that will be stored via whatever file storage solution you're using (currently available in AWS S3 only) + * You can also add metadata to the file that will be stored via whatever file storage solution you're using. * * **Available in Cloud Code only.** * - * Example: adding metadata and tags + * Example: Adding metadata and tags * ``` * Parse.Cloud.beforeSaveFile(({ file, user }) => { * file.addMetadata('foo', 'bar'); @@ -134,7 +133,7 @@ * Parse.Cloud.beforeSaveFile(({ file, user }) => { * const metadata = { foo: 'bar' }; * const tags = { createdBy: user.id }; - * const newFile = new Parse.File(file.name(), , 'text/plain', { metadata, tags }); + * const newFile = new Parse.File(file.name(), , 'text/plain', metadata, tags); * return newFile; * }); * @@ -145,7 +144,6 @@ * @param {Function} func The function to run before a file saves. This function should take one parameter, a {@link Parse.Cloud.FileTriggerRequest}. */ - /** * * Registers an after save file function. @@ -156,8 +154,8 @@ * ``` * Parse.Cloud.afterSaveFile(async ({ file, user }) => { * const fileObject = new Parse.Object('FileObject'); - * fileObject.set('metadata', file.getMetadata()); - * fileObject.set('tags', file.getTags()); + * fileObject.set('metadata', file.metadata()); + * fileObject.set('tags', file.tags()); * fileObject.set('name', file.name()); * fileObject.set('createdBy', user); * await fileObject.save({ sessionToken: user.getSessionToken() }); @@ -219,7 +217,6 @@ * @property {Parse.Object} original If set, the object, as currently stored. */ - /** * @typedef Parse.Cloud.FileTriggerRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/ParseFile.js b/src/ParseFile.js index 5ebcab531..c9cc25451 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -180,6 +180,7 @@ class ParseFile { this._data = result.base64; return this._data; } + /** * Gets the name of the file. Before save is called, this is the filename * given by the user. After save is called, that name gets prefixed with a @@ -208,6 +209,22 @@ class ParseFile { } } + /** + * Gets the metadata of the file. + * @return {Object} + */ + metadata(): Object { + return this._metadata; + } + + /** + * Gets the tags of the file. + * @return {Object} + */ + tags(): Object { + return this._tags; + } + /** * Saves the file to the Parse cloud. * @param {Object} options diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index 4b9d5b7dc..225b0be88 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -291,44 +291,44 @@ describe('ParseFile', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120], '', metadata, tags); expect(file._source.base64).toBe('ParseA=='); expect(file._source.type).toBe(''); - expect(file._metadata).toBe(metadata); - expect(file._tags).toBe(tags); + expect(file.metadata()).toBe(metadata); + expect(file.tags()).toBe(tags); }); it('should set metadata', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.setMetadata({ foo: 'bar' }); - expect(file._metadata).toEqual({ foo: 'bar' }); + expect(file.metadata()).toEqual({ foo: 'bar' }); }); it('should set metadata key', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.addMetadata('foo', 'bar'); - expect(file._metadata).toEqual({ foo: 'bar' }); + expect(file.metadata()).toEqual({ foo: 'bar' }); }); it('should not set metadata if key is not a string', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.addMetadata(10, ''); - expect(file._metadata).toEqual({}); + expect(file.metadata()).toEqual({}); }); it('should set tags', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.setTags({ foo: 'bar' }); - expect(file._tags).toEqual({ foo: 'bar' }); + expect(file.tags()).toEqual({ foo: 'bar' }); }); it('should set tag key', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.addTag('foo', 'bar'); - expect(file._tags).toEqual({ foo: 'bar' }); + expect(file.tags()).toEqual({ foo: 'bar' }); }); it('should not set tag if key is not a string', () => { const file = new ParseFile('parse.txt', [61, 170, 236, 120]); file.addTag(10, 'bar'); - expect(file._tags).toEqual({}); + expect(file.tags()).toEqual({}); }); }); From 4994edbe9eefab0104530d9dcb6ac284e1497d53 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 18 Jan 2020 21:01:02 -0600 Subject: [PATCH 16/19] clean up --- src/CloudCode.js | 2 +- src/ParseFile.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CloudCode.js b/src/CloudCode.js index b51d206f6..5169834a2 100644 --- a/src/CloudCode.js +++ b/src/CloudCode.js @@ -229,7 +229,7 @@ * @property {Object} log The current logger inside Parse Server. */ - /** +/** * @typedef Parse.Cloud.FunctionRequest * @property {String} installationId If set, the installationId triggering the request. * @property {Boolean} master If true, means the master key was used. diff --git a/src/ParseFile.js b/src/ParseFile.js index 20b59c75f..06edaebd4 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -101,8 +101,8 @@ class ParseFile { * @param type {String} Optional Content-Type header to use for the file. If * this is omitted, the content type will be inferred from the name's * extension. - * @param metadata {Object} Optional key value pairs to be stored with file object (S3 Only) - * @param tags {Object} Optional key value pairs to be stored with file object (S3 Only) + * @param metadata {Object} Optional key value pairs to be stored with file object + * @param tags {Object} Optional key value pairs to be stored with file object */ constructor(name: string, data?: FileData, type?: string, metadata?: Object, tags?: Object) { const specifiedType = type || ''; @@ -339,7 +339,7 @@ class ParseFile { * Sets metadata to be saved with file object. Overwrites existing metadata * @param {Object} metadata Key value pairs to be stored with file object */ - setMetadata(metadata: Object) { + setMetadata(metadata: any) { if (metadata && typeof metadata === 'object') { Object.keys(metadata).forEach((key) => { this.addMetadata(key, metadata[key]); @@ -352,7 +352,7 @@ class ParseFile { * @param {String} key * @param {Mixed} value */ - addMetadata(key: String, value: any) { + addMetadata(key: string, value: any) { if (typeof key === 'string') { this._metadata[key] = value; } @@ -362,7 +362,7 @@ class ParseFile { * Sets tags to be saved with file object. Overwrites existing tags * @param {Object} tags Key value pairs to be stored with file object */ - setTags(tags: Object) { + setTags(tags: any) { if (tags && typeof tags === 'object') { Object.keys(tags).forEach((key) => { this.addTag(key, tags[key]); @@ -375,7 +375,7 @@ class ParseFile { * @param {String} key * @param {Mixed} value */ - addTag(key: String, value: String) { + addTag(key: string, value: string) { if (typeof key === 'string') { this._tags[key] = value; } From 8496b7c635f1a457a737312442063ac8c0b44d79 Mon Sep 17 00:00:00 2001 From: Steve Date: Sun, 19 Jan 2020 18:58:19 -0500 Subject: [PATCH 17/19] added decrement function to ParseObject --- src/ParseObject.js | 18 +++++++++++++ src/__tests__/ParseObject-test.js | 44 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index 2f81cff78..a252db50f 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -764,6 +764,24 @@ class ParseObject { return this.set(attr, new IncrementOp(amount)); } + /** + * Atomically decrements the value of the given attribute the next time the + * object is saved. If no amount is specified, 1 is used by default. + * + * @param attr {String} The key. + * @param amount {Number} The amount to decrement by (optional). + * @return {(ParseObject|Boolean)} + */ + decrement(attr: string, amount?: number): ParseObject | boolean { + if (typeof amount === 'undefined') { + amount = 1; + } + if (typeof amount !== 'number') { + throw new Error('Cannot decrement by a non-numeric amount.'); + } + return this.set(attr, new IncrementOp(amount * -1)); + } + /** * Atomically add an object to the end of the array associated with a given * key. diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 77c4bd61f..cbb798d69 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -491,6 +491,50 @@ describe('ParseObject', () => { expect(o2.attributes).toEqual({ age: 41 }); }); + + it('can decrement a field', () => { + const o = new ParseObject('Person'); + o.decrement('age'); + expect(o.attributes).toEqual({ age: -1 }); + expect(o.op('age') instanceof IncrementOp).toBe(true); + expect(o.dirtyKeys()).toEqual(['age']); + expect(o._getSaveJSON()).toEqual({ + age: { __op: 'Increment', amount: -1 } + }); + + o.decrement('age', 4); + expect(o.attributes).toEqual({ age: -5 }); + expect(o._getSaveJSON()).toEqual({ + age: { __op: 'Increment', amount: -5 } + }); + + expect(o.decrement.bind(o, 'age', 'four')).toThrow( + 'Cannot decrement by a non-numeric amount.' + ); + expect(o.decrement.bind(o, 'age', null)).toThrow( + 'Cannot decrement by a non-numeric amount.' + ); + expect(o.decrement.bind(o, 'age', { amount: 4 })).toThrow( + 'Cannot decrement by a non-numeric amount.' + ); + + o.set('age', 30); + o.decrement('age'); + expect(o.attributes).toEqual({ age: 29 }); + expect(o._getSaveJSON()).toEqual({ + age: 29 + }); + + const o2 = new ParseObject('Person'); + o2._finishFetch({ + objectId: 'ABC123', + age: 40 + }); + expect(o2.attributes).toEqual({ age: 40 }); + o2.decrement('age'); + expect(o2.attributes).toEqual({ age: 39 }); + }); + it('can set nested field', () => { const o = new ParseObject('Person'); o._finishFetch({ From 8cfcbbf0cae7ab8e357e33f6a25f634e6e989742 Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Tue, 21 Jan 2020 15:30:53 -0500 Subject: [PATCH 18/19] udpated scope --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a354e4d67..5dd3f558f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "parse", + "name": "@leapllc/parse", "version": "2.11.0", "description": "The Parse JavaScript SDK", "homepage": "https://parseplatform.org/", @@ -11,7 +11,7 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "https://github.com/parse-community/Parse-SDK-JS" + "url": "https://github.com/stevestencil/Parse-SDK-JS.git" }, "bugs": "https://github.com/parse-community/Parse-SDK-JS/issues", "files": [ From 908536c0624eea7c11053beb819026df03c029eb Mon Sep 17 00:00:00 2001 From: Steve Stencil Date: Tue, 21 Jan 2020 16:13:53 -0500 Subject: [PATCH 19/19] reverted package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5dd3f558f..a354e4d67 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@leapllc/parse", + "name": "parse", "version": "2.11.0", "description": "The Parse JavaScript SDK", "homepage": "https://parseplatform.org/", @@ -11,7 +11,7 @@ "license": "BSD-3-Clause", "repository": { "type": "git", - "url": "https://github.com/stevestencil/Parse-SDK-JS.git" + "url": "https://github.com/parse-community/Parse-SDK-JS" }, "bugs": "https://github.com/parse-community/Parse-SDK-JS/issues", "files": [