From e706db192092dc6d1e0f55e149baab4ce224f61f Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:06:00 +0100 Subject: [PATCH 01/28] Prettier format --- lib/resolvers/parse/ast/util/index.js | 2 +- test/tests/queries/queries.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/resolvers/parse/ast/util/index.js b/lib/resolvers/parse/ast/util/index.js index 9ffc969b..d7970344 100644 --- a/lib/resolvers/parse/ast/util/index.js +++ b/lib/resolvers/parse/ast/util/index.js @@ -1,3 +1,3 @@ const isPlainObject = value => value !== null && typeof value === 'object' && !Buffer.isBuffer(value) -module.exports = { isPlainObject } \ No newline at end of file +module.exports = { isPlainObject } diff --git a/test/tests/queries/queries.test.js b/test/tests/queries/queries.test.js index e42c1f24..4034f9d0 100644 --- a/test/tests/queries/queries.test.js +++ b/test/tests/queries/queries.test.js @@ -35,7 +35,7 @@ describe('graphql - queries', () => { }) test('query with null result values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: "Moby-Dick" }) + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) const query = `#graphql { @@ -271,7 +271,7 @@ describe('graphql - queries', () => { }) test('query with null result values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: "Moby-Dick" }) + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) const query = `#graphql { From 7b7b9d47341f2ce6b4e34479bee1a597419c5cef Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:06:33 +0100 Subject: [PATCH 02/28] Add tests for null and undefined values in args --- test/tests/queries/filter.test.js | 107 ++++++++++++- test/tests/queries/variables.test.js | 219 ++++++++++++++++++++++++++- 2 files changed, 324 insertions(+), 2 deletions(-) diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index a1cd4a6a..9139a2f2 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -2,9 +2,10 @@ describe('graphql - filter', () => { const cds = require('@sap/cds/lib') const path = require('path') - 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', () => { @@ -28,6 +29,54 @@ describe('graphql - filter', () => { expect(response.data).toEqual({ data }) }) + test('query with filter of value null', async () => { + const query = `#graphql + { + AdminService { + Books(filter: null) { + title + stock + } + } + } + ` + const data = { + AdminService: { + Books: [ + { 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 }) + }) + + test('query with equality filter for null values', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = `#graphql + { + AdminService { + Books(filter: { stock: { eq: null } }) { + title + stock + } + } + } + ` + const data = { + AdminService: { + Books: [{ title: 'Moby-Dick', stock: null }] + } + } + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) + test('query with simple filter wrapped as lists', async () => { const query = `#graphql { @@ -235,6 +284,62 @@ describe('graphql - filter', () => { expect(response.data).toEqual({ data }) }) + test('query with filter of value null', async () => { + const query = `#graphql + { + 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 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 = `#graphql + { + 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 }) + }) + test('query with simple filter wrapped as lists', async () => { const query = `#graphql { diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 5cd2d058..7eff0a39 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -2,9 +2,10 @@ describe('graphql - variables', () => { const cds = require('@sap/cds/lib') const path = require('path') - 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 variables without connections', () => { @@ -132,6 +133,106 @@ 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 = `#graphql + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + ID + title + } + } + } + ` + const variables = { filter: undefined } + const data = { + AdminService: { + Books: [ + { 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 = `#graphql + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + ID + title + } + } + } + ` + const variables = { filter: null } + const data = { + AdminService: { + Books: [ + { 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 () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = `#graphql + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + title + stock + } + } + } + ` + const variables = { filter: { stock: { eq: undefined } } } + const data = { + AdminService: { + Books: [] + } + } + 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 () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = `#graphql + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + title + stock + } + } + } + ` + const variables = { filter: { stock: { eq: null } } } + const data = { + AdminService: { + Books: [{ title: 'Moby-Dick', stock: null }] + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) }) describe('queries with variables with connections', () => { @@ -279,5 +380,121 @@ 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 = `#graphql + 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 = `#graphql + 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 as a field of an argument', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = `#graphql + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + title + stock + } + } + } + } + ` + const variables = { filter: { stock: { eq: undefined } } } + const data = { + AdminService: { + Books: { + nodes: [] + } + } + } + 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 () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = `#graphql + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + title + stock + } + } + } + } + ` + const variables = { filter: { stock: { eq: null } } } + const data = { + AdminService: { + Books: { + nodes: [{ title: 'Moby-Dick', stock: null }] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) }) }) From cea11c56c1ceb9243e19767a2910d25f129eadb2 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:15:52 +0100 Subject: [PATCH 03/28] Ignore arguments with values of null --- lib/resolvers/parse/ast2cqn/utils/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast2cqn/utils/index.js b/lib/resolvers/parse/ast2cqn/utils/index.js index 5a23660d..ed73c603 100644 --- a/lib/resolvers/parse/ast2cqn/utils/index.js +++ b/lib/resolvers/parse/ast2cqn/utils/index.js @@ -1,3 +1,5 @@ -const getArgumentByName = (args, name) => args.find(arg => arg.name.value === name) +const { Kind } = require('graphql') + +const getArgumentByName = (args, name) => args.find(arg => arg.name.value === name && arg.value.kind !== Kind.NULL) module.exports = { getArgumentByName } From fc3c3455cb9efed2d9f7d52784262c4c09579080 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:26:08 +0100 Subject: [PATCH 04/28] Improve check if literal is a scalar to be parsed --- lib/resolvers/parse/ast/enrich.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast/enrich.js b/lib/resolvers/parse/ast/enrich.js index 7f7cce6f..244d62e8 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 (type.parseLiteral && _isScalarKind(value.kind)) value.value = type.parseLiteral(value) switch (value.kind) { case Kind.VARIABLE: From 3f98c537814c6a25125d088d8c83427901513f22 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:31:48 +0100 Subject: [PATCH 05/28] Replace variable null values with AST NullValues --- lib/resolvers/parse/ast/fromObject.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/resolvers/parse/ast/fromObject.js b/lib/resolvers/parse/ast/fromObject.js index 8d169f18..12865124 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,8 @@ const _variableToValue = variable => { return _arrayToListValue(variable) } else if (isPlainObject(variable)) { return _objectToObjectValue(variable) + } else if (variable === null) { + return _nullValue } return _valueToGenericScalarValue(variable) } From cb6ae61781944f2ee203792f0c3e187410d4cef1 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:44:09 +0100 Subject: [PATCH 06/28] Add convenience AST NullValue value --- lib/resolvers/parse/ast/enrich.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/resolvers/parse/ast/enrich.js b/lib/resolvers/parse/ast/enrich.js index 244d62e8..4508a489 100644 --- a/lib/resolvers/parse/ast/enrich.js +++ b/lib/resolvers/parse/ast/enrich.js @@ -42,6 +42,9 @@ const _traverseArgumentOrObjectField = (info, argumentOrObjectField, _fieldOr_ar _traverseObjectValue(info, value, type._fields) break } + + // Convenience value for both literal and variable values + if (argumentOrObjectField.value.kind === Kind.NULL) argumentOrObjectField.value.value = null } const _traverseSelectionSet = (info, selectionSet, _fields) => { From 588b1f9c0ef7cda1fa5887106d36f38e056d5d94 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:55:31 +0100 Subject: [PATCH 07/28] Substitute undefined variables with undefined --- lib/resolvers/parse/ast/enrich.js | 2 +- lib/resolvers/parse/ast/fromObject.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast/enrich.js b/lib/resolvers/parse/ast/enrich.js index 4508a489..8110d617 100644 --- a/lib/resolvers/parse/ast/enrich.js +++ b/lib/resolvers/parse/ast/enrich.js @@ -44,7 +44,7 @@ const _traverseArgumentOrObjectField = (info, argumentOrObjectField, _fieldOr_ar } // Convenience value for both literal and variable values - if (argumentOrObjectField.value.kind === Kind.NULL) argumentOrObjectField.value.value = null + 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 12865124..11240b9a 100644 --- a/lib/resolvers/parse/ast/fromObject.js +++ b/lib/resolvers/parse/ast/fromObject.js @@ -36,6 +36,8 @@ const _variableToValue = variable => { return _objectToObjectValue(variable) } else if (variable === null) { return _nullValue + } else if (variable === undefined) { + return undefined } return _valueToGenericScalarValue(variable) } From 01702b212fe0c625ed118a033629f9b8e37451f1 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 14:55:54 +0100 Subject: [PATCH 08/28] Ignore arguments with undefined variable values --- lib/resolvers/parse/ast2cqn/utils/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast2cqn/utils/index.js b/lib/resolvers/parse/ast2cqn/utils/index.js index ed73c603..ed1add90 100644 --- a/lib/resolvers/parse/ast2cqn/utils/index.js +++ b/lib/resolvers/parse/ast2cqn/utils/index.js @@ -1,5 +1,6 @@ const { Kind } = require('graphql') -const getArgumentByName = (args, name) => args.find(arg => arg.name.value === name && arg.value.kind !== Kind.NULL) +const getArgumentByName = (args, name) => + args.find(arg => arg.value && arg.name.value === name && arg.value.kind !== Kind.NULL) module.exports = { getArgumentByName } From 6618244aaf0b4bbff01670710e6e8bd4cf98906c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 9 Dec 2022 15:15:03 +0100 Subject: [PATCH 09/28] Add changelog entries --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53235807..1ae44148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ 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 + ### Fixed ### Removed From 8833e070fc54a1b8417936df8b7f253dbfcb4990 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 19 Jan 2023 17:47:23 +0100 Subject: [PATCH 10/28] Use gql tag in null/undefined tests --- test/tests/queries/filter.test.js | 8 ++++---- test/tests/queries/variables.test.js | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index df2d4cb0..7304f20f 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -31,7 +31,7 @@ describe('graphql - filter', () => { }) test('query with filter of value null', async () => { - const query = `#graphql + const query = gql` { AdminService { Books(filter: null) { @@ -59,7 +59,7 @@ describe('graphql - filter', () => { test('query with equality filter for null values', async () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = `#graphql + const query = gql` { AdminService { Books(filter: { stock: { eq: null } }) { @@ -278,7 +278,7 @@ describe('graphql - filter', () => { }) test('query with filter of value null', async () => { - const query = `#graphql + const query = gql` { AdminService { Books(filter: null) { @@ -310,7 +310,7 @@ describe('graphql - filter', () => { test('query with equality filter for null values', async () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = `#graphql + const query = gql` { AdminService { Books(filter: { stock: { eq: null } }) { diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index fddf03a6..e99b41ba 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -136,7 +136,7 @@ describe('graphql - variables', () => { }) test('query variable with undefined value as an argument', async () => { - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -163,7 +163,7 @@ describe('graphql - variables', () => { }) test('query variable with null value as an argument', async () => { - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -192,7 +192,7 @@ describe('graphql - variables', () => { test('query variable with undefined value as a field of an argument', async () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -215,7 +215,7 @@ describe('graphql - variables', () => { test('query variable with null value as a field of an argument', async () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -383,7 +383,7 @@ describe('graphql - variables', () => { }) test('query variable with undefined value as an argument', async () => { - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -414,7 +414,7 @@ describe('graphql - variables', () => { }) test('query variable with null value as an argument', async () => { - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -447,7 +447,7 @@ describe('graphql - variables', () => { test('query variable with undefined value as a field of an argument', async () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { @@ -474,7 +474,7 @@ describe('graphql - variables', () => { test('query variable with null value as a field of an argument', async () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = `#graphql + const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { Books(filter: $filter) { From df9e5d5df768e5aa041303836e0bbb865b16ebb8 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 27 Feb 2023 14:43:37 +0100 Subject: [PATCH 11/28] Move changelog entries to newest release --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8d24c0..fee06663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ 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 + ### Fixed ### Removed @@ -26,13 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version 0.2.0 - 2023-01-30 -- Support for filtering by `null` values - ### Changed - Register `aliasFieldResolver` during schema generation instead of passing it to the GraphQL server - The filters `contains`, `startswith`, and `endswith` now generate CQN function calls instead of generating `like` expressions directly -- Improved handling of `null` and `undefined` values in query arguments ### Fixed From 8a47426e1e740d78ff3d9829da8eeeb9f0518135 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 2 Mar 2023 16:51:11 +0100 Subject: [PATCH 12/28] Ignore undefined properties in filter objects --- lib/resolvers/crud/read.js | 5 ++++- lib/resolvers/parse/ast2cqn/where.js | 12 ++++++++---- test/tests/queries/variables.test.js | 9 ++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) 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/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 1b47085c..e8a7ffa4 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -49,13 +49,17 @@ const _arrayInsertBetweenFlat = (array, element) => const _joinedXprFrom_xprs = (_xprs, operator) => ({ xpr: _arrayInsertBetweenFlat(_xprs, operator) }) 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 + else if (_xprs.length === 1) return _xprs[0] + return _joinedXprFrom_xprs(_xprs, 'and') } 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 + else if (_xprs.length === 1) return _xprs[0] + return _joinedXprFrom_xprs(_xprs, 'or') } const astToWhere = filterArg => { diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index e99b41ba..cc4ff61d 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -463,7 +463,14 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [] + nodes: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 }, + { title: 'Moby-Dick', stock: null } + ] } } } From 55840b6e3f43cb99a75cccca3942c12d82bcef71 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 2 Mar 2023 18:21:15 +0100 Subject: [PATCH 13/28] Ignore incomplete filters like `{ ID: { stock: null } }` --- lib/resolvers/parse/ast2cqn/where.js | 2 +- test/tests/queries/variables.test.js | 128 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index e8a7ffa4..e3d3757a 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -40,7 +40,7 @@ const _parseObjectField = (objectField, columnName) => { case Kind.OBJECT: return _parseObjectValue(value, name) } - return _objectFieldTo_xpr(objectField, columnName) + if (columnName) return _objectFieldTo_xpr(objectField, columnName) } const _arrayInsertBetweenFlat = (array, element) => diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index cc4ff61d..824f530e 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -189,6 +189,66 @@ describe('graphql - variables', () => { expect(response.data).toEqual({ data }) }) + test('query variable with undefined value where an object value is expected', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + title + stock + } + } + } + ` + const variables = { filter: { stock: undefined } } + const data = { + AdminService: { + Books: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 }, + { title: 'Moby-Dick', stock: null } + ] + } + } + 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 () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + title + stock + } + } + } + ` + const variables = { filter: { stock: null } } + const data = { + AdminService: { + Books: [ + { title: 'Wuthering Heights', stock: 12 }, + { title: 'Jane Eyre', stock: 11 }, + { title: 'The Raven', stock: 333 }, + { title: 'Eleonora', stock: 555 }, + { title: 'Catweazle', stock: 22 }, + { title: 'Moby-Dick', stock: null } + ] + } + } + 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 () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) @@ -444,6 +504,74 @@ describe('graphql - variables', () => { expect(response.data).toEqual({ data }) }) + test('query variable with undefined value where an object value is expected', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + title + stock + } + } + } + } + ` + const variables = { filter: { stock: undefined } } + 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 }, + { title: 'Moby-Dick', stock: null } + ] + } + } + } + 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 () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + nodes { + title + stock + } + } + } + } + ` + const variables = { filter: { stock: null } } + 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 }, + { title: 'Moby-Dick', stock: null } + ] + } + } + } + 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 () => { await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) From a81f49cca730c32a3cff371dbfb807aacc0f40fa Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 2 Mar 2023 18:21:57 +0100 Subject: [PATCH 14/28] Don't check if parseLiteral function is defined --- lib/resolvers/parse/ast/enrich.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast/enrich.js b/lib/resolvers/parse/ast/enrich.js index 25a76242..c6d987a6 100644 --- a/lib/resolvers/parse/ast/enrich.js +++ b/lib/resolvers/parse/ast/enrich.js @@ -29,7 +29,7 @@ const _traverseArgumentOrObjectField = (info, argumentOrObjectField, _fieldOr_ar const value = argumentOrObjectField.value const type = _getTypeFrom_fieldOr_arg(_fieldOr_arg) - if (type.parseLiteral && _isScalarKind(value.kind)) value.value = type.parseLiteral(value) + if (_isScalarKind(value.kind)) value.value = type.parseLiteral(value) switch (value.kind) { case Kind.VARIABLE: From 66a5cc1b267dbc2fc530e2bb12e9069be1e880fa Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 3 Mar 2023 12:58:50 +0100 Subject: [PATCH 15/28] Add tests combining undefined/null and non-undefined/non-null variable values --- test/tests/queries/variables.test.js | 100 +++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 824f530e..a1527871 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -294,6 +294,52 @@ describe('graphql - variables', () => { const response = await POST('/graphql', { query, variables }) expect(response.data).toEqual({ data }) }) + + test('query variable combining undefined and non-undefined values', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + ID + title + } + } + } + ` + const variables = { filter: [{ ID: { eq: undefined } }, { ID: { eq: 251 } }, { ID: undefined }] } + const data = { + AdminService: { + Books: [{ ID: 251, title: 'The Raven' }] + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable combining null and non-null values', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + const query = gql` + query ($filter: [AdminService_Books_filter]) { + AdminService { + Books(filter: $filter) { + ID + title + } + } + } + ` + const variables = { filter: [{ ID: { eq: 251 } }, { ID: null }] } + const data = { + AdminService: { + Books: [{ ID: 251, title: 'The Raven' }] + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) }) describe('queries with variables with connections', () => { @@ -632,5 +678,59 @@ describe('graphql - variables', () => { const response = await POST('/graphql', { query, variables }) expect(response.data).toEqual({ data }) }) + + test('query variable combining undefined and non-undefined values', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + 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: 251, title: 'The Raven' }] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) + + test('query variable combining null and non-null values', async () => { + await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + + 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: 251, title: 'The Raven' }] + } + } + } + const response = await POST('/graphql', { query, variables }) + expect(response.data).toEqual({ data }) + }) }) }) From 78a3576b0e809cb03422d94133a701e28e972d63 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 6 Mar 2023 16:05:09 +0100 Subject: [PATCH 16/28] Prettier format --- lib/resolvers/parse/ast2cqn/where.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index e3d3757a..24719e4c 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -49,16 +49,18 @@ const _arrayInsertBetweenFlat = (array, element) => const _joinedXprFrom_xprs = (_xprs, operator) => ({ xpr: _arrayInsertBetweenFlat(_xprs, operator) }) const _parseObjectValue = (objectValue, columnName) => { - const _xprs = objectValue.fields.map(field => _parseObjectField(field, columnName)).filter(field => field !== undefined) + const _xprs = objectValue.fields + .map(field => _parseObjectField(field, columnName)) + .filter(field => field !== undefined) if (_xprs.length === 0) return - else if (_xprs.length === 1) return _xprs[0] + else if (_xprs.length === 1) return _xprs[0] return _joinedXprFrom_xprs(_xprs, 'and') } const _parseListValue = (listValue, columnName) => { const _xprs = listValue.values.map(value => _parseObjectValue(value, columnName)).filter(value => value !== undefined) if (_xprs.length === 0) return - else if (_xprs.length === 1) return _xprs[0] + else if (_xprs.length === 1) return _xprs[0] return _joinedXprFrom_xprs(_xprs, 'or') } From a63b8390e7eaaa9296f89f50e176d762e05e85c2 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 10 Mar 2023 11:19:46 +0100 Subject: [PATCH 17/28] Create false _xpr for empty filter lists --- lib/resolvers/parse/ast2cqn/where.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 24719e4c..42fd83cb 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -57,9 +57,11 @@ const _parseObjectValue = (objectValue, columnName) => { 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)).filter(value => value !== undefined) - if (_xprs.length === 0) return + if (_xprs.length === 0) return _false_xpr else if (_xprs.length === 1) return _xprs[0] return _joinedXprFrom_xprs(_xprs, 'or') } From 4bba695053ecfbe575a468a3d852d4859dd3acbe Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 16 Mar 2023 18:14:29 +0100 Subject: [PATCH 18/28] Adjust tests so that incomplete filters return no results --- test/tests/queries/variables.test.js | 33 +++------------------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index a1527871..2b2bb413 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -551,8 +551,6 @@ describe('graphql - variables', () => { }) test('query variable with undefined value where an object value is expected', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -569,14 +567,7 @@ describe('graphql - variables', () => { 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 }, - { title: 'Moby-Dick', stock: null } - ] + nodes: [] } } } @@ -585,8 +576,6 @@ describe('graphql - variables', () => { }) test('query variable with null value where an object value is expected', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -603,14 +592,7 @@ describe('graphql - variables', () => { 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 }, - { title: 'Moby-Dick', stock: null } - ] + nodes: [] } } } @@ -619,8 +601,6 @@ describe('graphql - variables', () => { }) test('query variable with undefined value as a field of an argument', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -637,14 +617,7 @@ describe('graphql - variables', () => { 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 }, - { title: 'Moby-Dick', stock: null } - ] + nodes: [] } } } From 28ca365bdaa4fc1fc5ab18b69b9edc673acd3a39 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 16 Mar 2023 18:20:00 +0100 Subject: [PATCH 19/28] Also adjust tests without connections --- test/tests/queries/variables.test.js | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 2b2bb413..379578de 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -190,8 +190,6 @@ describe('graphql - variables', () => { }) test('query variable with undefined value where an object value is expected', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -205,14 +203,7 @@ describe('graphql - variables', () => { const variables = { filter: { stock: undefined } } const data = { AdminService: { - Books: [ - { title: 'Wuthering Heights', stock: 12 }, - { title: 'Jane Eyre', stock: 11 }, - { title: 'The Raven', stock: 333 }, - { title: 'Eleonora', stock: 555 }, - { title: 'Catweazle', stock: 22 }, - { title: 'Moby-Dick', stock: null } - ] + Books: [] } } const response = await POST('/graphql', { query, variables }) @@ -220,8 +211,6 @@ describe('graphql - variables', () => { }) test('query variable with null value where an object value is expected', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -235,14 +224,7 @@ describe('graphql - variables', () => { const variables = { filter: { stock: null } } const data = { AdminService: { - Books: [ - { title: 'Wuthering Heights', stock: 12 }, - { title: 'Jane Eyre', stock: 11 }, - { title: 'The Raven', stock: 333 }, - { title: 'Eleonora', stock: 555 }, - { title: 'Catweazle', stock: 22 }, - { title: 'Moby-Dick', stock: null } - ] + Books: [] } } const response = await POST('/graphql', { query, variables }) @@ -250,8 +232,6 @@ describe('graphql - variables', () => { }) test('query variable with undefined value as a field of an argument', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { From e33f9a1d347d14104fde269fa9277e886cc59064 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 17 Mar 2023 18:48:02 +0100 Subject: [PATCH 20/28] Empty filter objects return true _xprs --- lib/resolvers/parse/ast2cqn/where.js | 4 +- test/tests/queries/filter.test.js | 170 +++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 42fd83cb..24700c0d 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -48,11 +48,13 @@ 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)) .filter(field => field !== undefined) - if (_xprs.length === 0) return + if (_xprs.length === 0) return _true_xpr else if (_xprs.length === 1) return _xprs[0] return _joinedXprFrom_xprs(_xprs, 'and') } diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index 7304f20f..bff54e1b 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -307,6 +307,174 @@ describe('graphql - filter', () => { expect(response.data).toEqual({ data }) }) + test('query with filter of value 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 }) + }) + + test('query with filter of value empty list', async () => { + const query = gql` + { + AdminService { + Books(filter: []) { + nodes { + title + stock + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [] + } + } + } + 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 }) + }) + + 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 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 }) + }) + + 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 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' }) @@ -535,5 +703,7 @@ describe('graphql - filter', () => { const response = await POST('/graphql', { query }) expect(response.data).toEqual({ data }) }) + + describe('queries with filter argument containing null and undefined values', () => {}) }) }) From 83c69b5d1c104f22c5956046339d225a8c59631d Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 20 Mar 2023 17:02:27 +0100 Subject: [PATCH 21/28] Adjust tests so that incomplete filters are ignored --- test/tests/queries/variables.test.js | 50 +++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 379578de..9c0b7137 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -536,8 +536,8 @@ describe('graphql - variables', () => { AdminService { Books(filter: $filter) { nodes { + ID title - stock } } } @@ -547,7 +547,13 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [] + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] } } } @@ -561,8 +567,8 @@ describe('graphql - variables', () => { AdminService { Books(filter: $filter) { nodes { + ID title - stock } } } @@ -572,7 +578,13 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [] + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] } } } @@ -586,8 +598,8 @@ describe('graphql - variables', () => { AdminService { Books(filter: $filter) { nodes { + ID title - stock } } } @@ -597,7 +609,13 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [] + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] } } } @@ -633,8 +651,6 @@ describe('graphql - variables', () => { }) test('query variable combining undefined and non-undefined values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -651,7 +667,13 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [{ ID: 251, title: 'The Raven' }] + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] } } } @@ -660,8 +682,6 @@ describe('graphql - variables', () => { }) test('query variable combining null and non-null values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -678,7 +698,13 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [{ ID: 251, title: 'The Raven' }] + nodes: [ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ] } } } From c4d1339eb33f7a34db758dfadaf1c4897ac35ec1 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 20 Mar 2023 17:02:53 +0100 Subject: [PATCH 22/28] Remove empty describe block --- test/tests/queries/filter.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index bff54e1b..7e75f3d9 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -703,7 +703,5 @@ describe('graphql - filter', () => { const response = await POST('/graphql', { query }) expect(response.data).toEqual({ data }) }) - - describe('queries with filter argument containing null and undefined values', () => {}) }) }) From 7fcce28fe9f98dd10c41df1cebaad3c9e43223dd Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 20 Mar 2023 17:03:58 +0100 Subject: [PATCH 23/28] Move up filter leaf guard clause --- lib/resolvers/parse/ast2cqn/where.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 24700c0d..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) } - if (columnName) return _objectFieldTo_xpr(objectField, columnName) } const _arrayInsertBetweenFlat = (array, element) => From 2a9fd9ef395c03f980d2bde6123904db6c3a8b13 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 23 Mar 2023 09:48:53 +0100 Subject: [PATCH 24/28] Remove duplicated tests --- test/tests/queries/filter.test.js | 48 ------- test/tests/queries/variables.test.js | 186 --------------------------- 2 files changed, 234 deletions(-) diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index 7e75f3d9..2285c52e 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -30,54 +30,6 @@ describe('graphql - filter', () => { expect(response.data).toEqual({ data }) }) - test('query with filter of value null', async () => { - const query = gql` - { - AdminService { - Books(filter: null) { - title - stock - } - } - } - ` - const data = { - AdminService: { - Books: [ - { 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 }) - }) - - 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 } }) { - title - stock - } - } - } - ` - const data = { - AdminService: { - Books: [{ title: 'Moby-Dick', stock: null }] - } - } - const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data }) - }) - test('query with simple filter wrapped as lists', async () => { const query = gql` { diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 9c0b7137..11e39d1f 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -134,192 +134,6 @@ 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) { - ID - title - } - } - } - ` - const variables = { filter: undefined } - const data = { - AdminService: { - Books: [ - { 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) { - ID - title - } - } - } - ` - const variables = { filter: null } - const data = { - AdminService: { - Books: [ - { 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) { - title - stock - } - } - } - ` - const variables = { filter: { stock: undefined } } - const data = { - AdminService: { - Books: [] - } - } - 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) { - title - stock - } - } - } - ` - const variables = { filter: { stock: null } } - const data = { - AdminService: { - Books: [] - } - } - 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) { - title - stock - } - } - } - ` - const variables = { filter: { stock: { eq: undefined } } } - const data = { - AdminService: { - Books: [] - } - } - 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 () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - - const query = gql` - query ($filter: [AdminService_Books_filter]) { - AdminService { - Books(filter: $filter) { - title - stock - } - } - } - ` - const variables = { filter: { stock: { eq: null } } } - const data = { - AdminService: { - Books: [{ title: 'Moby-Dick', stock: null }] - } - } - const response = await POST('/graphql', { query, variables }) - expect(response.data).toEqual({ data }) - }) - - test('query variable combining undefined and non-undefined values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - - const query = gql` - query ($filter: [AdminService_Books_filter]) { - AdminService { - Books(filter: $filter) { - ID - title - } - } - } - ` - const variables = { filter: [{ ID: { eq: undefined } }, { ID: { eq: 251 } }, { ID: undefined }] } - const data = { - AdminService: { - Books: [{ ID: 251, title: 'The Raven' }] - } - } - const response = await POST('/graphql', { query, variables }) - expect(response.data).toEqual({ data }) - }) - - test('query variable combining null and non-null values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - - const query = gql` - query ($filter: [AdminService_Books_filter]) { - AdminService { - Books(filter: $filter) { - ID - title - } - } - } - ` - const variables = { filter: [{ ID: { eq: 251 } }, { ID: null }] } - const data = { - AdminService: { - Books: [{ ID: 251, title: 'The Raven' }] - } - } - const response = await POST('/graphql', { query, variables }) - expect(response.data).toEqual({ data }) - }) }) describe('queries with variables with connections', () => { From 7d495b57c443c3a916b57f27d012228ec481bac9 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 23 Mar 2023 09:49:46 +0100 Subject: [PATCH 25/28] Remove insert from variables test --- test/tests/queries/variables.test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 11e39d1f..6f1e0714 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -3,10 +3,9 @@ describe('graphql - variables', () => { const path = require('path') const { gql } = require('../../util') - const { axios, POST, data } = cds.test(path.join(__dirname, '../../resources/bookshop-graphql')) + const { axios, POST } = 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 variables without connections', () => { @@ -438,8 +437,6 @@ describe('graphql - variables', () => { }) test('query variable with null value as a field of an argument', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) - const query = gql` query ($filter: [AdminService_Books_filter]) { AdminService { @@ -456,7 +453,7 @@ describe('graphql - variables', () => { const data = { AdminService: { Books: { - nodes: [{ title: 'Moby-Dick', stock: null }] + nodes: [] } } } From 3ff022fe71d7f1b5765ab679eff86fa3a2ab27ca Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 23 Mar 2023 10:07:00 +0100 Subject: [PATCH 26/28] Add tests for empty filter structures joined with equality filters --- test/tests/queries/filter.test.js | 102 ++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index 2285c52e..cc649b2e 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -427,6 +427,108 @@ describe('graphql - filter', () => { 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 }) + }) + + 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 + } + } + } + } + ` + const data = { + AdminService: { + Books: { + nodes: [] + } + } + } + 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 }) + }) + + 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 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' }) From 69f229ad2ebd2766234c80e6f2fda3d68883bad8 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 23 Mar 2023 10:08:50 +0100 Subject: [PATCH 27/28] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1203c06..28dbf018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 From 741cf092cb6a12283be9a6c60b06ff3caff1ca30 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 23 Mar 2023 10:27:05 +0100 Subject: [PATCH 28/28] Split filter tests into two describe blocks --- test/tests/queries/filter.test.js | 1186 +++++++++++++++-------------- 1 file changed, 596 insertions(+), 590 deletions(-) diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index cc649b2e..57a49a86 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -10,752 +10,758 @@ describe('graphql - filter', () => { // 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 }) - }) + 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 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 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 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 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: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' } - ] - } - } - 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 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 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 OR on the same field', async () => { - const query = gql` - { - AdminServiceBasic { - Books(filter: { ID: [{ eq: 201 }, { eq: 251 }] }) { - 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: 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 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 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 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 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 + 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: 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 } }) { + test('query with filters on nested fields', async () => { + const query = gql` + { + AdminServiceBasic { + Authors(filter: { ID: { gt: 110 } }) { ID - title + 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 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 }) + }) }) }) 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 + 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 = { - 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 of value null', async () => { - const query = gql` - { - AdminService { - Books(filter: null) { - nodes { - title - stock + 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: [ - { 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 of value empty input object', async () => { - const query = gql` - { - AdminService { - Books(filter: {}) { - nodes { - title - stock + 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: [ - { 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: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' } + ] + } } } - } - 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 of value empty list', async () => { - const query = gql` - { - AdminService { - Books(filter: []) { - nodes { - title - stock + 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: [] + ` + 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 filter list containing empty input object', async () => { - const query = gql` - { - AdminService { - Books(filter: [{}]) { - nodes { - title - stock + 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 + } } } } - } - ` - 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 }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter column of value empty input object', async () => { - const query = gql` - { - AdminService { - Books(filter: { ID: {} }) { - nodes { - title - stock + 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: [ - { 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 }) - }) + 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 + 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: [] + ` + 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 }) - }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - test('query with filter column list containing empty input object', async () => { - const query = gql` - { - AdminService { - Books(filter: { ID: [{}] }) { - nodes { - title - stock + 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: { - 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: { + 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 }) + 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 + 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: 'Jane Eyre', stock: 11 }] + ` + 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 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 + test('query with filter of value empty input object', async () => { + const query = gql` + { + AdminService { + Books(filter: {}) { + nodes { + title + stock + } } } } - } - ` - const data = { - AdminService: { - Books: { - nodes: [] + ` + 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 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 + test('query with filter of value empty list', 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 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 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 + 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: 'Jane Eyre', stock: 11 }] + ` + 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 }) - }) - - test('query with equality filter for null values', async () => { - await INSERT.into('sap.capire.bookshop.Books').entries({ title: 'Moby-Dick' }) + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) - const query = gql` - { - AdminService { - Books(filter: { stock: { eq: null } }) { - nodes { - title - stock + 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: 'Moby-Dick', stock: null }] + ` + 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 simple filter wrapped as lists', async () => { - const query = gql` - { - AdminService { - Books(filter: [{ ID: [{ eq: 201 }] }]) { - nodes { - ID - title + 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 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 column list containing empty input object', async () => { + const query = gql` + { + AdminService { + Books(filter: { ID: [{}] }) { + 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 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 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 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 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 + } } } } - } - ` - const data = { - AdminService: { - Books: { - nodes: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' } - ] + ` + 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 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: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' } - ] + ` + 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 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 OR 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: [ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 251, title: 'The Raven' }, - { ID: 271, title: 'Catweazle' } - ] + ` + 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 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 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: { - 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: [{ 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 }) + }) }) }) })