diff --git a/packages/backend/src/apps/webhook/assets/favicon.svg b/packages/backend/src/apps/webhook/assets/favicon.svg
new file mode 100644
index 00000000..140ebd66
--- /dev/null
+++ b/packages/backend/src/apps/webhook/assets/favicon.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/packages/backend/src/apps/webhook/index.d.ts b/packages/backend/src/apps/webhook/index.d.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/backend/src/apps/webhook/index.ts b/packages/backend/src/apps/webhook/index.ts
new file mode 100644
index 00000000..107ee47a
--- /dev/null
+++ b/packages/backend/src/apps/webhook/index.ts
@@ -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,
+});
diff --git a/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts b/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts
new file mode 100644
index 00000000..88c52f9f
--- /dev/null
+++ b/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.ts
@@ -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: '',
+ }
+ });
+ }
+ },
+});
diff --git a/packages/backend/src/apps/webhook/triggers/index.ts b/packages/backend/src/apps/webhook/triggers/index.ts
new file mode 100644
index 00000000..49159004
--- /dev/null
+++ b/packages/backend/src/apps/webhook/triggers/index.ts
@@ -0,0 +1,3 @@
+import catchRawWebhook from './catch-raw-webhook';
+
+export default [catchRawWebhook];
diff --git a/packages/backend/src/controllers/webhooks/handler.ts b/packages/backend/src/controllers/webhooks/handler.ts
index 72329adc..48717696 100644
--- a/packages/backend/src/controllers/webhooks/handler.ts
+++ b/packages/backend/src/controllers/webhooks/handler.ts
@@ -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}`;
diff --git a/packages/backend/src/graphql/mutations/update-flow-status.ts b/packages/backend/src/graphql/mutations/update-flow-status.ts
index 1cb80d5a..99a504c2 100644
--- a/packages/backend/src/graphql/mutations/update-flow-status.ts
+++ b/packages/backend/src/graphql/mutations/update-flow-status.ts
@@ -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 {
diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql
index bd0c39b9..3f9714a3 100644
--- a/packages/backend/src/graphql/schema.graphql
+++ b/packages/backend/src/graphql/schema.graphql
@@ -342,6 +342,7 @@ type Step {
key: String
appKey: String
iconUrl: String
+ webhookUrl: String
type: StepEnumType
parameters: JSONObject
connection: Connection
diff --git a/packages/backend/src/helpers/global-variable.ts b/packages/backend/src/helpers/global-variable.ts
index de9a09e8..6834b081 100644
--- a/packages/backend/src/helpers/global-variable.ts
+++ b/packages/backend/src/helpers/global-variable.ts
@@ -77,6 +77,7 @@ const globalVariable = async (
id: execution?.id,
testRun,
},
+ lastExecutionStep: (await step?.getLastExecutionStep())?.toJSON(),
triggerOutput: {
data: [],
},
diff --git a/packages/backend/src/models/step.ts b/packages/backend/src/models/step.ts
index 719fbbf3..294671af 100644
--- a/packages/backend/src/models/step.ts
+++ b/packages/backend/src/models/step.ts
@@ -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');
diff --git a/packages/backend/src/routes/webhooks.ts b/packages/backend/src/routes/webhooks.ts
index c26f4add..ca040cc0 100644
--- a/packages/backend/src/routes/webhooks.ts
+++ b/packages/backend/src/routes/webhooks.ts
@@ -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;
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 0b2fb1f8..1386f962 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -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;
- verifyCredentials($: IGlobalVariable): Promise;
- isStillVerified($: IGlobalVariable): Promise;
+ verifyCredentials?($: IGlobalVariable): Promise;
+ isStillVerified?($: IGlobalVariable): Promise;
refreshToken?($: IGlobalVariable): Promise;
verifyWebhook?($: IGlobalVariable): Promise;
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;
diff --git a/packages/web/src/components/AddAppConnection/index.tsx b/packages/web/src/components/AddAppConnection/index.tsx
index 7b54e0fe..38ba620b 100644
--- a/packages/web/src/components/AddAppConnection/index.tsx
+++ b/packages/web/src/components/AddAppConnection/index.tsx
@@ -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) =>
- (
-
- {str}
-
- );
-
type AddAppConnectionProps = {
onClose: (response: Record) => void;
application: IApp;
@@ -112,7 +105,7 @@ export default function AddAppConnection(
{formatMessage('addAppConnection.callToDocs', {
appName: name,
- docsLink: generateDocsLink(authDocUrl),
+ docsLink: generateExternalLink(authDocUrl),
})}
)}
diff --git a/packages/web/src/components/TestSubstep/index.tsx b/packages/web/src/components/TestSubstep/index.tsx
index f2b895c3..09a5f0c7 100644
--- a/packages/web/src/components/TestSubstep/index.tsx
+++ b/packages/web/src/components/TestSubstep/index.tsx
@@ -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 {
)}
+ {step.webhookUrl && }
+
{hasNoOutput && (
diff --git a/packages/web/src/components/WebhookUrlInfo/index.tsx b/packages/web/src/components/WebhookUrlInfo/index.tsx
new file mode 100644
index 00000000..cc5ca10d
--- /dev/null
+++ b/packages/web/src/components/WebhookUrlInfo/index.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+ );
+}
+
+export default WebhookUrlInfo;
diff --git a/packages/web/src/components/WebhookUrlInfo/style.ts b/packages/web/src/components/WebhookUrlInfo/style.ts
new file mode 100644
index 00000000..b74a3834
--- /dev/null
+++ b/packages/web/src/components/WebhookUrlInfo/style.ts
@@ -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%'
+ }
+}));
diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts
index 27214fe0..5f0830f0 100644
--- a/packages/web/src/config/urls.ts
+++ b/packages/web/src/config/urls.ts
@@ -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'
diff --git a/packages/web/src/graphql/queries/get-flow.ts b/packages/web/src/graphql/queries/get-flow.ts
index 34345984..f3e86446 100644
--- a/packages/web/src/graphql/queries/get-flow.ts
+++ b/packages/web/src/graphql/queries/get-flow.ts
@@ -11,6 +11,8 @@ export const GET_FLOW = gql`
type
key
appKey
+ iconUrl
+ webhookUrl
status
position
connection {
diff --git a/packages/web/src/helpers/translation-values.tsx b/packages/web/src/helpers/translation-values.tsx
new file mode 100644
index 00000000..86e84ce5
--- /dev/null
+++ b/packages/web/src/helpers/translation-values.tsx
@@ -0,0 +1,6 @@
+export const generateExternalLink = (link: string) => (str: string) =>
+ (
+
+ {str}
+
+ );
diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json
index 4d79ee2b..1732120f 100644
--- a/packages/web/src/locales/en.json
+++ b/packages/web/src/locales/en.json
@@ -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. Learn more about webhooks.",
+ "webhookUrlInfo.copy": "Copy"
}
\ No newline at end of file