diff --git a/CHANGELOG.md b/CHANGELOG.md index 9644da52..f9bf64a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@graphiql/plugin-explorer` version to `^1` +- When compiling to GraphQL using the CDS API or CLI, only generate GraphQL schemas for services that are annotated with GraphQL protocol annotations ### Fixed diff --git a/lib/compile.js b/lib/compile.js index 2e34e54b..54d0e502 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -2,9 +2,30 @@ const cds = require('@sap/cds') const { generateSchema4 } = require('./schema') const { lexicographicSortSchema, printSchema } = require('graphql') +const _isServiceAnnotatedWithGraphQL = service => { + const { definition } = service + + if (definition['@graphql']) return true + + const protocol = definition['@protocol'] + if (protocol) { + // @protocol: 'graphql' or @protocol: ['graphql', 'odata'] + const protocols = Array.isArray(protocol) ? protocol : [protocol] + // Normalize objects such as { kind: 'graphql' } to strings + return protocols.map(p => (typeof p === 'object' ? p.kind : p)).some(p => p.match(/graphql/i)) + } + + return false +} + function cds_compile_to_gql(csn, options = {}) { const m = cds.linked(csn) - const services = Object.fromEntries(m.services.map(s => [s.name, new cds.ApplicationService(s.name, m)])) + const services = Object.fromEntries( + m.services + .map(s => [s.name, new cds.ApplicationService(s.name, m)]) + // Only compile services with GraphQL endpoints + .filter(([_, service]) => _isServiceAnnotatedWithGraphQL(service)) + ) let schema = generateSchema4(services) diff --git a/test/resources/annotations/srv/protocols.cds b/test/resources/annotations/srv/protocols.cds index d1b5bcab..c4d0947c 100644 --- a/test/resources/annotations/srv/protocols.cds +++ b/test/resources/annotations/srv/protocols.cds @@ -4,6 +4,20 @@ service NotAnnotated { } } +@protocol: 'none' +service AnnotatedWithAtProtocolNone { + entity A { + key id : UUID; + } +} + +@protocol: 'odata' +service AnnotatedWithNonGraphQL { + entity A { + key id : UUID; + } +} + @graphql service AnnotatedWithAtGraphQL { entity A { diff --git a/test/resources/model-structure/srv/composition-of-aspect.cds b/test/resources/model-structure/srv/composition-of-aspect.cds index bdd37470..1fd8be65 100644 --- a/test/resources/model-structure/srv/composition-of-aspect.cds +++ b/test/resources/model-structure/srv/composition-of-aspect.cds @@ -1,3 +1,4 @@ +@protocol: ['graphql'] service CompositionOfAspectService { entity Books { key id : UUID; diff --git a/test/resources/models.json b/test/resources/models.json index 6eee3046..86fd429b 100644 --- a/test/resources/models.json +++ b/test/resources/models.json @@ -7,6 +7,10 @@ ], "requires_cds": ">=6.8.0" }, + { + "name": "annotations", + "files": ["./annotations/srv/protocols.cds"] + }, { "name": "types", "files": ["./types/srv/types.cds"] diff --git a/test/schemas/annotations.gql b/test/schemas/annotations.gql new file mode 100644 index 00000000..9918197c --- /dev/null +++ b/test/schemas/annotations.gql @@ -0,0 +1,164 @@ +type AnnotatedWithAtGraphQL { + A(filter: [AnnotatedWithAtGraphQL_A_filter], orderBy: [AnnotatedWithAtGraphQL_A_orderBy], skip: Int, top: Int): AnnotatedWithAtGraphQL_A_connection +} + +type AnnotatedWithAtGraphQL_A { + id: ID +} + +input AnnotatedWithAtGraphQL_A_C { + id: ID +} + +type AnnotatedWithAtGraphQL_A_connection { + nodes: [AnnotatedWithAtGraphQL_A] + totalCount: Int +} + +input AnnotatedWithAtGraphQL_A_filter { + id: [ID_filter] +} + +type AnnotatedWithAtGraphQL_A_input { + create(input: [AnnotatedWithAtGraphQL_A_C]!): [AnnotatedWithAtGraphQL_A] + delete(filter: [AnnotatedWithAtGraphQL_A_filter]!): Int +} + +input AnnotatedWithAtGraphQL_A_orderBy { + id: SortDirection +} + +type AnnotatedWithAtGraphQL_input { + A: AnnotatedWithAtGraphQL_A_input +} + +type AnnotatedWithAtProtocolObjectList { + A(filter: [AnnotatedWithAtProtocolObjectList_A_filter], orderBy: [AnnotatedWithAtProtocolObjectList_A_orderBy], skip: Int, top: Int): AnnotatedWithAtProtocolObjectList_A_connection +} + +type AnnotatedWithAtProtocolObjectList_A { + id: ID +} + +input AnnotatedWithAtProtocolObjectList_A_C { + id: ID +} + +type AnnotatedWithAtProtocolObjectList_A_connection { + nodes: [AnnotatedWithAtProtocolObjectList_A] + totalCount: Int +} + +input AnnotatedWithAtProtocolObjectList_A_filter { + id: [ID_filter] +} + +type AnnotatedWithAtProtocolObjectList_A_input { + create(input: [AnnotatedWithAtProtocolObjectList_A_C]!): [AnnotatedWithAtProtocolObjectList_A] + delete(filter: [AnnotatedWithAtProtocolObjectList_A_filter]!): Int +} + +input AnnotatedWithAtProtocolObjectList_A_orderBy { + id: SortDirection +} + +type AnnotatedWithAtProtocolObjectList_input { + A: AnnotatedWithAtProtocolObjectList_A_input +} + +type AnnotatedWithAtProtocolString { + A(filter: [AnnotatedWithAtProtocolString_A_filter], orderBy: [AnnotatedWithAtProtocolString_A_orderBy], skip: Int, top: Int): AnnotatedWithAtProtocolString_A_connection +} + +type AnnotatedWithAtProtocolStringList { + A(filter: [AnnotatedWithAtProtocolStringList_A_filter], orderBy: [AnnotatedWithAtProtocolStringList_A_orderBy], skip: Int, top: Int): AnnotatedWithAtProtocolStringList_A_connection +} + +type AnnotatedWithAtProtocolStringList_A { + id: ID +} + +input AnnotatedWithAtProtocolStringList_A_C { + id: ID +} + +type AnnotatedWithAtProtocolStringList_A_connection { + nodes: [AnnotatedWithAtProtocolStringList_A] + totalCount: Int +} + +input AnnotatedWithAtProtocolStringList_A_filter { + id: [ID_filter] +} + +type AnnotatedWithAtProtocolStringList_A_input { + create(input: [AnnotatedWithAtProtocolStringList_A_C]!): [AnnotatedWithAtProtocolStringList_A] + delete(filter: [AnnotatedWithAtProtocolStringList_A_filter]!): Int +} + +input AnnotatedWithAtProtocolStringList_A_orderBy { + id: SortDirection +} + +type AnnotatedWithAtProtocolStringList_input { + A: AnnotatedWithAtProtocolStringList_A_input +} + +type AnnotatedWithAtProtocolString_A { + id: ID +} + +input AnnotatedWithAtProtocolString_A_C { + id: ID +} + +type AnnotatedWithAtProtocolString_A_connection { + nodes: [AnnotatedWithAtProtocolString_A] + totalCount: Int +} + +input AnnotatedWithAtProtocolString_A_filter { + id: [ID_filter] +} + +type AnnotatedWithAtProtocolString_A_input { + create(input: [AnnotatedWithAtProtocolString_A_C]!): [AnnotatedWithAtProtocolString_A] + delete(filter: [AnnotatedWithAtProtocolString_A_filter]!): Int +} + +input AnnotatedWithAtProtocolString_A_orderBy { + id: SortDirection +} + +type AnnotatedWithAtProtocolString_input { + A: AnnotatedWithAtProtocolString_A_input +} + +input ID_filter { + eq: ID + ge: ID + gt: ID + in: [ID] + le: ID + lt: ID + ne: [ID] +} + +type Mutation { + AnnotatedWithAtGraphQL: AnnotatedWithAtGraphQL_input + AnnotatedWithAtProtocolObjectList: AnnotatedWithAtProtocolObjectList_input + AnnotatedWithAtProtocolString: AnnotatedWithAtProtocolString_input + AnnotatedWithAtProtocolStringList: AnnotatedWithAtProtocolStringList_input +} + +type Query { + AnnotatedWithAtGraphQL: AnnotatedWithAtGraphQL + AnnotatedWithAtProtocolObjectList: AnnotatedWithAtProtocolObjectList + AnnotatedWithAtProtocolString: AnnotatedWithAtProtocolString + AnnotatedWithAtProtocolStringList: AnnotatedWithAtProtocolStringList +} + +enum SortDirection { + asc + desc +} \ No newline at end of file diff --git a/test/tests/annotations.test.js b/test/tests/annotations.test.js index e4cdce73..df1d4806 100644 --- a/test/tests/annotations.test.js +++ b/test/tests/annotations.test.js @@ -26,6 +26,38 @@ describe('graphql - annotations', () => { expect(response.data.errors[0].message).toMatch(/^Cannot query field "NotAnnotated" on type "Query"\./) }) + test('service annotated with "@protocol: \'none\'" is not served', async () => { + const query = gql` + { + AnnotatedWithAtProtocolNone { + A { + nodes { + id + } + } + } + } + ` + const response = await POST(path, { 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 () => { + const query = gql` + { + AnnotatedWithNonGraphQL { + A { + nodes { + id + } + } + } + } + ` + const response = await POST(path, { query }) + expect(response.data.errors[0].message).toMatch(/^Cannot query field "AnnotatedWithNonGraphQL" on type "Query"\./) + }) + test('service annotated with @graphql is served at configured path', async () => { const query = gql` {