Compare commits

..

2 Commits

Author SHA1 Message Date
Ali BARIN
6bafdf5257 refactor(http-client): inherit interceptors from parent instance 2024-07-25 07:52:27 +00:00
Ali BARIN
ade6282013 feat(formatter/text): add regex support in replace transfomer 2024-07-25 07:52:21 +00:00
11 changed files with 174 additions and 259 deletions

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development',
isTest: appEnv === 'test',
isProd: appEnv === 'production',
version: '0.13.0',
version: '0.12.0',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.13.0',
version: '0.12.0',
},
meta: {
count: 1,

View File

@@ -3,11 +3,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
import appConfig from '../config/app.js';
export function createInstance(customConfig = {}, { requestInterceptor, responseErrorInterceptor } = {}) {
const config = {
...axios.defaults,
...customConfig
};
const config = axios.defaults;
const httpProxyUrl = appConfig.httpProxy;
const httpsProxyUrl = appConfig.httpsProxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
@@ -26,7 +22,7 @@ export function createInstance(customConfig = {}, { requestInterceptor, response
config.proxy = false;
}
const instance = axios.create(config);
const axiosWithProxyInstance = axios.create(config);
function shouldSkipProxy(hostname) {
return noProxyHosts.some(noProxyHost => {
@@ -37,7 +33,7 @@ export function createInstance(customConfig = {}, { requestInterceptor, response
/**
* The interceptors are executed in the reverse order they are added.
*/
instance.interceptors.request.use(
axiosWithProxyInstance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname;
@@ -48,25 +44,11 @@ export function createInstance(customConfig = {}, { requestInterceptor, response
return requestConfig;
},
(error) => Promise.reject(error)
undefined,
{ synchronous: true }
);
// not always we have custom request interceptors
if (requestInterceptor) {
instance.interceptors.request.use(
async function customInterceptor(requestConfig) {
let newRequestConfig = requestConfig;
for (const interceptor of requestInterceptor) {
newRequestConfig = await interceptor(newRequestConfig);
}
return newRequestConfig;
}
);
}
instance.interceptors.request.use(
axiosWithProxyInstance.interceptors.request.use(
function removeBaseUrlForAbsoluteUrls(requestConfig) {
/**
* If the URL is an absolute URL, we remove its origin out of the URL
@@ -79,24 +61,12 @@ export function createInstance(customConfig = {}, { requestInterceptor, response
requestConfig.url = url.pathname + url.search;
return requestConfig;
} catch (err) {
} catch {
return requestConfig;
}
},
(error) => Promise.reject(error)
undefined,
{ synchronous: true}
);
// not always we have custom response error interceptor
if (responseErrorInterceptor) {
instance.interceptors.response.use(
(response) => response,
responseErrorInterceptor
);
}
return instance;
}
const defaultInstance = createInstance();
export default defaultInstance;
export default axiosWithProxyInstance;

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
describe('Custom default axios with proxy', () => {
describe('Axios with proxy', () => {
beforeEach(() => {
vi.resetModules();
});
@@ -23,13 +23,7 @@ describe('Custom default axios with proxy', () => {
expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls');
});
it('should throw with invalid url (consisting of path alone)', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
await expect(() => axios('/just-a-path')).rejects.toThrowError('Invalid URL');
});
describe('with skipProxyIfInNoProxy interceptor', () => {
describe('skipProxyIfInNoProxy', () => {
let appConfig, axios;
beforeEach(async() => {
appConfig = (await import('../config/app.js')).default;
@@ -73,7 +67,7 @@ describe('Custom default axios with proxy', () => {
});
});
describe('with removeBaseUrlForAbsoluteUrls interceptor', () => {
describe('removeBaseUrlForAbsoluteUrls', () => {
let axios;
beforeEach(async() => {
axios = (await import('./axios-with-proxy.js')).default;
@@ -122,48 +116,4 @@ describe('Custom default axios with proxy', () => {
expect(interceptedRequestConfig.url).toBe('/path?query=1');
});
});
describe('with extra requestInterceptors', () => {
it('should apply extra request interceptors in the middle', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.test = true;
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
const requestInterceptors = instance.interceptors.request.handlers;
const customInterceptor = requestInterceptors[1].fulfilled;
expect(requestInterceptors.length).toBe(3);
await expect(customInterceptor({})).resolves.toStrictEqual({ test: true });
});
it('should work with a custom interceptor setting a baseURL and a request to path', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.baseURL = 'http://localhost';
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
try {
await instance.get('/just-a-path');
} catch (error) {
expect(error.config.baseURL).toBe('http://localhost');
expect(error.config.url).toBe('/just-a-path');
}
})
});
});

View File

@@ -11,7 +11,6 @@ export default function computeParameters(parameters, executionSteps) {
const computedValue = parts
.map((part) => {
const isVariable = part.match(variableRegExp);
if (isVariable) {
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '');
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
@@ -21,33 +20,18 @@ export default function computeParameters(parameters, executionSteps) {
});
const data = executionStep?.dataOut;
const dataValue = get(data, keyPath);
// Covers both arrays and objects
if (typeof dataValue === 'object') {
return JSON.stringify(dataValue);
}
return dataValue;
}
return part;
}).join('');
})
.join('');
// challenge the input to see if it is stringifies object or array
try {
const parsedValue = JSON.parse(computedValue);
return {
...result,
[key]: parsedValue,
};
} catch (error) {
return {
...result,
[key]: computedValue,
};
}
}
if (Array.isArray(value)) {
return {

View File

@@ -1,8 +1,41 @@
import HttpError from '../../errors/http.js';
import { createInstance } from '../axios-with-proxy.js';
import axios from '../axios-with-proxy.js';
// Mutates the `toInstance` by copying the request interceptors from `fromInstance`
const copyRequestInterceptors = (fromInstance, toInstance) => {
// Copy request interceptors
fromInstance.interceptors.request.forEach(interceptor => {
toInstance.interceptors.request.use(
interceptor.fulfilled,
interceptor.rejected,
{
synchronous: interceptor.synchronous,
runWhen: interceptor.runWhen
}
);
});
}
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
async function interceptResponseError(error) {
const instance = axios.create({
baseURL,
});
// 1. apply the beforeRequest functions from the app
instance.interceptors.request.use((requestConfig) => {
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
return beforeRequestFunc($, newConfig);
}, requestConfig);
return result;
});
// 2. inherit the request inceptors from the parent instance
copyRequestInterceptors(axios, instance);
instance.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error;
// Do not destructure `status` from `error.response` because it might not exist
const status = response?.status;
@@ -25,19 +58,8 @@ export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
}
throw new HttpError(error);
};
const instance = createInstance(
{
baseURL,
},
{
requestInterceptor: beforeRequest.map((originalBeforeRequest) => {
return async (requestConfig) => await originalBeforeRequest($, requestConfig);
}),
responseErrorInterceptor: interceptResponseError,
}
)
);
return instance;
}

View File

@@ -63,8 +63,6 @@ export default async (flowId, request, response) => {
});
if (testRun) {
response.status(204).end();
// in case of testing, we do not process the whole process.
continue;
}
@@ -76,12 +74,6 @@ export default async (flowId, request, response) => {
executionId,
});
if (actionStep.appKey === 'filter' && !actionExecutionStep.dataOut) {
response.status(422).end();
break;
}
if (actionStep.key === 'respondWith' && !response.headersSent) {
const { headers, statusCode, body } = actionExecutionStep.dataOut;

View File

@@ -14,7 +14,7 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
7. Click on the **Select all** and then click on the **Create** button.
8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch.
9. Write any screen name to be displayed in Automatisch.
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatisch.
11. If you are using self-hosted Appwrite project, you can paste the instance url into **Appwrite instance URL** field in Automatisch.
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich.
11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch.
12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL.
13. Start using Appwrite integration with Automatisch!

View File

@@ -1,7 +1,7 @@
---
favicon: /favicons/appwrite.svg
items:
- name: New documents
- name: New documets
desc: Triggers when a new document is created.
---

View File

@@ -3,6 +3,7 @@ import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useFormContext, useWatch } from 'react-hook-form';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
@@ -11,7 +12,6 @@ import AddIcon from '@mui/icons-material/Add';
import useFormatMessage from 'hooks/useFormatMessage';
import InputCreator from 'components/InputCreator';
import { EditorContext } from 'contexts/Editor';
import { Grid } from '@mui/material';
const createGroupItem = () => ({
key: '',
@@ -122,7 +122,7 @@ function FilterConditions(props) {
<React.Fragment>
<Stack sx={{ width: '100%' }} direction="column" spacing={2} mt={2}>
{groups?.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
<>
{groupIndex !== 0 && <Divider />}
<Typography variant="subtitle2" gutterBottom>
@@ -133,16 +133,19 @@ function FilterConditions(props) {
</Typography>
{group?.and?.map((groupItem, groupItemIndex) => (
<Grid container key={`item-${groupItem.id}`}>
<Grid
container
spacing={2}
item
xs={12}
sm={11}
sx={{ order: { xs: 2, sm: 1 } }}
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 2 }}
sx={{ display: 'flex', flex: 1 }}
>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<Grid item xs={12} md={4}>
<InputCreator
schema={createStringArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.key`,
@@ -152,8 +155,15 @@ function FilterConditions(props) {
stepId={stepId}
disabled={editorContext.readOnly}
/>
</Grid>
<Grid item xs={12} md={4}>
</Box>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator
schema={createDropdownArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.operator`,
@@ -164,8 +174,15 @@ function FilterConditions(props) {
stepId={stepId}
disabled={editorContext.readOnly}
/>
</Grid>
<Grid item xs={12} md={4}>
</Box>
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
maxWidth: ['100%', '33%'],
}}
>
<InputCreator
schema={createStringArgument({
key: `or.${groupIndex}.and.${groupItemIndex}.value`,
@@ -175,28 +192,18 @@ function FilterConditions(props) {
stepId={stepId}
disabled={editorContext.readOnly}
/>
</Grid>
</Grid>
<Grid item xs={12} sm={1} sx={{ order: { xs: 1, sm: 2 } }}>
<Stack justifyContent="center" alignItems="flex-end">
</Box>
</Stack>
<IconButton
size="small"
edge="start"
onClick={() =>
removeGroupItem(groupIndex, groupItemIndex)
}
sx={{
width: 40,
height: 40,
mb: { xs: 2, sm: 0 },
}}
disabled={groups.length === 1 && group.and.length === 1}
onClick={() => removeGroupItem(groupIndex, groupItemIndex)}
sx={{ width: 61, height: 61 }}
>
<RemoveIcon />
</IconButton>
</Stack>
</Grid>
</Grid>
))}
<Stack spacing={1} direction="row">
@@ -220,7 +227,7 @@ function FilterConditions(props) {
</IconButton>
)}
</Stack>
</React.Fragment>
</>
))}
</Stack>
</React.Fragment>

View File

@@ -19,13 +19,7 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
const value = joinBy('.', parentKey, index?.toString(), name);
if (Array.isArray(sampleValue)) {
const arrayItself = {
label,
value,
sampleValue: JSON.stringify(sampleValue),
};
const arrayItems = sampleValue.flatMap((item, index) =>
return sampleValue.flatMap((item, index) =>
process({
data: item,
parentKey: value,
@@ -33,9 +27,6 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
parentLabel: label,
}),
);
// TODO: remove spreading
return [arrayItself, ...arrayItems];
}
if (typeof sampleValue === 'object' && sampleValue !== null) {
@@ -45,7 +36,6 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
parentLabel: label,
});
}
return [
{
label,