diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb07dc0..da496b00 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 (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 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 ## Version 0.12.0 - 2025-05-05 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/args/filter.js b/lib/schema/args/filter.js index 6367872c..022bccaf 100644 --- a/lib/schema/args/filter.js +++ b/lib/schema/args/filter.js @@ -25,15 +25,15 @@ 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 + 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) => { @@ -84,15 +84,14 @@ 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) - 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 8d631ef1..6c5ed170 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -8,12 +8,11 @@ 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 }) - 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] @@ -22,9 +21,12 @@ 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.set(entityName) + return + } - return newEntityInputObjectType + return entityInputObjectType } const _elementToInputObjectType = (element, isUpdate) => { @@ -38,7 +40,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/args/orderBy.js b/lib/schema/args/orderBy.js index bf3afb21..cdd4a6ca 100644 --- a/lib/schema/args/orderBy.js +++ b/lib/schema/args/orderBy.js @@ -5,15 +5,15 @@ 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 + 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) => { @@ -38,16 +38,15 @@ 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({ + 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/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 } 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 }) } diff --git a/lib/schema/query.js b/lib/schema/query.js index a3a96f89..1c98bcae 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -1,23 +1,34 @@ +const cds = require('@sap/cds') +const LOG = cds.log('graphql') const { GraphQLObjectType } = require('graphql') 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) => { + const name = 'Query' const fields = {} 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 }) + // Empty root query object type is not allowed, so we add a placeholder field + 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, fields }) } const _serviceToObjectType = service => { @@ -30,11 +41,17 @@ 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 } } + 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), // REVISIT: Passed services currently don't directly contain doc property 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') } diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index 55ded662..d5947bac 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') @@ -9,46 +11,57 @@ 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 }) - cache.set(name, newEntityObjectConnectionType) + const entityObjectConnectionType = new GraphQLObjectType({ name, fields: () => fields }) + cache.set(name, entityObjectConnectionType) - fields[CONNECTION_FIELDS.nodes] = { type: new GraphQLList(entityToObjectType(entity)) } + const objectType = entityToObjectType(entity) + if (!objectType) { + cache.set(name) + return + } + + fields[CONNECTION_FIELDS.nodes] = { type: new GraphQLList(objectType) } fields[CONNECTION_FIELDS.totalCount] = { type: GraphQLInt } - return newEntityObjectConnectionType + return entityObjectConnectionType } 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({ + 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] // 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) { + LOG.warn(`Entity "${entity.name}" has no fields and has therefore been excluded from the schema.`) + cache.set(entityName, undefined) + return } - return newEntityObjectType + return entityObjectType } const _elementToObjectType = element => { 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/empty-csn-definitions/srv/empty-aspect.cds b/test/resources/empty-csn-definitions/srv/empty-aspect.cds new file mode 100644 index 00000000..7cc499bc --- /dev/null +++ b/test/resources/empty-csn-definitions/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/empty-csn-definitions/srv/empty-entity.cds b/test/resources/empty-csn-definitions/srv/empty-entity.cds new file mode 100644 index 00000000..77b11a10 --- /dev/null +++ b/test/resources/empty-csn-definitions/srv/empty-entity.cds @@ -0,0 +1,13 @@ +@graphql +service EmptyEntityService { + entity NonEmptyEntity { + key ID : UUID; + } + + entity EmptyEntity {} +} + +@graphql +service WillBecomeEmptyService { + entity EmptyEntity {} +} diff --git a/test/resources/empty-csn-definitions/srv/empty-service.cds b/test/resources/empty-csn-definitions/srv/empty-service.cds new file mode 100644 index 00000000..934f524e --- /dev/null +++ b/test/resources/empty-csn-definitions/srv/empty-service.cds @@ -0,0 +1,2 @@ +@graphql +service EmptyService {} diff --git a/test/resources/models.json b/test/resources/models.json index c3a9326a..d54c9b95 100644 --- a/test/resources/models.json +++ b/test/resources/models.json @@ -26,5 +26,17 @@ { "name": "edge-cases/fields-with-connection-names", "files": ["./edge-cases/srv/fields-with-connection-names.cds"] + }, + { + "name": "empty-csn-definitions/service", + "files": ["./empty-csn-definitions/srv/empty-service.cds"] + }, + { + "name": "empty-csn-definitions/entity", + "files": ["./empty-csn-definitions/srv/empty-entity.cds"] + }, + { + "name": "empty-csn-definitions/aspect", + "files": ["./empty-csn-definitions/srv/empty-aspect.cds"] } ] 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/schemas/empty-csn-definitions/aspect.gql b/test/schemas/empty-csn-definitions/aspect.gql new file mode 100644 index 00000000..b146f410 --- /dev/null +++ b/test/schemas/empty-csn-definitions/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 diff --git a/test/schemas/empty-csn-definitions/entity.gql b/test/schemas/empty-csn-definitions/entity.gql new file mode 100644 index 00000000..52d8f883 --- /dev/null +++ b/test/schemas/empty-csn-definitions/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/empty-csn-definitions/service.gql b/test/schemas/empty-csn-definitions/service.gql new file mode 100644 index 00000000..bf7a922b --- /dev/null +++ b/test/schemas/empty-csn-definitions/service.gql @@ -0,0 +1,8 @@ +type Query { + _: 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 diff --git a/test/tests/empty-query-root-type.test.js b/test/tests/empty-query-root-type.test.js new file mode 100644 index 00000000..0b6c5cd7 --- /dev/null +++ b/test/tests/empty-query-root-type.test.js @@ -0,0 +1,25 @@ +describe('graphql - empty query root operation 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('_ placeholder field of type Void returns null', async () => { + const query = gql` + { + _ + } + ` + const data = { + _: null + } + + const response = await POST('/graphql', { query }) + expect(response.data).toEqual({ data }) + }) +}) diff --git a/test/tests/invalid-schema.test.js b/test/tests/invalid-schema.test.js new file mode 100644 index 00000000..1750fc69 --- /dev/null +++ b/test/tests/invalid-schema.test.js @@ -0,0 +1,30 @@ +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 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('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 element name', async () => { + const csn = await cds.load(RES + '/special-chars/srv/element') + expect(() => { + cds.compile(csn).to.graphql() + }).toThrow(/myÄÖÜElement/) + }) + }) +})