Skip to content

Commit

Permalink
feat: use serviceinfo to determine services to compile (#178)
Browse files Browse the repository at this point in the history
Use `serviceinfo` to determine services to compile instead of directly
inspecting service definition for `@protocol` or `@graphql` annotations,
since this logic is duplicated in `@sap/cds`.
  • Loading branch information
schwma authored Jul 29, 2024
1 parent a56c0cb commit b659df8
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 40 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Bump required `@sap/cds` version to `>=7.8`
- To improve performance, binary payloads are no longer validated to check if they are properly base64 or base64url encoded
- Bump required `node` version to `^16` due to usage of `Buffer.toString('base64url')`
- Use `cds.compile.to.serviceinfo` to determine if a service should be compiled to GraphQL schema

### Fixed

Expand Down
27 changes: 7 additions & 20 deletions lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,16 @@ 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 model = cds.linked(csn)
const serviceinfo = cds.compile.to.serviceinfo(csn, options)
const services = Object.fromEntries(
m.services
.map(s => [s.name, new cds.ApplicationService(s.name, m)])
model.services
.map(s => [s.name, new cds.ApplicationService(s.name, model)])
// Only compile services with GraphQL endpoints
.filter(([_, service]) => _isServiceAnnotatedWithGraphQL(service))
.filter(([_, service]) =>
serviceinfo.find(s => s.name === service.name)?.endpoints.some(e => e.kind === 'graphql')
)
)

let schema = generateSchema4(services)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"graphql-http": "^1.18.0"
},
"peerDependencies": {
"@sap/cds": ">=7.3"
"@sap/cds": ">=7.8"
},
"devDependencies": {
"@cap-js/graphql": "file:.",
Expand Down
40 changes: 21 additions & 19 deletions test/resources/annotations/srv/protocols.cds
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
service NotAnnotated {
context protocols {
entity A {
key id : UUID;
}
}

service NotAnnotated {
entity A as projection on protocols.A;
}

@protocol: 'none'
service AnnotatedWithAtProtocolNone {
entity A {
key id : UUID;
}
entity A as projection on protocols.A;
}

@protocol: 'odata'
service AnnotatedWithNonGraphQL {
entity A {
key id : UUID;
}
entity A as projection on protocols.A;
}

@graphql
service AnnotatedWithAtGraphQL {
entity A {
key id : UUID;
}
entity A as projection on protocols.A;
}

@protocol: 'graphql'
service AnnotatedWithAtProtocolString {
entity A {
key id : UUID;
}
entity A as projection on protocols.A;
}

@protocol: ['graphql']
service AnnotatedWithAtProtocolStringList {
entity A {
key id : UUID;
}
entity A as projection on protocols.A;
}

@protocol: [{kind: 'graphql'}]
service AnnotatedWithAtProtocolObjectList {
entity A {
key id : UUID;
}
entity A as projection on protocols.A;
}

@protocol: { graphql }
service AnnotatedWithAtProtocolObjectWithKey {
entity A as projection on protocols.A;
}

@protocol: { graphql: 'dummy' }
service AnnotatedWithAtProtocolObjectWithKeyAndValue {
entity A as projection on protocols.A;
}
72 changes: 72 additions & 0 deletions test/schemas/annotations.gql
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,74 @@ type AnnotatedWithAtProtocolObjectList_input {
A: AnnotatedWithAtProtocolObjectList_A_input
}

type AnnotatedWithAtProtocolObjectWithKey {
A(filter: [AnnotatedWithAtProtocolObjectWithKey_A_filter], orderBy: [AnnotatedWithAtProtocolObjectWithKey_A_orderBy], skip: Int, top: Int): AnnotatedWithAtProtocolObjectWithKey_A_connection
}

type AnnotatedWithAtProtocolObjectWithKeyAndValue {
A(filter: [AnnotatedWithAtProtocolObjectWithKeyAndValue_A_filter], orderBy: [AnnotatedWithAtProtocolObjectWithKeyAndValue_A_orderBy], skip: Int, top: Int): AnnotatedWithAtProtocolObjectWithKeyAndValue_A_connection
}

type AnnotatedWithAtProtocolObjectWithKeyAndValue_A {
id: ID
}

input AnnotatedWithAtProtocolObjectWithKeyAndValue_A_C {
id: ID
}

type AnnotatedWithAtProtocolObjectWithKeyAndValue_A_connection {
nodes: [AnnotatedWithAtProtocolObjectWithKeyAndValue_A]
totalCount: Int
}

input AnnotatedWithAtProtocolObjectWithKeyAndValue_A_filter {
id: [ID_filter]
}

type AnnotatedWithAtProtocolObjectWithKeyAndValue_A_input {
create(input: [AnnotatedWithAtProtocolObjectWithKeyAndValue_A_C]!): [AnnotatedWithAtProtocolObjectWithKeyAndValue_A]
delete(filter: [AnnotatedWithAtProtocolObjectWithKeyAndValue_A_filter]!): Int
}

input AnnotatedWithAtProtocolObjectWithKeyAndValue_A_orderBy {
id: SortDirection
}

type AnnotatedWithAtProtocolObjectWithKeyAndValue_input {
A: AnnotatedWithAtProtocolObjectWithKeyAndValue_A_input
}

type AnnotatedWithAtProtocolObjectWithKey_A {
id: ID
}

input AnnotatedWithAtProtocolObjectWithKey_A_C {
id: ID
}

type AnnotatedWithAtProtocolObjectWithKey_A_connection {
nodes: [AnnotatedWithAtProtocolObjectWithKey_A]
totalCount: Int
}

input AnnotatedWithAtProtocolObjectWithKey_A_filter {
id: [ID_filter]
}

type AnnotatedWithAtProtocolObjectWithKey_A_input {
create(input: [AnnotatedWithAtProtocolObjectWithKey_A_C]!): [AnnotatedWithAtProtocolObjectWithKey_A]
delete(filter: [AnnotatedWithAtProtocolObjectWithKey_A_filter]!): Int
}

input AnnotatedWithAtProtocolObjectWithKey_A_orderBy {
id: SortDirection
}

type AnnotatedWithAtProtocolObjectWithKey_input {
A: AnnotatedWithAtProtocolObjectWithKey_A_input
}

type AnnotatedWithAtProtocolString {
A(filter: [AnnotatedWithAtProtocolString_A_filter], orderBy: [AnnotatedWithAtProtocolString_A_orderBy], skip: Int, top: Int): AnnotatedWithAtProtocolString_A_connection
}
Expand Down Expand Up @@ -147,13 +215,17 @@ input ID_filter {
type Mutation {
AnnotatedWithAtGraphQL: AnnotatedWithAtGraphQL_input
AnnotatedWithAtProtocolObjectList: AnnotatedWithAtProtocolObjectList_input
AnnotatedWithAtProtocolObjectWithKey: AnnotatedWithAtProtocolObjectWithKey_input
AnnotatedWithAtProtocolObjectWithKeyAndValue: AnnotatedWithAtProtocolObjectWithKeyAndValue_input
AnnotatedWithAtProtocolString: AnnotatedWithAtProtocolString_input
AnnotatedWithAtProtocolStringList: AnnotatedWithAtProtocolStringList_input
}

type Query {
AnnotatedWithAtGraphQL: AnnotatedWithAtGraphQL
AnnotatedWithAtProtocolObjectList: AnnotatedWithAtProtocolObjectList
AnnotatedWithAtProtocolObjectWithKey: AnnotatedWithAtProtocolObjectWithKey
AnnotatedWithAtProtocolObjectWithKeyAndValue: AnnotatedWithAtProtocolObjectWithKeyAndValue
AnnotatedWithAtProtocolString: AnnotatedWithAtProtocolString
AnnotatedWithAtProtocolStringList: AnnotatedWithAtProtocolStringList
}
Expand Down
32 changes: 32 additions & 0 deletions test/tests/annotations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,37 @@ describe('graphql - annotations', () => {
const response = await POST(path, { query })
expect(response.data).not.toHaveProperty('errors')
})

test('service annotated with "@protocol: { graphql }" is served at configured path', async () => {
const query = gql`
{
AnnotatedWithAtProtocolObjectWithKey {
A {
nodes {
id
}
}
}
}
`
const response = await POST(path, { query })
expect(response.data).not.toHaveProperty('errors')
})

test('service annotated with "@protocol: { graphql: \'dummy\' }" is served at configured path', async () => {
const query = gql`
{
AnnotatedWithAtProtocolObjectWithKeyAndValue {
A {
nodes {
id
}
}
}
}
`
const response = await POST(path, { query })
expect(response.data).not.toHaveProperty('errors')
})
})
})

0 comments on commit b659df8

Please sign in to comment.