diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 9afc194956..11d1705762 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -568,6 +568,70 @@ describe('Parse.Object testing', () => { ); }); + it_only_db('mongo')('can increment array nested fields', async () => { + const obj = new TestObject(); + obj.set('items', [ { value: 'a', count: 5 }, { value: 'b', count: 1 } ]); + await obj.save(); + obj.increment('items.0.count', 15); + obj.increment('items.1.count', 4); + await obj.save(); + expect(obj.toJSON().items[0].value).toBe('a'); + expect(obj.toJSON().items[1].value).toBe('b'); + expect(obj.toJSON().items[0].count).toBe(20); + expect(obj.toJSON().items[1].count).toBe(5); + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + expect(result.get('items')[0].value).toBe('a'); + expect(result.get('items')[1].value).toBe('b'); + expect(result.get('items')[0].count).toBe(20); + expect(result.get('items')[1].count).toBe(5); + expect(result.get('items')).toEqual(obj.get('items')); + }); + + it_only_db('mongo')('can increment array nested fields missing index', async () => { + const obj = new TestObject(); + obj.set('items', []); + await obj.save(); + obj.increment('items.1.count', 15); + await obj.save(); + expect(obj.toJSON().items[0]).toBe(null); + expect(obj.toJSON().items[1].count).toBe(15); + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + expect(result.get('items')[0]).toBe(null); + expect(result.get('items')[1].count).toBe(15); + expect(result.get('items')).toEqual(obj.get('items')); + }); + + it('can query array nested fields', async () => { + const objects = []; + for (let i = 0; i < 10; i++) { + const obj = new TestObject(); + obj.set('items', [i, { value: i }]); + objects.push(obj); + } + await Parse.Object.saveAll(objects); + let query = new Parse.Query(TestObject); + query.greaterThan('items.1.value', 5); + let result = await query.find(); + expect(result.length).toBe(4); + + query = new Parse.Query(TestObject); + query.lessThan('items.0', 3); + result = await query.find(); + expect(result.length).toBe(3); + + query = new Parse.Query(TestObject); + query.equalTo('items.0', 5); + result = await query.find(); + expect(result.length).toBe(1); + + query = new Parse.Query(TestObject); + query.notEqualTo('items.0', 5); + result = await query.find(); + expect(result.length).toBe(9); + }); + it('addUnique with object', function (done) { const x1 = new Parse.Object('X'); x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index cae66fb51a..77e20297d0 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -175,6 +175,8 @@ const toPostgresSchema = schema => { return schema; }; +const isArrayIndex = (arrayIndex) => Array.from(arrayIndex).every(c => c >= '0' && c <= '9'); + const handleDotFields = object => { Object.keys(object).forEach(fieldName => { if (fieldName.indexOf('.') > -1) { @@ -207,7 +209,11 @@ const transformDotFieldToComponents = fieldName => { if (index === 0) { return `"${cmpt}"`; } - return `'${cmpt}'`; + if (isArrayIndex(cmpt)) { + return Number(cmpt); + } else { + return `'${cmpt}'`; + } }); }; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c5c3d1ab05..6d70e95028 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1851,6 +1851,14 @@ class DatabaseController { // only valid ops that produce an actionable result // the op may have happened on a keypath this._expandResultOnKeyPath(response, key, result); + // Revert array to object conversion on dot notation for arrays (e.g. "field.0.key") + if (key.includes('.')) { + const [field, index] = key.split('.'); + const isArrayIndex = Array.from(index).every(c => c >= '0' && c <= '9'); + if (isArrayIndex && Array.isArray(result[field]) && !Array.isArray(response[field])) { + response[field] = result[field]; + } + } } }); return Promise.resolve(response); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ad3699aaa5..d8638e3ecc 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1096,9 +1096,17 @@ export default class SchemaController { maintenance?: boolean ) { if (fieldName.indexOf('.') > 0) { - // subdocument key (x.y) => ok if x is of type 'object' - fieldName = fieldName.split('.')[0]; - type = 'Object'; + // "." for Nested Arrays + // "." for Nested Objects + // JSON Arrays are treated as Nested Objects + const [x, y] = fieldName.split('.'); + fieldName = x; + const isArrayIndex = Array.from(y).every(c => c >= '0' && c <= '9'); + if (isArrayIndex && !['sentPerUTCOffset', 'failedPerUTCOffset'].includes(fieldName)) { + type = 'Array'; + } else { + type = 'Object'; + } } let fieldNameToValidate = `${fieldName}`; if (maintenance && fieldNameToValidate.charAt(0) === '_') {