diff --git a/CHANGELOG.md b/CHANGELOG.md index fc970f21..28dbf018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for filtering by `null` values + ### Changed +- Improved handling of `null` and `undefined` values in query arguments +- Empty filter lists resolve to `false` and empty filter objects resolve to `true` + ### Fixed - Handling of GraphQL queries that are sent via `GET` requests using the `query` URL parameter if GraphiQL is enabled diff --git a/lib/resolvers/crud/read.js b/lib/resolvers/crud/read.js index c005e8d3..2a009fb1 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -11,7 +11,10 @@ module.exports = async (service, entity, selection) => { query.columns(astToColumns(selection.selectionSet.selections)) const filter = getArgumentByName(args, ARGS.filter) - if (filter) query.where(astToWhere(filter)) + if (filter) { + const where = astToWhere(filter) + if (where) query.where(where) + } const orderBy = getArgumentByName(args, ARGS.orderBy) if (orderBy) query.orderBy(astToOrderBy(orderBy)) diff --git a/lib/resolvers/parse/ast/enrich.js b/lib/resolvers/parse/ast/enrich.js index a14e3e29..c6d987a6 100644 --- a/lib/resolvers/parse/ast/enrich.js +++ b/lib/resolvers/parse/ast/enrich.js @@ -23,11 +23,13 @@ const _traverseListValue = (info, listValue, _fields) => { } } +const _isScalarKind = kind => kind === Kind.INT || kind === Kind.FLOAT || kind === Kind.STRING || kind === Kind.BOOLEAN + const _traverseArgumentOrObjectField = (info, argumentOrObjectField, _fieldOr_arg) => { const value = argumentOrObjectField.value const type = _getTypeFrom_fieldOr_arg(_fieldOr_arg) - if (type.parseLiteral && value.kind !== Kind.VARIABLE) value.value = type.parseLiteral(value) + if (_isScalarKind(value.kind)) value.value = type.parseLiteral(value) switch (value.kind) { case Kind.VARIABLE: @@ -40,6 +42,9 @@ const _traverseArgumentOrObjectField = (info, argumentOrObjectField, _fieldOr_ar _traverseObjectValue(info, value, type.getFields()) break } + + // Convenience value for both literal and variable values + if (argumentOrObjectField.value?.kind === Kind.NULL) argumentOrObjectField.value.value = null } const _traverseSelectionSet = (info, selectionSet, _fields) => { diff --git a/lib/resolvers/parse/ast/fromObject.js b/lib/resolvers/parse/ast/fromObject.js index 8d169f18..11240b9a 100644 --- a/lib/resolvers/parse/ast/fromObject.js +++ b/lib/resolvers/parse/ast/fromObject.js @@ -1,6 +1,8 @@ const { Kind } = require('graphql') const { isPlainObject } = require('./util') +const _nullValue = { kind: Kind.NULL } + const _valueToGenericScalarValue = value => ({ kind: 'GenericScalarValue', value @@ -32,6 +34,10 @@ const _variableToValue = variable => { return _arrayToListValue(variable) } else if (isPlainObject(variable)) { return _objectToObjectValue(variable) + } else if (variable === null) { + return _nullValue + } else if (variable === undefined) { + return undefined } return _valueToGenericScalarValue(variable) } diff --git a/lib/resolvers/parse/ast2cqn/utils/index.js b/lib/resolvers/parse/ast2cqn/utils/index.js index 5a23660d..ed1add90 100644 --- a/lib/resolvers/parse/ast2cqn/utils/index.js +++ b/lib/resolvers/parse/ast2cqn/utils/index.js @@ -1,3 +1,6 @@ -const getArgumentByName = (args, name) => args.find(arg => arg.name.value === name) +const { Kind } = require('graphql') + +const getArgumentByName = (args, name) => + args.find(arg => arg.value && arg.name.value === name && arg.value.kind !== Kind.NULL) module.exports = { getArgumentByName } diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 1b47085c..2d69bd07 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -32,6 +32,8 @@ const _objectFieldTo_xpr = (objectField, columnName) => { } const _parseObjectField = (objectField, columnName) => { + if (columnName) return _objectFieldTo_xpr(objectField, columnName) + const value = objectField.value const name = objectField.name.value switch (value.kind) { @@ -40,7 +42,6 @@ const _parseObjectField = (objectField, columnName) => { case Kind.OBJECT: return _parseObjectValue(value, name) } - return _objectFieldTo_xpr(objectField, columnName) } const _arrayInsertBetweenFlat = (array, element) => @@ -48,14 +49,24 @@ const _arrayInsertBetweenFlat = (array, element) => const _joinedXprFrom_xprs = (_xprs, operator) => ({ xpr: _arrayInsertBetweenFlat(_xprs, operator) }) +const _true_xpr = [{ val: 1 }, '=', { val: 1 }] + const _parseObjectValue = (objectValue, columnName) => { - const _xprs = objectValue.fields.map(field => _parseObjectField(field, columnName)) - return _xprs.length === 1 ? _xprs[0] : _joinedXprFrom_xprs(_xprs, 'and') + const _xprs = objectValue.fields + .map(field => _parseObjectField(field, columnName)) + .filter(field => field !== undefined) + if (_xprs.length === 0) return _true_xpr + else if (_xprs.length === 1) return _xprs[0] + return _joinedXprFrom_xprs(_xprs, 'and') } +const _false_xpr = [{ val: 0 }, '=', { val: 1 }] + const _parseListValue = (listValue, columnName) => { - const _xprs = listValue.values.map(value => _parseObjectValue(value, columnName)) - return _xprs.length === 1 ? _xprs[0] : _joinedXprFrom_xprs(_xprs, 'or') + const _xprs = listValue.values.map(value => _parseObjectValue(value, columnName)).filter(value => value !== undefined) + if (_xprs.length === 0) return _false_xpr + else if (_xprs.length === 1) return _xprs[0] + return _joinedXprFrom_xprs(_xprs, 'or') } const astToWhere = filterArg => { diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index 5d3301b1..57a49a86 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -3,432 +3,765 @@ describe('graphql - filter', () => { const path = require('path') const { gql } = require('../../util') - const { axios, POST } = cds.test(path.join(__dirname, '../../resources/bookshop-graphql')) + const { axios, POST, data } = cds.test(path.join(__dirname, '../../resources/bookshop-graphql')) // Prevent axios from throwing errors for non 2xx status codes axios.defaults.validateStatus = false + data.autoReset(true) // REVISIT: unskip for support of configurable schema flavors describe.skip('queries with filter argument without connections', () => { - test('query with simple filter', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: { ID: { eq: 201 } }) { - ID - title + describe('regular filters', () => { + test('query with simple filter', async () => { + const query = gql` + { + AdminServiceBasic { + Books(filter: { ID: { eq: 201 } }) { + ID + title + } } } + ` + const data = { + AdminServiceBasic: { + Books: [{ ID: 201, title: 'Wuthering Heights' }] + } } - ` - const data = { - AdminServiceBasic: { - Books: [{ ID: 201, title: 'Wuthering Heights' }] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with simple filter wrapped as lists', async () => { + const query = gql` + { + AdminServiceBasic { + Books(filter: [{ ID: [{ eq: 201 }] }]) { + ID + title + } + } + } + ` + const data = { + AdminServiceBasic: { + Books: [{ ID: 201, title: 'Wuthering Heights' }] + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with simple filter wrapped as lists', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: [{ ID: [{ eq: 201 }] }]) { - ID - title + test('query with filter joined by AND on the same field', async () => { + const query = gql` + { + AdminServiceBasic { + Books(filter: { ID: { gt: 250, lt: 260 } }) { + ID + title + } } } + ` + const data = { + AdminServiceBasic: { + Books: [ + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' } + ] + } } - ` - const data = { - AdminServiceBasic: { - Books: [{ ID: 201, title: 'Wuthering Heights' }] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filter joined by AND on different fields', async () => { + const query = gql` + { + AdminServiceBasic { + Books(filter: { ID: { eq: 251 }, title: { eq: "The Raven" } }) { + ID + title + } + } + } + ` + const data = { + AdminServiceBasic: { + Books: [{ ID: 251, title: 'The Raven' }] + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by AND on the same field', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: { ID: { gt: 250, lt: 260 } }) { - ID - title + test('query with filter joined by OR on the same field', async () => { + const query = gql` + { + AdminServiceBasic { + Books(filter: { ID: [{ eq: 201 }, { eq: 251 }] }) { + ID + title + } } } + ` + const data = { + AdminServiceBasic: { + Books: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 251, title: 'The Raven' } + ] + } } - ` - const data = { - AdminServiceBasic: { - Books: [ - { ID: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' } - ] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filter joined by OR on different fields', async () => { + const query = gql` + { + AdminServiceBasic { + Books(filter: [{ ID: { eq: 201 } }, { title: { eq: "The Raven" } }]) { + ID + title + } + } + } + ` + const data = { + AdminServiceBasic: { + Books: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 251, title: 'The Raven' } + ] + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by AND on different fields', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: { ID: { eq: 251 }, title: { eq: "The Raven" } }) { - ID - title + test('query with complex filter', async () => { + const query = gql` + { + AdminServiceBasic { + Books( + filter: [ + { + title: [{ startswith: "the", endswith: "raven" }, { contains: "height" }] + ID: [{ eq: 201 }, { eq: 251 }] + } + { title: { contains: "cat" } } + ] + ) { + ID + title + } } } + ` + const data = { + AdminServiceBasic: { + Books: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 251, title: 'The Raven' }, + { ID: 271, title: 'Catweazle' } + ] + } } - ` - const data = { - AdminServiceBasic: { - Books: [{ ID: 251, title: 'The Raven' }] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filters on nested fields', async () => { + const query = gql` + { + AdminServiceBasic { + Authors(filter: { ID: { gt: 110 } }) { + ID + name + books(filter: { ID: { lt: 260 } }) { + ID + title + } + } + } + } + ` + const data = { + AdminServiceBasic: { + Authors: [ + { + ID: 150, + books: [ + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' } + ], + name: 'Edgar Allen Poe' + }, + { ID: 170, books: [], name: 'Richard Carpenter' } + ] + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) }) + }) - test('query with filter joined by OR on the same field', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: { ID: [{ eq: 201 }, { eq: 251 }] }) { - ID - title + describe('queries with filter argument with connections', () => { + describe('regular filters', () => { + test('query with simple filter', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: { eq: 201 } }) { + nodes { + ID + title + } + } } } + ` + const data = { + AdminService: { + Books: { nodes: [{ ID: 201, title: 'Wuthering Heights' }] } + } } - ` - const data = { - AdminServiceBasic: { - Books: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' } - ] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with simple filter wrapped as lists', async () => { + const query = gql` + { + AdminService { + Books(filter: [{ ID: [{ eq: 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 }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by OR on different fields', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: [{ ID: { eq: 201 } }, { title: { eq: "The Raven" } }]) { - ID - title + test('query with filter joined by AND on the same field', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: { gt: 250, lt: 260 } }) { + nodes { + ID + title + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' } + ] } } } - ` - const data = { - AdminServiceBasic: { - Books: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' } - ] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filter joined by AND on different fields', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: { eq: 251 }, title: { eq: "The Raven" } }) { + nodes { + ID + title + } + } + } + } + ` + const data = { + AdminService: { + Books: { nodes: [{ ID: 251, title: 'The Raven' }] } + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with complex filter', async () => { - const query = gql` - { - AdminServiceBasic { - Books( - filter: [ - { - title: [{ startswith: "the", endswith: "raven" }, { contains: "height" }] - ID: [{ eq: 201 }, { eq: 251 }] + test('query with filter joined by OR on the same field', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: [{ eq: 201 }, { eq: 251 }] }) { + nodes { + ID + title } - { title: { contains: "cat" } } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 251, title: 'The Raven' } ] - ) { - ID - title } } } - ` - const data = { - AdminServiceBasic: { - Books: [ - { 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 }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filters on nested fields', async () => { - const query = gql` - { - AdminServiceBasic { - Authors(filter: { ID: { gt: 110 } }) { - ID - name - books(filter: { ID: { lt: 260 } }) { - ID - title + test('query with filter joined by OR on different fields', async () => { + const query = gql` + { + AdminService { + Books(filter: [{ ID: { eq: 201 } }, { title: { eq: "The Raven" } }]) { + nodes { + ID + title + } } } } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 251, title: 'The Raven' } + ] + } + } } - ` - const data = { - AdminServiceBasic: { - Authors: [ - { - ID: 150, - books: [ + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with complex filter', async () => { + const query = gql` + { + AdminService { + Books( + filter: [ + { + title: [{ startswith: "the", endswith: "raven" }, { contains: "height" }] + ID: [{ eq: 201 }, { eq: 251 }] + } + { title: { contains: "cat" } } + ] + ) { + nodes { + ID + title + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, { ID: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' } - ], - name: 'Edgar Allen Poe' - }, - { ID: 170, books: [], name: 'Richard Carpenter' } - ] + { ID: 271, title: 'Catweazle' } + ] + } + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - describe('queries with filter argument with connections', () => { - test('query with simple filter', async () => { - const query = gql` - { - AdminService { - Books(filter: { ID: { eq: 201 } }) { - nodes { - ID - title + test('query with filters on nested fields', async () => { + const query = gql` + { + AdminService { + Authors(filter: { ID: { gt: 110 } }) { + nodes { + ID + name + books(filter: { ID: { lt: 260 } }) { + nodes { + ID + title + } + } + } } } } + ` + const data = { + AdminService: { + Authors: { + nodes: [ + { + ID: 150, + books: { + nodes: [ + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' } + ] + }, + name: 'Edgar Allen Poe' + }, + { ID: 170, books: { nodes: [] }, name: 'Richard Carpenter' } + ] + } + } } - ` - const data = { - AdminService: { - Books: { nodes: [{ ID: 201, title: 'Wuthering Heights' }] } - } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) }) - test('query with simple filter wrapped as lists', async () => { - const query = gql` - { - AdminService { - Books(filter: [{ ID: [{ eq: 201 }] }]) { - nodes { - ID - title + describe('filters containing null values or empty input objects and lists', () => { + test('query with filter of value null', async () => { + const query = gql` + { + AdminService { + Books(filter: null) { + nodes { + title + stock + } } } } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 } + ] + } + } } - ` - const data = { - AdminService: { - Books: { nodes: [{ ID: 201, title: 'Wuthering Heights' }] } - } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by AND on the same field', async () => { - const query = gql` - { - AdminService { - Books(filter: { ID: { gt: 250, lt: 260 } }) { - nodes { - ID - title + test('query with filter of value empty input object', async () => { + const query = gql` + { + AdminService { + Books(filter: {}) { + nodes { + title + stock + } } } } - } - ` - const data = { - AdminService: { - Books: { - nodes: [ - { ID: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' } - ] + ` + const data = { + AdminService: { + Books: { + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 } + ] + } } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by AND on different fields', async () => { - const query = gql` - { - AdminService { - Books(filter: { ID: { eq: 251 }, title: { eq: "The Raven" } }) { - nodes { - ID - title + test('query with filter of value empty list', async () => { + const query = gql` + { + AdminService { + Books(filter: []) { + nodes { + title + stock + } } } } + ` + const data = { + AdminService: { + Books: { + nodes: [] + } + } } - ` - const data = { - AdminService: { - Books: { nodes: [{ ID: 251, title: 'The Raven' }] } + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filter list containing empty input object', async () => { + const query = gql` + { + AdminService { + Books(filter: [{}]) { + nodes { + title + stock + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 } + ] + } + } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by OR on the same field', async () => { - const query = gql` - { - AdminService { - Books(filter: { ID: [{ eq: 201 }, { eq: 251 }] }) { - nodes { - ID - title + test('query with filter column of value empty input object', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: {} }) { + nodes { + title + stock + } } } } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 } + ] + } + } } - ` - const data = { - AdminService: { - Books: { - nodes: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' } - ] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filter column of value empty list', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: [] }) { + nodes { + title + stock + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [] + } } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter joined by OR on different fields', async () => { - const query = gql` - { - AdminService { - Books(filter: [{ ID: { eq: 201 } }, { title: { eq: "The Raven" } }]) { - nodes { - ID - title + test('query with filter column list containing empty input object', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: [{}] }) { + nodes { + title + stock + } } } } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 } + ] + } + } } - ` - const data = { - AdminService: { - Books: { - nodes: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' } - ] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with filter column of empty input object, which resolves to true, joined by AND with equality filter', async () => { + const query = gql` + { + AdminService { + Books(filter: { title: {}, stock: { eq: 11 } }) { + nodes { + title + stock + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [{ title: 'Jane Eyre', stock: 11 }] + } } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with complex filter', async () => { - const query = gql` - { - AdminService { - Books( - filter: [ - { - title: [{ startswith: "the", endswith: "raven" }, { contains: "height" }] - ID: [{ eq: 201 }, { eq: 251 }] + test('query with filter column of empty list, which resolves to false, joined by AND with equality filter', async () => { + const query = gql` + { + AdminService { + Books(filter: { title: [], stock: { eq: 11 } }) { + nodes { + title + stock } - { title: { contains: "cat" } } - ] - ) { - nodes { - ID - title } } } + ` + const data = { + AdminService: { + Books: { + nodes: [] + } + } } - ` - 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 filter column of empty input object, which resolves to true, joined by OR with equality filter', async () => { + const query = gql` + { + AdminService { + Books(filter: [{ title: {} }, { stock: { eq: 11 } }]) { + nodes { + title + stock + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 } + ] + } } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filters on nested fields', async () => { - const query = gql` - { - AdminService { - Authors(filter: { ID: { gt: 110 } }) { - nodes { - ID - name - books(filter: { ID: { lt: 260 } }) { - nodes { - ID - title - } + test('query with filter column of empty list, which resolves to false, joined by OR with equality filter', async () => { + const query = gql` + { + AdminService { + Books(filter: [{ title: [] }, { stock: { eq: 11 } }]) { + nodes { + title + stock } } } } + ` + const data = { + AdminService: { + Books: { + nodes: [{ title: 'Jane Eyre', stock: 11 }] + } + } } - ` - const data = { - AdminService: { - Authors: { - nodes: [ - { - ID: 150, - books: { - nodes: [ - { ID: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' } - ] - }, - name: 'Edgar Allen Poe' - }, - { ID: 170, books: { nodes: [] }, name: 'Richard Carpenter' } - ] + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + + test('query with equality filter for null values', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + { + AdminService { + Books(filter: { stock: { eq: null } }) { + nodes { + title + stock + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [{ title: 'Moby-Dick', stock: null }] + } } } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) }) }) }) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index f44b19a8..6f1e0714 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -280,5 +280,247 @@ describe('graphql - variables', () => { const response = await POST('/graphql', { query, variables }) expect(response.data).toEqual({ data }) }) + + test('query variable with undefined value as an argument', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: undefined } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable with null value as an argument', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: null } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable with undefined value where an object value is expected', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: { stock: undefined } } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable with null value where an object value is expected', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: { stock: null } } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable with undefined value as a field of an argument', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: { stock: { eq: undefined } } } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable with null value as a field of an argument', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + title + stock + } + } + } + } + ` + const variables = { filter: { stock: { eq: null } } } + const data = { + AdminService: { + Books: { + nodes: [] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable combining undefined and non-undefined values', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: [{ ID: { eq: undefined } }, { ID: { eq: 251 } }, { ID: undefined }] } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable combining null and non-null values', async () => { + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + ID + title + } + } + } + } + ` + const variables = { filter: [{ ID: { eq: 251 } }, { ID: null }] } + const data = { + AdminService: { + Books: { + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) }) })