Skip to content

Commit

Permalink
Merge pull request #144 from cap-js/filter-operator-in
Browse files Browse the repository at this point in the history
feat: added operator `in` for filtering on lists of values
  • Loading branch information
etimr authored Jan 8, 2024
2 parents 43cf8ca + 2a82c98 commit ac85708
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Support for generating GraphQL descriptions from CDS doc comments of services, entities, and elements
- Support for operator `in` for filtering on lists of values

### Changed

Expand Down
10 changes: 8 additions & 2 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@ const RELATIONAL_OPERATORS = {
lt: 'lt'
}

const LOGICAL_OPERATORS = {
in: 'in'
}

const STRING_OPERATIONS = {
startswith: 'startswith',
endswith: 'endswith',
contains: 'contains'
}

const OPERATOR_CONJUNCTION_SUPPORT = {
const OPERATOR_LIST_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,
[LOGICAL_OPERATORS.in]: true,
[STRING_OPERATIONS.startswith]: false,
[STRING_OPERATIONS.endswith]: false,
[STRING_OPERATIONS.contains]: true
Expand All @@ -42,6 +47,7 @@ module.exports = {
CONNECTION_FIELDS,
ARGS,
RELATIONAL_OPERATORS,
LOGICAL_OPERATORS,
STRING_OPERATIONS,
OPERATOR_CONJUNCTION_SUPPORT
OPERATOR_LIST_SUPPORT
}
17 changes: 12 additions & 5 deletions lib/resolvers/parse/ast2cqn/where.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { RELATIONAL_OPERATORS, STRING_OPERATIONS } = require('../../../constants')
const { RELATIONAL_OPERATORS, LOGICAL_OPERATORS, STRING_OPERATIONS } = require('../../../constants')
const { Kind } = require('graphql')

const GQL_TO_CDS_STRING_OPERATIONS = {
Expand All @@ -13,7 +13,8 @@ const GQL_TO_CDS_QL_OPERATOR = {
[RELATIONAL_OPERATORS.gt]: '>',
[RELATIONAL_OPERATORS.ge]: '>=',
[RELATIONAL_OPERATORS.le]: '<=',
[RELATIONAL_OPERATORS.lt]: '<'
[RELATIONAL_OPERATORS.lt]: '<',
[LOGICAL_OPERATORS.in]: 'in'
}

const _gqlOperatorToCdsOperator = gqlOperator =>
Expand All @@ -30,13 +31,19 @@ const _to_xpr = (ref, gqlOperator, value) => {
const _objectFieldTo_xpr = (objectField, columnName) => {
const ref = { ref: [columnName] }
const gqlOperator = objectField.name.value
const operand = objectField.value

if (objectField.value.kind === Kind.LIST) {
const _xprs = objectField.value.values.map(value => _to_xpr(ref, gqlOperator, value.value))
if (gqlOperator === LOGICAL_OPERATORS.in) {
const list = operand.kind === Kind.LIST ? operand.values.map(value => ({ val: value.value })) : [{ val: operand.value }]
return [ref, _gqlOperatorToCdsOperator(gqlOperator), { list }]
}

if (operand.kind === Kind.LIST) {
const _xprs = operand.values.map(value => _to_xpr(ref, gqlOperator, value.value))
return _joinedXprFrom_xprs(_xprs, 'and')
}

return _to_xpr(ref, gqlOperator, objectField.value.value)
return _to_xpr(ref, gqlOperator, operand.value)
}

const _parseObjectField = (objectField, columnName) => {
Expand Down
34 changes: 16 additions & 18 deletions lib/schema/args/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const {
const { gqlName } = require('../../utils')
const { hasScalarFields, shouldElementBeIgnored } = require('../util')
const { cdsToGraphQLScalarType } = require('../types/scalar')
const { RELATIONAL_OPERATORS, STRING_OPERATIONS, OPERATOR_CONJUNCTION_SUPPORT } = require('../../constants')
const { RELATIONAL_OPERATORS, LOGICAL_OPERATORS, STRING_OPERATIONS, OPERATOR_LIST_SUPPORT } = require('../../constants')
const {
GraphQLBinary,
GraphQLDate,
Expand Down Expand Up @@ -56,26 +56,27 @@ module.exports = cache => {
}

const _generateScalarFilter = gqlType => {
const numericOperators = Object.values({ ...RELATIONAL_OPERATORS, ...LOGICAL_OPERATORS })
const filterType = {
// REVISIT: which filters for binary
[GraphQLBinary.name]: _generateFilterType(GraphQLBinary, [RELATIONAL_OPERATORS.eq, RELATIONAL_OPERATORS.ne]),
[GraphQLBoolean.name]: _generateFilterType(GraphQLBoolean, [RELATIONAL_OPERATORS.eq, RELATIONAL_OPERATORS.ne]),
[GraphQLDate.name]: _generateFilterType(GraphQLDate, Object.values(RELATIONAL_OPERATORS)),
[GraphQLDateTime.name]: _generateFilterType(GraphQLDateTime, Object.values(RELATIONAL_OPERATORS)),
[GraphQLDecimal.name]: _generateFilterType(GraphQLDecimal, Object.values(RELATIONAL_OPERATORS)),
// REVISIT: should 'eq'/'ne' be generated since exact comparisons could be difficult due to floating point errors?
[GraphQLFloat.name]: _generateFilterType(GraphQLFloat, Object.values(RELATIONAL_OPERATORS)),
[GraphQLID.name]: _generateFilterType(GraphQLID, Object.values(RELATIONAL_OPERATORS)),
[GraphQLInt.name]: _generateFilterType(GraphQLInt, Object.values(RELATIONAL_OPERATORS)),
[GraphQLInt16.name]: _generateFilterType(GraphQLInt16, Object.values(RELATIONAL_OPERATORS)),
[GraphQLInt64.name]: _generateFilterType(GraphQLInt64, Object.values(RELATIONAL_OPERATORS)),
[GraphQLDate.name]: _generateFilterType(GraphQLDate, numericOperators),
[GraphQLDateTime.name]: _generateFilterType(GraphQLDateTime, numericOperators),
[GraphQLDecimal.name]: _generateFilterType(GraphQLDecimal, numericOperators),
// REVISIT: should 'eq'/'ne'/'in' be generated since exact comparisons could be difficult due to floating point errors?
[GraphQLFloat.name]: _generateFilterType(GraphQLFloat, numericOperators),
[GraphQLID.name]: _generateFilterType(GraphQLID, numericOperators),
[GraphQLInt.name]: _generateFilterType(GraphQLInt, numericOperators),
[GraphQLInt16.name]: _generateFilterType(GraphQLInt16, numericOperators),
[GraphQLInt64.name]: _generateFilterType(GraphQLInt64, numericOperators),
[GraphQLString.name]: _generateFilterType(
GraphQLString,
Object.values({ ...RELATIONAL_OPERATORS, ...STRING_OPERATIONS })
Object.values({ ...RELATIONAL_OPERATORS, ...LOGICAL_OPERATORS, ...STRING_OPERATIONS })
),
[GraphQLTime.name]: _generateFilterType(GraphQLTime, Object.values(RELATIONAL_OPERATORS)),
[GraphQLTimestamp.name]: _generateFilterType(GraphQLTimestamp, Object.values(RELATIONAL_OPERATORS)),
[GraphQLUInt8.name]: _generateFilterType(GraphQLUInt8, Object.values(RELATIONAL_OPERATORS))
[GraphQLTime.name]: _generateFilterType(GraphQLTime, numericOperators),
[GraphQLTimestamp.name]: _generateFilterType(GraphQLTimestamp, numericOperators),
[GraphQLUInt8.name]: _generateFilterType(GraphQLUInt8, numericOperators)
}[gqlType.name]
return new GraphQLList(filterType)
}
Expand All @@ -86,10 +87,7 @@ module.exports = cache => {
const cachedFilterType = cache.get(filterName)
if (cachedFilterType) return cachedFilterType

const ops = operations.map(op => [
[op],
{ type: OPERATOR_CONJUNCTION_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType }
])
const ops = operations.map(op => [[op], { type: OPERATOR_LIST_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType }])
const fields = Object.fromEntries(ops)
const newFilterType = new GraphQLInputObjectType({ name: filterName, fields })
cache.set(filterName, newFilterType)
Expand Down
6 changes: 6 additions & 0 deletions test/schemas/bookshop-graphql.gql
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,7 @@ input Date_filter {
eq: Date
ge: Date
gt: Date
in: [Date]
le: Date
lt: Date
ne: [Date]
Expand All @@ -1110,6 +1111,7 @@ input Decimal_filter {
eq: Decimal
ge: Decimal
gt: Decimal
in: [Decimal]
le: Decimal
lt: Decimal
ne: [Decimal]
Expand All @@ -1124,6 +1126,7 @@ input Int16_filter {
eq: Int16
ge: Int16
gt: Int16
in: [Int16]
le: Int16
lt: Int16
ne: [Int16]
Expand All @@ -1133,6 +1136,7 @@ input Int_filter {
eq: Int
ge: Int
gt: Int
in: [Int]
le: Int
lt: Int
ne: [Int]
Expand All @@ -1159,6 +1163,7 @@ input String_filter {
eq: String
ge: String
gt: String
in: [String]
le: String
lt: String
ne: [String]
Expand All @@ -1174,6 +1179,7 @@ input Timestamp_filter {
eq: Timestamp
ge: Timestamp
gt: Timestamp
in: [Timestamp]
le: Timestamp
lt: Timestamp
ne: [Timestamp]
Expand Down
2 changes: 2 additions & 0 deletions test/schemas/edge-cases/field-named-localized.gql
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ input Int_filter {
eq: Int
ge: Int
gt: Int
in: [Int]
le: Int
lt: Int
ne: [Int]
Expand All @@ -112,6 +113,7 @@ input String_filter {
eq: String
ge: String
gt: String
in: [String]
le: String
lt: String
ne: [String]
Expand Down
2 changes: 2 additions & 0 deletions test/schemas/edge-cases/fields-with-connection-names.gql
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ input ID_filter {
eq: ID
ge: ID
gt: ID
in: [ID]
le: ID
lt: ID
ne: [ID]
Expand All @@ -119,6 +120,7 @@ input String_filter {
eq: String
ge: String
gt: String
in: [String]
le: String
lt: String
ne: [String]
Expand Down
1 change: 1 addition & 0 deletions test/schemas/model-structure/composition-of-aspect.gql
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ input ID_filter {
eq: ID
ge: ID
gt: ID
in: [ID]
le: ID
lt: ID
ne: [ID]
Expand Down
12 changes: 12 additions & 0 deletions test/schemas/types.gql
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ input DateTime_filter {
eq: DateTime
ge: DateTime
gt: DateTime
in: [DateTime]
le: DateTime
lt: DateTime
ne: [DateTime]
Expand All @@ -36,6 +37,7 @@ input Date_filter {
eq: Date
ge: Date
gt: Date
in: [Date]
le: Date
lt: Date
ne: [Date]
Expand All @@ -50,6 +52,7 @@ input Decimal_filter {
eq: Decimal
ge: Decimal
gt: Decimal
in: [Decimal]
le: Decimal
lt: Decimal
ne: [Decimal]
Expand All @@ -59,6 +62,7 @@ input Float_filter {
eq: Float
ge: Float
gt: Float
in: [Float]
le: Float
lt: Float
ne: [Float]
Expand All @@ -68,6 +72,7 @@ input ID_filter {
eq: ID
ge: ID
gt: ID
in: [ID]
le: ID
lt: ID
ne: [ID]
Expand All @@ -82,6 +87,7 @@ input Int16_filter {
eq: Int16
ge: Int16
gt: Int16
in: [Int16]
le: Int16
lt: Int16
ne: [Int16]
Expand All @@ -96,6 +102,7 @@ input Int64_filter {
eq: Int64
ge: Int64
gt: Int64
in: [Int64]
le: Int64
lt: Int64
ne: [Int64]
Expand All @@ -105,6 +112,7 @@ input Int_filter {
eq: Int
ge: Int
gt: Int
in: [Int]
le: Int
lt: Int
ne: [Int]
Expand All @@ -129,6 +137,7 @@ input String_filter {
eq: String
ge: String
gt: String
in: [String]
le: String
lt: String
ne: [String]
Expand All @@ -144,6 +153,7 @@ input Time_filter {
eq: Time
ge: Time
gt: Time
in: [Time]
le: Time
lt: Time
ne: [Time]
Expand All @@ -158,6 +168,7 @@ input Timestamp_filter {
eq: Timestamp
ge: Timestamp
gt: Timestamp
in: [Timestamp]
le: Timestamp
lt: Timestamp
ne: [Timestamp]
Expand Down Expand Up @@ -300,6 +311,7 @@ input UInt8_filter {
eq: UInt8
ge: UInt8
gt: UInt8
in: [UInt8]
le: UInt8
lt: UInt8
ne: [UInt8]
Expand Down
52 changes: 52 additions & 0 deletions test/tests/queries/filter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,58 @@ describe('graphql - filter', () => {
expect(response.data).toEqual({ data })
})

test('query with filter with in operator with single value', async () => {
const query = gql`
{
AdminService {
Books(filter: { ID: { in: 201 } }) {
nodes {
ID
title
}
}
}
}
`
const data = {
AdminService: {
Books: {
nodes: [{ ID: 201, title: 'Wuthering Heights' }]
}
}
}
const response = await POST('/graphql', { query })
expect(response.data).toEqual({ data })
})

test('query with filter with in operator with multiple values', async () => {
const query = gql`
{
AdminService {
Books(filter: [{ ID: { in: [201, 251] } }, { title: { contains: "cat" } }]) {
nodes {
ID
title
}
}
}
}
`
const data = {
AdminService: {
Books: {
nodes: [
{ ID: 201, title: 'Wuthering Heights' },
{ ID: 251, title: 'The Raven' },
{ ID: 271, title: 'Catweazle' }
]
}
}
}
const response = await POST('/graphql', { query })
expect(response.data).toEqual({ data })
})

test('query with simple filter wrapped as lists', async () => {
const query = gql`
{
Expand Down

0 comments on commit ac85708

Please sign in to comment.