From 181161eed2e95d8d4e541b883e0daa209241981e Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Fri, 21 Jun 2019 15:12:34 -0300 Subject: [PATCH 1/8] GraphQL Object constraints Implements the GraphQL Object constraints, which allows us to filter queries results using the `$eq`, `$lt`, `$gt`, `$in`, and other Parse supported constraints. Example: ``` query objects { findMyClass(where: { objField: { _eq: { key: 'foo.bar', value: 'hello' }, _gt: { key: 'foo.number', value: 10 }, _lt: { key: 'anotherNumber', value: 5 } } }) { results { objectId } } } ``` In the example above, we have the `findMyClass` query (automatically generated for the `MyClass` class), and a field named `objField` whose type is Object. The object below represents a valid `objField` value and would satisfy all constraints: ``` { "foo": { "bar": "hello", "number": 11 }, "anotherNumber": 4 } ``` The Object constraint is applied only when using Parse class object type queries. When using "generic" queries such as `get` and `find`, this type of constraint is not available. --- spec/ParseGraphQLServer.spec.js | 17 +++++-- src/GraphQL/loaders/defaultGraphQLTypes.js | 43 ++++++++++++---- src/GraphQL/loaders/parseClassQueries.js | 58 +++++++++++++++++++++- 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 74fc3b6f76..ca0a973c92 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4072,7 +4072,7 @@ describe('ParseGraphQLServer', () => { }); it('should support object values', async () => { - const someFieldValue = { foo: 'bar' }; + const someFieldValue = { foo: 'bar', lorem: 'ipsum', number: 10 }; const createResult = await apolloClient.mutate({ mutation: gql` @@ -4115,10 +4115,13 @@ describe('ParseGraphQLServer', () => { const getResult = await apolloClient.query({ query: gql` - query GetSomeObject($objectId: ID!) { + query GetSomeObject( + $objectId: ID! + $where: SomeClassConstraints + ) { objects { get(className: "SomeClass", objectId: $objectId) - findSomeClass(where: { someField: { _exists: true } }) { + findSomeClass(where: $where) { results { objectId } @@ -4128,6 +4131,14 @@ describe('ParseGraphQLServer', () => { `, variables: { objectId: createResult.data.objects.create.objectId, + where: { + someField: [ + { _eq: { key: 'foo', value: 'bar' } }, + { _eq: { key: 'lorem', value: 'ipsum' } }, + { _gt: { key: 'number', value: 9 } }, + { _lt: { key: 'number', value: 11 } }, + ], + }, }, }); diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index ad7b60335c..9aeb48dd1c 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -846,21 +846,42 @@ const ARRAY_CONSTRAINT = new GraphQLInputObjectType({ }, }); -const OBJECT_CONSTRAINT = new GraphQLInputObjectType({ - name: 'ObjectConstraint', - description: - 'The ObjectConstraint input type is used in operations that involve filtering objects by a field of type Object.', +const OBJECT_ENTRY = new GraphQLInputObjectType({ + name: 'ObjectEntry', + description: 'An entry from an object, i.e., a pair of key and value.', fields: { - _eq: _eq(OBJECT), - _ne: _ne(OBJECT), - _in: _in(OBJECT), - _nin: _nin(OBJECT), - _exists, - _select, - _dontSelect, + key: { + description: 'The key used to retrieve the value of this entry.', + type: new GraphQLNonNull(GraphQLString), + }, + value: { + description: 'The value of the entry. Could be any type of scalar data.', + type: ANY, + }, }, }); +const OBJECT_CONSTRAINT = new GraphQLList( + new GraphQLInputObjectType({ + name: 'ObjectConstraint', + description: + 'The ObjectConstraint input type is used in operations that involve filtering result by a field of type Object.', + fields: { + _eq: _eq(OBJECT_ENTRY), + _ne: _ne(OBJECT_ENTRY), + _in: _in(OBJECT_ENTRY), + _nin: _nin(OBJECT_ENTRY), + _lt: _lt(OBJECT_ENTRY), + _lte: _lte(OBJECT_ENTRY), + _gt: _gt(OBJECT_ENTRY), + _gte: _gte(OBJECT_ENTRY), + _exists, + _select, + _dontSelect, + }, + }) +); + const DATE_CONSTRAINT = new GraphQLInputObjectType({ name: 'DateConstraint', description: diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 7c8a048467..494564a994 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -57,7 +57,6 @@ const load = (parseGraphQLSchema, parseClass) => { async resolve(_source, args, context, queryInfo) { try { const { - where, order, skip, limit, @@ -65,6 +64,7 @@ const load = (parseGraphQLSchema, parseClass) => { includeReadPreference, subqueryReadPreference, } = args; + let { where } = args; const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); @@ -75,6 +75,62 @@ const load = (parseGraphQLSchema, parseClass) => { ); const parseOrder = order && order.join(','); + if (where) { + const newConstraints = Object.keys(where).reduce( + (newConstraints, fieldName) => { + // If the field type is Object, we need to transform the constraints to the + // format supported by Parse. + if ( + parseClass.fields[fieldName] && + parseClass.fields[fieldName].type === 'Object' + ) { + const parseNewConstraints = where[fieldName].reduce( + (parseNewConstraints, gqlObjectConstraint) => { + const gqlConstraintEntries = Object.entries( + gqlObjectConstraint + ); + + // Each GraphQL ObjectConstraint should be composed by: + // { : { : } } + // Example: _eq : { 'foo.bar' : 'myobjectfield.foo.bar value' } + gqlConstraintEntries.forEach( + ([constraintName, constraintValue]) => { + const { key, value } = constraintValue; // the object entry () + + // Transformed to: + // { : { : } } + // Example: 'myobjectfield.foo.bar': { _eq: 'myobjectfield.foo.bar value' } + const absoluteFieldKey = `${fieldName}.${key}`; + parseNewConstraints[absoluteFieldKey] = { + ...parseNewConstraints[absoluteFieldKey], + [constraintName]: value, + }; + } + ); + return parseNewConstraints; + }, + {} + ); + // Removes the original field constraint from the where statement, now + // that we have extracted the supported constraints from it. + delete where[fieldName]; + + // Returns the new constraints along with the existing ones. + return { + ...newConstraints, + ...parseNewConstraints, + }; + } + return newConstraints; + }, + {} + ); + where = { + ...where, + ...newConstraints, + }; + } + return await objectsQueries.findObjects( className, where, From 9b34f07c01be2c6b5921cd68c0671dce1c4f83e1 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Mon, 24 Jun 2019 20:55:54 -0300 Subject: [PATCH 2/8] Objects constraints not working on Postgres Fixes the $eq, $ne, $gt, and $lt constraints when applied on an Object type field. --- spec/ParseGraphQLServer.spec.js | 16 ++++++- .../Postgres/PostgresStorageAdapter.js | 43 +++++++++++++++---- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index ca0a973c92..980b2dd88d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4072,7 +4072,11 @@ describe('ParseGraphQLServer', () => { }); it('should support object values', async () => { - const someFieldValue = { foo: 'bar', lorem: 'ipsum', number: 10 }; + const someFieldValue = { + foo: { bar: 'baz' }, + lorem: 'ipsum', + number: 10, + }; const createResult = await apolloClient.mutate({ mutation: gql` @@ -4124,6 +4128,7 @@ describe('ParseGraphQLServer', () => { findSomeClass(where: $where) { results { objectId + someField } } } @@ -4133,7 +4138,8 @@ describe('ParseGraphQLServer', () => { objectId: createResult.data.objects.create.objectId, where: { someField: [ - { _eq: { key: 'foo', value: 'bar' } }, + { _eq: { key: 'foo.bar', value: 'baz' } }, + { _ne: { key: 'foo.bar', value: 'bat' } }, { _eq: { key: 'lorem', value: 'ipsum' } }, { _gt: { key: 'number', value: 9 } }, { _lt: { key: 'number', value: 11 } }, @@ -4148,6 +4154,12 @@ describe('ParseGraphQLServer', () => { expect(getResult.data.objects.findSomeClass.results.length).toEqual( 2 ); + expect( + getResult.data.objects.findSomeClass.results[0].someField + ).toEqual(someFieldValue); + expect( + getResult.data.objects.findSomeClass.results[1].someField + ).toEqual(someFieldValue); }); it('should support array values', async () => { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index dda05ae659..349f15a771 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -298,7 +298,7 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { patterns.push(`(${name})::jsonb @> '[${inPatterns.join()}]'::jsonb`); } else if (fieldValue.$regex) { // Handle later - } else { + } else if (typeof fieldValue !== 'object') { patterns.push(`${name} = '${fieldValue}'`); } } @@ -366,8 +366,13 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { 2}) OR $${index}:name IS NULL)` ); } else { + const constraintFieldName = + fieldName.indexOf('.') >= 0 + ? transformDotField(fieldName) + : `$${index}:name`; patterns.push( - `($${index}:name <> $${index + 1} OR $${index}:name IS NULL)` + `(${constraintFieldName} <> $${index + + 1} OR ${constraintFieldName} IS NULL)` ); } } @@ -388,9 +393,12 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { values.push(fieldName); index += 1; } else { - patterns.push(`$${index}:name = $${index + 1}`); - values.push(fieldName, fieldValue.$eq); - index += 2; + const constraintFieldName = + fieldName.indexOf('.') >= 0 + ? transformDotField(fieldName) + : `$${index}:name`; + values.push(fieldValue.$eq); + patterns.push(`${constraintFieldName} = $${index++}`); } } const isInOrNin = @@ -757,9 +765,28 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { Object.keys(ParseToPosgresComparator).forEach(cmp => { if (fieldValue[cmp] || fieldValue[cmp] === 0) { const pgComparator = ParseToPosgresComparator[cmp]; - patterns.push(`$${index}:name ${pgComparator} $${index + 1}`); - values.push(fieldName, toPostgresValue(fieldValue[cmp])); - index += 2; + const postgresValue = toPostgresValue(fieldValue[cmp]); + let constraintFieldName; + if (fieldName.indexOf('.') >= 0) { + let castType; + switch (typeof postgresValue) { + case 'number': + castType = 'double precision'; + break; + case 'boolean': + castType = 'boolean'; + break; + default: + castType = undefined; + } + constraintFieldName = castType + ? `CAST ((${transformDotField(fieldName)}) AS ${castType})` + : transformDotField(fieldName); + } else { + constraintFieldName = fieldName; + } + values.push(postgresValue); + patterns.push(`${constraintFieldName} ${pgComparator} $${index++}`); } }); From efe3e325913b48a7b642d249e7b65f43385f5e0b Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Tue, 25 Jun 2019 13:34:18 -0300 Subject: [PATCH 3/8] Fix object constraint field name --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 349f15a771..23d0cdfeb9 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -783,7 +783,8 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { ? `CAST ((${transformDotField(fieldName)}) AS ${castType})` : transformDotField(fieldName); } else { - constraintFieldName = fieldName; + constraintFieldName = `$${index++}:name`; + values.push(fieldName); } values.push(postgresValue); patterns.push(`${constraintFieldName} ${pgComparator} $${index++}`); From 12ece92d34d7c74b678ede27ace191ddb1dc2e76 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Tue, 25 Jun 2019 15:39:32 -0300 Subject: [PATCH 4/8] Fix Postgres constraints indexes --- .../Postgres/PostgresStorageAdapter.js | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 23d0cdfeb9..dcd71162a8 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -366,14 +366,16 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { 2}) OR $${index}:name IS NULL)` ); } else { - const constraintFieldName = - fieldName.indexOf('.') >= 0 - ? transformDotField(fieldName) - : `$${index}:name`; - patterns.push( - `(${constraintFieldName} <> $${index + - 1} OR ${constraintFieldName} IS NULL)` - ); + if (fieldName.indexOf('.') >= 0) { + const constraintFieldName = transformDotField(fieldName); + patterns.push( + `(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)` + ); + } else { + patterns.push( + `($${index}:name <> $${index + 1} OR $${index}:name IS NULL)` + ); + } } } } @@ -393,12 +395,14 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { values.push(fieldName); index += 1; } else { - const constraintFieldName = - fieldName.indexOf('.') >= 0 - ? transformDotField(fieldName) - : `$${index}:name`; - values.push(fieldValue.$eq); - patterns.push(`${constraintFieldName} = $${index++}`); + if (fieldName.indexOf('.') >= 0) { + values.push(fieldValue.$eq); + patterns.push(`${transformDotField(fieldName)} = $${index++}`); + } else { + values.push(fieldName, fieldValue.$eq); + patterns.push(`$${index}:name = $${index + 1}`); + index += 2; + } } } const isInOrNin = From 199e6567ba806a92044e34c6d6e11624109461f5 Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Wed, 24 Jul 2019 11:34:31 -0300 Subject: [PATCH 5/8] fix: Object type composed constraints not working --- spec/ParseGraphQLServer.spec.js | 112 +++++++++++++++++++-- src/GraphQL/loaders/defaultGraphQLTypes.js | 44 ++++---- src/GraphQL/loaders/parseClassQueries.js | 104 +++++++++---------- 3 files changed, 172 insertions(+), 88 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 47c337565c..ce13aa1706 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4414,7 +4414,6 @@ describe('ParseGraphQLServer', () => { it('should support object values', async () => { const someFieldValue = { foo: { bar: 'baz' }, - lorem: 'ipsum', number: 10, }; @@ -4477,13 +4476,12 @@ describe('ParseGraphQLServer', () => { variables: { objectId: createResult.data.objects.create.objectId, where: { - someField: [ - { _eq: { key: 'foo.bar', value: 'baz' } }, - { _ne: { key: 'foo.bar', value: 'bat' } }, - { _eq: { key: 'lorem', value: 'ipsum' } }, - { _gt: { key: 'number', value: 9 } }, - { _lt: { key: 'number', value: 11 } }, - ], + someField: { + _eq: { key: 'foo.bar', value: 'baz' }, + _ne: { key: 'foo.bar', value: 'bat' }, + _gt: { key: 'number', value: 9 }, + _lt: { key: 'number', value: 11 }, + }, }, }, }); @@ -4502,6 +4500,104 @@ describe('ParseGraphQLServer', () => { ).toEqual(someFieldValue); }); + it('should support object composed queries', async () => { + const someFieldValue = { + lorem: 'ipsum', + number: 10, + }; + const someFieldValue2 = { + foo: { + test: 'bar', + }, + number: 10, + }; + + const createResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeObject($fields: Object, $fields2: Object) { + objects { + create1: create(className: "SomeClass", fields: $fields) { + objectId + } + create2: create(className: "SomeClass", fields: $fields2) { + objectId + } + } + } + `, + variables: { + fields: { + someField: someFieldValue, + }, + fields2: { + someField: someFieldValue2, + }, + }, + }); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const findResult = await apolloClient.query({ + query: gql` + query FindSomeObject($where: SomeClassConstraints) { + objects { + findSomeClass(where: $where) { + results { + objectId + someField + } + } + } + } + `, + variables: { + where: { + _and: [ + { + someField: { + _gt: { key: 'number', value: 9 }, + }, + }, + { + someField: { + _lt: { key: 'number', value: 11 }, + }, + }, + { + _or: [ + { + someField: { + _eq: { key: 'lorem', value: 'ipsum' }, + }, + }, + { + someField: { + _eq: { key: 'foo.test', value: 'bar' }, + }, + }, + ], + }, + ], + }, + }, + }); + + const { results } = findResult.data.objects.findSomeClass; + expect(results.length).toEqual(2); + expect( + results.find( + result => + result.objectId === createResult.data.objects.create1.objectId + ).someField + ).toEqual(someFieldValue); + expect( + results.find( + result => + result.objectId === createResult.data.objects.create2.objectId + ).someField + ).toEqual(someFieldValue2); + }); + it('should support array values', async () => { const someFieldValue = [1, 'foo', ['bar'], { lorem: 'ipsum' }, true]; diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index 9aeb48dd1c..50b98a1b95 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -846,8 +846,8 @@ const ARRAY_CONSTRAINT = new GraphQLInputObjectType({ }, }); -const OBJECT_ENTRY = new GraphQLInputObjectType({ - name: 'ObjectEntry', +const KEY_VALUE = new GraphQLInputObjectType({ + name: 'KeyValue', description: 'An entry from an object, i.e., a pair of key and value.', fields: { key: { @@ -856,31 +856,29 @@ const OBJECT_ENTRY = new GraphQLInputObjectType({ }, value: { description: 'The value of the entry. Could be any type of scalar data.', - type: ANY, + type: new GraphQLNonNull(ANY), }, }, }); -const OBJECT_CONSTRAINT = new GraphQLList( - new GraphQLInputObjectType({ - name: 'ObjectConstraint', - description: - 'The ObjectConstraint input type is used in operations that involve filtering result by a field of type Object.', - fields: { - _eq: _eq(OBJECT_ENTRY), - _ne: _ne(OBJECT_ENTRY), - _in: _in(OBJECT_ENTRY), - _nin: _nin(OBJECT_ENTRY), - _lt: _lt(OBJECT_ENTRY), - _lte: _lte(OBJECT_ENTRY), - _gt: _gt(OBJECT_ENTRY), - _gte: _gte(OBJECT_ENTRY), - _exists, - _select, - _dontSelect, - }, - }) -); +const OBJECT_CONSTRAINT = new GraphQLInputObjectType({ + name: 'ObjectConstraint', + description: + 'The ObjectConstraint input type is used in operations that involve filtering result by a field of type Object.', + fields: { + _eq: _eq(KEY_VALUE), + _ne: _ne(KEY_VALUE), + _in: _in(KEY_VALUE), + _nin: _nin(KEY_VALUE), + _lt: _lt(KEY_VALUE), + _lte: _lte(KEY_VALUE), + _gt: _gt(KEY_VALUE), + _gte: _gte(KEY_VALUE), + _exists, + _select, + _dontSelect, + }, +}); const DATE_CONSTRAINT = new GraphQLInputObjectType({ name: 'DateConstraint', diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 494564a994..a7522745c6 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -4,6 +4,47 @@ import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; import * as parseClassTypes from './parseClassTypes'; +const objectTypeConstraintToParse = (fieldName, objectConstraint) => { + return Object.entries(objectConstraint).reduce( + (acc, [constraint, keyValue]) => { + const { key, value } = keyValue; + return { + ...acc, + [`${fieldName}.${key}`]: { + [constraint]: value, + }, + }; + }, + {} + ); +}; + +const transformConstraintsToParse = (constraints, fields) => { + if (!constraints || typeof constraints !== 'object') { + return constraints; + } + const parseConstraints = {}; + Object.entries(constraints).forEach(([constraint, fieldValue]) => { + // If constraint is a field name, and its type is Object + if (fields[constraint] && fields[constraint].type === 'Object') { + const objectConstraints = objectTypeConstraintToParse( + constraint, + fieldValue + ); + Object.entries(objectConstraints).forEach(([key, value]) => { + parseConstraints[key] = value; + }); + } else if (['_and', '_or', '_nor'].includes(constraint)) { + parseConstraints[constraint] = fieldValue.map(innerConstraints => + transformConstraintsToParse(innerConstraints, fields) + ); + } else { + parseConstraints[constraint] = fieldValue; + } + }); + return parseConstraints; +}; + const load = (parseGraphQLSchema, parseClass) => { const className = parseClass.className; @@ -57,6 +98,7 @@ const load = (parseGraphQLSchema, parseClass) => { async resolve(_source, args, context, queryInfo) { try { const { + where: graphQLWhere, order, skip, limit, @@ -64,10 +106,14 @@ const load = (parseGraphQLSchema, parseClass) => { includeReadPreference, subqueryReadPreference, } = args; - let { where } = args; const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); + const where = transformConstraintsToParse( + graphQLWhere, + parseClass.fields + ); + const { keys, include } = parseClassTypes.extractKeysAndInclude( selectedFields .filter(field => field.includes('.')) @@ -75,62 +121,6 @@ const load = (parseGraphQLSchema, parseClass) => { ); const parseOrder = order && order.join(','); - if (where) { - const newConstraints = Object.keys(where).reduce( - (newConstraints, fieldName) => { - // If the field type is Object, we need to transform the constraints to the - // format supported by Parse. - if ( - parseClass.fields[fieldName] && - parseClass.fields[fieldName].type === 'Object' - ) { - const parseNewConstraints = where[fieldName].reduce( - (parseNewConstraints, gqlObjectConstraint) => { - const gqlConstraintEntries = Object.entries( - gqlObjectConstraint - ); - - // Each GraphQL ObjectConstraint should be composed by: - // { : { : } } - // Example: _eq : { 'foo.bar' : 'myobjectfield.foo.bar value' } - gqlConstraintEntries.forEach( - ([constraintName, constraintValue]) => { - const { key, value } = constraintValue; // the object entry () - - // Transformed to: - // { : { : } } - // Example: 'myobjectfield.foo.bar': { _eq: 'myobjectfield.foo.bar value' } - const absoluteFieldKey = `${fieldName}.${key}`; - parseNewConstraints[absoluteFieldKey] = { - ...parseNewConstraints[absoluteFieldKey], - [constraintName]: value, - }; - } - ); - return parseNewConstraints; - }, - {} - ); - // Removes the original field constraint from the where statement, now - // that we have extracted the supported constraints from it. - delete where[fieldName]; - - // Returns the new constraints along with the existing ones. - return { - ...newConstraints, - ...parseNewConstraints, - }; - } - return newConstraints; - }, - {} - ); - where = { - ...where, - ...newConstraints, - }; - } - return await objectsQueries.findObjects( className, where, From 483a70dc7c1e92134275eaeb70fe319b17f80d6c Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Thu, 1 Aug 2019 19:15:12 -0300 Subject: [PATCH 6/8] fix: Rename key and value fields --- spec/ParseGraphQLServer.spec.js | 16 ++++++++-------- src/GraphQL/loaders/defaultGraphQLTypes.js | 4 ++-- src/GraphQL/loaders/parseClassQueries.js | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index ce13aa1706..c6e7069c7d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4477,10 +4477,10 @@ describe('ParseGraphQLServer', () => { objectId: createResult.data.objects.create.objectId, where: { someField: { - _eq: { key: 'foo.bar', value: 'baz' }, - _ne: { key: 'foo.bar', value: 'bat' }, - _gt: { key: 'number', value: 9 }, - _lt: { key: 'number', value: 11 }, + _eq: { _key: 'foo.bar', _value: 'baz' }, + _ne: { _key: 'foo.bar', _value: 'bat' }, + _gt: { _key: 'number', _value: 9 }, + _lt: { _key: 'number', _value: 11 }, }, }, }, @@ -4555,24 +4555,24 @@ describe('ParseGraphQLServer', () => { _and: [ { someField: { - _gt: { key: 'number', value: 9 }, + _gt: { _key: 'number', _value: 9 }, }, }, { someField: { - _lt: { key: 'number', value: 11 }, + _lt: { _key: 'number', _value: 11 }, }, }, { _or: [ { someField: { - _eq: { key: 'lorem', value: 'ipsum' }, + _eq: { _key: 'lorem', _value: 'ipsum' }, }, }, { someField: { - _eq: { key: 'foo.test', value: 'bar' }, + _eq: { _key: 'foo.test', _value: 'bar' }, }, }, ], diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index 50b98a1b95..7961132fa8 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -850,11 +850,11 @@ const KEY_VALUE = new GraphQLInputObjectType({ name: 'KeyValue', description: 'An entry from an object, i.e., a pair of key and value.', fields: { - key: { + _key: { description: 'The key used to retrieve the value of this entry.', type: new GraphQLNonNull(GraphQLString), }, - value: { + _value: { description: 'The value of the entry. Could be any type of scalar data.', type: new GraphQLNonNull(ANY), }, diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index a7522745c6..30a37a9be6 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -7,11 +7,11 @@ import * as parseClassTypes from './parseClassTypes'; const objectTypeConstraintToParse = (fieldName, objectConstraint) => { return Object.entries(objectConstraint).reduce( (acc, [constraint, keyValue]) => { - const { key, value } = keyValue; + const { _key, _value } = keyValue; return { ...acc, - [`${fieldName}.${key}`]: { - [constraint]: value, + [`${fieldName}.${_key}`]: { + [constraint]: _value, }, }; }, From 25d5611ffd59f53f43da908ec93b35ae6b4c00ba Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Fri, 2 Aug 2019 12:36:23 -0300 Subject: [PATCH 7/8] refactor: Object constraints for generic queries --- spec/ParseGraphQLServer.spec.js | 147 ++++++++++++++--------- src/GraphQL/loaders/objectsQueries.js | 44 ++++++- src/GraphQL/loaders/parseClassQueries.js | 48 +------- 3 files changed, 132 insertions(+), 107 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index c6e7069c7d..638082dd64 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4456,11 +4456,20 @@ describe('ParseGraphQLServer', () => { }, }); - const getResult = await apolloClient.query({ + const where = { + someField: { + _eq: { _key: 'foo.bar', _value: 'baz' }, + _ne: { _key: 'foo.bar', _value: 'bat' }, + _gt: { _key: 'number', _value: 9 }, + _lt: { _key: 'number', _value: 11 }, + }, + }; + const queryResult = await apolloClient.query({ query: gql` query GetSomeObject( $objectId: ID! $where: SomeClassConstraints + $genericWhere: Object ) { objects { get(className: "SomeClass", objectId: $objectId) @@ -4470,34 +4479,38 @@ describe('ParseGraphQLServer', () => { someField } } + find(className: "SomeClass", where: $genericWhere) { + results + } } } `, variables: { objectId: createResult.data.objects.create.objectId, - where: { - someField: { - _eq: { _key: 'foo.bar', _value: 'baz' }, - _ne: { _key: 'foo.bar', _value: 'bat' }, - _gt: { _key: 'number', _value: 9 }, - _lt: { _key: 'number', _value: 11 }, - }, - }, + where, + genericWhere: where, // where and genericWhere types are different }, }); - const { someField } = getResult.data.objects.get; + const { + get: getResult, + findSomeClass, + find, + } = queryResult.data.objects; + + const { someField } = getResult; expect(typeof someField).toEqual('object'); expect(someField).toEqual(someFieldValue); - expect(getResult.data.objects.findSomeClass.results.length).toEqual( - 2 - ); - expect( - getResult.data.objects.findSomeClass.results[0].someField - ).toEqual(someFieldValue); - expect( - getResult.data.objects.findSomeClass.results[1].someField - ).toEqual(someFieldValue); + + // Checks class query results + expect(findSomeClass.results.length).toEqual(2); + expect(findSomeClass.results[0].someField).toEqual(someFieldValue); + expect(findSomeClass.results[1].someField).toEqual(someFieldValue); + + // Checks generic query results + expect(find.results.length).toEqual(2); + expect(find.results[0].someField).toEqual(someFieldValue); + expect(find.results[1].someField).toEqual(someFieldValue); }); it('should support object composed queries', async () => { @@ -4537,9 +4550,40 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + const where = { + _and: [ + { + someField: { + _gt: { _key: 'number', _value: 9 }, + }, + }, + { + someField: { + _lt: { _key: 'number', _value: 11 }, + }, + }, + { + _or: [ + { + someField: { + _eq: { _key: 'lorem', _value: 'ipsum' }, + }, + }, + { + someField: { + _eq: { _key: 'foo.test', _value: 'bar' }, + }, + }, + ], + }, + ], + }; const findResult = await apolloClient.query({ query: gql` - query FindSomeObject($where: SomeClassConstraints) { + query FindSomeObject( + $where: SomeClassConstraints + $genericWhere: Object + ) { objects { findSomeClass(where: $where) { results { @@ -4547,54 +4591,43 @@ describe('ParseGraphQLServer', () => { someField } } + find(className: "SomeClass", where: $genericWhere) { + results + } } } `, variables: { - where: { - _and: [ - { - someField: { - _gt: { _key: 'number', _value: 9 }, - }, - }, - { - someField: { - _lt: { _key: 'number', _value: 11 }, - }, - }, - { - _or: [ - { - someField: { - _eq: { _key: 'lorem', _value: 'ipsum' }, - }, - }, - { - someField: { - _eq: { _key: 'foo.test', _value: 'bar' }, - }, - }, - ], - }, - ], - }, + where, + genericWhere: where, // where and genericWhere types are different }, }); - const { results } = findResult.data.objects.findSomeClass; + const { create1, create2 } = createResult.data.objects; + const { findSomeClass, find } = findResult.data.objects; + + // Checks class query results + const { results } = findSomeClass; expect(results.length).toEqual(2); expect( - results.find( - result => - result.objectId === createResult.data.objects.create1.objectId - ).someField + results.find(result => result.objectId === create1.objectId) + .someField + ).toEqual(someFieldValue); + expect( + results.find(result => result.objectId === create2.objectId) + .someField + ).toEqual(someFieldValue2); + + // Checks generic query results + const { results: genericResults } = find; + expect(genericResults.length).toEqual(2); + expect( + genericResults.find(result => result.objectId === create1.objectId) + .someField ).toEqual(someFieldValue); expect( - results.find( - result => - result.objectId === createResult.data.objects.create2.objectId - ).someField + genericResults.find(result => result.objectId === create2.objectId) + .someField ).toEqual(someFieldValue2); }); diff --git a/src/GraphQL/loaders/objectsQueries.js b/src/GraphQL/loaders/objectsQueries.js index 210c84612e..f7430196ad 100644 --- a/src/GraphQL/loaders/objectsQueries.js +++ b/src/GraphQL/loaders/objectsQueries.js @@ -96,13 +96,51 @@ const parseMap = { _point: '$point', }; -const transformToParse = constraints => { +const transformToParse = (constraints, parentFieldName, parentConstraints) => { if (!constraints || typeof constraints !== 'object') { return; } Object.keys(constraints).forEach(fieldName => { let fieldValue = constraints[fieldName]; - if (parseMap[fieldName]) { + + /** + * If we have a key-value pair, we need to change the way the constraint is structured. + * + * Example: + * From: + * { + * "someField": { + * "_lt": { + * "_key":"foo.bar", + * "_value": 100 + * }, + * "_gt": { + * "_key":"foo.bar", + * "_value": 10 + * } + * } + * } + * + * To: + * { + * "someField.foo.bar": { + * "$lt": 100, + * "$gt": 10 + * } + * } + */ + if ( + fieldValue._key && + fieldValue._value && + parentConstraints && + parentFieldName + ) { + delete parentConstraints[parentFieldName]; + parentConstraints[`${parentFieldName}.${fieldValue._key}`] = { + ...parentConstraints[`${parentFieldName}.${fieldValue._key}`], + [parseMap[fieldName]]: fieldValue._value, + }; + } else if (parseMap[fieldName]) { delete constraints[fieldName]; fieldName = parseMap[fieldName]; constraints[fieldName] = fieldValue; @@ -160,7 +198,7 @@ const transformToParse = constraints => { break; } if (typeof fieldValue === 'object') { - transformToParse(fieldValue); + transformToParse(fieldValue, fieldName, constraints); } }); }; diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 30a37a9be6..7c8a048467 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -4,47 +4,6 @@ import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; import * as parseClassTypes from './parseClassTypes'; -const objectTypeConstraintToParse = (fieldName, objectConstraint) => { - return Object.entries(objectConstraint).reduce( - (acc, [constraint, keyValue]) => { - const { _key, _value } = keyValue; - return { - ...acc, - [`${fieldName}.${_key}`]: { - [constraint]: _value, - }, - }; - }, - {} - ); -}; - -const transformConstraintsToParse = (constraints, fields) => { - if (!constraints || typeof constraints !== 'object') { - return constraints; - } - const parseConstraints = {}; - Object.entries(constraints).forEach(([constraint, fieldValue]) => { - // If constraint is a field name, and its type is Object - if (fields[constraint] && fields[constraint].type === 'Object') { - const objectConstraints = objectTypeConstraintToParse( - constraint, - fieldValue - ); - Object.entries(objectConstraints).forEach(([key, value]) => { - parseConstraints[key] = value; - }); - } else if (['_and', '_or', '_nor'].includes(constraint)) { - parseConstraints[constraint] = fieldValue.map(innerConstraints => - transformConstraintsToParse(innerConstraints, fields) - ); - } else { - parseConstraints[constraint] = fieldValue; - } - }); - return parseConstraints; -}; - const load = (parseGraphQLSchema, parseClass) => { const className = parseClass.className; @@ -98,7 +57,7 @@ const load = (parseGraphQLSchema, parseClass) => { async resolve(_source, args, context, queryInfo) { try { const { - where: graphQLWhere, + where, order, skip, limit, @@ -109,11 +68,6 @@ const load = (parseGraphQLSchema, parseClass) => { const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); - const where = transformConstraintsToParse( - graphQLWhere, - parseClass.fields - ); - const { keys, include } = parseClassTypes.extractKeysAndInclude( selectedFields .filter(field => field.includes('.')) From 595d7d865c202f7da6dc8791eff6215dc8119ded Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Fri, 2 Aug 2019 14:47:34 -0300 Subject: [PATCH 8/8] fix: Object constraints not working on Postgres --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index c92fbae63e..791be6c520 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -288,7 +288,7 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { index += 2; } else if (fieldValue.$regex) { // Handle later - } else { + } else if (typeof fieldValue !== 'object') { patterns.push(`$${index}:raw = $${index + 1}::text`); values.push(name, fieldValue); index += 2;