diff --git a/packages/api/src/routes/request/callRequest.js b/packages/api/src/routes/request/callRequest.js index 931fb04ed2..9765a8545c 100644 --- a/packages/api/src/routes/request/callRequest.js +++ b/packages/api/src/routes/request/callRequest.js @@ -27,9 +27,12 @@ import getRequestConfig from './getRequestConfig.js'; import getRequestResolver from './getRequestResolver.js'; import validateSchemas from './validateSchemas.js'; -async function callRequest(context, { pageId, payload, requestId }) { +async function callRequest(context, { blockId, pageId, payload, requestId }) { const { logger } = context; - logger.debug({ route: 'request', params: { pageId, payload, requestId } }, 'Started request'); + logger.debug( + { route: 'request', params: { blockId, pageId, payload, requestId } }, + 'Started request' + ); const requestConfig = await getRequestConfig(context, { pageId, requestId }); const connectionConfig = await getConnectionConfig(context, { requestConfig }); authorizeRequest(context, { requestConfig }); diff --git a/packages/engine/src/Requests.js b/packages/engine/src/Requests.js index 90a56e6f59..17247c11d3 100644 --- a/packages/engine/src/Requests.js +++ b/packages/engine/src/Requests.js @@ -30,11 +30,11 @@ class Requests { }); } - callRequests({ actions, arrayIndices, event, params } = {}) { + callRequests({ actions, arrayIndices, blockId, event, params } = {}) { if (type.isObject(params) && params.all === true) { return Promise.all( Object.keys(this.requestConfig).map((requestId) => - this.callRequest({ requestId, event, arrayIndices }) + this.callRequest({ arrayIndices, blockId, event, requestId }) ) ); } @@ -43,68 +43,71 @@ class Requests { if (type.isString(params)) requestIds = [params]; if (type.isArray(params)) requestIds = params; - return Promise.all( - requestIds.map((requestId) => this.callRequest({ actions, requestId, event, arrayIndices })) + const requests = requestIds.map((requestId) => + this.callRequest({ actions, requestId, blockId, event, arrayIndices }) ); + this.context._internal.update(); // update to render request reset + return Promise.all(requests); } - callRequest({ actions, arrayIndices, event, requestId }) { - const request = this.requestConfig[requestId]; - if (!request) { + async callRequest({ actions, arrayIndices, blockId, event, requestId }) { + const requestConfig = this.requestConfig[requestId]; + if (!this.context.requests[requestId]) { + this.context.requests[requestId] = []; + } + if (!requestConfig) { const error = new Error(`Configuration Error: Request ${requestId} not defined on page.`); - this.context.requests[requestId] = { + this.context.requests[requestId].unshift({ + blockId: 'block_id', + error, loading: false, + requestId, response: null, - error: [error], - }; - return Promise.reject(error); - } - - if (!this.context.requests[requestId]) { - this.context.requests[requestId] = { - loading: true, - response: null, - error: [], - }; + }); + throw error; } - const { output: payload, errors: parserErrors } = this.context._internal.parser.parse({ actions, event, arrayIndices, - input: request.payload, + input: requestConfig.payload, location: requestId, }); - - // TODO: We are throwing this error differently to the request does not exist error if (parserErrors.length > 0) { throw parserErrors[0]; } - - return this.fetch({ requestId, payload }); + const request = { + blockId, + loading: true, + payload, + requestId, + response: null, + }; + this.context.requests[requestId].unshift(request); + return this.fetch(request); } - async fetch({ requestId, payload }) { - this.context.requests[requestId].loading = true; - + async fetch(request) { + request.loading = true; try { const response = await this.context._internal.lowdefy._internal.callRequest({ + blockId: request.blockId, pageId: this.context.pageId, - payload: serializer.serialize(payload), - requestId, + payload: serializer.serialize(request.payload), + requestId: request.requestId, }); const deserializedResponse = serializer.deserialize( get(response, 'response', { default: null, }) ); - this.context.requests[requestId].response = deserializedResponse; - this.context.requests[requestId].loading = false; + request.response = deserializedResponse; + request.loading = false; this.context._internal.update(); return deserializedResponse; } catch (error) { - this.context.requests[requestId].error.unshift(error); - this.context.requests[requestId].loading = false; + request.error = error; + request.loading = false; this.context._internal.update(); throw error; } diff --git a/packages/engine/src/actions/createGetRequestDetails.test.js b/packages/engine/src/actions/createGetRequestDetails.test.js index 11f857433c..2be0893ce4 100644 --- a/packages/engine/src/actions/createGetRequestDetails.test.js +++ b/packages/engine/src/actions/createGetRequestDetails.test.js @@ -116,11 +116,15 @@ test('getRequestDetails params is true', async () => { }, b: { response: { - req_one: { - error: [], - loading: false, - response: 1, - }, + req_one: [ + { + blockId: 'button', + loading: false, + payload: {}, + requestId: 'req_one', + response: 1, + }, + ], }, index: 1, type: 'Action', @@ -177,11 +181,15 @@ test('getRequestDetails params is req_one', async () => { type: 'Request', }, b: { - response: { - error: [], - loading: false, - response: 1, - }, + response: [ + { + blockId: 'button', + loading: false, + payload: {}, + requestId: 'req_one', + response: 1, + }, + ], index: 1, type: 'Action', }, @@ -366,11 +374,15 @@ test('getRequestDetails params.all is true', async () => { }, b: { response: { - req_one: { - error: [], - loading: false, - response: 1, - }, + req_one: [ + { + blockId: 'button', + loading: false, + payload: {}, + requestId: 'req_one', + response: 1, + }, + ], }, index: 1, type: 'Action', @@ -505,11 +517,15 @@ test('getRequestDetails params.key is req_one', async () => { type: 'Request', }, b: { - response: { - error: [], - loading: false, - response: 1, - }, + response: [ + { + blockId: 'button', + loading: false, + payload: {}, + requestId: 'req_one', + response: 1, + }, + ], index: 1, type: 'Action', }, diff --git a/packages/engine/src/actions/createRequest.js b/packages/engine/src/actions/createRequest.js index f3d3d7a961..eeefcd57ae 100644 --- a/packages/engine/src/actions/createRequest.js +++ b/packages/engine/src/actions/createRequest.js @@ -14,9 +14,15 @@ limitations under the License. */ -function createRequest({ actions, arrayIndices, context, event }) { +function createRequest({ actions, arrayIndices, blockId, context, event }) { return async function request(params) { - return await context._internal.Requests.callRequests({ actions, arrayIndices, event, params }); + return await context._internal.Requests.callRequests({ + actions, + arrayIndices, + blockId, + event, + params, + }); }; } diff --git a/packages/engine/src/actions/createRequest.test.js b/packages/engine/src/actions/createRequest.test.js index f9b2c189b9..fb312c6a6d 100644 --- a/packages/engine/src/actions/createRequest.test.js +++ b/packages/engine/src/actions/createRequest.test.js @@ -101,11 +101,15 @@ test('Request call one request', async () => { }); const button = context._internal.RootBlocks.map['button']; const promise = button.triggerEvent({ name: 'onClick' }); - expect(context.requests.req_one).toEqual({ - error: [], - loading: true, - response: null, - }); + expect(context.requests.req_one).toEqual([ + { + blockId: 'button', + loading: true, + payload: {}, + requestId: 'req_one', + response: null, + }, + ]); const res = await promise; expect(res).toEqual({ blockId: 'button', @@ -137,6 +141,9 @@ test('Request call all requests', async () => { { id: 'req_two', type: 'Fetch', + payload: { + x: 1, + }, }, ], blocks: [ @@ -156,29 +163,49 @@ test('Request call all requests', async () => { const button = context._internal.RootBlocks.map['button']; const promise = button.triggerEvent({ name: 'onClick' }); expect(context.requests).toEqual({ - req_one: { - error: [], - loading: true, - response: null, - }, - req_two: { - error: [], - loading: true, - response: null, - }, + req_one: [ + { + blockId: 'button', + loading: true, + payload: {}, + requestId: 'req_one', + response: null, + }, + ], + req_two: [ + { + blockId: 'button', + loading: true, + payload: { + x: 1, + }, + requestId: 'req_two', + response: null, + }, + ], }); const res = await promise; expect(context.requests).toEqual({ - req_one: { - error: [], - loading: false, - response: 1, - }, - req_two: { - error: [], - loading: false, - response: 2, - }, + req_one: [ + { + blockId: 'button', + loading: false, + payload: {}, + requestId: 'req_one', + response: 1, + }, + ], + req_two: [ + { + blockId: 'button', + loading: false, + payload: { + x: 1, + }, + requestId: 'req_two', + response: 2, + }, + ], }); expect(res).toEqual({ blockId: 'button', @@ -210,6 +237,7 @@ test('Request call array of requests', async () => { { id: 'req_two', type: 'Fetch', + payload: { x: 1 }, }, ], blocks: [ @@ -229,29 +257,49 @@ test('Request call array of requests', async () => { const button = context._internal.RootBlocks.map['button']; const promise = button.triggerEvent({ name: 'onClick' }); expect(context.requests).toEqual({ - req_one: { - error: [], - loading: true, - response: null, - }, - req_two: { - error: [], - loading: true, - response: null, - }, + req_one: [ + { + blockId: 'button', + loading: true, + payload: {}, + requestId: 'req_one', + response: null, + }, + ], + req_two: [ + { + blockId: 'button', + loading: true, + payload: { + x: 1, + }, + requestId: 'req_two', + response: null, + }, + ], }); const res = await promise; expect(context.requests).toEqual({ - req_one: { - error: [], - loading: false, - response: 1, - }, - req_two: { - error: [], - loading: false, - response: 2, - }, + req_one: [ + { + blockId: 'button', + loading: false, + payload: {}, + requestId: 'req_one', + response: 1, + }, + ], + req_two: [ + { + blockId: 'button', + loading: false, + payload: { + x: 1, + }, + requestId: 'req_two', + response: 2, + }, + ], }); expect(res).toEqual({ blockId: 'button', @@ -283,6 +331,7 @@ test('Request pass if params are none', async () => { { id: 'req_two', type: 'Fetch', + payload: { x: 1 }, }, ], blocks: [ @@ -330,11 +379,16 @@ test('Request call request error', async () => { }); const button = context._internal.RootBlocks.map['button']; const res = await button.triggerEvent({ name: 'onClick' }); - expect(context.requests.req_error).toEqual({ - error: [new Error('Request error')], - loading: false, - response: null, - }); + expect(context.requests.req_error).toEqual([ + { + blockId: 'button', + error: new Error('Request error'), + loading: false, + payload: {}, + requestId: 'req_error', + response: null, + }, + ]); expect(res).toEqual({ blockId: 'button', bounced: false, diff --git a/packages/engine/test/Requests.test.js b/packages/engine/test/Requests.test.js index b82e28e0b2..b1f6daf0be 100644 --- a/packages/engine/test/Requests.test.js +++ b/packages/engine/test/Requests.test.js @@ -94,6 +94,7 @@ const arrayIndices = []; const lowdefy = { lowdefyGlobal: { array: ['a', 'b', 'c'] }, }; +const blockId = 'block_id'; // Comment out to use console console.log = () => {}; @@ -111,13 +112,21 @@ test('callRequest', async () => { pageConfig, }); context._internal.lowdefy._internal.callRequest = mockCallRequest; - await context._internal.Requests.callRequest({ requestId: 'req_one' }); + await context._internal.Requests.callRequest({ requestId: 'req_one', blockId }); expect(context.requests).toEqual({ - req_one: { - error: [], - loading: false, - response: 1, - }, + req_one: [ + { + blockId: 'block_id', + loading: false, + response: 1, + requestId: 'req_one', + payload: { + action: null, + arrayIndices: null, + sum: 2, + }, + }, + ], }); }); @@ -130,12 +139,14 @@ test('callRequest, payload operators are evaluated', async () => { }); context._internal.lowdefy._internal.callRequest = mockCallRequest; await context._internal.Requests.callRequest({ + blockId, requestId: 'req_one', event: { event: true }, actions: { action1: 'action1' }, arrayIndices: [1], }); expect(mockCallRequest.mock.calls[0][0]).toEqual({ + blockId: 'block_id', pageId: 'page1', requestId: 'req_one', payload: { @@ -154,50 +165,88 @@ test('callRequests all requests', async () => { pageConfig, }); context._internal.lowdefy._internal.callRequest = mockCallRequest; - const promise = context._internal.Requests.callRequests({ - actions, - arrayIndices, - event, - params: { all: true }, - }); - expect(context.requests).toEqual({ - req_one: { - error: [], - loading: true, - response: null, - }, - req_error: { - error: [], - loading: true, - response: null, - }, - req_two: { - error: [], - loading: true, - response: null, - }, - }); + let before; try { + const promise = context._internal.Requests.callRequests({ + actions, + arrayIndices, + blockId, + event, + params: { all: true }, + }); + before = JSON.parse(JSON.stringify(context.requests)); await promise; } catch (e) { // catch thrown errors } + expect(before).toEqual({ + req_error: [ + { + blockId: 'block_id', + loading: true, + payload: {}, + requestId: 'req_error', + response: null, + }, + ], + req_one: [ + { + blockId: 'block_id', + loading: true, + payload: { + action: null, + arrayIndices: null, + event: {}, + sum: 2, + }, + requestId: 'req_one', + response: null, + }, + ], + req_two: [ + { + blockId: 'block_id', + loading: true, + payload: {}, + requestId: 'req_two', + response: null, + }, + ], + }); expect(context.requests).toEqual({ - req_one: { - error: [], - loading: false, - response: 1, - }, - req_error: { - error: [new Error('mock error')], - loading: false, - response: null, - }, - req_two: { - error: [], - loading: false, - response: 2, - }, + req_error: [ + { + blockId: 'block_id', + error: new Error('mock error'), + loading: false, + payload: {}, + requestId: 'req_error', + response: null, + }, + ], + req_one: [ + { + blockId: 'block_id', + loading: false, + payload: { + action: null, + arrayIndices: null, + event: {}, + sum: 2, + }, + requestId: 'req_one', + response: 1, + }, + ], + req_two: [ + { + blockId: 'block_id', + loading: false, + payload: {}, + requestId: 'req_two', + response: 2, + }, + ], }); expect(mockCallRequest).toHaveBeenCalledTimes(3); }); @@ -212,23 +261,42 @@ test('callRequests', async () => { const promise = context._internal.Requests.callRequests({ actions, arrayIndices, + blockId, event, params: ['req_one'], }); expect(context.requests).toEqual({ - req_one: { - error: [], - loading: true, - response: null, - }, + req_one: [ + { + blockId: 'block_id', + loading: true, + payload: { + action: null, + arrayIndices: null, + event: {}, + sum: 2, + }, + requestId: 'req_one', + response: null, + }, + ], }); await promise; expect(context.requests).toEqual({ - req_one: { - error: [], - loading: false, - response: 1, - }, + req_one: [ + { + blockId: 'block_id', + loading: false, + payload: { + action: null, + arrayIndices: null, + event: {}, + sum: 2, + }, + requestId: 'req_one', + response: 1, + }, + ], }); expect(mockCallRequest).toHaveBeenCalledTimes(1); }); @@ -241,24 +309,42 @@ test('callRequest error', async () => { }); context._internal.lowdefy._internal.callRequest = mockCallRequest; await expect( - context._internal.Requests.callRequest({ requestId: 'req_error' }) + context._internal.Requests.callRequest({ requestId: 'req_error', blockId }) ).rejects.toThrow(); expect(context.requests).toEqual({ - req_error: { - error: [new Error('mock error')], - loading: false, - response: null, - }, + req_error: [ + { + blockId: 'block_id', + error: new Error('mock error'), + loading: false, + payload: {}, + requestId: 'req_error', + response: null, + }, + ], }); await expect( - context._internal.Requests.callRequest({ requestId: 'req_error' }) + context._internal.Requests.callRequest({ requestId: 'req_error', blockId }) ).rejects.toThrow(); expect(context.requests).toEqual({ - req_error: { - error: [new Error('mock error'), new Error('mock error')], - loading: false, - response: null, - }, + req_error: [ + { + blockId: 'block_id', + error: new Error('mock error'), + loading: false, + payload: {}, + requestId: 'req_error', + response: null, + }, + { + blockId: 'block_id', + error: new Error('mock error'), + loading: false, + payload: {}, + requestId: 'req_error', + response: null, + }, + ], }); }); @@ -270,14 +356,18 @@ test('callRequest request does not exist', async () => { }); context._internal.lowdefy._internal.callRequest = mockCallRequest; await expect( - context._internal.Requests.callRequest({ requestId: 'req_does_not_exist' }) + context._internal.Requests.callRequest({ requestId: 'req_does_not_exist', blockId }) ).rejects.toThrow('Configuration Error: Request req_does_not_exist not defined on page.'); expect(context.requests).toEqual({ - req_does_not_exist: { - error: [new Error('Configuration Error: Request req_does_not_exist not defined on page.')], - loading: false, - response: null, - }, + req_does_not_exist: [ + { + blockId: 'block_id', + error: new Error('Configuration Error: Request req_does_not_exist not defined on page.'), + loading: false, + requestId: 'req_does_not_exist', + response: null, + }, + ], }); }); @@ -290,10 +380,28 @@ test('update function should be called', async () => { }); context._internal.lowdefy._internal.callRequest = mockCallRequest; context._internal.update = updateFunction; - await context._internal.Requests.callRequest({ requestId: 'req_one' }); + await context._internal.Requests.callRequest({ requestId: 'req_one', blockId }); expect(updateFunction).toHaveBeenCalledTimes(1); }); +test('update function should be called before all requests are fired and once for every request return', async () => { + const pageConfig = getPageConfig(); + const updateFunction = jest.fn(); + const context = await testContext({ + lowdefy, + pageConfig, + }); + context._internal.update = updateFunction; + await context._internal.Requests.callRequests({ + actions: { params: ['req_one', 'req_two'] }, + arrayIndices, + blockId, + event, + params: { all: true }, + }); + expect(updateFunction).toHaveBeenCalledTimes(3); +}); + test('update function should be called if error', async () => { const pageConfig = getPageConfig(); const updateFunction = jest.fn(); @@ -322,8 +430,119 @@ test('fetch should set call query every time it is called', async () => { context._internal.RootBlocks = { update: jest.fn(), }; - await context._internal.Requests.callRequest({ requestId: 'req_one', onlyNew: true }); + await context._internal.Requests.callRequest({ requestId: 'req_one', onlyNew: true, blockId }); expect(mockCallRequest).toHaveBeenCalledTimes(1); await context._internal.Requests.fetch({ requestId: 'req_one' }); expect(mockCallRequest).toHaveBeenCalledTimes(2); }); + +test('trigger request from event end to end and parse payload', async () => { + const pageConfig = { + id: 'page1', + type: 'Box', + events: { + onInit: [ + { + id: 'init', + type: 'SetState', + params: { + a: 1, + }, + }, + ], + }, + requests: [ + { + id: 'req_one', + type: 'Fetch', + payload: { + _state: true, + }, + }, + { + id: 'req_error', + type: 'Fetch', + }, + { + id: 'req_two', + type: 'Fetch', + }, + ], + blocks: [ + { + id: 'button', + type: 'Button', + events: { + onClick: [ + { + id: 'click', + type: 'Request', + params: ['req_one'], + }, + ], + }, + }, + { + id: 'inc', + type: 'Button', + events: { + onClick: [ + { + id: 'add', + type: 'SetState', + params: { + a: { + _sum: [{ _state: 'a' }, 1], + }, + }, + }, + ], + }, + }, + ], + }; + const context = await testContext({ + lowdefy, + pageConfig, + }); + context._internal.lowdefy._internal.callRequest = mockCallRequest; + const { button, inc } = context._internal.RootBlocks.map; + await button.triggerEvent({ name: 'onClick' }); + expect(context.requests).toEqual({ + req_one: [ + { + blockId: 'button', + loading: false, + payload: { + a: 1, + }, + requestId: 'req_one', + response: 1, + }, + ], + }); + await inc.triggerEvent({ name: 'onClick' }); + await button.triggerEvent({ name: 'onClick' }); + expect(context.requests).toEqual({ + req_one: [ + { + blockId: 'button', + loading: false, + payload: { + a: 2, + }, + requestId: 'req_one', + response: 1, + }, + { + blockId: 'button', + loading: false, + payload: { + a: 1, + }, + requestId: 'req_one', + response: 1, + }, + ], + }); +}); diff --git a/packages/engine/test/getContext.test.js b/packages/engine/test/getContext.test.js index e3f974247e..330c9cbb95 100644 --- a/packages/engine/test/getContext.test.js +++ b/packages/engine/test/getContext.test.js @@ -91,7 +91,6 @@ test('page is required input', async () => { }); test('memoize context and reset', async () => { - const resetContext = { reset: true, setReset: () => {} }; const lowdefy = getLowdefy(); const page = { id: 'pageId', diff --git a/packages/plugins/operators/operators-js/src/operators/client/request.js b/packages/plugins/operators/operators-js/src/operators/client/request.js index 5311709ff3..6dbb44bb8f 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/request.js +++ b/packages/plugins/operators/operators-js/src/operators/client/request.js @@ -26,12 +26,12 @@ function _request({ arrayIndices, params, requests, location }) { } const splitKey = params.split('.'); const [requestId, ...keyParts] = splitKey; - if (requestId in requests && !requests[requestId].loading) { + if (requestId in requests && !requests[requestId][0].loading) { if (splitKey.length === 1) { - return serializer.copy(requests[requestId].response); + return serializer.copy(requests[requestId][0].response); } const key = keyParts.reduce((acc, value) => (acc === '' ? value : acc.concat('.', value)), ''); - return get(requests[requestId].response, applyArrayIndices(arrayIndices, key), { + return get(requests[requestId][0].response, applyArrayIndices(arrayIndices, key), { copy: true, default: null, }); diff --git a/packages/plugins/operators/operators-js/src/operators/client/request.test.js b/packages/plugins/operators/operators-js/src/operators/client/request.test.js index 0d76315ef3..b6ecac14a3 100644 --- a/packages/plugins/operators/operators-js/src/operators/client/request.test.js +++ b/packages/plugins/operators/operators-js/src/operators/client/request.test.js @@ -56,21 +56,27 @@ const context = { eventLog: [{ eventLog: true }], id: 'id', requests: { - arr: { - response: [{ a: 'request a1' }, { a: 'request a2' }], - loading: false, - error: [], - }, - number: { - response: 500, - loading: false, - error: [], - }, - string: { - response: 'request String', - loading: false, - error: [], - }, + arr: [ + { + response: [{ a: 'request a1' }, { a: 'request a2' }], + loading: false, + error: [], + }, + ], + number: [ + { + response: 500, + loading: false, + error: [], + }, + ], + string: [ + { + response: 'request String', + loading: false, + error: [], + }, + ], }, state: { state: true }, }; diff --git a/packages/server-dev/pages/api/request/[pageId]/[requestId].js b/packages/server-dev/pages/api/request/[pageId]/[requestId].js index 8e121733a1..2d464aa13f 100644 --- a/packages/server-dev/pages/api/request/[pageId]/[requestId].js +++ b/packages/server-dev/pages/api/request/[pageId]/[requestId].js @@ -36,9 +36,9 @@ export default async function handler(req, res) { session, }); - const { pageId, requestId } = req.query; + const { blockId, pageId, requestId } = req.query; const { payload } = req.body; - const response = await callRequest(apiContext, { pageId, payload, requestId }); + const response = await callRequest(apiContext, { blockId, pageId, payload, requestId }); res.status(200).json(response); } catch (error) { res.status(500).json({ name: error.name, message: error.message }); diff --git a/packages/server/pages/api/request/[pageId]/[requestId].js b/packages/server/pages/api/request/[pageId]/[requestId].js index 4557b98570..2e204dcad2 100644 --- a/packages/server/pages/api/request/[pageId]/[requestId].js +++ b/packages/server/pages/api/request/[pageId]/[requestId].js @@ -38,9 +38,9 @@ export default async function handler(req, res) { session, }); - const { pageId, requestId } = req.query; + const { blockId, pageId, requestId } = req.query; const { payload } = req.body; - const response = await callRequest(apiContext, { pageId, payload, requestId }); + const response = await callRequest(apiContext, { blockId, pageId, payload, requestId }); res.status(200).json(response); } catch (error) { res.status(500).json({ name: error.name, message: error.message });