Skip to content

feat: Add support for dot notation on array fields of Parse Object #9115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 8, 2024
64 changes: 64 additions & 0 deletions spec/ParseObject.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }]);
Expand Down
8 changes: 7 additions & 1 deletion src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@
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) {
Expand Down Expand Up @@ -207,7 +209,11 @@
if (index === 0) {
return `"${cmpt}"`;
}
return `'${cmpt}'`;
if (isArrayIndex(cmpt)) {
return Number(cmpt);

Check warning on line 213 in src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

View check run for this annotation

Codecov / codecov/patch

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js#L212-L213

Added lines #L212 - L213 were not covered by tests
} else {
return `'${cmpt}'`;

Check warning on line 215 in src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

View check run for this annotation

Codecov / codecov/patch

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js#L215

Added line #L215 was not covered by tests
}
});
};

Expand Down
8 changes: 8 additions & 0 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 11 additions & 3 deletions src/Controllers/SchemaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
// "<array>.<index>" for Nested Arrays
// "<embedded document>.<field>" 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) === '_') {
Expand Down
Loading