diff --git a/CHANGELOG.md b/CHANGELOG.md index 61001a71..7f30d2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/index.js b/lib/index.js index 3b439e6a..a76b31c6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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 } diff --git a/lib/resolvers/crud/create.js b/lib/resolvers/crud/create.js index 25557d96..31c42853 100644 --- a/lib/resolvers/crud/create.js +++ b/lib/resolvers/crud/create.js @@ -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) diff --git a/lib/resolvers/crud/delete.js b/lib/resolvers/crud/delete.js index b24a80b7..98de99dd 100644 --- a/lib/resolvers/crud/delete.js +++ b/lib/resolvers/crud/delete.js @@ -1,9 +1,10 @@ -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) @@ -11,7 +12,7 @@ module.exports = async (service, entity, selection) => { 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 diff --git a/lib/resolvers/crud/read.js b/lib/resolvers/crud/read.js index a3c338df..65aea11e 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -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 @@ -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 diff --git a/lib/resolvers/crud/update.js b/lib/resolvers/crud/update.js index 90031aeb..325d43d7 100644 --- a/lib/resolvers/crud/update.js +++ b/lib/resolvers/crud/update.js @@ -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) @@ -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 diff --git a/lib/resolvers/mutation.js b/lib/resolvers/mutation.js index 312cb146..d53fe3ac 100644 --- a/lib/resolvers/mutation.js +++ b/lib/resolvers/mutation.js @@ -1,7 +1,7 @@ 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) { @@ -9,7 +9,7 @@ module.exports = async (service, entity, field) => { 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) } diff --git a/lib/resolvers/root.js b/lib/resolvers/root.js index 35d4bfe9..ed1192b7 100644 --- a/lib/resolvers/root.js +++ b/lib/resolvers/root.js @@ -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 } } diff --git a/test/resources/cds.Request/server.js b/test/resources/cds.Request/server.js new file mode 100644 index 00000000..6808b2bb --- /dev/null +++ b/test/resources/cds.Request/server.js @@ -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') } +} \ No newline at end of file diff --git a/test/resources/cds.Request/srv/request.cds b/test/resources/cds.Request/srv/request.cds new file mode 100644 index 00000000..e1706516 --- /dev/null +++ b/test/resources/cds.Request/srv/request.cds @@ -0,0 +1,6 @@ +service RequestService { + entity A { + key id : UUID; + my_header : String; + } +} diff --git a/test/resources/cds.Request/srv/request.js b/test/resources/cds.Request/srv/request.js new file mode 100644 index 00000000..27d0ee87 --- /dev/null +++ b/test/resources/cds.Request/srv/request.js @@ -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 + }) +} \ No newline at end of file diff --git a/test/tests/request.test.js b/test/tests/request.test.js new file mode 100644 index 00000000..07841efd --- /dev/null +++ b/test/tests/request.test.js @@ -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 }) + }) + }) +})