Merge pull request #780 from automatisch/generic-webhook
Add webhook integration
This commit is contained in:
8
packages/backend/src/apps/webhook/assets/favicon.svg
Normal file
8
packages/backend/src/apps/webhook/assets/favicon.svg
Normal 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 |
0
packages/backend/src/apps/webhook/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/webhook/index.d.ts
vendored
Normal file
14
packages/backend/src/apps/webhook/index.ts
Normal file
14
packages/backend/src/apps/webhook/index.ts
Normal 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,
|
||||
});
|
@@ -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: '',
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
3
packages/backend/src/apps/webhook/triggers/index.ts
Normal file
3
packages/backend/src/apps/webhook/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import catchRawWebhook from './catch-raw-webhook';
|
||||
|
||||
export default [catchRawWebhook];
|
@@ -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}`;
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -342,6 +342,7 @@ type Step {
|
||||
key: String
|
||||
appKey: String
|
||||
iconUrl: String
|
||||
webhookUrl: String
|
||||
type: StepEnumType
|
||||
parameters: JSONObject
|
||||
connection: Connection
|
||||
|
@@ -77,6 +77,7 @@ const globalVariable = async (
|
||||
id: execution?.id,
|
||||
testRun,
|
||||
},
|
||||
lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(),
|
||||
triggerOutput: {
|
||||
data: [],
|
||||
},
|
||||
|
@@ -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');
|
||||
|
||||
|
@@ -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;
|
||||
|
15
packages/types/index.d.ts
vendored
15
packages/types/index.d.ts
vendored
@@ -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;
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
@@ -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 }}>
|
||||
|
44
packages/web/src/components/WebhookUrlInfo/index.tsx
Normal file
44
packages/web/src/components/WebhookUrlInfo/index.tsx
Normal 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;
|
14
packages/web/src/components/WebhookUrlInfo/style.ts
Normal file
14
packages/web/src/components/WebhookUrlInfo/style.ts
Normal 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%'
|
||||
}
|
||||
}));
|
@@ -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'
|
||||
|
@@ -11,6 +11,8 @@ export const GET_FLOW = gql`
|
||||
type
|
||||
key
|
||||
appKey
|
||||
iconUrl
|
||||
webhookUrl
|
||||
status
|
||||
position
|
||||
connection {
|
||||
|
6
packages/web/src/helpers/translation-values.tsx
Normal file
6
packages/web/src/helpers/translation-values.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export const generateExternalLink = (link: string) => (str: string) =>
|
||||
(
|
||||
<a href={link} target="_blank">
|
||||
{str}
|
||||
</a>
|
||||
);
|
@@ -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"
|
||||
}
|
Reference in New Issue
Block a user