From 9c0096db2058d3a295e3d02a45769a4f2b1c5036 Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 11:48:14 +0200 Subject: [PATCH 1/9] [FEATURE!] Enable Conjunction for Certain Operators in CAP GraphQL Adapter This PR enables conjunction for specific operators in the CAP GraphQL Adapter, allowing for combining multiple conditions in queries. The proposed configuration has been refined based on team discussions and stakeholders and incorporates valuable input from all members. The final decision ensures a balanced approach, considering use cases and potential conflicts. Changes Made: - Updated the configuration in the CAP GraphQL Adapter to enable conjunction for the following operators: - `ne` (Not Equal) - `contains` - Disabled conjunction for the following operators: - `eq` (Equal) - `gt` (Greater Than) - `ge` (Greater Than or Equal) - `le` (Less Than or Equal) - `lt` (Less Than) - `startswith` - `endswith` Sample using `contains`: query MyQuery { AdminService { Books(filter: {title: {contains: ["Wuthering", "Heights"]}}) { nodes { title } } } } Reasoning: - Enabling conjunction for `ne` and `contains` allows combining multiple inequality and string condition in queries, providing greater flexibility. - Disabling conjunction for other operators simplifies the query evaluation process. When multiple values are provided for operators such as `eq`, `gt`, `ge`, `le`, `lt`, only the lowest or highest value in the list will be considered, making the other values without effect. By disabling conjunction for these operators, the query can be simplified to a single value without evaluating additional values, enhancing query performance and reducing complexity. --- lib/constants.js | 15 +- lib/resolvers/parse/ast/fromObject.js | 13 +- lib/resolvers/parse/ast2cqn/where.js | 4 +- lib/schema/args/filter.js | 8 +- test/schemas/bookshop-graphql.gql | 66 ++++----- .../edge-cases/field-named-localized.gql | 24 ++-- .../model-structure/composition-of-aspect.gql | 10 +- test/schemas/types.gql | 128 +++++++++--------- 8 files changed, 146 insertions(+), 122 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index 11ee32d9..64730323 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -26,9 +26,22 @@ const STRING_OPERATIONS = { contains: 'contains' } +const CONJUNCTION_OPERATIONS_SUPPORT = { + [RELATIONAL_OPERATORS.eq]: false, + [RELATIONAL_OPERATORS.ne]: true, + [RELATIONAL_OPERATORS.gt]: false, + [RELATIONAL_OPERATORS.ge]: false, + [RELATIONAL_OPERATORS.le]: false, + [RELATIONAL_OPERATORS.lt]: false, + [STRING_OPERATIONS.startswith]: false, + [STRING_OPERATIONS.endswith]: false, + [STRING_OPERATIONS.contains]: true +} + module.exports = { CONNECTION_FIELDS, ARGS, RELATIONAL_OPERATORS, - STRING_OPERATIONS + STRING_OPERATIONS, + CONJUNCTION_OPERATIONS_SUPPORT } diff --git a/lib/resolvers/parse/ast/fromObject.js b/lib/resolvers/parse/ast/fromObject.js index 02a8a809..cf3d0fac 100644 --- a/lib/resolvers/parse/ast/fromObject.js +++ b/lib/resolvers/parse/ast/fromObject.js @@ -32,13 +32,20 @@ const _arrayToListValue = array => ({ const _variableToValue = variable => { if (Array.isArray(variable)) { return _arrayToListValue(variable) - } else if (isPlainObject(variable)) { + } + + if (isPlainObject(variable)) { return _objectToObjectValue(variable) - } else if (variable === null) { + } + + if (variable === null) { return _nullValue - } else if (variable === undefined) { + } + + if (variable === undefined) { return undefined } + return _valueToGenericScalarValue(variable) } diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index e94d3c4a..82788260 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -24,7 +24,6 @@ const _to_xpr = (ref, gqlOperator, value) => { const val = { val: value } if (STRING_OPERATIONS[gqlOperator]) return [{ func: cdsOperator, args: [ref, val] }] - return [ref, cdsOperator, val] } @@ -34,7 +33,8 @@ const _objectFieldTo_xpr = (objectField, columnName) => { if (objectField.value.kind === Kind.LIST) { const _xprs = objectField.value.values.map(value => _to_xpr(ref, gqlOperator, value.value)) - return _joinedXprFrom_xprs(_xprs, 'and') + const joined_xprs = _joinedXprFrom_xprs(_xprs, 'and') + return joined_xprs } return _to_xpr(ref, gqlOperator, objectField.value.value) diff --git a/lib/schema/args/filter.js b/lib/schema/args/filter.js index d386176e..ede27d87 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -10,7 +10,7 @@ const { const { gqlName } = require('../../utils') const { hasScalarFields, shouldElementBeIgnored } = require('../util') const { cdsToGraphQLScalarType } = require('../types/scalar') -const { RELATIONAL_OPERATORS, STRING_OPERATIONS } = require('../../constants') +const { RELATIONAL_OPERATORS, STRING_OPERATIONS, CONJUNCTION_OPERATIONS_SUPPORT } = require('../../constants') const { GraphQLBinary, GraphQLDate, @@ -86,7 +86,11 @@ module.exports = cache => { const cacheFilterType = cache.get(filterName) if (cacheFilterType) return cacheFilterType - const fields = Object.fromEntries(operations.map(op => [[op], { type: new GraphQLList(gqlType) }])) + const ops = operations.map(op => [ + [op], + { type: CONJUNCTION_OPERATIONS_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType } + ]) + const fields = Object.fromEntries(ops) const newFilterType = new GraphQLInputObjectType({ name: filterName, fields }) cache.set(filterName, newFilterType) diff --git a/test/schemas/bookshop-graphql.gql b/test/schemas/bookshop-graphql.gql index d7de93e6..b30eabca 100644 --- a/test/schemas/bookshop-graphql.gql +++ b/test/schemas/bookshop-graphql.gql @@ -635,7 +635,7 @@ The `Binary` scalar type represents binary values as `base64url` encoded strings scalar Binary input Binary_filter { - eq: [Binary] + eq: Binary ne: [Binary] } @@ -1277,11 +1277,11 @@ The `Date` scalar type represents date values as strings in the ISO 8601 format scalar Date input Date_filter { - eq: [Date] - ge: [Date] - gt: [Date] - le: [Date] - lt: [Date] + eq: Date + ge: Date + gt: Date + le: Date + lt: Date ne: [Date] } @@ -1291,11 +1291,11 @@ The `Decimal` scalar type represents exact signed decimal values. Decimal repres scalar Decimal input Decimal_filter { - eq: [Decimal] - ge: [Decimal] - gt: [Decimal] - le: [Decimal] - lt: [Decimal] + eq: Decimal + ge: Decimal + gt: Decimal + le: Decimal + lt: Decimal ne: [Decimal] } @@ -1305,20 +1305,20 @@ The `Int16` scalar type represents 16-bit non-fractional signed whole numeric va scalar Int16 input Int16_filter { - eq: [Int16] - ge: [Int16] - gt: [Int16] - le: [Int16] - lt: [Int16] + eq: Int16 + ge: Int16 + gt: Int16 + le: Int16 + lt: Int16 ne: [Int16] } input Int_filter { - eq: [Int] - ge: [Int] - gt: [Int] - le: [Int] - lt: [Int] + eq: Int + ge: Int + gt: Int + le: Int + lt: Int ne: [Int] } @@ -1339,14 +1339,14 @@ enum SortDirection { input String_filter { contains: [String] - endswith: [String] - eq: [String] - ge: [String] - gt: [String] - le: [String] - lt: [String] + endswith: String + eq: String + ge: String + gt: String + le: String + lt: String ne: [String] - startswith: [String] + startswith: String } """ @@ -1355,10 +1355,10 @@ The `Timestamp` scalar type represents timestamp values as strings in the ISO 86 scalar Timestamp input Timestamp_filter { - eq: [Timestamp] - ge: [Timestamp] - gt: [Timestamp] - le: [Timestamp] - lt: [Timestamp] + eq: Timestamp + ge: Timestamp + gt: Timestamp + le: Timestamp + lt: Timestamp ne: [Timestamp] } diff --git a/test/schemas/edge-cases/field-named-localized.gql b/test/schemas/edge-cases/field-named-localized.gql index 7edbea38..884aea15 100644 --- a/test/schemas/edge-cases/field-named-localized.gql +++ b/test/schemas/edge-cases/field-named-localized.gql @@ -114,11 +114,11 @@ input FieldNamedLocalizedService_localized_orderBy { } input Int_filter { - eq: [Int] - ge: [Int] - gt: [Int] - le: [Int] - lt: [Int] + eq: Int + ge: Int + gt: Int + le: Int + lt: Int ne: [Int] } @@ -137,12 +137,12 @@ enum SortDirection { input String_filter { contains: [String] - endswith: [String] - eq: [String] - ge: [String] - gt: [String] - le: [String] - lt: [String] + endswith: String + eq: String + ge: String + gt: String + le: String + lt: String ne: [String] - startswith: [String] + startswith: String } diff --git a/test/schemas/model-structure/composition-of-aspect.gql b/test/schemas/model-structure/composition-of-aspect.gql index a6b68cbe..73ff916b 100644 --- a/test/schemas/model-structure/composition-of-aspect.gql +++ b/test/schemas/model-structure/composition-of-aspect.gql @@ -107,11 +107,11 @@ type CompositionOfAspectService_input { } input ID_filter { - eq: [ID] - ge: [ID] - gt: [ID] - le: [ID] - lt: [ID] + eq: ID + ge: ID + gt: ID + le: ID + lt: ID ne: [ID] } diff --git a/test/schemas/types.gql b/test/schemas/types.gql index fe8f8fae..ef7c447e 100644 --- a/test/schemas/types.gql +++ b/test/schemas/types.gql @@ -4,12 +4,12 @@ The `Binary` scalar type represents binary values as `base64url` encoded strings scalar Binary input Binary_filter { - eq: [Binary] + eq: Binary ne: [Binary] } input Boolean_filter { - eq: [Boolean] + eq: Boolean ne: [Boolean] } @@ -24,20 +24,20 @@ The `DateTime` scalar type represents datetime values as strings in the ISO 8601 scalar DateTime input DateTime_filter { - eq: [DateTime] - ge: [DateTime] - gt: [DateTime] - le: [DateTime] - lt: [DateTime] + eq: DateTime + ge: DateTime + gt: DateTime + le: DateTime + lt: DateTime ne: [DateTime] } input Date_filter { - eq: [Date] - ge: [Date] - gt: [Date] - le: [Date] - lt: [Date] + eq: Date + ge: Date + gt: Date + le: Date + lt: Date ne: [Date] } @@ -47,29 +47,29 @@ The `Decimal` scalar type represents exact signed decimal values. Decimal repres scalar Decimal input Decimal_filter { - eq: [Decimal] - ge: [Decimal] - gt: [Decimal] - le: [Decimal] - lt: [Decimal] + eq: Decimal + ge: Decimal + gt: Decimal + le: Decimal + lt: Decimal ne: [Decimal] } input Float_filter { - eq: [Float] - ge: [Float] - gt: [Float] - le: [Float] - lt: [Float] + eq: Float + ge: Float + gt: Float + le: Float + lt: Float ne: [Float] } input ID_filter { - eq: [ID] - ge: [ID] - gt: [ID] - le: [ID] - lt: [ID] + eq: ID + ge: ID + gt: ID + le: ID + lt: ID ne: [ID] } @@ -79,11 +79,11 @@ The `Int16` scalar type represents 16-bit non-fractional signed whole numeric va scalar Int16 input Int16_filter { - eq: [Int16] - ge: [Int16] - gt: [Int16] - le: [Int16] - lt: [Int16] + eq: Int16 + ge: Int16 + gt: Int16 + le: Int16 + lt: Int16 ne: [Int16] } @@ -93,20 +93,20 @@ The `Int64` scalar type represents 64-bit non-fractional signed whole numeric va scalar Int64 input Int64_filter { - eq: [Int64] - ge: [Int64] - gt: [Int64] - le: [Int64] - lt: [Int64] + eq: Int64 + ge: Int64 + gt: Int64 + le: Int64 + lt: Int64 ne: [Int64] } input Int_filter { - eq: [Int] - ge: [Int] - gt: [Int] - le: [Int] - lt: [Int] + eq: Int + ge: Int + gt: Int + le: Int + lt: Int ne: [Int] } @@ -125,14 +125,14 @@ enum SortDirection { input String_filter { contains: [String] - endswith: [String] - eq: [String] - ge: [String] - gt: [String] - le: [String] - lt: [String] + endswith: String + eq: String + ge: String + gt: String + le: String + lt: String ne: [String] - startswith: [String] + startswith: String } """ @@ -141,11 +141,11 @@ The `Time` scalar type represents time values as strings in the ISO 8601 format scalar Time input Time_filter { - eq: [Time] - ge: [Time] - gt: [Time] - le: [Time] - lt: [Time] + eq: Time + ge: Time + gt: Time + le: Time + lt: Time ne: [Time] } @@ -155,11 +155,11 @@ The `Timestamp` scalar type represents timestamp values as strings in the ISO 86 scalar Timestamp input Timestamp_filter { - eq: [Timestamp] - ge: [Timestamp] - gt: [Timestamp] - le: [Timestamp] - lt: [Timestamp] + eq: Timestamp + ge: Timestamp + gt: Timestamp + le: Timestamp + lt: Timestamp ne: [Timestamp] } @@ -309,10 +309,10 @@ The `UInt8` scalar type represents 8-bit non-fractional unsigned whole numeric v scalar UInt8 input UInt8_filter { - eq: [UInt8] - ge: [UInt8] - gt: [UInt8] - le: [UInt8] - lt: [UInt8] + eq: UInt8 + ge: UInt8 + gt: UInt8 + le: UInt8 + lt: UInt8 ne: [UInt8] } From 847beff3e00553d7ff39379b0517a2c4d91d2018 Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:03:02 +0200 Subject: [PATCH 2/9] Disabled conjunction for certain operators --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f752fec3..f07bc994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't generate fields that represent compositions of aspects within mutation types that represent services - Moved the `GraphQLAdapter` module to the root directory (`index.js`), simplifying the import process and reducing the required typing. +- Disabled conjunction for the following operators: + - `eq` (Equal) + - `gt` (Greater Than) + - `ge` (Greater Than or Equal) + - `le` (Less Than or Equal) + - `lt` (Less Than) + - `startswith` + - `endswith` ### Fixed From 90c8431c401535503e4b27b5948e09dc5407606a Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:15:04 +0200 Subject: [PATCH 3/9] adapt test --- test/tests/queries/variables.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index e016c104..91bfbf94 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -111,7 +111,7 @@ describe('graphql - variables', () => { test('query variable of type scalar value passed as a field of an argument', async () => { const query = gql` - query ($filter: [Int]) { + query ($filter: Int) { AdminServiceBasic { Books(filter: { ID: { ge: $filter } }) { ID @@ -254,7 +254,7 @@ describe('graphql - variables', () => { test('query variable of type scalar value passed as a field of an argument', async () => { const query = gql` - query ($filter: [Int]) { + query ($filter: Int) { AdminService { Books(filter: { ID: { ge: $filter } }) { nodes { From 6a6dcea40540476c32caf81ad599b9ce8fb8c8a3 Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:25:24 +0200 Subject: [PATCH 4/9] Update lib/schema/args/filter.js Co-authored-by: Marcel Schwarz --- lib/schema/args/filter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/schema/args/filter.js b/lib/schema/args/filter.js index ede27d87..d9413335 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -83,8 +83,8 @@ module.exports = cache => { const _generateFilterType = (gqlType, operations) => { const filterName = gqlType.name + '_filter' - const cacheFilterType = cache.get(filterName) - if (cacheFilterType) return cacheFilterType + const cachedFilterType = cache.get(filterName) + if (cachedFilterType) return cachedFilterType const ops = operations.map(op => [ [op], From 24704b6ac1bf23c145dd91cf96daf65dab912b56 Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:25:38 +0200 Subject: [PATCH 5/9] Update lib/resolvers/parse/ast2cqn/where.js Co-authored-by: Marcel Schwarz --- lib/resolvers/parse/ast2cqn/where.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 82788260..1bfe5de4 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -33,8 +33,7 @@ const _objectFieldTo_xpr = (objectField, columnName) => { if (objectField.value.kind === Kind.LIST) { const _xprs = objectField.value.values.map(value => _to_xpr(ref, gqlOperator, value.value)) - const joined_xprs = _joinedXprFrom_xprs(_xprs, 'and') - return joined_xprs + return _joinedXprFrom_xprs(_xprs, 'and') } return _to_xpr(ref, gqlOperator, objectField.value.value) From f075eaf33e3ad8fe79dfc80a42a99b698ce0defc Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:29:35 +0200 Subject: [PATCH 6/9] update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07bc994..875564dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't generate fields that represent compositions of aspects within mutation types that represent services - Moved the `GraphQLAdapter` module to the root directory (`index.js`), simplifying the import process and reducing the required typing. -- Disabled conjunction for the following operators: - - `eq` (Equal) +- Disabled conjunction on the same field for the following operators: + + `eq` (Equal) - `gt` (Greater Than) - `ge` (Greater Than or Equal) - `le` (Less Than or Equal) From 1ee1ab8ace31cad6fba53690f71ff64ba4b0af7f Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:30:33 +0200 Subject: [PATCH 7/9] update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875564dc..3d892353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't generate fields that represent compositions of aspects within mutation types that represent services - Moved the `GraphQLAdapter` module to the root directory (`index.js`), simplifying the import process and reducing the required typing. - Disabled conjunction on the same field for the following operators: - + `eq` (Equal) + - `eq` (Equal) - `gt` (Greater Than) - `ge` (Greater Than or Equal) - `le` (Less Than or Equal) From 0a31bf334bcecc25c23eae8c73c84ea65d9536ae Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:44:35 +0200 Subject: [PATCH 8/9] update CHANGELOG.md --- CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d892353..dc8853ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,13 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't generate fields that represent compositions of aspects within mutation types that represent services - Moved the `GraphQLAdapter` module to the root directory (`index.js`), simplifying the import process and reducing the required typing. - Disabled conjunction on the same field for the following operators: - - `eq` (Equal) - - `gt` (Greater Than) - - `ge` (Greater Than or Equal) - - `le` (Less Than or Equal) - - `lt` (Less Than) - - `startswith` - - `endswith` + + `eq` (Equal) + + `gt` (Greater Than) + + `ge` (Greater Than or Equal) + + `le` (Less Than or Equal) + + `lt` (Less Than) + + `startswith` + + `endswith` ### Fixed From 6b6a120caa42e19d1a21539582950e922b1f86ea Mon Sep 17 00:00:00 2001 From: Arley Triana Morin Date: Tue, 13 Jun 2023 12:48:02 +0200 Subject: [PATCH 9/9] minor renaming of constant --- lib/constants.js | 4 ++-- lib/schema/args/filter.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index 64730323..cc41d5c7 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -26,7 +26,7 @@ const STRING_OPERATIONS = { contains: 'contains' } -const CONJUNCTION_OPERATIONS_SUPPORT = { +const OPERATOR_CONJUNCTION_SUPPORT = { [RELATIONAL_OPERATORS.eq]: false, [RELATIONAL_OPERATORS.ne]: true, [RELATIONAL_OPERATORS.gt]: false, @@ -43,5 +43,5 @@ module.exports = { ARGS, RELATIONAL_OPERATORS, STRING_OPERATIONS, - CONJUNCTION_OPERATIONS_SUPPORT + OPERATOR_CONJUNCTION_SUPPORT } diff --git a/lib/schema/args/filter.js b/lib/schema/args/filter.js index d9413335..921220a1 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -10,7 +10,7 @@ const { const { gqlName } = require('../../utils') const { hasScalarFields, shouldElementBeIgnored } = require('../util') const { cdsToGraphQLScalarType } = require('../types/scalar') -const { RELATIONAL_OPERATORS, STRING_OPERATIONS, CONJUNCTION_OPERATIONS_SUPPORT } = require('../../constants') +const { RELATIONAL_OPERATORS, STRING_OPERATIONS, OPERATOR_CONJUNCTION_SUPPORT } = require('../../constants') const { GraphQLBinary, GraphQLDate, @@ -88,7 +88,7 @@ module.exports = cache => { const ops = operations.map(op => [ [op], - { type: CONJUNCTION_OPERATIONS_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType } + { type: OPERATOR_CONJUNCTION_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType } ]) const fields = Object.fromEntries(ops) const newFilterType = new GraphQLInputObjectType({ name: filterName, fields })