Merge pull request #780 from automatisch/generic-webhook

Add webhook integration
This commit is contained in:
Ömer Faruk Aydın
2022-12-08 19:20:52 +01:00
committed by GitHub
20 changed files with 182 additions and 35 deletions

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" width="48px" height="48px"><g id="surface56721297">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,12.156863%,32.156864%);fill-opacity:1;" d="M 35 37 C 32.800781 37 31 35.199219 31 33 C 31 30.800781 32.800781 29 35 29 C 37.199219 29 39 30.800781 39 33 C 39 35.199219 37.199219 37 35 37 Z M 35 37 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,12.156863%,32.156864%);fill-opacity:1;" d="M 35 43 C 32 43 29.101562 41.601562 27.199219 39.300781 L 30.300781 36.800781 C 31.398438 38.199219 33.199219 39.101562 35 39.101562 C 38.300781 39.101562 41 36.398438 41 33.101562 C 41 29.800781 38.300781 27.101562 35 27.101562 C 34 27.101562 33 27.398438 32.101562 27.800781 L 30.398438 28.800781 L 23.300781 16 L 26.800781 14.101562 L 32.101562 23.5 C 33.101562 23.199219 34.101562 23 35.101562 23 C 40.601562 23 45.101562 27.5 45.101562 33 C 45.101562 38.5 40.5 43 35 43 Z M 35 43 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,12.156863%,32.156864%);fill-opacity:1;" d="M 14 43 C 8.5 43 4 38.5 4 33 C 4 28.398438 7.101562 24.5 11.5 23.300781 L 12.5 27.199219 C 9.898438 27.898438 8 30.300781 8 33 C 8 36.300781 10.699219 39 14 39 C 17.300781 39 20 36.300781 20 33 L 20 31 L 35 31 L 35 35 L 23.800781 35 C 22.898438 39.601562 18.800781 43 14 43 Z M 14 43 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,34.901962%,96.862745%);fill-opacity:1;" d="M 14 37 C 11.800781 37 10 35.199219 10 33 C 10 30.800781 11.800781 29 14 29 C 16.199219 29 18 30.800781 18 33 C 18 35.199219 16.199219 37 14 37 Z M 14 37 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,12.156863%,32.156864%);fill-opacity:1;" d="M 25 19 C 22.800781 19 21 17.199219 21 15 C 21 12.800781 22.800781 11 25 11 C 27.199219 11 29 12.800781 29 15 C 29 17.199219 27.199219 19 25 19 Z M 25 19 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,34.901962%,96.862745%);fill-opacity:1;" d="M 15.699219 34 L 12.300781 32 L 18.199219 22.300781 C 16.199219 20.398438 15 17.800781 15 15 C 15 9.5 19.5 5 25 5 C 30.5 5 35 9.5 35 15 C 35 15.898438 34.898438 16.699219 34.699219 17.5 L 30.800781 16.5 C 30.898438 16 31 15.5 31 15 C 31 11.699219 28.300781 9 25 9 C 21.699219 9 19 11.699219 19 15 C 19 17.101562 20.101562 19 21.898438 20.101562 L 23.601562 21.101562 Z M 15.699219 34 "/>
</g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

View File

@@ -0,0 +1,14 @@
import defineApp from '../../helpers/define-app';
import triggers from './triggers';
export default defineApp({
name: 'Webhook',
key: 'webhook',
iconUrl: '{BASE_URL}/apps/webhook/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/webhook/connection',
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '0059F7',
triggers,
});

View File

@@ -0,0 +1,20 @@
import isEmpty from 'lodash/isEmpty';
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'Catch raw webhook',
key: 'catchRawWebhook',
type: 'webhook',
description: 'Triggers when the webhook receives a request.',
async testRun($) {
if (!isEmpty($.lastExecutionStep?.dataOut)) {
$.pushTriggerItem({
raw: $.lastExecutionStep.dataOut,
meta: {
internalId: '',
}
});
}
},
});

View File

@@ -0,0 +1,3 @@
import catchRawWebhook from './catch-raw-webhook';
export default [catchRawWebhook];

View File

@@ -13,20 +13,21 @@ export default async (request: IRequest, response: Response) => {
.findById(request.params.flowId)
.throwIfNotFound();
if (!flow.active) {
return response.send(404);
}
const testRun = !flow.active;
const triggerStep = await flow.getTriggerStep();
const triggerCommand = await triggerStep.getTriggerCommand();
const app = await triggerStep.getApp();
const isWebhookApp = app.key === 'webhook';
if (triggerCommand.type !== 'webhook') {
return response.send(404);
if (testRun && !isWebhookApp) {
return response.sendStatus(404);
}
const app = await triggerStep.getApp();
if (triggerCommand.type !== 'webhook') {
return response.sendStatus(404);
}
if (app.auth.verifyWebhook) {
if (app.auth?.verifyWebhook) {
const $ = await globalVariable({
flow,
connection: await triggerStep.$relatedQuery('connection'),
@@ -42,8 +43,20 @@ export default async (request: IRequest, response: Response) => {
}
}
// in case trigger type is 'webhook'
let payload = request.body;
// in case it's our built-in generic webhook trigger
if (isWebhookApp) {
payload = {
headers: request.headers,
body: request.body,
query: request.query,
}
}
const triggerItem: ITriggerItem = {
raw: request.body,
raw: payload,
meta: {
internalId: await bcrypt.hash(request.rawBody, 1),
},
@@ -53,8 +66,13 @@ export default async (request: IRequest, response: Response) => {
flowId: flow.id,
stepId: triggerStep.id,
triggerItem,
testRun
});
if (testRun) {
return response.sendStatus(200);
}
const nextStep = await triggerStep.getNextStep();
const jobName = `${executionId}-${nextStep.id}`;

View File

@@ -49,9 +49,9 @@ const updateFlowStatus = async (
testRun: false,
});
if (flow.active) {
if (flow.active && trigger.registerHook) {
await trigger.registerHook($);
} else {
} else if (!flow.active && trigger.unregisterHook) {
await trigger.unregisterHook($);
}
} else {

View File

@@ -342,6 +342,7 @@ type Step {
key: String
appKey: String
iconUrl: String
webhookUrl: String
type: StepEnumType
parameters: JSONObject
connection: Connection

View File

@@ -77,6 +77,7 @@ const globalVariable = async (
id: execution?.id,
testRun,
},
lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(),
triggerOutput: {
data: [],
},

View File

@@ -1,10 +1,11 @@
import { URL } from 'node:url';
import { QueryContext, ModelOptions } from 'objection';
import type { IJSONObject, IStep } from '@automatisch/types';
import Base from './base';
import App from './app';
import Flow from './flow';
import Connection from './connection';
import ExecutionStep from './execution-step';
import type { IJSONObject, IStep } from '@automatisch/types';
import Telemetry from '../helpers/telemetry';
import appConfig from '../config/app';
@@ -46,7 +47,7 @@ class Step extends Base {
};
static get virtualAttributes() {
return ['iconUrl'];
return ['iconUrl', 'webhookUrl'];
}
static relationMappings = () => ({
@@ -82,6 +83,13 @@ class Step extends Base {
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
}
get webhookUrl() {
if (this.appKey !== 'webhook') return null;
const url = new URL(`/webhooks/${this.flowId}`, appConfig.webhookUrl);
return url.toString();
}
async $afterInsert(queryContext: QueryContext) {
await super.$afterInsert(queryContext);
Telemetry.stepCreated(this);
@@ -106,6 +114,14 @@ class Step extends Base {
return await App.findOneByKey(this.appKey);
}
async getLastExecutionStep() {
const lastExecutionStep = await this.$relatedQuery('executionSteps')
.orderBy('created_at', 'desc')
.first();
return lastExecutionStep;
}
async getNextStep() {
const flow = await this.$relatedQuery('flow');

View File

@@ -3,6 +3,8 @@ import webhookHandler from '../controllers/webhooks/handler';
const router = Router();
router.get('/:flowId', webhookHandler);
router.put('/:flowId', webhookHandler);
router.post('/:flowId', webhookHandler);
export default router;

View File

@@ -54,6 +54,7 @@ export interface IStep {
key?: string;
appKey?: string;
iconUrl: string;
webhookUrl: string;
type: 'action' | 'trigger';
connectionId?: string;
status: string;
@@ -180,23 +181,16 @@ export interface IDynamicData {
export interface IAuth {
generateAuthUrl?($: IGlobalVariable): Promise<void>;
verifyCredentials($: IGlobalVariable): Promise<void>;
isStillVerified($: IGlobalVariable): Promise<boolean>;
verifyCredentials?($: IGlobalVariable): Promise<void>;
isStillVerified?($: IGlobalVariable): Promise<boolean>;
refreshToken?($: IGlobalVariable): Promise<void>;
verifyWebhook?($: IGlobalVariable): Promise<boolean>;
isRefreshTokenRequested?: boolean;
fields: IField[];
fields?: IField[];
authenticationSteps?: IAuthenticationStep[];
reconnectionSteps?: IAuthenticationStep[];
}
export interface IService {
authenticationClient?: IAuthentication;
triggers?: any;
actions?: any;
data?: any;
}
export interface ITriggerOutput {
data: ITriggerItem[];
error?: IJSONObject;
@@ -300,6 +294,7 @@ export type IGlobalVariable = {
id: string;
testRun: boolean;
};
lastExecutionStep?: IExecutionStep;
webhookUrl?: string;
triggerOutput?: ITriggerOutput;
actionOutput?: IActionOutput;

View File

@@ -6,22 +6,15 @@ import DialogContentText from '@mui/material/DialogContentText';
import Dialog from '@mui/material/Dialog';
import LoadingButton from '@mui/lab/LoadingButton';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { IJSONObject } from '@automatisch/types';
import type { IApp, IJSONObject, IField } from '@automatisch/types';
import useFormatMessage from 'hooks/useFormatMessage';
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
import { processStep } from 'helpers/authenticationSteps';
import InputCreator from 'components/InputCreator';
import type { IApp, IField } from '@automatisch/types';
import { generateExternalLink } from '../../helpers/translation-values';
import { Form } from './style';
const generateDocsLink = (link: string) => (str: string) =>
(
<a href={link} target="_blank">
{str}
</a>
);
type AddAppConnectionProps = {
onClose: (response: Record<string, unknown>) => void;
application: IApp;
@@ -112,7 +105,7 @@ export default function AddAppConnection(
<Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage('addAppConnection.callToDocs', {
appName: name,
docsLink: generateDocsLink(authDocUrl),
docsLink: generateExternalLink(authDocUrl),
})}
</Alert>
)}

View File

@@ -9,8 +9,9 @@ import LoadingButton from '@mui/lab/LoadingButton';
import { EditorContext } from 'contexts/Editor';
import useFormatMessage from 'hooks/useFormatMessage';
import JSONViewer from 'components/JSONViewer';
import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow';
import JSONViewer from 'components/JSONViewer';
import WebhookUrlInfo from 'components/WebhookUrlInfo';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import type { IStep, ISubstep } from '@automatisch/types';
@@ -115,6 +116,8 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
</Alert>
)}
{step.webhookUrl && <WebhookUrlInfo webhookUrl={step.webhookUrl} />}
{hasNoOutput && (
<Alert severity="warning" sx={{ mb: 1, width: '100%' }}>
<AlertTitle sx={{ fontWeight: 700 }}>

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import Typography from '@mui/material/Typography';
import { generateExternalLink } from '../../helpers/translation-values';
import { WEBHOOK_DOCS } from '../../config/urls';
import TextField from '../TextField';
import { Alert } from './style';
type WebhookUrlInfoProps = {
webhookUrl: string;
};
function WebhookUrlInfo(props: WebhookUrlInfoProps): React.ReactElement {
const { webhookUrl } = props;
return (
<Alert icon={false} color="info">
<Typography variant="body2" textAlign="center">
<FormattedMessage id="webhookUrlInfo.title" />
</Typography>
<Typography variant="body1" textAlign="center">
<FormattedMessage id="webhookUrlInfo.description" />
</Typography>
<TextField
readOnly
clickToCopy={true}
name="webhookUrl"
fullWidth
defaultValue={webhookUrl}
helperText={
<FormattedMessage
id="webhookUrlInfo.helperText"
values={{ link: generateExternalLink(WEBHOOK_DOCS) }}
/>
}
/>
</Alert>
);
}
export default WebhookUrlInfo;

View File

@@ -0,0 +1,14 @@
import { styled } from '@mui/material/styles';
import MuiAlert, { alertClasses } from '@mui/material/Alert';
export const Alert = styled(MuiAlert)(({ theme }) => ({
[`&.${alertClasses.root}`]: {
fontWeight: 300,
width: '100%',
display: 'flex',
flexDirection: 'column'
},
[`& .${alertClasses.message}`]: {
width: '100%'
}
}));

View File

@@ -65,3 +65,6 @@ export const UPDATES = '/updates';
export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`;
export const DASHBOARD = FLOWS;
// External links
export const WEBHOOK_DOCS = 'https://automatisch.io/docs'

View File

@@ -11,6 +11,8 @@ export const GET_FLOW = gql`
type
key
appKey
iconUrl
webhookUrl
status
position
connection {

View File

@@ -0,0 +1,6 @@
export const generateExternalLink = (link: string) => (str: string) =>
(
<a href={link} target="_blank">
{str}
</a>
);

View File

@@ -87,5 +87,9 @@
"profileSettings.updatedPassword": "Your password has been updated.",
"profileSettings.updatePassword": "Update password",
"notifications.title": "Notifications",
"notification.releasedAt": "Released {relativeDate}"
"notification.releasedAt": "Released {relativeDate}",
"webhookUrlInfo.title": "Your webhook URL",
"webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.",
"webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. <link>Learn more about webhooks</link>.",
"webhookUrlInfo.copy": "Copy"
}