From 28bff8d7e53603a324222a31e87f568a87326e18 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 2 May 2024 09:58:47 -0500 Subject: [PATCH 1/6] feat: Support dot notation on Array fields --- spec/ParseObject.spec.js | 15 +++++++++++++++ src/Controllers/SchemaController.js | 12 +++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index b67b3ed670..06e433d0d8 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -568,6 +568,21 @@ describe('Parse.Object testing', () => { ); }); + it('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(); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + expect(result.get('items')[0].count).toBe(20); + expect(result.get('items')[1].count).toBe(5); + }); + it('addUnique with object', function (done) { const x1 = new Parse.Object('X'); x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ad3699aaa5..b0264db196 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1096,9 +1096,15 @@ 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 Array + // "." for Nested Object + const [x, y] = fieldName.split('.'); + fieldName = x; + if (!isNaN(y)) { + type = 'Array'; + } else { + type = 'Object'; + } } let fieldNameToValidate = `${fieldName}`; if (maintenance && fieldNameToValidate.charAt(0) === '_') { From be3601092e3f657c4b406155e14e273c1b3e0f17 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 2 May 2024 11:14:35 -0500 Subject: [PATCH 2/6] fix tests --- spec/ParseObject.spec.js | 2 +- src/Controllers/SchemaController.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 06e433d0d8..4afbeccf3a 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -568,7 +568,7 @@ describe('Parse.Object testing', () => { ); }); - it('can increment array nested fields', async () => { + 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(); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b0264db196..c7d35510f9 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1096,11 +1096,12 @@ export default class SchemaController { maintenance?: boolean ) { if (fieldName.indexOf('.') > 0) { - // "." for Nested Array - // "." for Nested Object + // "." for Nested Arrays + // "." for Nested Objects + // JSON Arrays are treated as Nested Objects const [x, y] = fieldName.split('.'); fieldName = x; - if (!isNaN(y)) { + if (!isNaN(y) && !['sentPerUTCOffset', 'failedPerUTCOffset'].includes(fieldName)) { type = 'Array'; } else { type = 'Object'; From 123828f2526c6fca5634c5d9265cf2b2a8b9ec2e Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 6 Jul 2024 20:18:12 -0500 Subject: [PATCH 3/6] prevent conversion of array to object --- spec/ParseObject.spec.js | 24 ++++++++++++++++++++++-- src/Controllers/DatabaseController.js | 7 +++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 4afbeccf3a..0ea60a2435 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -572,15 +572,35 @@ describe('Parse.Object testing', () => { 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('addUnique with object', function (done) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c5c3d1ab05..ef32e3fd08 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1851,6 +1851,13 @@ 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('.'); + if (!isNaN(index) && Array.isArray(result[field]) && !Array.isArray(response[field])) { + response[field] = result[field]; + } + } } }); return Promise.resolve(response); From 0022e6f1556a8306461464f39b82ee34d621133c Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sun, 7 Jul 2024 08:29:13 -0500 Subject: [PATCH 4/6] Add stricter array index check --- src/Controllers/SchemaController.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index c7d35510f9..d8638e3ecc 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1101,7 +1101,8 @@ export default class SchemaController { // JSON Arrays are treated as Nested Objects const [x, y] = fieldName.split('.'); fieldName = x; - if (!isNaN(y) && !['sentPerUTCOffset', 'failedPerUTCOffset'].includes(fieldName)) { + const isArrayIndex = Array.from(y).every(c => c >= '0' && c <= '9'); + if (isArrayIndex && !['sentPerUTCOffset', 'failedPerUTCOffset'].includes(fieldName)) { type = 'Array'; } else { type = 'Object'; From c439796ebc142cd379c78ddbf4a8feaca017bd70 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 8 Jul 2024 13:19:15 -0500 Subject: [PATCH 5/6] can query on array index --- spec/ParseObject.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 626ad7f593..757963ccb2 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -603,6 +603,25 @@ describe('Parse.Object testing', () => { expect(result.get('items')).toEqual(obj.get('items')); }); + it_only_db('mongo')('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); + }); + it('addUnique with object', function (done) { const x1 = new Parse.Object('X'); x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]); From cd5b74e98c87370d9fb4efb9f495e2ba24dd404b Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 8 Jul 2024 14:13:14 -0500 Subject: [PATCH 6/6] Add Posgres query support --- spec/ParseObject.spec.js | 12 +++++++++++- .../Storage/Postgres/PostgresStorageAdapter.js | 8 +++++++- src/Controllers/DatabaseController.js | 3 ++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 757963ccb2..11d1705762 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -603,7 +603,7 @@ describe('Parse.Object testing', () => { expect(result.get('items')).toEqual(obj.get('items')); }); - it_only_db('mongo')('can query array nested fields', async () => { + it('can query array nested fields', async () => { const objects = []; for (let i = 0; i < 10; i++) { const obj = new TestObject(); @@ -620,6 +620,16 @@ describe('Parse.Object testing', () => { 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) { 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 ef32e3fd08..6d70e95028 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1854,7 +1854,8 @@ class DatabaseController { // Revert array to object conversion on dot notation for arrays (e.g. "field.0.key") if (key.includes('.')) { const [field, index] = key.split('.'); - if (!isNaN(index) && Array.isArray(result[field]) && !Array.isArray(response[field])) { + 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]; } }