Merge pull request #1225 from lowdefy/request-fix

feat(engine): Set request to null and update before calling request.
This commit is contained in:
Sam 2022-06-08 10:13:17 +02:00 committed by GitHub
commit d091a6c5af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 513 additions and 207 deletions

View File

@ -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 });

View File

@ -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) {
const error = new Error(`Configuration Error: Request ${requestId} not defined on page.`);
this.context.requests[requestId] = {
loading: false,
response: null,
error: [error],
};
return Promise.reject(error);
}
async callRequest({ actions, arrayIndices, blockId, event, requestId }) {
const requestConfig = this.requestConfig[requestId];
if (!this.context.requests[requestId]) {
this.context.requests[requestId] = {
loading: true,
response: null,
error: [],
};
this.context.requests[requestId] = [];
}
if (!requestConfig) {
const error = new Error(`Configuration Error: Request ${requestId} not defined on page.`);
this.context.requests[requestId].unshift({
blockId: 'block_id',
error,
loading: false,
requestId,
response: null,
});
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;
}

View File

@ -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',
},

View File

@ -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,
});
};
}

View File

@ -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,

View File

@ -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,
},
],
});
});

View File

@ -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',

View File

@ -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,
});

View File

@ -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 },
};

View File

@ -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 });

View File

@ -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 });