From 3d61c8888f1a09c0eea5799585704a60dcf500fd Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Tue, 28 Mar 2023 18:00:35 +0200 Subject: [PATCH 01/12] Add test for HTTP header access in custom handlers --- test/resources/request/server.js | 6 +++++ test/resources/request/srv/request.cds | 6 +++++ test/resources/request/srv/request.js | 6 +++++ test/tests/request.test.js | 33 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 test/resources/request/server.js create mode 100644 test/resources/request/srv/request.cds create mode 100644 test/resources/request/srv/request.js create mode 100644 test/tests/request.test.js diff --git a/test/resources/request/server.js b/test/resources/request/server.js new file mode 100644 index 00000000..6808b2bb --- /dev/null +++ b/test/resources/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/request/srv/request.cds b/test/resources/request/srv/request.cds new file mode 100644 index 00000000..e1706516 --- /dev/null +++ b/test/resources/request/srv/request.cds @@ -0,0 +1,6 @@ +service RequestService { + entity A { + key id : UUID; + my_header : String; + } +} diff --git a/test/resources/request/srv/request.js b/test/resources/request/srv/request.js new file mode 100644 index 00000000..6e0c9fd0 --- /dev/null +++ b/test/resources/request/srv/request.js @@ -0,0 +1,6 @@ +module.exports = srv => { + srv.on('READ', 'A', req => { + const { my_header } = req.headers + return [{ id: 'df81ea80-bbff-479a-bc25-8eb16efbfaec', my_header }] + }) +} \ 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..9c40d4c3 --- /dev/null +++ b/test/tests/request.test.js @@ -0,0 +1,33 @@ +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/request')) + // Prevent axios from throwing errors for non 2xx status codes + axios.defaults.validateStatus = false + + test('HTTP headers are correctly passed to custom handlers', async () => { + const query = gql` + { + RequestService { + A { + nodes { + my_header + } + } + } + } + ` + const my_header = 'my header value' + const data = { + RequestService: { + A: { + nodes: [{ my_header }] + } + } + } + const response = await POST('/graphql', { query }, { headers: { my_header } }) + expect(response.data).toEqual({ data }) + }) +}) From 8b431fb4c0654f709b3bf630a52bc6c3f2c49a9e Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 29 Mar 2023 09:53:40 +0200 Subject: [PATCH 02/12] Add cds.request custom handler tests for CUD --- test/resources/request/srv/request.js | 7 +- test/tests/request.test.js | 102 ++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/test/resources/request/srv/request.js b/test/resources/request/srv/request.js index 6e0c9fd0..df2b04bb 100644 --- a/test/resources/request/srv/request.js +++ b/test/resources/request/srv/request.js @@ -1,6 +1,11 @@ module.exports = srv => { - srv.on('READ', 'A', req => { + srv.on(['CREATE', 'READ', 'UPDATE'], 'A', req => { const { my_header } = req.headers return [{ id: 'df81ea80-bbff-479a-bc25-8eb16efbfaec', my_header }] }) + + srv.on('DELETE', 'A', req => { + 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 index 9c40d4c3..aa66ff37 100644 --- a/test/tests/request.test.js +++ b/test/tests/request.test.js @@ -7,27 +7,97 @@ describe('graphql - cds.request', () => { // Prevent axios from throwing errors for non 2xx status codes axios.defaults.validateStatus = false - test('HTTP headers are correctly passed to custom handlers', async () => { - const query = gql` - { - RequestService { - A { - nodes { - my_header + describe('HTTP 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 my_header = 'my header value' - const data = { - RequestService: { - A: { - nodes: [{ 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 }) + const response = await POST('/graphql', { query }, { headers: { my_header } }) + expect(response.data).toEqual({ data }) + }) }) }) From 89b2b5ac96e0f1c67a56546a48ea34084bd97069 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 29 Mar 2023 14:17:54 +0200 Subject: [PATCH 03/12] Use cds.Request and srv.dispatch instead of srv.run --- lib/index.js | 2 +- lib/resolvers/crud/create.js | 6 ++++-- lib/resolvers/crud/delete.js | 6 ++++-- lib/resolvers/crud/read.js | 6 ++++-- lib/resolvers/crud/update.js | 10 +++++++--- lib/resolvers/mutation.js | 4 ++-- lib/resolvers/root.js | 2 +- test/tests/queries/variables.test.js | 2 +- 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/index.js b/lib/index.js index 3b439e6a..5502d857 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 }, ...options })(req, res)) return router } diff --git a/lib/resolvers/crud/create.js b/lib/resolvers/crud/create.js index 3b98a451..fa050ec7 100644 --- a/lib/resolvers/crud/create.js +++ b/lib/resolvers/crud/create.js @@ -1,17 +1,19 @@ +const cds = require('@sap/cds') const { INSERT } = require('@sap/cds/lib').ql const { ARGS } = require('../../constants') const formatResult = require('../parse/ast/result') const { getArgumentByName, astToEntries } = require('../parse/ast2cqn') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (service, entity, selection) => { +module.exports = async (context, 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 req = new cds.Request({ req: context.req, query }) + const result = await service.dispatch(req) const resultInArray = Array.isArray(result) ? result : [result] return formatResult(selection, resultInArray, true) diff --git a/lib/resolvers/crud/delete.js b/lib/resolvers/crud/delete.js index db235697..18d17729 100644 --- a/lib/resolvers/crud/delete.js +++ b/lib/resolvers/crud/delete.js @@ -1,8 +1,9 @@ +const cds = require('@sap/cds') const { DELETE } = require('@sap/cds/lib').ql const { ARGS } = require('../../constants') const { getArgumentByName, astToWhere } = require('../parse/ast2cqn') -module.exports = async (service, entity, selection) => { +module.exports = async (context, service, entity, selection) => { let query = DELETE.from(entity) const filter = getArgumentByName(selection.arguments, ARGS.filter) @@ -10,7 +11,8 @@ module.exports = async (service, entity, selection) => { let result try { - result = await service.run(query) + const req = new cds.Request({ req: context.req, query }) + result = await service.dispatch(req) } 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 2a009fb1..69869bea 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -1,9 +1,10 @@ +const cds = require('@sap/cds') const { SELECT } = require('@sap/cds/lib').ql const { ARGS, CONNECTION_FIELDS } = require('../../constants') const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } = require('../parse/ast2cqn') const formatResult = require('../parse/ast/result') -module.exports = async (service, entity, selection) => { +module.exports = async (context, service, entity, selection) => { const selections = selection.selectionSet.selections const args = selection.arguments @@ -25,7 +26,8 @@ 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 req = new cds.Request({ req: context.req, query }) + const result = await service.dispatch(req) return formatResult(selection, result) } diff --git a/lib/resolvers/crud/update.js b/lib/resolvers/crud/update.js index c9ea4f15..1f0b5a1d 100644 --- a/lib/resolvers/crud/update.js +++ b/lib/resolvers/crud/update.js @@ -1,10 +1,11 @@ const { SELECT, UPDATE } = require('@sap/cds/lib').ql +const cds = require('@sap/cds') const { ARGS } = require('../../constants') const formatResult = require('../parse/ast/result') const { getArgumentByName, astToColumns, astToWhere, astToEntries } = require('../parse/ast2cqn') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (service, entity, selection) => { +module.exports = async (context, service, entity, selection) => { const args = selection.arguments const filter = getArgumentByName(args, ARGS.filter) @@ -25,9 +26,12 @@ 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) + const reqBeforeUpdate = new cds.Request({ req: context.req, query: queryBeforeUpdate }) + resultBeforeUpdate = await tx.dispatch(reqBeforeUpdate) if (resultBeforeUpdate.length === 0) return [] - return tx.run(query) + + const req = new cds.Request({ req: context.req, query }) + return await tx.dispatch(req) }) // Merge selected fields with updated data 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/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 6f1e0714..91bfbf94 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -515,7 +515,7 @@ describe('graphql - variables', () => { { ID: 251, title: 'The Raven' }, { ID: 252, title: 'Eleonora' }, { ID: 271, title: 'Catweazle' } - ] + ] } } } From 5032e354061656706414b333ed82980c2384941c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 29 Mar 2023 14:25:00 +0200 Subject: [PATCH 04/12] Pass req directly instead of wrapped in context --- lib/resolvers/crud/create.js | 5 ++--- lib/resolvers/crud/delete.js | 5 ++--- lib/resolvers/crud/read.js | 5 ++--- lib/resolvers/crud/update.js | 8 +++----- lib/resolvers/mutation.js | 4 ++-- lib/resolvers/root.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/resolvers/crud/create.js b/lib/resolvers/crud/create.js index fa050ec7..9640b440 100644 --- a/lib/resolvers/crud/create.js +++ b/lib/resolvers/crud/create.js @@ -5,15 +5,14 @@ const formatResult = require('../parse/ast/result') const { getArgumentByName, astToEntries } = require('../parse/ast2cqn') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (context, service, entity, selection) => { +module.exports = async (req, 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 req = new cds.Request({ req: context.req, query }) - const result = await service.dispatch(req) + const result = await service.dispatch(new cds.Request({ req, query })) const resultInArray = Array.isArray(result) ? result : [result] return formatResult(selection, resultInArray, true) diff --git a/lib/resolvers/crud/delete.js b/lib/resolvers/crud/delete.js index 18d17729..2b9b2742 100644 --- a/lib/resolvers/crud/delete.js +++ b/lib/resolvers/crud/delete.js @@ -3,7 +3,7 @@ const { DELETE } = require('@sap/cds/lib').ql const { ARGS } = require('../../constants') const { getArgumentByName, astToWhere } = require('../parse/ast2cqn') -module.exports = async (context, service, entity, selection) => { +module.exports = async (req, service, entity, selection) => { let query = DELETE.from(entity) const filter = getArgumentByName(selection.arguments, ARGS.filter) @@ -11,8 +11,7 @@ module.exports = async (context, service, entity, selection) => { let result try { - const req = new cds.Request({ req: context.req, query }) - result = await service.dispatch(req) + result = await service.dispatch(new cds.Request({ req, 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 69869bea..c5cf3a64 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -4,7 +4,7 @@ const { ARGS, CONNECTION_FIELDS } = require('../../constants') const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } = require('../parse/ast2cqn') const formatResult = require('../parse/ast/result') -module.exports = async (context, service, entity, selection) => { +module.exports = async (req, service, entity, selection) => { const selections = selection.selectionSet.selections const args = selection.arguments @@ -26,8 +26,7 @@ module.exports = async (context, service, entity, selection) => { if (selections.find(s => s.name.value === CONNECTION_FIELDS.totalCount)) query.SELECT.count = true - const req = new cds.Request({ req: context.req, query }) - const result = await service.dispatch(req) + const result = await service.dispatch(new cds.Request({ req, query })) return formatResult(selection, result) } diff --git a/lib/resolvers/crud/update.js b/lib/resolvers/crud/update.js index 1f0b5a1d..f0a331e5 100644 --- a/lib/resolvers/crud/update.js +++ b/lib/resolvers/crud/update.js @@ -5,7 +5,7 @@ const formatResult = require('../parse/ast/result') const { getArgumentByName, astToColumns, astToWhere, astToEntries } = require('../parse/ast2cqn') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (context, service, entity, selection) => { +module.exports = async (req, service, entity, selection) => { const args = selection.arguments const filter = getArgumentByName(args, ARGS.filter) @@ -26,12 +26,10 @@ module.exports = async (context, 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) - const reqBeforeUpdate = new cds.Request({ req: context.req, query: queryBeforeUpdate }) - resultBeforeUpdate = await tx.dispatch(reqBeforeUpdate) + resultBeforeUpdate = await tx.dispatch(new cds.Request({ req, query: queryBeforeUpdate })) if (resultBeforeUpdate.length === 0) return [] - const req = new cds.Request({ req: context.req, query }) - return await tx.dispatch(req) + return await tx.dispatch(new cds.Request({ req, query })) }) // Merge selected fields with updated data diff --git a/lib/resolvers/mutation.js b/lib/resolvers/mutation.js index d53fe3ac..917d2c49 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 (context, service, entity, field) => { +module.exports = async (req, service, entity, field) => { const response = {} for (const selection of field.selectionSet.selections) { @@ -9,7 +9,7 @@ module.exports = async (context, service, entity, field) => { const responseKey = selection.alias?.value || operation const executeOperation = { create: executeCreate, update: executeUpdate, delete: executeDelete }[operation] - const value = executeOperation(context, service, entity, selection) + const value = executeOperation(req, service, entity, selection) await setResponse(response, responseKey, value) } diff --git a/lib/resolvers/root.js b/lib/resolvers/root.js index ed1192b7..1f6371fa 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(context, service, entity, field) + const value = resolver(context.req, service, entity, field) return { key: responseKey, value } } From a9f883e380c98b4e382c73eeae4947059a8e1639 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 29 Mar 2023 14:41:10 +0200 Subject: [PATCH 05/12] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5923d187..2d802e4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 } } ``` +- Use `cds.Request` when running CDS queries to set HTTP headers for custom handlers ### Changed From 8d5653f79c6b46c992a03be9c39b91af2b735d5a Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 29 Mar 2023 14:49:23 +0200 Subject: [PATCH 06/12] Use `@sap/cds/lib` instead of `@sap/cds` --- lib/resolvers/crud/create.js | 4 ++-- lib/resolvers/crud/delete.js | 4 ++-- lib/resolvers/crud/read.js | 4 ++-- lib/resolvers/crud/update.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/resolvers/crud/create.js b/lib/resolvers/crud/create.js index 9640b440..0ede3f9d 100644 --- a/lib/resolvers/crud/create.js +++ b/lib/resolvers/crud/create.js @@ -1,5 +1,5 @@ -const cds = require('@sap/cds') -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') diff --git a/lib/resolvers/crud/delete.js b/lib/resolvers/crud/delete.js index 2b9b2742..f9e3985d 100644 --- a/lib/resolvers/crud/delete.js +++ b/lib/resolvers/crud/delete.js @@ -1,5 +1,5 @@ -const cds = require('@sap/cds') -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') diff --git a/lib/resolvers/crud/read.js b/lib/resolvers/crud/read.js index c5cf3a64..d70e70cc 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -1,5 +1,5 @@ -const cds = require('@sap/cds') -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') diff --git a/lib/resolvers/crud/update.js b/lib/resolvers/crud/update.js index f0a331e5..f04d98da 100644 --- a/lib/resolvers/crud/update.js +++ b/lib/resolvers/crud/update.js @@ -1,5 +1,5 @@ -const { SELECT, UPDATE } = require('@sap/cds/lib').ql -const cds = require('@sap/cds') +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') From f19072e22ee2c3a1ebf83e7bdc14535d7deb69e7 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Wed, 29 Mar 2023 17:00:55 +0200 Subject: [PATCH 07/12] Rename request test project to cds.Request --- test/resources/{request => cds.Request}/server.js | 0 test/resources/{request => cds.Request}/srv/request.cds | 0 test/resources/{request => cds.Request}/srv/request.js | 0 test/tests/request.test.js | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename test/resources/{request => cds.Request}/server.js (100%) rename test/resources/{request => cds.Request}/srv/request.cds (100%) rename test/resources/{request => cds.Request}/srv/request.js (100%) diff --git a/test/resources/request/server.js b/test/resources/cds.Request/server.js similarity index 100% rename from test/resources/request/server.js rename to test/resources/cds.Request/server.js diff --git a/test/resources/request/srv/request.cds b/test/resources/cds.Request/srv/request.cds similarity index 100% rename from test/resources/request/srv/request.cds rename to test/resources/cds.Request/srv/request.cds diff --git a/test/resources/request/srv/request.js b/test/resources/cds.Request/srv/request.js similarity index 100% rename from test/resources/request/srv/request.js rename to test/resources/cds.Request/srv/request.js diff --git a/test/tests/request.test.js b/test/tests/request.test.js index aa66ff37..b092171e 100644 --- a/test/tests/request.test.js +++ b/test/tests/request.test.js @@ -3,7 +3,7 @@ describe('graphql - cds.request', () => { const path = require('path') const { gql } = require('../util') - const { axios, POST } = cds.test(path.join(__dirname, '../resources/request')) + 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 From 37f92391479e8f6a44f3cd2811d3becbd6e3ba2c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 31 Mar 2023 17:06:39 +0200 Subject: [PATCH 08/12] Add tests for res in custom handlers --- test/resources/cds.Request/srv/request.js | 4 ++ test/tests/request.test.js | 68 ++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/test/resources/cds.Request/srv/request.js b/test/resources/cds.Request/srv/request.js index df2b04bb..27d0ee87 100644 --- a/test/resources/cds.Request/srv/request.js +++ b/test/resources/cds.Request/srv/request.js @@ -1,10 +1,14 @@ 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 }) diff --git a/test/tests/request.test.js b/test/tests/request.test.js index b092171e..07841efd 100644 --- a/test/tests/request.test.js +++ b/test/tests/request.test.js @@ -7,7 +7,7 @@ describe('graphql - cds.request', () => { // Prevent axios from throwing errors for non 2xx status codes axios.defaults.validateStatus = false - describe('HTTP headers are correctly passed to custom handlers', () => { + describe('HTTP request headers are correctly passed to custom handlers', () => { const my_header = 'my header value' test('Create', async () => { @@ -100,4 +100,70 @@ describe('graphql - cds.request', () => { 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 }) + }) + }) }) From 27300c4f2b97400ea2a0a564f44e55f167ec8ddf Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 31 Mar 2023 17:07:50 +0200 Subject: [PATCH 09/12] Add res to context and cds.Request initialization --- lib/index.js | 2 +- lib/resolvers/crud/create.js | 4 ++-- lib/resolvers/crud/delete.js | 4 ++-- lib/resolvers/crud/read.js | 4 ++-- lib/resolvers/crud/update.js | 6 +++--- lib/resolvers/mutation.js | 4 ++-- lib/resolvers/root.js | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5502d857..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((req, res) => createHandler({ schema, context: { req }, ...options })(req, res)) + 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 0ede3f9d..0e3b5879 100644 --- a/lib/resolvers/crud/create.js +++ b/lib/resolvers/crud/create.js @@ -5,14 +5,14 @@ const formatResult = require('../parse/ast/result') const { getArgumentByName, astToEntries } = require('../parse/ast2cqn') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (req, service, entity, selection) => { +module.exports = async (context, 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.dispatch(new cds.Request({ req, query })) + const result = await service.dispatch(new cds.Request({ ...{req, res} = context, query })) const resultInArray = Array.isArray(result) ? result : [result] return formatResult(selection, resultInArray, true) diff --git a/lib/resolvers/crud/delete.js b/lib/resolvers/crud/delete.js index f9e3985d..410988d4 100644 --- a/lib/resolvers/crud/delete.js +++ b/lib/resolvers/crud/delete.js @@ -3,7 +3,7 @@ const { DELETE } = cds.ql const { ARGS } = require('../../constants') const { getArgumentByName, astToWhere } = require('../parse/ast2cqn') -module.exports = async (req, service, entity, selection) => { +module.exports = async (context, service, entity, selection) => { let query = DELETE.from(entity) const filter = getArgumentByName(selection.arguments, ARGS.filter) @@ -11,7 +11,7 @@ module.exports = async (req, service, entity, selection) => { let result try { - result = await service.dispatch(new cds.Request({ req, query })) + result = await service.dispatch(new cds.Request({ ...{req, res} = context, 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 d70e70cc..f376483b 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -4,7 +4,7 @@ const { ARGS, CONNECTION_FIELDS } = require('../../constants') const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } = require('../parse/ast2cqn') const formatResult = require('../parse/ast/result') -module.exports = async (req, service, entity, selection) => { +module.exports = async (context, service, entity, selection) => { const selections = selection.selectionSet.selections const args = selection.arguments @@ -26,7 +26,7 @@ module.exports = async (req, service, entity, selection) => { if (selections.find(s => s.name.value === CONNECTION_FIELDS.totalCount)) query.SELECT.count = true - const result = await service.dispatch(new cds.Request({ req, query })) + const result = await service.dispatch(new cds.Request({ ...{req, res} = context, query })) return formatResult(selection, result) } diff --git a/lib/resolvers/crud/update.js b/lib/resolvers/crud/update.js index f04d98da..dd30ffc5 100644 --- a/lib/resolvers/crud/update.js +++ b/lib/resolvers/crud/update.js @@ -5,7 +5,7 @@ const formatResult = require('../parse/ast/result') const { getArgumentByName, astToColumns, astToWhere, astToEntries } = require('../parse/ast2cqn') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (req, service, entity, selection) => { +module.exports = async (context, service, entity, selection) => { const args = selection.arguments const filter = getArgumentByName(args, ARGS.filter) @@ -26,10 +26,10 @@ module.exports = async (req, 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.dispatch(new cds.Request({ req, query: queryBeforeUpdate })) + resultBeforeUpdate = await tx.dispatch(new cds.Request({ ...{req, res} = context, query: queryBeforeUpdate })) if (resultBeforeUpdate.length === 0) return [] - return await tx.dispatch(new cds.Request({ req, query })) + return await tx.dispatch(new cds.Request({ ...{req, res} = context, query })) }) // Merge selected fields with updated data diff --git a/lib/resolvers/mutation.js b/lib/resolvers/mutation.js index 917d2c49..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 (req, 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 (req, service, entity, field) => { const responseKey = selection.alias?.value || operation const executeOperation = { create: executeCreate, update: executeUpdate, delete: executeDelete }[operation] - const value = executeOperation(req, 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 1f6371fa..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(context.req, service, entity, field) + const value = resolver(context, service, entity, field) return { key: responseKey, value } } From 9bdedc9d469fed56f4156753b725a63b938a9f7f Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 3 Apr 2023 17:17:51 +0200 Subject: [PATCH 10/12] Move changelog entry to latest planned release --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac9cb4f2..c8e5294f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Use `cds.Request` when running CDS queries to set HTTP headers for custom handlers + ### Changed ### Fixed @@ -39,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 } } ``` -- Use `cds.Request` when running CDS queries to set HTTP headers for custom handlers ### Changed From b62eac7eca3e5c12c833ce18c9db1914969e6ea4 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 21 Apr 2023 15:54:36 +0200 Subject: [PATCH 11/12] Deconstruct context in function args --- lib/resolvers/crud/create.js | 4 ++-- lib/resolvers/crud/delete.js | 4 ++-- lib/resolvers/crud/read.js | 4 ++-- lib/resolvers/crud/update.js | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/resolvers/crud/create.js b/lib/resolvers/crud/create.js index 17accb81..31c42853 100644 --- a/lib/resolvers/crud/create.js +++ b/lib/resolvers/crud/create.js @@ -6,14 +6,14 @@ const { getArgumentByName, astToEntries } = require('../parse/ast2cqn') const { isPlainObject } = require('../utils') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (context, 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.dispatch(new cds.Request({ ...{req, res} = context, 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 9d808e5e..98de99dd 100644 --- a/lib/resolvers/crud/delete.js +++ b/lib/resolvers/crud/delete.js @@ -4,7 +4,7 @@ const { ARGS } = require('../../constants') const { getArgumentByName, astToWhere } = require('../parse/ast2cqn') const { isPlainObject } = require('../utils') -module.exports = async (context, service, entity, selection) => { +module.exports = async ({ req, res }, service, entity, selection) => { let query = DELETE.from(entity) const filter = getArgumentByName(selection.arguments, ARGS.filter) @@ -12,7 +12,7 @@ module.exports = async (context, service, entity, selection) => { let result try { - result = await service.dispatch(new cds.Request({ ...{req, res} = context, 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 591a8b61..65aea11e 100644 --- a/lib/resolvers/crud/read.js +++ b/lib/resolvers/crud/read.js @@ -5,7 +5,7 @@ const { getArgumentByName, astToColumns, astToWhere, astToOrderBy, astToLimit } const formatResult = require('../parse/ast/result') const { isPlainObject } = require('../utils') -module.exports = async (context, service, entity, selection) => { +module.exports = async ({ req, res }, service, entity, selection) => { const selections = selection.selectionSet.selections const args = selection.arguments @@ -27,7 +27,7 @@ module.exports = async (context, service, entity, selection) => { if (selections.find(s => s.name.value === CONNECTION_FIELDS.totalCount)) query.SELECT.count = true - const result = await service.dispatch(new cds.Request({ ...{req, res} = context, 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 515196f3..325d43d7 100644 --- a/lib/resolvers/crud/update.js +++ b/lib/resolvers/crud/update.js @@ -6,7 +6,7 @@ const { getArgumentByName, astToColumns, astToWhere, astToEntries } = require('. const { isPlainObject } = require('../utils') const { entriesStructureToEntityStructure } = require('./utils') -module.exports = async (context, service, entity, selection) => { +module.exports = async ({ req, res }, service, entity, selection) => { const args = selection.arguments const filter = getArgumentByName(args, ARGS.filter) @@ -27,10 +27,10 @@ module.exports = async (context, 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.dispatch(new cds.Request({ ...{req, res} = context, query: queryBeforeUpdate })) + resultBeforeUpdate = await tx.dispatch(new cds.Request({ req, res, query: queryBeforeUpdate })) if (resultBeforeUpdate.length === 0) return {} - return await tx.dispatch(new cds.Request({ ...{req, res} = context, query })) + return await tx.dispatch(new cds.Request({ req, res, query })) }) let mergedResults = result From dca4bec0db425be7afb575a7adc1a4cb65ac5fa4 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Fri, 21 Apr 2023 17:11:18 +0200 Subject: [PATCH 12/12] Improve changelog entry --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ebb820..7f30d2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Use `cds.Request` when running CDS queries to set HTTP headers for custom handlers - ### Changed - Improved consistency of handling results of different types returned by custom handlers in CRUD resolvers: @@ -20,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