Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: only compile services with GraphQL protocol annotations #145

Merged
merged 15 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading