Skip to content

Commit

Permalink
feat: only compile services with GraphQL protocol annotations (#145)
Browse files Browse the repository at this point in the history
* Only compile services annotated with GraphQL protocol

* Shorter services definition

* Add schema test for compiling with protocol annotations

* Add test for non-GraphQL protocol annotation

* Make protocol check case insensitive

* Regenerate test schemas

* Add changelog entry

* Directly check annotations instead of `srv.endpoints`

* Add test for `@protocol: 'none'`

* Fix expect regex

* Annotate non GraphQL service with `odata` instead of `odata-v4`
  • Loading branch information
schwma authored Jan 30, 2024
1 parent 58b9db2 commit aa66bf2
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 22 additions & 1 deletion lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 14 additions & 0 deletions test/resources/annotations/srv/protocols.cds
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@protocol: ['graphql']
service CompositionOfAspectService {
entity Books {
key id : UUID;
Expand Down
4 changes: 4 additions & 0 deletions test/resources/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
],
"requires_cds": ">=6.8.0"
},
{
"name": "annotations",
"files": ["./annotations/srv/protocols.cds"]
},
{
"name": "types",
"files": ["./types/srv/types.cds"]
Expand Down
164 changes: 164 additions & 0 deletions test/schemas/annotations.gql
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions test/tests/annotations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
{
Expand Down

0 comments on commit aa66bf2

Please sign in to comment.