From 132afcdc05cfc91e0d3dbb34433b2bc2a01e20a4 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 7 Mar 2024 11:51:55 +0100 Subject: [PATCH] [main] release hotfix 0.10.1 (#159) --- .gitignore | 1 + CHANGELOG.md | 8 ++- README.md | 4 +- lib/resolvers/crud/read.js | 3 +- lib/resolvers/parse/ast/literal.js | 4 +- lib/resolvers/parse/ast2cqn/where.js | 3 +- package.json | 5 +- test/resources/annotations/package.json | 3 + test/resources/bookshop-graphql/package.json | 4 ++ test/resources/bookshop/package.json | 3 + test/resources/cds.Request/package.json | 3 + test/resources/concurrency/package.json | 3 + .../custom-error-formatter/package.json | 3 + test/resources/custom-handlers/package.json | 5 ++ test/resources/edge-cases/package.json | 3 + test/resources/error-handling/package.json | 3 + test/resources/types/package.json | 5 ++ test/tests/annotations.test.js | 4 +- test/tests/enrich.test.js | 21 ++++++- test/tests/mutations/delete.test.js | 29 +++++++++ test/tests/queries/filter.test.js | 6 +- test/tests/queries/paging-offset.test.js | 61 +++++++++++++------ test/tests/queries/totalCount.test.js | 35 ++++++++--- 23 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 test/resources/custom-handlers/package.json create mode 100644 test/resources/types/package.json diff --git a/.gitignore b/.gitignore index 667f8301..90e7afba 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ package-lock.json .idea .DS_Store .vscode +**/_out # cds-typer @cds-models/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 26973a95..f7c7603e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 0.10.0 - 2023-01-30 +## Version 0.10.1 - 2024-03-07 + +### Fixed + +- Type parsing error for literal values passed within arguments on fields of scalar type differing from the literal type. This case occurred for delete mutations when the filter operands had a type other than `Int`. + +## Version 0.10.0 - 2024-01-30 ### Added diff --git a/README.md b/README.md index e9da5757..3035dec0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A GraphQL protocol adapter for [SAP Cloud Application Programming Model](https://cap.cloud.sap) Node.js. This adapter generically generates a GraphQL schema for the models of an application and serves an endpoint that allows you to query your services using the GraphQL query language. -_**WARNING:** This package is in an early general availability state. This means that it is general available, with stable APIs unless otherwise indicated, and you can use it for production. However, please note the [current limitations](#limitations) listed below._ +_**WARNING:** This package is in an early general availability state. This means that it is generally available, with stable APIs unless otherwise indicated, and you can use it for production. However, please note the [current limitations](#limitations) listed below._ ## Requirements and Setup @@ -16,7 +16,7 @@ _**WARNING:** This package is in an early general availability state. This means npm add @cap-js/graphql ``` -> This will automatically plugin to `@sap/cds` runtime, enabling the new [middlewares architecture](https://cap.cloud.sap/docs/node.js/middlewares) in Node.js, and register a GraphQL endpoint at `/graphql` serving all CRUD requests for the application services found in your model. +> This command will set up the GraphQL plug-in with the `@sap/cds` runtime. It enables the new [middlewares architecture](https://cap.cloud.sap/docs/node.js/middlewares) in Node.js and registers a GraphQL endpoint at `/graphql` serving all CRUD requests for the application services found in your model. 2. Annotate the services you want to serve, e.g. using `@graphql` or `@protocol: 'graphql'`. diff --git a/lib/resolvers/crud/read.js b/lib/resolvers/crud/read.js index 4c8c8b96..e74d7028 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -10,7 +10,8 @@ module.exports = async ({ req, res }, service, entity, selection) => { const args = selection.arguments let query = SELECT.from(entity) - query.columns(astToColumns(entity, selection.selectionSet.selections, true)) + const columns = astToColumns(entity, selection.selectionSet.selections, true) + if (columns.length) query.columns(columns) const filter = getArgumentByName(args, ARGS.filter) if (filter) { diff --git a/lib/resolvers/parse/ast/literal.js b/lib/resolvers/parse/ast/literal.js index 653a360f..aedb46f2 100644 --- a/lib/resolvers/parse/ast/literal.js +++ b/lib/resolvers/parse/ast/literal.js @@ -12,8 +12,8 @@ const _getTypeFrom_fields = (_fields, path, index = 0) => { const _field = _fields[name] const type = _getTypeFrom_fieldOr_arg(_field) - // If type has the parseLiteral function it is a scalar type -> leaf -> end of path - if (type.parseLiteral) return type + // If we are at the end of the path, this field is a leaf and therefore is of scalar type with a parseLiteral function + if (index === path.length) return type const next = path[index] // Is the next path element an argument? If yes, follow the argument diff --git a/lib/resolvers/parse/ast2cqn/where.js b/lib/resolvers/parse/ast2cqn/where.js index 3004c3a1..ed3ef4c9 100644 --- a/lib/resolvers/parse/ast2cqn/where.js +++ b/lib/resolvers/parse/ast2cqn/where.js @@ -34,7 +34,8 @@ const _objectFieldTo_xpr = (objectField, columnName) => { const operand = objectField.value if (gqlOperator === LOGICAL_OPERATORS.in) { - const list = operand.kind === Kind.LIST ? operand.values.map(value => ({ val: value.value })) : [{ val: operand.value }] + const list = + operand.kind === Kind.LIST ? operand.values.map(value => ({ val: value.value })) : [{ val: operand.value }] return [ref, _gqlOperatorToCdsOperator(gqlOperator), { list }] } diff --git a/package.json b/package.json index 54379c17..456afb6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/graphql", - "version": "0.10.0", + "version": "0.10.1", "description": "CDS protocol adapter for GraphQL", "keywords": [ "CAP", @@ -44,8 +44,7 @@ "eslint": "^8", "express": "^4.17.1", "jest": "^29.3.1", - "prettier": "3.2.4", "semver": "^7.4.0", - "sqlite3": "^5.0.2" + "@cap-js/sqlite": "^1" } } diff --git a/test/resources/annotations/package.json b/test/resources/annotations/package.json index d3aa051f..b87d22ed 100644 --- a/test/resources/annotations/package.json +++ b/test/resources/annotations/package.json @@ -2,6 +2,9 @@ "dependencies": { "@cap-js/graphql": "*" }, + "devDependencies": { + "@cap-js/sqlite": "*" + }, "cds": { "protocols": { "graphql": { diff --git a/test/resources/bookshop-graphql/package.json b/test/resources/bookshop-graphql/package.json index feae891a..8b38e8b0 100644 --- a/test/resources/bookshop-graphql/package.json +++ b/test/resources/bookshop-graphql/package.json @@ -2,10 +2,14 @@ "dependencies": { "@cap-js/graphql": "*" }, + "devDependencies": { + "@cap-js/sqlite": "*" + }, "cds": { "requires": { "db": { "kind": "sqlite", + "impl": "@cap-js/sqlite", "credentials": { "database": ":memory:" } diff --git a/test/resources/bookshop/package.json b/test/resources/bookshop/package.json index 0e0b6de2..5e7113e1 100644 --- a/test/resources/bookshop/package.json +++ b/test/resources/bookshop/package.json @@ -13,6 +13,9 @@ "@sap/cds": ">=5.9", "express": "^4.17.1" }, + "devDependencies": { + "@cap-js/sqlite": "*" + }, "scripts": { "genres": "cds serve test/genres.cds", "start": "cds run", diff --git a/test/resources/cds.Request/package.json b/test/resources/cds.Request/package.json index a713a269..768a96c1 100644 --- a/test/resources/cds.Request/package.json +++ b/test/resources/cds.Request/package.json @@ -1,5 +1,8 @@ { "dependencies": { "@cap-js/graphql": "*" + }, + "devDependencies": { + "@cap-js/sqlite": "*" } } diff --git a/test/resources/concurrency/package.json b/test/resources/concurrency/package.json index a713a269..768a96c1 100644 --- a/test/resources/concurrency/package.json +++ b/test/resources/concurrency/package.json @@ -1,5 +1,8 @@ { "dependencies": { "@cap-js/graphql": "*" + }, + "devDependencies": { + "@cap-js/sqlite": "*" } } diff --git a/test/resources/custom-error-formatter/package.json b/test/resources/custom-error-formatter/package.json index aa93bde8..db0204a2 100644 --- a/test/resources/custom-error-formatter/package.json +++ b/test/resources/custom-error-formatter/package.json @@ -2,6 +2,9 @@ "dependencies": { "@cap-js/graphql": "*" }, + "devDependencies": { + "@cap-js/sqlite": "*" + }, "cds": { "protocols": { "graphql": { diff --git a/test/resources/custom-handlers/package.json b/test/resources/custom-handlers/package.json new file mode 100644 index 00000000..580c138c --- /dev/null +++ b/test/resources/custom-handlers/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@cap-js/sqlite": "*" + } +} \ No newline at end of file diff --git a/test/resources/edge-cases/package.json b/test/resources/edge-cases/package.json index a713a269..768a96c1 100644 --- a/test/resources/edge-cases/package.json +++ b/test/resources/edge-cases/package.json @@ -1,5 +1,8 @@ { "dependencies": { "@cap-js/graphql": "*" + }, + "devDependencies": { + "@cap-js/sqlite": "*" } } diff --git a/test/resources/error-handling/package.json b/test/resources/error-handling/package.json index 83fbf746..0a6173f6 100644 --- a/test/resources/error-handling/package.json +++ b/test/resources/error-handling/package.json @@ -2,6 +2,9 @@ "dependencies": { "@cap-js/graphql": "*" }, + "devDependencies": { + "@cap-js/sqlite": "*" + }, "cds": { "requires": { "db": { diff --git a/test/resources/types/package.json b/test/resources/types/package.json new file mode 100644 index 00000000..717be87e --- /dev/null +++ b/test/resources/types/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@cap-js/sqlite": "*" + } +} \ No newline at end of file diff --git a/test/tests/annotations.test.js b/test/tests/annotations.test.js index df1d4806..696174ba 100644 --- a/test/tests/annotations.test.js +++ b/test/tests/annotations.test.js @@ -39,7 +39,9 @@ describe('graphql - annotations', () => { } ` const response = await POST(path, { query }) - expect(response.data.errors[0].message).toMatch(/^Cannot query field "AnnotatedWithAtProtocolNone" on type "Query"\./) + expect(response.data.errors[0].message).toMatch( + /^Cannot query field "AnnotatedWithAtProtocolNone" on type "Query"\./ + ) }) test('service annotated with non-GraphQL protocol is not served', async () => { diff --git a/test/tests/enrich.test.js b/test/tests/enrich.test.js index 168e73ff..311329e7 100644 --- a/test/tests/enrich.test.js +++ b/test/tests/enrich.test.js @@ -179,7 +179,7 @@ describe('graphql - enrich AST ', () => { expect(value).toEqual(2) }) - test('parsing of literal value in nested input value', async () => { + test('parsing of literal value in nested input value passed as arg on field with sub-selection of fields', async () => { const query = gql` { AdminService { @@ -198,6 +198,25 @@ describe('graphql - enrich AST ', () => { const value = enrichedAST[0].selectionSet.selections[0].arguments[0].value.fields[0].value.fields[0].value.value expect(value).toEqual(201) }) + + test('parsing of literal value in nested input value passed as arg on field of scalar type', async () => { + const query = gql` + mutation { + AdminService { + Authors { + delete(filter: { dateOfBirth: { eq: "1818-07-30T00:00:00.000Z" } }) + } + } + } + ` + const document = parse(query) + const fakeInfo = fakeInfoObject(document, bookshopSchema, 'Mutation') + const enrichedAST = enrich(fakeInfo) + const value = + enrichedAST[0].selectionSet.selections[0].selectionSet.selections[0].arguments[0].value.fields[0].value + .fields[0].value.value + expect(value).toEqual('1818-07-30') + }) }) describe('variable values are substituted into the AST', () => { diff --git a/test/tests/mutations/delete.test.js b/test/tests/mutations/delete.test.js index 919a19c0..50bfbc1b 100644 --- a/test/tests/mutations/delete.test.js +++ b/test/tests/mutations/delete.test.js @@ -100,6 +100,35 @@ describe('graphql - delete mutations', () => { ]) }) + test('delete single entry by filtering for non-key field', async () => { + const query = gql` + mutation { + AdminService { + Books { + delete(filter: { title: { eq: "Jane Eyre" } }) + } + } + } + ` + const data = { + AdminService: { + Books: { + delete: 1 + } + } + } + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + + const result = await SELECT.from('sap.capire.bookshop.Books').columns('ID', 'title') + expect(result).toEqual([ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' } + ]) + }) + test('delete multiple entries', async () => { const query = gql` mutation ($filter: AdminService_Books_filter) { diff --git a/test/tests/queries/filter.test.js b/test/tests/queries/filter.test.js index 9a7d9a6c..fa8b4b55 100644 --- a/test/tests/queries/filter.test.js +++ b/test/tests/queries/filter.test.js @@ -290,7 +290,7 @@ describe('graphql - filter', () => { const query = gql` { AdminService { - Books(filter: [{ ID: { in: [201, 251] } }, { title: { contains: "cat" } }]) { + Books(filter: [{ ID: { in: [201, 251] } }, { title: { contains: "Cat" } }]) { nodes { ID title @@ -468,10 +468,10 @@ describe('graphql - filter', () => { Books( filter: [ { - title: [{ startswith: "the", endswith: "raven" }, { contains: "height" }] + title: [{ startswith: "The", endswith: "Raven" }, { contains: "Height" }] ID: [{ eq: 201 }, { eq: 251 }] } - { title: { contains: "cat" } } + { title: { contains: "Cat" } } ] ) { nodes { diff --git a/test/tests/queries/paging-offset.test.js b/test/tests/queries/paging-offset.test.js index 5e4c7ae2..67ef4f97 100644 --- a/test/tests/queries/paging-offset.test.js +++ b/test/tests/queries/paging-offset.test.js @@ -61,19 +61,27 @@ describe('graphql - offset-based paging', () => { } ` const data = { - AdminServiceBasic: { - Authors: null + AdminService: { + Authors: [ + { + name: 'Edgar Allen Poe', + books: [ + // Edgar Allen Poe has 2 books, but only 1 requested. + { + title: 'Eleonora' + } + ] + }, + { + name: 'Richard Carpenter', + books: [] + } + ] } } - const errors = [ - { - locations: [{ column: 13, line: 4 }], - message: 'Pagination is not supported in expand', - path: ['AdminServiceBasic', 'Authors'] - } - ] + const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data, errors }) + expect(response.data).toEqual({ data }) }) }) @@ -139,19 +147,32 @@ describe('graphql - offset-based paging', () => { ` const data = { AdminService: { - Authors: null + Authors: { + nodes: [ + { + name: 'Edgar Allen Poe', + books: { + // Edgar Allen Poe has 2 books, but only 1 requested. + nodes: [ + { + title: 'Eleonora' + } + ] + } + }, + { + name: 'Richard Carpenter', + books: { + nodes: [] + } + } + ] + } } } - const errors = [ - { - locations: [{ column: 13, line: 4 }], - message: 'Pagination is not supported in expand', - path: ['AdminService', 'Authors'], - extensions: expect.any(Object) - } - ] + const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data, errors }) + expect(response.data).toEqual({ data }) }) }) }) diff --git a/test/tests/queries/totalCount.test.js b/test/tests/queries/totalCount.test.js index ee47312c..07f75366 100644 --- a/test/tests/queries/totalCount.test.js +++ b/test/tests/queries/totalCount.test.js @@ -160,19 +160,34 @@ describe('graphql - queries with totalCount', () => { ` const data = { AdminService: { - Authors: null + Authors: { + totalCount: 4, + nodes: [ + { + name: 'Edgar Allen Poe', + books: { + totalCount: null, // REVISIT: expected 2 + nodes: [ + { + title: 'Eleonora' + } + ] + } + }, + { + name: 'Richard Carpenter', + books: { + totalCount: null, // REVISIT: expected 0 + nodes: [] + } + } + ] + } } } - const errors = [ - { - locations: [{ column: 11, line: 4 }], - message: 'Pagination is not supported in expand', - path: ['AdminService', 'Authors'], - extensions: expect.any(Object) - } - ] + const response = await POST('/graphql', { query }) - expect(response.data).toEqual({ data, errors }) + expect(response.data).toEqual({ data }) }) test('query with aliases on totalCount on nested fields', async () => {