Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Aligned `cds.Request` instantiation with other protocols for more consistent usage in custom handlers

### Removed

## Version 0.4.1 - 2023-03-29
Expand Down
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function GraphQLAdapter(services, options) {
if (options.graphiql) router.get('/', graphiql)

const schema = generateSchema4(services)
router.use(createHandler({ schema, ...options }))
router.use((req, res) => createHandler({ schema, context: { req, res }, ...options })(req, res))

return router
}
Expand Down
7 changes: 4 additions & 3 deletions lib/resolvers/crud/create.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
const { INSERT } = require('@sap/cds/lib').ql
const cds = require('@sap/cds/lib')
const { INSERT } = cds.ql
const { ARGS } = require('../../constants')
const formatResult = require('../parse/ast/result')
const { getArgumentByName, astToEntries } = require('../parse/ast2cqn')
const { isPlainObject } = require('../utils')
const { entriesStructureToEntityStructure } = require('./utils')

module.exports = async (service, entity, selection) => {
module.exports = async ({ req, res }, service, entity, selection) => {
let query = INSERT.into(entity)

const input = getArgumentByName(selection.arguments, ARGS.input)
const entries = entriesStructureToEntityStructure(service, entity, astToEntries(input))
query.entries(entries)

const result = await service.run(query)
const result = await service.dispatch(new cds.Request({ req, res, query }))
const resultInArray = isPlainObject(result) ? [result] : result

return formatResult(selection, resultInArray, true)
Expand Down
7 changes: 4 additions & 3 deletions lib/resolvers/crud/delete.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
const { DELETE } = require('@sap/cds/lib').ql
const cds = require('@sap/cds/lib')
const { DELETE } = cds.ql
const { ARGS } = require('../../constants')
const { getArgumentByName, astToWhere } = require('../parse/ast2cqn')
const { isPlainObject } = require('../utils')

module.exports = async (service, entity, selection) => {
module.exports = async ({ req, res }, service, entity, selection) => {
let query = DELETE.from(entity)

const filter = getArgumentByName(selection.arguments, ARGS.filter)
if (filter) query.where(astToWhere(filter))

let result
try {
result = await service.run(query)
result = await service.dispatch(new cds.Request({ req, res, query }))
} catch (e) {
if (e.code === 404) result = 0
else throw e
Expand Down
7 changes: 4 additions & 3 deletions lib/resolvers/crud/read.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const { SELECT } = require('@sap/cds/lib').ql
const cds = require('@sap/cds/lib')
const { SELECT } = cds.ql
const { ARGS, CONNECTION_FIELDS } = require('../../constants')
const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } = require('../parse/ast2cqn')
const formatResult = require('../parse/ast/result')
const { isPlainObject } = require('../utils')

module.exports = async (service, entity, selection) => {
module.exports = async ({ req, res }, service, entity, selection) => {
const selections = selection.selectionSet.selections
const args = selection.arguments

Expand All @@ -26,7 +27,7 @@ module.exports = async (service, entity, selection) => {

if (selections.find(s => s.name.value === CONNECTION_FIELDS.totalCount)) query.SELECT.count = true

const result = await service.run(query)
const result = await service.dispatch(new cds.Request({ req, res, query }))

const resultInArray = isPlainObject(result) ? [result] : result

Expand Down
10 changes: 6 additions & 4 deletions lib/resolvers/crud/update.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const { SELECT, UPDATE } = require('@sap/cds/lib').ql
const cds = require('@sap/cds/lib')
const { SELECT, UPDATE } = cds.ql
const { ARGS } = require('../../constants')
const formatResult = require('../parse/ast/result')
const { getArgumentByName, astToColumns, astToWhere, astToEntries } = require('../parse/ast2cqn')
const { isPlainObject } = require('../utils')
const { entriesStructureToEntityStructure } = require('./utils')

module.exports = async (service, entity, selection) => {
module.exports = async ({ req, res }, service, entity, selection) => {
const args = selection.arguments

const filter = getArgumentByName(args, ARGS.filter)
Expand All @@ -26,9 +27,10 @@ module.exports = async (service, entity, selection) => {
let resultBeforeUpdate
const result = await service.tx(async tx => {
// read needs to be done before the update, otherwise the where clause might become invalid (case that properties in where clause are updated by the mutation)
resultBeforeUpdate = await tx.run(queryBeforeUpdate)
resultBeforeUpdate = await tx.dispatch(new cds.Request({ req, res, query: queryBeforeUpdate }))
if (resultBeforeUpdate.length === 0) return {}
return tx.run(query)

return await tx.dispatch(new cds.Request({ req, res, query }))
})

let mergedResults = result
Expand Down
4 changes: 2 additions & 2 deletions lib/resolvers/mutation.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
const { executeCreate, executeUpdate, executeDelete } = require('./crud')
const { setResponse } = require('./utils')

module.exports = async (service, entity, field) => {
module.exports = async (context, service, entity, field) => {
const response = {}

for (const selection of field.selectionSet.selections) {
const operation = selection.name.value
const responseKey = selection.alias?.value || operation

const executeOperation = { create: executeCreate, update: executeUpdate, delete: executeDelete }[operation]
const value = executeOperation(service, entity, selection)
const value = executeOperation(context, service, entity, selection)
await setResponse(response, responseKey, value)
}

Expand Down
2 changes: 1 addition & 1 deletion lib/resolvers/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const _wrapResolver = (service, resolver, parallel) =>
const entity = service.entities[fieldName]
const responseKey = field.alias?.value || fieldName

const value = resolver(service, entity, field)
const value = resolver(context, service, entity, field)

return { key: responseKey, value }
}
Expand Down
6 changes: 6 additions & 0 deletions test/resources/cds.Request/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const cds = require('@sap/cds')
const path = require('path')

cds.env.protocols = {
graphql: { path: '/graphql', impl: path.join(__dirname, '../../../index.js') }
}
6 changes: 6 additions & 0 deletions test/resources/cds.Request/srv/request.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
service RequestService {
entity A {
key id : UUID;
my_header : String;
}
}
15 changes: 15 additions & 0 deletions test/resources/cds.Request/srv/request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = srv => {
const my_res_header = 'my res header value'

srv.on(['CREATE', 'READ', 'UPDATE'], 'A', req => {
req.res?.header(Object.keys({my_res_header})[0], my_res_header)
const { my_header } = req.headers
return [{ id: 'df81ea80-bbff-479a-bc25-8eb16efbfaec', my_header }]
})

srv.on('DELETE', 'A', req => {
req.res?.header(Object.keys({my_res_header})[0], my_res_header)
const { my_header } = req.headers
return my_header ? 999 : 0
})
}
169 changes: 169 additions & 0 deletions test/tests/request.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
describe('graphql - cds.request', () => {
const cds = require('@sap/cds/lib')
const path = require('path')
const { gql } = require('../util')

const { axios, POST } = cds.test(path.join(__dirname, '../resources/cds.Request'))
// Prevent axios from throwing errors for non 2xx status codes
axios.defaults.validateStatus = false

describe('HTTP request headers are correctly passed to custom handlers', () => {
const my_header = 'my header value'

test('Create', async () => {
const query = gql`
mutation {
RequestService {
A {
create(input: {}) {
my_header
}
}
}
}
`
const data = {
RequestService: {
A: {
create: [{ my_header }]
}
}
}
const response = await POST('/graphql', { query }, { headers: { my_header } })
expect(response.data).toEqual({ data })
})

test('Read', async () => {
const query = gql`
{
RequestService {
A {
nodes {
my_header
}
}
}
}
`
const data = {
RequestService: {
A: {
nodes: [{ my_header }]
}
}
}
const response = await POST('/graphql', { query }, { headers: { my_header } })
expect(response.data).toEqual({ data })
})

test('Update', async () => {
const query = gql`
mutation {
RequestService {
A {
update(filter: [], input: {}) {
my_header
}
}
}
}
`
const data = {
RequestService: {
A: {
update: [{ my_header }]
}
}
}
const response = await POST('/graphql', { query }, { headers: { my_header } })
expect(response.data).toEqual({ data })
})

test('Delete', async () => {
const query = gql`
mutation {
RequestService {
A {
delete(filter: [])
}
}
}
`
const data = {
RequestService: {
A: {
delete: 999
}
}
}
const response = await POST('/graphql', { query }, { headers: { my_header } })
expect(response.data).toEqual({ data })
})
})

describe('HTTP response headers are correctly set in custom handlers', () => {
const my_res_header = 'my res header value'

test('Create', async () => {
const query = gql`
mutation {
RequestService {
A {
create(input: {}) {
id
}
}
}
}
`
const response = await POST('/graphql', { query })
expect(response.headers).toMatchObject({ my_res_header })
})

test('Read', async () => {
const query = gql`
{
RequestService {
A {
nodes {
id
}
}
}
}
`
const response = await POST('/graphql', { query })
expect(response.headers).toMatchObject({ my_res_header })
})

test('Update', async () => {
const query = gql`
mutation {
RequestService {
A {
update(filter: [], input: {}) {
id
}
}
}
}
`
const response = await POST('/graphql', { query })
expect(response.headers).toMatchObject({ my_res_header })
})

test('Delete', async () => {
const query = gql`
mutation {
RequestService {
A {
delete(filter: [])
}
}
}
`
const response = await POST('/graphql', { query })
expect(response.headers).toMatchObject({ my_res_header })
})
})
})