From 1e9bc7fe1a1f62fa834ff416d89fffbc9b4e14b6 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 13 Jun 2025 16:50:36 +0200 Subject: [PATCH 01/22] Add schema validation to schema generation --- lib/schema/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/schema/index.js b/lib/schema/index.js index 2640e265..52955889 100644 --- a/lib/schema/index.js +++ b/lib/schema/index.js @@ -1,15 +1,24 @@ const queryGenerator = require('./query') const mutationGenerator = require('./mutation') -const { GraphQLSchema } = require('graphql') +const { GraphQLSchema, validateSchema } = require('graphql') const { createRootResolvers, registerAliasFieldResolvers } = require('../resolvers') function generateSchema4(services) { const resolvers = createRootResolvers(services) const cache = new Map() + const query = queryGenerator(cache).generateQueryObjectType(services, resolvers.Query) const mutation = mutationGenerator(cache).generateMutationObjectType(services, resolvers.Mutation) const schema = new GraphQLSchema({ query, mutation }) + registerAliasFieldResolvers(schema) + + const schemaValidationErrors = validateSchema(schema) + if (schemaValidationErrors.length) { + schemaValidationErrors.forEach(error => (error.severity = 'Error')) // Needed for cds-dk to decide logging based on log level + throw new AggregateError(schemaValidationErrors, 'GraphQL schema validation failed') + } + return schema } From a1c37f08589b205970c1bb547ff56ac6787f2287 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 13 Jun 2025 16:54:09 +0200 Subject: [PATCH 02/22] Add schema generation test for empty aspects --- test/resources/edge-cases/srv/empty-aspect.cds | 10 ++++++++++ test/resources/models.json | 4 ++++ 2 files changed, 14 insertions(+) create mode 100644 test/resources/edge-cases/srv/empty-aspect.cds diff --git a/test/resources/edge-cases/srv/empty-aspect.cds b/test/resources/edge-cases/srv/empty-aspect.cds new file mode 100644 index 00000000..7cc499bc --- /dev/null +++ b/test/resources/edge-cases/srv/empty-aspect.cds @@ -0,0 +1,10 @@ +@graphql +service EmptyAspectService { + entity Root { + key ID : UUID; + emptyAspect : Composition of EmptyAspect; + emptyAspects : Composition of many EmptyAspect; + } + + aspect EmptyAspect {} +} diff --git a/test/resources/models.json b/test/resources/models.json index c3a9326a..8326a9c3 100644 --- a/test/resources/models.json +++ b/test/resources/models.json @@ -26,5 +26,9 @@ { "name": "edge-cases/fields-with-connection-names", "files": ["./edge-cases/srv/fields-with-connection-names.cds"] + }, + { + "name": "edge-cases/empty-aspect", + "files": ["./edge-cases/srv/empty-aspect.cds"] } ] From cc403298e839b79292020f1a811b7215b3317587 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 16 Jun 2025 16:36:16 +0200 Subject: [PATCH 03/22] Fix schema generation for empty aspects (one and many) --- lib/schema/args/input.js | 7 ++- lib/schema/query.js | 1 + lib/schema/types/object.js | 24 +++++++--- test/schemas/edge-cases/empty-aspect.gql | 56 ++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 test/schemas/edge-cases/empty-aspect.gql diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 8d631ef1..592fea9c 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -22,7 +22,10 @@ module.exports = cache => { } // fields is empty if update input object is generated for an entity that only contains key elements - if (Object.keys(fields).length === 0) return + if (!Object.keys(fields).length) { + cache.delete(entityName) + return + } return newEntityInputObjectType } @@ -38,7 +41,7 @@ module.exports = cache => { if (element.isAssociation || element.isComposition) { // Input objects in deep updates overwrite previous entries with new entries and therefore always act as create input objects const type = gqlScalarType || entityToInputObjectType(element._target, false) - return element.is2one ? type : new GraphQLList(type) + if (type) return element.is2one ? type : new GraphQLList(type) } else if (gqlScalarType) { return gqlScalarType } diff --git a/lib/schema/query.js b/lib/schema/query.js index a3a96f89..b7813711 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -30,6 +30,7 @@ module.exports = cache => { // REVISIT: requires differentiation for support of configurable schema flavors const type = objectGenerator(cache).entityToObjectConnectionType(entity) + if (!type) continue const args = argsGenerator(cache).generateArgumentsForType(entity) fields[gqlName(key)] = { type, args } diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index 55ded662..7df8c03c 100644 --- a/lib/schema/types/object.js +++ b/lib/schema/types/object.js @@ -16,7 +16,13 @@ module.exports = cache => { const newEntityObjectConnectionType = new GraphQLObjectType({ name, fields: () => fields }) cache.set(name, newEntityObjectConnectionType) - fields[CONNECTION_FIELDS.nodes] = { type: new GraphQLList(entityToObjectType(entity)) } + const objectType = entityToObjectType(entity) + if (!objectType) { + cache.delete(name) + return + } + + fields[CONNECTION_FIELDS.nodes] = { type: new GraphQLList(objectType) } fields[CONNECTION_FIELDS.totalCount] = { type: GraphQLInt } return newEntityObjectConnectionType @@ -41,11 +47,17 @@ module.exports = cache => { // REVISIT: requires differentiation for support of configurable schema flavors const type = element.is2many ? _elementToObjectConnectionType(element) : _elementToObjectType(element) - if (type) { - const field = { type, description: element.doc } - if (element.is2many) field.args = argsGenerator(cache).generateArgumentsForType(element) - fields[gqlName(name)] = field - } + if (!type) continue + + const field = { type, description: element.doc } + if (element.is2many) field.args = argsGenerator(cache).generateArgumentsForType(element) + fields[gqlName(name)] = field + } + + // fields is empty e.g. for empty aspects + if (!Object.keys(fields).length) { + cache.delete(entityName) + return } return newEntityObjectType diff --git a/test/schemas/edge-cases/empty-aspect.gql b/test/schemas/edge-cases/empty-aspect.gql new file mode 100644 index 00000000..b146f410 --- /dev/null +++ b/test/schemas/edge-cases/empty-aspect.gql @@ -0,0 +1,56 @@ +type EmptyAspectService { + Root(filter: [EmptyAspectService_Root_filter], orderBy: [EmptyAspectService_Root_orderBy], skip: Int, top: Int): EmptyAspectService_Root_connection +} + +type EmptyAspectService_Root { + ID: ID +} + +input EmptyAspectService_Root_C { + ID: ID +} + +type EmptyAspectService_Root_connection { + nodes: [EmptyAspectService_Root] + totalCount: Int +} + +input EmptyAspectService_Root_filter { + ID: [ID_filter] +} + +type EmptyAspectService_Root_input { + create(input: [EmptyAspectService_Root_C]!): [EmptyAspectService_Root] + delete(filter: [EmptyAspectService_Root_filter]!): Int +} + +input EmptyAspectService_Root_orderBy { + ID: SortDirection +} + +type EmptyAspectService_input { + Root: EmptyAspectService_Root_input +} + +input ID_filter { + eq: ID + ge: ID + gt: ID + in: [ID] + le: ID + lt: ID + ne: [ID] +} + +type Mutation { + EmptyAspectService: EmptyAspectService_input +} + +type Query { + EmptyAspectService: EmptyAspectService +} + +enum SortDirection { + asc + desc +} \ No newline at end of file From 6dfb6edff87c741847bd7add435be811970da8fe Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 16 Jun 2025 16:36:38 +0200 Subject: [PATCH 04/22] Move up early return --- lib/schema/args/filter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/schema/args/filter.js b/lib/schema/args/filter.js index 6367872c..647481f8 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -25,11 +25,12 @@ const { module.exports = cache => { const generateFilterForEntity = entity => { + if (!hasScalarFields(entity)) return + const filterName = gqlName(entity.name) + '_filter' const cachedFilterInputType = cache.get(filterName) if (cachedFilterInputType) return cachedFilterInputType - if (!hasScalarFields(entity)) return const fields = {} const newFilterInputType = new GraphQLList(new GraphQLInputObjectType({ name: filterName, fields: () => fields })) From 577dd0cc8bbf84019ebdb3cea31306936f6fe227 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 16 Jun 2025 16:55:25 +0200 Subject: [PATCH 05/22] Simpler array length check syntax --- lib/schema/mutation.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index 636d3ee5..9edb3234 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -18,7 +18,7 @@ module.exports = cache => { if (type) fields[serviceName] = { type, resolve } } - if (Object.keys(fields).length === 0) return + if (!Object.keys(fields).length) return return new GraphQLObjectType({ name: 'Mutation', fields }) } @@ -36,7 +36,7 @@ module.exports = cache => { if (type) fields[entityName] = { type } } - if (Object.keys(fields).length === 0) return + if (!Object.keys(fields).length) return return new GraphQLObjectType({ name: gqlName(service.name) + '_input', fields }) } @@ -51,7 +51,7 @@ module.exports = cache => { }).filter(([_, v]) => v) ) - if (Object.keys(fields).length === 0) return + if (!Object.keys(fields).length) return return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) } From 7019c57b0d633cec4ac1ab8c5310cd590e6f0b1c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 16 Jun 2025 19:00:46 +0200 Subject: [PATCH 06/22] Omit services without entities and empty services --- lib/schema/query.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/schema/query.js b/lib/schema/query.js index b7813711..15484284 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -11,10 +11,9 @@ module.exports = cache => { for (const key in services) { const service = services[key] const serviceName = gqlName(service.name) - fields[serviceName] = { - type: _serviceToObjectType(service), - resolve: resolvers[serviceName] - } + const type = _serviceToObjectType(service) + if (!type) continue + fields[serviceName] = { type, resolve: resolvers[serviceName] } } return new GraphQLObjectType({ name: 'Query', fields }) @@ -36,6 +35,8 @@ module.exports = cache => { fields[gqlName(key)] = { type, args } } + if (!Object.keys(fields).length) return + return new GraphQLObjectType({ name: gqlName(service.name), // REVISIT: Passed services currently don't directly contain doc property From eb96e6a18fe78c51d972bf2e7a72345c52a9a15c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 17:22:32 +0200 Subject: [PATCH 07/22] Add `GraphQLVoid` scalar type and add `_empty` field with type `Void` to `Query` type that would otherwise be empty --- lib/resolvers/field.js | 2 +- lib/schema/query.js | 3 +++ lib/schema/types/custom/GraphQLVoid.js | 15 +++++++++++++++ lib/schema/types/custom/index.js | 3 ++- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 lib/schema/types/custom/GraphQLVoid.js diff --git a/lib/resolvers/field.js b/lib/resolvers/field.js index 67131f31..54d5bf51 100644 --- a/lib/resolvers/field.js +++ b/lib/resolvers/field.js @@ -3,7 +3,7 @@ const { isObjectType, isIntrospectionType } = require('graphql') // The GraphQL.js defaultFieldResolver does not support returning aliased values that resolve to fields with aliases function aliasFieldResolver(source, args, contextValue, info) { const responseKey = info.fieldNodes[0].alias ? info.fieldNodes[0].alias.value : info.fieldName - return source[responseKey] + return source?.[responseKey] } const registerAliasFieldResolvers = schema => { diff --git a/lib/schema/query.js b/lib/schema/query.js index 15484284..c7de76fd 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -3,6 +3,7 @@ const { gqlName } = require('../utils') const objectGenerator = require('./types/object') const argsGenerator = require('./args') const { isCompositionOfAspect } = require('./util') +const { GraphQLVoid } = require('./types/custom') module.exports = cache => { const generateQueryObjectType = (services, resolvers) => { @@ -16,6 +17,8 @@ module.exports = cache => { fields[serviceName] = { type, resolve: resolvers[serviceName] } } + if (!Object.keys(fields).length) fields._empty = { type: GraphQLVoid } + return new GraphQLObjectType({ name: 'Query', fields }) } diff --git a/lib/schema/types/custom/GraphQLVoid.js b/lib/schema/types/custom/GraphQLVoid.js new file mode 100644 index 00000000..772ba7ab --- /dev/null +++ b/lib/schema/types/custom/GraphQLVoid.js @@ -0,0 +1,15 @@ +const { GraphQLScalarType } = require('graphql') + +const serialize = () => null + +const parseValue = () => null + +const parseLiteral = () => null + +module.exports = new GraphQLScalarType({ + name: 'Void', + description: 'The `Void` scalar type represents the absence of a value. Void can only represent the value `null`.', + serialize, + parseValue, + parseLiteral +}) diff --git a/lib/schema/types/custom/index.js b/lib/schema/types/custom/index.js index bc22a4fd..ec92cc9d 100644 --- a/lib/schema/types/custom/index.js +++ b/lib/schema/types/custom/index.js @@ -7,5 +7,6 @@ module.exports = { GraphQLInt64: require('./GraphQLInt64'), GraphQLTime: require('./GraphQLTime'), GraphQLTimestamp: require('./GraphQLTimestamp'), - GraphQLUInt8: require('./GraphQLUInt8') + GraphQLUInt8: require('./GraphQLUInt8'), + GraphQLVoid: require('./GraphQLVoid') } From 9980bbabb13f12c066176071adc3be909aba7f5f Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 17:26:14 +0200 Subject: [PATCH 08/22] Add tests for empty service and empty entity definitions --- .../resources/edge-cases/srv/empty-entity.cds | 8 +++ .../edge-cases/srv/empty-service.cds | 2 + test/resources/models.json | 8 +++ test/schemas/edge-cases/empty-entity.gql | 56 +++++++++++++++++++ test/schemas/edge-cases/empty-service.gql | 8 +++ 5 files changed, 82 insertions(+) create mode 100644 test/resources/edge-cases/srv/empty-entity.cds create mode 100644 test/resources/edge-cases/srv/empty-service.cds create mode 100644 test/schemas/edge-cases/empty-entity.gql create mode 100644 test/schemas/edge-cases/empty-service.gql diff --git a/test/resources/edge-cases/srv/empty-entity.cds b/test/resources/edge-cases/srv/empty-entity.cds new file mode 100644 index 00000000..3374209b --- /dev/null +++ b/test/resources/edge-cases/srv/empty-entity.cds @@ -0,0 +1,8 @@ +@graphql +service EmptyEntityService { + entity NonEmptyEntity { + key ID : UUID; + } + + entity EmptyEntity {} +} diff --git a/test/resources/edge-cases/srv/empty-service.cds b/test/resources/edge-cases/srv/empty-service.cds new file mode 100644 index 00000000..934f524e --- /dev/null +++ b/test/resources/edge-cases/srv/empty-service.cds @@ -0,0 +1,2 @@ +@graphql +service EmptyService {} diff --git a/test/resources/models.json b/test/resources/models.json index 8326a9c3..9c2a1d0d 100644 --- a/test/resources/models.json +++ b/test/resources/models.json @@ -27,6 +27,14 @@ "name": "edge-cases/fields-with-connection-names", "files": ["./edge-cases/srv/fields-with-connection-names.cds"] }, + { + "name": "edge-cases/empty-service", + "files": ["./edge-cases/srv/empty-service.cds"] + }, + { + "name": "edge-cases/empty-entity", + "files": ["./edge-cases/srv/empty-entity.cds"] + }, { "name": "edge-cases/empty-aspect", "files": ["./edge-cases/srv/empty-aspect.cds"] diff --git a/test/schemas/edge-cases/empty-entity.gql b/test/schemas/edge-cases/empty-entity.gql new file mode 100644 index 00000000..52d8f883 --- /dev/null +++ b/test/schemas/edge-cases/empty-entity.gql @@ -0,0 +1,56 @@ +type EmptyEntityService { + NonEmptyEntity(filter: [EmptyEntityService_NonEmptyEntity_filter], orderBy: [EmptyEntityService_NonEmptyEntity_orderBy], skip: Int, top: Int): EmptyEntityService_NonEmptyEntity_connection +} + +type EmptyEntityService_NonEmptyEntity { + ID: ID +} + +input EmptyEntityService_NonEmptyEntity_C { + ID: ID +} + +type EmptyEntityService_NonEmptyEntity_connection { + nodes: [EmptyEntityService_NonEmptyEntity] + totalCount: Int +} + +input EmptyEntityService_NonEmptyEntity_filter { + ID: [ID_filter] +} + +type EmptyEntityService_NonEmptyEntity_input { + create(input: [EmptyEntityService_NonEmptyEntity_C]!): [EmptyEntityService_NonEmptyEntity] + delete(filter: [EmptyEntityService_NonEmptyEntity_filter]!): Int +} + +input EmptyEntityService_NonEmptyEntity_orderBy { + ID: SortDirection +} + +type EmptyEntityService_input { + NonEmptyEntity: EmptyEntityService_NonEmptyEntity_input +} + +input ID_filter { + eq: ID + ge: ID + gt: ID + in: [ID] + le: ID + lt: ID + ne: [ID] +} + +type Mutation { + EmptyEntityService: EmptyEntityService_input +} + +type Query { + EmptyEntityService: EmptyEntityService +} + +enum SortDirection { + asc + desc +} \ No newline at end of file diff --git a/test/schemas/edge-cases/empty-service.gql b/test/schemas/edge-cases/empty-service.gql new file mode 100644 index 00000000..74007070 --- /dev/null +++ b/test/schemas/edge-cases/empty-service.gql @@ -0,0 +1,8 @@ +type Query { + _empty: Void +} + +""" +The `Void` scalar type represents the absence of a value. Void can only represent the value `null`. +""" +scalar Void \ No newline at end of file From 8cc339a4284ca51162fc221af67b136639199223 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 18:00:10 +0200 Subject: [PATCH 09/22] Move empty csn definition schema tests to own test project --- test/resources/empty-csn-definitions/package.json | 8 ++++++++ .../srv/empty-aspect.cds | 0 .../srv/empty-entity.cds | 0 .../srv/empty-service.cds | 0 test/resources/models.json | 12 ++++++------ .../aspect.gql} | 0 .../entity.gql} | 0 .../service.gql} | 0 8 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 test/resources/empty-csn-definitions/package.json rename test/resources/{edge-cases => empty-csn-definitions}/srv/empty-aspect.cds (100%) rename test/resources/{edge-cases => empty-csn-definitions}/srv/empty-entity.cds (100%) rename test/resources/{edge-cases => empty-csn-definitions}/srv/empty-service.cds (100%) rename test/schemas/{edge-cases/empty-aspect.gql => empty-csn-definitions/aspect.gql} (100%) rename test/schemas/{edge-cases/empty-entity.gql => empty-csn-definitions/entity.gql} (100%) rename test/schemas/{edge-cases/empty-service.gql => empty-csn-definitions/service.gql} (100%) diff --git a/test/resources/empty-csn-definitions/package.json b/test/resources/empty-csn-definitions/package.json new file mode 100644 index 00000000..768a96c1 --- /dev/null +++ b/test/resources/empty-csn-definitions/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@cap-js/graphql": "*" + }, + "devDependencies": { + "@cap-js/sqlite": "*" + } +} diff --git a/test/resources/edge-cases/srv/empty-aspect.cds b/test/resources/empty-csn-definitions/srv/empty-aspect.cds similarity index 100% rename from test/resources/edge-cases/srv/empty-aspect.cds rename to test/resources/empty-csn-definitions/srv/empty-aspect.cds diff --git a/test/resources/edge-cases/srv/empty-entity.cds b/test/resources/empty-csn-definitions/srv/empty-entity.cds similarity index 100% rename from test/resources/edge-cases/srv/empty-entity.cds rename to test/resources/empty-csn-definitions/srv/empty-entity.cds diff --git a/test/resources/edge-cases/srv/empty-service.cds b/test/resources/empty-csn-definitions/srv/empty-service.cds similarity index 100% rename from test/resources/edge-cases/srv/empty-service.cds rename to test/resources/empty-csn-definitions/srv/empty-service.cds diff --git a/test/resources/models.json b/test/resources/models.json index 9c2a1d0d..d54c9b95 100644 --- a/test/resources/models.json +++ b/test/resources/models.json @@ -28,15 +28,15 @@ "files": ["./edge-cases/srv/fields-with-connection-names.cds"] }, { - "name": "edge-cases/empty-service", - "files": ["./edge-cases/srv/empty-service.cds"] + "name": "empty-csn-definitions/service", + "files": ["./empty-csn-definitions/srv/empty-service.cds"] }, { - "name": "edge-cases/empty-entity", - "files": ["./edge-cases/srv/empty-entity.cds"] + "name": "empty-csn-definitions/entity", + "files": ["./empty-csn-definitions/srv/empty-entity.cds"] }, { - "name": "edge-cases/empty-aspect", - "files": ["./edge-cases/srv/empty-aspect.cds"] + "name": "empty-csn-definitions/aspect", + "files": ["./empty-csn-definitions/srv/empty-aspect.cds"] } ] diff --git a/test/schemas/edge-cases/empty-aspect.gql b/test/schemas/empty-csn-definitions/aspect.gql similarity index 100% rename from test/schemas/edge-cases/empty-aspect.gql rename to test/schemas/empty-csn-definitions/aspect.gql diff --git a/test/schemas/edge-cases/empty-entity.gql b/test/schemas/empty-csn-definitions/entity.gql similarity index 100% rename from test/schemas/edge-cases/empty-entity.gql rename to test/schemas/empty-csn-definitions/entity.gql diff --git a/test/schemas/edge-cases/empty-service.gql b/test/schemas/empty-csn-definitions/service.gql similarity index 100% rename from test/schemas/edge-cases/empty-service.gql rename to test/schemas/empty-csn-definitions/service.gql From 98844e492dae2a4663e102b5a8772493523950bf Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 18:33:23 +0200 Subject: [PATCH 10/22] Add comment explaining `_empty` field --- lib/schema/query.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/schema/query.js b/lib/schema/query.js index c7de76fd..e8977f81 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -17,6 +17,7 @@ module.exports = cache => { fields[serviceName] = { type, resolve: resolvers[serviceName] } } + // Empty root query object type is not allowed, so we add a placeholder field if (!Object.keys(fields).length) fields._empty = { type: GraphQLVoid } return new GraphQLObjectType({ name: 'Query', fields }) From 8065a11a3a95d43af01c9202c8ac3a3f2819130c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 18:34:48 +0200 Subject: [PATCH 11/22] Add test testing `_empty` placeholder field of root query type --- test/tests/empty-root-query-type.test.js | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/tests/empty-root-query-type.test.js diff --git a/test/tests/empty-root-query-type.test.js b/test/tests/empty-root-query-type.test.js new file mode 100644 index 00000000..83172ca0 --- /dev/null +++ b/test/tests/empty-root-query-type.test.js @@ -0,0 +1,25 @@ +describe('graphql - empty root query type', () => { + const cds = require('@sap/cds') + const path = require('path') + const { gql } = require('../util') + + const { axios, POST } = cds + .test('serve', 'srv/empty-service.cds') + .in(path.join(__dirname, '../resources/empty-csn-definitions')) + // Prevent axios from throwing errors for non 2xx status codes + axios.defaults.validateStatus = false + + test('_empty placeholder of type Void returns null', async () => { + const query = gql` + { + _empty + } + ` + const data = { + _empty: null + } + + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) +}) From bd00c32dd2de31c4b52af8c3bf63dd0bf1d4896e Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 18:36:48 +0200 Subject: [PATCH 12/22] Add tests for invalid schemas caused by special chars in CSN defs --- test/resources/special-chars/package.json | 8 ++++++ test/resources/special-chars/srv/element.cds | 7 +++++ test/resources/special-chars/srv/entity.cds | 6 +++++ test/resources/special-chars/srv/service.cds | 6 +++++ test/tests/invalid-schema.test.js | 28 ++++++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 test/resources/special-chars/package.json create mode 100644 test/resources/special-chars/srv/element.cds create mode 100644 test/resources/special-chars/srv/entity.cds create mode 100644 test/resources/special-chars/srv/service.cds create mode 100644 test/tests/invalid-schema.test.js diff --git a/test/resources/special-chars/package.json b/test/resources/special-chars/package.json new file mode 100644 index 00000000..768a96c1 --- /dev/null +++ b/test/resources/special-chars/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@cap-js/graphql": "*" + }, + "devDependencies": { + "@cap-js/sqlite": "*" + } +} diff --git a/test/resources/special-chars/srv/element.cds b/test/resources/special-chars/srv/element.cds new file mode 100644 index 00000000..a92a564b --- /dev/null +++ b/test/resources/special-chars/srv/element.cds @@ -0,0 +1,7 @@ +@graphql +service SpecialCharsElementService { + entity Root { + key ID : UUID; + myÄÖÜElement : String; + } +} diff --git a/test/resources/special-chars/srv/entity.cds b/test/resources/special-chars/srv/entity.cds new file mode 100644 index 00000000..231b6c76 --- /dev/null +++ b/test/resources/special-chars/srv/entity.cds @@ -0,0 +1,6 @@ +@graphql +service SpecialCharsEntityService { + entity RootÄÖÜEntity { + key ID : UUID; + } +} diff --git a/test/resources/special-chars/srv/service.cds b/test/resources/special-chars/srv/service.cds new file mode 100644 index 00000000..e5496e99 --- /dev/null +++ b/test/resources/special-chars/srv/service.cds @@ -0,0 +1,6 @@ +@graphql +service SpecialCharsÄÖÜService { + entity Root { + key ID : UUID; + } +} diff --git a/test/tests/invalid-schema.test.js b/test/tests/invalid-schema.test.js new file mode 100644 index 00000000..ef62d795 --- /dev/null +++ b/test/tests/invalid-schema.test.js @@ -0,0 +1,28 @@ +const path = require('path') +// Load @cap-js/graphql plugin to ensure .to.gql and .to.graphql compile targets are registered +require('../../cds-plugin') + +const RES = path.join(__dirname, '../resources') + +describe('graphql - schema generation fails due to incompatibility with modelling', () => { + it('special characters in service name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/service') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/SpecialCharsÄÖÜService/) + }) + + it('special characters in entity name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/entity') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/RootÄÖÜEntity/) + }) + + it('special characters in element name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/element') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/myÄÖÜElement/) + }) +}) From 086f4d312a7f3fe2a3a6ec76438308e99260e4be Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 17 Jun 2025 19:15:12 +0200 Subject: [PATCH 13/22] Add changelog entries --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb07dc0..a9604b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Schema validation after schema generation using the `graphql-js` `validateSchema` function to ensure that the generated schema is valid according to the GraphQL specification. As such, schema generation runs both on server startup and when manually compiling to GraphQL schema. +- Custom scalar type `Void` that is used to denote the absence of a return value. Currently only needed as a return type for placeholder fields. As with all custom scalar types, this type will only appear in the generated schema if it is referenced. + ### Changed ### Fixed +- Improved robustness of schema generation where CDS modelling would could cause empty types to be generated, which are not allowed in GraphQL. These types are omitted from the generated schema and all parent types that are empty as a result are also omitted. Types that are now omitted if empty are entities, aspects and services. If an empty root query type would be generated, a placeholder field `_empty` is added to the root query type to ensure that the schema is valid. This placeholder field has type `Void` which is a custom scalar type that only allows `null` values. + ### Removed ## Version 0.12.0 - 2025-05-05 From f7c3606c2e2b1af39be6bca3a1e18dc051e7791a Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 18 Jun 2025 10:33:16 +0200 Subject: [PATCH 14/22] Rename `_empty` to `_` --- CHANGELOG.md | 2 +- lib/schema/query.js | 2 +- test/schemas/empty-csn-definitions/service.gql | 2 +- test/tests/empty-root-query-type.test.js | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9604b81..a5a130fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Improved robustness of schema generation where CDS modelling would could cause empty types to be generated, which are not allowed in GraphQL. These types are omitted from the generated schema and all parent types that are empty as a result are also omitted. Types that are now omitted if empty are entities, aspects and services. If an empty root query type would be generated, a placeholder field `_empty` is added to the root query type to ensure that the schema is valid. This placeholder field has type `Void` which is a custom scalar type that only allows `null` values. +- Improved robustness of schema generation where CDS modelling would could cause empty types to be generated, which are not allowed in GraphQL. These types are omitted from the generated schema and all parent types that are empty as a result are also omitted. Types that are now omitted if empty are entities, aspects and services. If an empty root query type would be generated, a placeholder field `_` is added to the root query type to ensure that the schema is valid. This placeholder field has type `Void` which is a custom scalar type that only allows `null` values. ### Removed diff --git a/lib/schema/query.js b/lib/schema/query.js index e8977f81..b7155c17 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -18,7 +18,7 @@ module.exports = cache => { } // Empty root query object type is not allowed, so we add a placeholder field - if (!Object.keys(fields).length) fields._empty = { type: GraphQLVoid } + if (!Object.keys(fields).length) fields._ = { type: GraphQLVoid } return new GraphQLObjectType({ name: 'Query', fields }) } diff --git a/test/schemas/empty-csn-definitions/service.gql b/test/schemas/empty-csn-definitions/service.gql index 74007070..bf7a922b 100644 --- a/test/schemas/empty-csn-definitions/service.gql +++ b/test/schemas/empty-csn-definitions/service.gql @@ -1,5 +1,5 @@ type Query { - _empty: Void + _: Void } """ diff --git a/test/tests/empty-root-query-type.test.js b/test/tests/empty-root-query-type.test.js index 83172ca0..42ab39d4 100644 --- a/test/tests/empty-root-query-type.test.js +++ b/test/tests/empty-root-query-type.test.js @@ -9,14 +9,14 @@ describe('graphql - empty root query type', () => { // Prevent axios from throwing errors for non 2xx status codes axios.defaults.validateStatus = false - test('_empty placeholder of type Void returns null', async () => { + test('_ placeholder field of type Void returns null', async () => { const query = gql` { - _empty + _ } ` const data = { - _empty: null + _: null } const response = await POST('/graphql', { query }) From 05fa2e0995ad136eaec1a80f45b71c2e32043a03 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 18 Jun 2025 11:06:56 +0200 Subject: [PATCH 15/22] Shorten phrasing of changelog entries --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a130fe..9855f70b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Schema validation after schema generation using the `graphql-js` `validateSchema` function to ensure that the generated schema is valid according to the GraphQL specification. As such, schema generation runs both on server startup and when manually compiling to GraphQL schema. -- Custom scalar type `Void` that is used to denote the absence of a return value. Currently only needed as a return type for placeholder fields. As with all custom scalar types, this type will only appear in the generated schema if it is referenced. +- Schema validation after schema generation (on server startup or manual compilation) to ensure that the generated schema is valid according to the GraphQL specification +- Scalar type `Void` that represents the absence of a return value and only allows value `null` ### Changed ### Fixed -- Improved robustness of schema generation where CDS modelling would could cause empty types to be generated, which are not allowed in GraphQL. These types are omitted from the generated schema and all parent types that are empty as a result are also omitted. Types that are now omitted if empty are entities, aspects and services. If an empty root query type would be generated, a placeholder field `_` is added to the root query type to ensure that the schema is valid. This placeholder field has type `Void` which is a custom scalar type that only allows `null` values. +- Improved schema generation robustness where specific CDS modelling could cause empty GraphQL types to be generated (which is not allowed) for entities, aspects and services. These types and any resulting empty parent types are omitted from the generated schema. If schema generation would result in an empty query root type, a placeholder field `_` of type `Void` is added to keep the schema valid. ### Removed From 470e9701773180329f9c89e11004e808940c7a3e Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 18 Jun 2025 11:18:31 +0200 Subject: [PATCH 16/22] Rename test suite --- ...ty-root-query-type.test.js => empty-query-root-type.test.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/tests/{empty-root-query-type.test.js => empty-query-root-type.test.js} (90%) diff --git a/test/tests/empty-root-query-type.test.js b/test/tests/empty-query-root-type.test.js similarity index 90% rename from test/tests/empty-root-query-type.test.js rename to test/tests/empty-query-root-type.test.js index 42ab39d4..0b6c5cd7 100644 --- a/test/tests/empty-root-query-type.test.js +++ b/test/tests/empty-query-root-type.test.js @@ -1,4 +1,4 @@ -describe('graphql - empty root query type', () => { +describe('graphql - empty query root operation type', () => { const cds = require('@sap/cds') const path = require('path') const { gql } = require('../util') From d9ce5cb645c0f68c94679236e56264033f23311f Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 18 Jun 2025 11:21:21 +0200 Subject: [PATCH 17/22] Add additional describe block to test suite --- test/tests/invalid-schema.test.js | 38 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/test/tests/invalid-schema.test.js b/test/tests/invalid-schema.test.js index ef62d795..1750fc69 100644 --- a/test/tests/invalid-schema.test.js +++ b/test/tests/invalid-schema.test.js @@ -4,25 +4,27 @@ require('../../cds-plugin') const RES = path.join(__dirname, '../resources') -describe('graphql - schema generation fails due to incompatibility with modelling', () => { - it('special characters in service name', async () => { - const csn = await cds.load(RES + '/special-chars/srv/service') - expect(() => { - cds.compile(csn).to.graphql() - }).toThrow(/SpecialCharsÄÖÜService/) - }) +describe('graphql - schema generation fails due to incompatible modelling', () => { + describe('special characters', () => { + it('in service name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/service') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/SpecialCharsÄÖÜService/) + }) - it('special characters in entity name', async () => { - const csn = await cds.load(RES + '/special-chars/srv/entity') - expect(() => { - cds.compile(csn).to.graphql() - }).toThrow(/RootÄÖÜEntity/) - }) + it('in entity name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/entity') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/RootÄÖÜEntity/) + }) - it('special characters in element name', async () => { - const csn = await cds.load(RES + '/special-chars/srv/element') - expect(() => { - cds.compile(csn).to.graphql() - }).toThrow(/myÄÖÜElement/) + it('in element name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/element') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/myÄÖÜElement/) + }) }) }) From a6553a079eb2f6519f0b7ce3a3decee8b8000e8c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 27 Jun 2025 17:10:48 +0200 Subject: [PATCH 18/22] Log when empty query types are omitted during schema generation --- CHANGELOG.md | 2 +- lib/schema/query.js | 17 ++++++++++++++--- lib/schema/types/object.js | 3 +++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9855f70b..da496b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Improved schema generation robustness where specific CDS modelling could cause empty GraphQL types to be generated (which is not allowed) for entities, aspects and services. These types and any resulting empty parent types are omitted from the generated schema. If schema generation would result in an empty query root type, a placeholder field `_` of type `Void` is added to keep the schema valid. +- Improved schema generation robustness where specific CDS modelling could cause empty GraphQL types to be generated (which is not allowed) for entities, aspects and services. These types and any resulting empty parent types are omitted from the generated schema and a warning is logged for them. If schema generation would result in an empty query root type, a placeholder field `_` of type `Void` is added to keep the schema valid. ### Removed diff --git a/lib/schema/query.js b/lib/schema/query.js index b7155c17..1c98bcae 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -1,3 +1,5 @@ +const cds = require('@sap/cds') +const LOG = cds.log('graphql') const { GraphQLObjectType } = require('graphql') const { gqlName } = require('../utils') const objectGenerator = require('./types/object') @@ -7,6 +9,7 @@ const { GraphQLVoid } = require('./types/custom') module.exports = cache => { const generateQueryObjectType = (services, resolvers) => { + const name = 'Query' const fields = {} for (const key in services) { @@ -18,9 +21,14 @@ module.exports = cache => { } // Empty root query object type is not allowed, so we add a placeholder field - if (!Object.keys(fields).length) fields._ = { type: GraphQLVoid } + if (!Object.keys(fields).length) { + fields._ = { type: GraphQLVoid } + LOG.warn( + `Root query object type "${name}" is empty. A placeholder field has been added to ensure a valid schema.` + ) + } - return new GraphQLObjectType({ name: 'Query', fields }) + return new GraphQLObjectType({ name, fields }) } const _serviceToObjectType = service => { @@ -39,7 +47,10 @@ module.exports = cache => { fields[gqlName(key)] = { type, args } } - if (!Object.keys(fields).length) return + if (!Object.keys(fields).length) { + LOG.warn(`Service "${service.name}" has no fields and has therefore been excluded from the schema.`) + return + } return new GraphQLObjectType({ name: gqlName(service.name), diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index 7df8c03c..3c250fb4 100644 --- a/lib/schema/types/object.js +++ b/lib/schema/types/object.js @@ -1,3 +1,5 @@ +const cds = require('@sap/cds') +const LOG = cds.log('graphql') const { GraphQLObjectType, GraphQLList, GraphQLInt } = require('graphql') const { gqlName } = require('../../utils') const argsGenerator = require('../args') @@ -56,6 +58,7 @@ module.exports = cache => { // fields is empty e.g. for empty aspects if (!Object.keys(fields).length) { + LOG.warn(`Entity "${entity.name}" has no fields and has therefore been excluded from the schema.`) cache.delete(entityName) return } From c06db9972f0e535f773a7d841e4e78f40ac9c499 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 2 Jul 2025 16:18:31 +0200 Subject: [PATCH 19/22] Move up early return due to missing scalar fields --- lib/schema/args/orderBy.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/schema/args/orderBy.js b/lib/schema/args/orderBy.js index bf3afb21..87d699d6 100644 --- a/lib/schema/args/orderBy.js +++ b/lib/schema/args/orderBy.js @@ -5,11 +5,12 @@ const { cdsToGraphQLScalarType } = require('../types/scalar') module.exports = cache => { const generateOrderByForEntity = entity => { + if (!hasScalarFields(entity)) return + const orderByName = gqlName(entity.name) + '_orderBy' const cachedOrderByInputType = cache.get(orderByName) if (cachedOrderByInputType) return cachedOrderByInputType - if (!hasScalarFields(entity)) return const fields = {} const newOrderByInputType = new GraphQLList(new GraphQLInputObjectType({ name: orderByName, fields: () => fields })) From a94e1e2e0110c3141639c906d58b391d2688a9b4 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 2 Jul 2025 16:49:56 +0200 Subject: [PATCH 20/22] Also cache and retrieve empty -> `undefined` types --- lib/schema/args/filter.js | 6 ++---- lib/schema/args/input.js | 5 ++--- lib/schema/args/orderBy.js | 6 ++---- lib/schema/types/object.js | 10 ++++------ 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/schema/args/filter.js b/lib/schema/args/filter.js index 647481f8..8ee621d6 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -29,8 +29,7 @@ module.exports = cache => { const filterName = gqlName(entity.name) + '_filter' - const cachedFilterInputType = cache.get(filterName) - if (cachedFilterInputType) return cachedFilterInputType + if (cache.has(filterName)) return cache.get(filterName) const fields = {} const newFilterInputType = new GraphQLList(new GraphQLInputObjectType({ name: filterName, fields: () => fields })) @@ -85,8 +84,7 @@ module.exports = cache => { const _generateFilterType = (gqlType, operations) => { const filterName = gqlType.name + '_filter' - const cachedFilterType = cache.get(filterName) - if (cachedFilterType) return cachedFilterType + if (cache.has(filterName)) return cache.get(filterName) const ops = operations.map(op => [[op], { type: OPERATOR_LIST_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType }]) const fields = Object.fromEntries(ops) diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 592fea9c..583eb023 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -8,8 +8,7 @@ module.exports = cache => { const suffix = isUpdate ? '_U' : '_C' const entityName = gqlName(entity.name) + suffix - const cachedEntityInputObjectType = cache.get(entityName) - if (cachedEntityInputObjectType) return cachedEntityInputObjectType + if (cache.has(entityName)) return cache.get(entityName) const fields = {} const newEntityInputObjectType = new GraphQLInputObjectType({ name: entityName, fields: () => fields }) @@ -23,7 +22,7 @@ module.exports = cache => { // fields is empty if update input object is generated for an entity that only contains key elements if (!Object.keys(fields).length) { - cache.delete(entityName) + cache.set(entityName) return } diff --git a/lib/schema/args/orderBy.js b/lib/schema/args/orderBy.js index 87d699d6..f826ffbd 100644 --- a/lib/schema/args/orderBy.js +++ b/lib/schema/args/orderBy.js @@ -9,8 +9,7 @@ module.exports = cache => { const orderByName = gqlName(entity.name) + '_orderBy' - const cachedOrderByInputType = cache.get(orderByName) - if (cachedOrderByInputType) return cachedOrderByInputType + if (cache.has(orderByName)) return cache.get(orderByName) const fields = {} const newOrderByInputType = new GraphQLList(new GraphQLInputObjectType({ name: orderByName, fields: () => fields })) @@ -39,8 +38,7 @@ module.exports = cache => { const _generateSortDirectionEnum = () => { const enumName = 'SortDirection' - const cachedSortDirectionEnum = cache.get(enumName) - if (cachedSortDirectionEnum) return cachedSortDirectionEnum + if (cache.has(enumName)) return cache.get(enumName) const newSortDirectionEnum = new GraphQLEnumType({ name: enumName, diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index 3c250fb4..1f799828 100644 --- a/lib/schema/types/object.js +++ b/lib/schema/types/object.js @@ -11,8 +11,7 @@ module.exports = cache => { const entityToObjectConnectionType = entity => { const name = gqlName(entity.name) + '_connection' - const cachedEntityObjectConnectionType = cache.get(name) - if (cachedEntityObjectConnectionType) return cachedEntityObjectConnectionType + if (cache.has(name)) return cache.get(name) const fields = {} const newEntityObjectConnectionType = new GraphQLObjectType({ name, fields: () => fields }) @@ -20,7 +19,7 @@ module.exports = cache => { const objectType = entityToObjectType(entity) if (!objectType) { - cache.delete(name) + cache.set(name) return } @@ -33,8 +32,7 @@ module.exports = cache => { const entityToObjectType = entity => { const entityName = gqlName(entity.name) - const cachedEntityObjectType = cache.get(entityName) - if (cachedEntityObjectType) return cachedEntityObjectType + if (cache.has(entityName)) return cache.get(entityName) const fields = {} const newEntityObjectType = new GraphQLObjectType({ @@ -59,7 +57,7 @@ module.exports = cache => { // fields is empty e.g. for empty aspects if (!Object.keys(fields).length) { LOG.warn(`Entity "${entity.name}" has no fields and has therefore been excluded from the schema.`) - cache.delete(entityName) + cache.set(entityName, undefined) return } From 17d5ce49b6fe14f9da2cc0f334ca457ddb8d3e46 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 2 Jul 2025 16:53:25 +0200 Subject: [PATCH 21/22] Remove `new` prefix from type variable names --- lib/schema/args/filter.js | 12 ++++++------ lib/schema/args/input.js | 6 +++--- lib/schema/args/orderBy.js | 12 ++++++------ lib/schema/types/object.js | 12 ++++++------ 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/schema/args/filter.js b/lib/schema/args/filter.js index 8ee621d6..022bccaf 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -32,8 +32,8 @@ module.exports = cache => { if (cache.has(filterName)) return cache.get(filterName) const fields = {} - const newFilterInputType = new GraphQLList(new GraphQLInputObjectType({ name: filterName, fields: () => fields })) - cache.set(filterName, newFilterInputType) + const filterInputType = new GraphQLList(new GraphQLInputObjectType({ name: filterName, fields: () => fields })) + cache.set(filterName, filterInputType) for (const name in entity.elements) { const element = entity.elements[name] @@ -41,7 +41,7 @@ module.exports = cache => { if (type) fields[gqlName(name)] = { type } } - return newFilterInputType + return filterInputType } const generateFilterForElement = (element, followAssocOrComp) => { @@ -88,10 +88,10 @@ module.exports = cache => { const ops = operations.map(op => [[op], { type: OPERATOR_LIST_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType }]) const fields = Object.fromEntries(ops) - const newFilterType = new GraphQLInputObjectType({ name: filterName, fields }) - cache.set(filterName, newFilterType) + const filterType = new GraphQLInputObjectType({ name: filterName, fields }) + cache.set(filterName, filterType) - return newFilterType + return filterType } return { generateFilterForEntity, generateFilterForElement } diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 583eb023..6c5ed170 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -11,8 +11,8 @@ module.exports = cache => { if (cache.has(entityName)) return cache.get(entityName) const fields = {} - const newEntityInputObjectType = new GraphQLInputObjectType({ name: entityName, fields: () => fields }) - cache.set(entityName, newEntityInputObjectType) + const entityInputObjectType = new GraphQLInputObjectType({ name: entityName, fields: () => fields }) + cache.set(entityName, entityInputObjectType) for (const name in entity.elements) { const element = entity.elements[name] @@ -26,7 +26,7 @@ module.exports = cache => { return } - return newEntityInputObjectType + return entityInputObjectType } const _elementToInputObjectType = (element, isUpdate) => { diff --git a/lib/schema/args/orderBy.js b/lib/schema/args/orderBy.js index f826ffbd..cdd4a6ca 100644 --- a/lib/schema/args/orderBy.js +++ b/lib/schema/args/orderBy.js @@ -12,8 +12,8 @@ module.exports = cache => { if (cache.has(orderByName)) return cache.get(orderByName) const fields = {} - const newOrderByInputType = new GraphQLList(new GraphQLInputObjectType({ name: orderByName, fields: () => fields })) - cache.set(orderByName, newOrderByInputType) + const orderByInputType = new GraphQLList(new GraphQLInputObjectType({ name: orderByName, fields: () => fields })) + cache.set(orderByName, orderByInputType) for (const name in entity.elements) { const element = entity.elements[name] @@ -21,7 +21,7 @@ module.exports = cache => { if (type) fields[gqlName(name)] = { type } } - return newOrderByInputType + return orderByInputType } const generateOrderByForElement = (element, followAssocOrComp) => { @@ -40,13 +40,13 @@ module.exports = cache => { if (cache.has(enumName)) return cache.get(enumName) - const newSortDirectionEnum = new GraphQLEnumType({ + const sortDirectionEnum = new GraphQLEnumType({ name: enumName, values: { asc: { value: 'asc' }, desc: { value: 'desc' } } }) - cache.set(enumName, newSortDirectionEnum) + cache.set(enumName, sortDirectionEnum) - return newSortDirectionEnum + return sortDirectionEnum } return { generateOrderByForEntity, generateOrderByForElement } diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index 1f799828..d5947bac 100644 --- a/lib/schema/types/object.js +++ b/lib/schema/types/object.js @@ -14,8 +14,8 @@ module.exports = cache => { if (cache.has(name)) return cache.get(name) const fields = {} - const newEntityObjectConnectionType = new GraphQLObjectType({ name, fields: () => fields }) - cache.set(name, newEntityObjectConnectionType) + const entityObjectConnectionType = new GraphQLObjectType({ name, fields: () => fields }) + cache.set(name, entityObjectConnectionType) const objectType = entityToObjectType(entity) if (!objectType) { @@ -26,7 +26,7 @@ module.exports = cache => { fields[CONNECTION_FIELDS.nodes] = { type: new GraphQLList(objectType) } fields[CONNECTION_FIELDS.totalCount] = { type: GraphQLInt } - return newEntityObjectConnectionType + return entityObjectConnectionType } const entityToObjectType = entity => { @@ -35,12 +35,12 @@ module.exports = cache => { if (cache.has(entityName)) return cache.get(entityName) const fields = {} - const newEntityObjectType = new GraphQLObjectType({ + const entityObjectType = new GraphQLObjectType({ name: entityName, description: entity.doc, fields: () => fields }) - cache.set(entityName, newEntityObjectType) + cache.set(entityName, entityObjectType) for (const name in entity.elements) { const element = entity.elements[name] @@ -61,7 +61,7 @@ module.exports = cache => { return } - return newEntityObjectType + return entityObjectType } const _elementToObjectType = element => { From bfd3918ec28e7751612a0331b9fe8867ed92cc2d Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 2 Jul 2025 17:34:53 +0200 Subject: [PATCH 22/22] Add test with service that will become empty since it only contains an empty entity --- test/resources/empty-csn-definitions/srv/empty-entity.cds | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/resources/empty-csn-definitions/srv/empty-entity.cds b/test/resources/empty-csn-definitions/srv/empty-entity.cds index 3374209b..77b11a10 100644 --- a/test/resources/empty-csn-definitions/srv/empty-entity.cds +++ b/test/resources/empty-csn-definitions/srv/empty-entity.cds @@ -6,3 +6,8 @@ service EmptyEntityService { entity EmptyEntity {} } + +@graphql +service WillBecomeEmptyService { + entity EmptyEntity {} +}