diff --git a/packages/backend/.env-example b/packages/backend/.env-example
index 53505520..924bd16c 100644
--- a/packages/backend/.env-example
+++ b/packages/backend/.env-example
@@ -2,6 +2,7 @@ HOST=localhost
PROTOCOL=http
PORT=3000
WEB_APP_URL=http://localhost:3001
+WEBHOOK_URL=http://localhost:3000
APP_ENV=development
POSTGRES_DATABASE=automatisch_development
POSTGRES_PORT=5432
diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts
index e078652a..a42a1009 100644
--- a/packages/backend/src/app.ts
+++ b/packages/backend/src/app.ts
@@ -1,8 +1,7 @@
import createError from 'http-errors';
-import express, { Request, Response, NextFunction } from 'express';
+import express from 'express';
import cors from 'cors';
import corsOptions from './config/cors-options';
-import graphQLInstance from './helpers/graphql-instance';
import morgan from './helpers/morgan';
import appAssetsHandler from './helpers/app-assets-handler';
import webUIHandler from './helpers/web-ui-handler';
@@ -13,6 +12,8 @@ import {
serverAdapter,
} from './helpers/create-bull-board-handler';
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
+import router from './routes';
+import { IRequest } from '@automatisch/types';
createBullBoardHandler(serverAdapter);
@@ -23,15 +24,21 @@ injectBullBoardHandler(app, serverAdapter);
appAssetsHandler(app);
app.use(morgan);
-app.use(express.json());
+app.use(
+ express.json({
+ verify: (req, res, buf) => {
+ (req as IRequest).rawBody = buf;
+ },
+ })
+);
app.use(express.urlencoded({ extended: false }));
app.use(cors(corsOptions));
-app.use('/graphql', graphQLInstance);
+app.use('/', router);
webUIHandler(app);
// catch 404 and forward to error handler
-app.use(function (req: Request, res: Response, next: NextFunction) {
+app.use(function (req, res, next) {
next(createError(404));
});
diff --git a/packages/backend/src/apps/typeform/assets/favicon.svg b/packages/backend/src/apps/typeform/assets/favicon.svg
new file mode 100644
index 00000000..f0fabb1c
--- /dev/null
+++ b/packages/backend/src/apps/typeform/assets/favicon.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/backend/src/apps/typeform/auth/generate-auth-url.ts b/packages/backend/src/apps/typeform/auth/generate-auth-url.ts
new file mode 100644
index 00000000..14ca84b8
--- /dev/null
+++ b/packages/backend/src/apps/typeform/auth/generate-auth-url.ts
@@ -0,0 +1,21 @@
+import { IField, IGlobalVariable } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+import authScope from '../common/auth-scope';
+
+export default async function generateAuthUrl($: IGlobalVariable) {
+ const oauthRedirectUrl = $.app.auth.fields.find(
+ (field: IField) => field.key == 'oAuthRedirectUrl'
+ ).value;
+
+ const searchParams = new URLSearchParams({
+ client_id: $.auth.data.clientId as string,
+ redirect_uri: oauthRedirectUrl as string,
+ scope: authScope.join(' '),
+ });
+
+ const url = `${$.app.apiBaseUrl}/oauth/authorize?${searchParams.toString()}`;
+
+ await $.auth.set({
+ url,
+ });
+}
diff --git a/packages/backend/src/apps/typeform/auth/index.ts b/packages/backend/src/apps/typeform/auth/index.ts
new file mode 100644
index 00000000..0aa070f2
--- /dev/null
+++ b/packages/backend/src/apps/typeform/auth/index.ts
@@ -0,0 +1,50 @@
+import generateAuthUrl from './generate-auth-url';
+import verifyCredentials from './verify-credentials';
+import isStillVerified from './is-still-verified';
+import refreshToken from './refresh-token';
+import verifyWebhook from './verify-webhook';
+
+export default {
+ fields: [
+ {
+ key: 'oAuthRedirectUrl',
+ label: 'OAuth Redirect URL',
+ type: 'string' as const,
+ required: true,
+ readOnly: true,
+ value: '{WEB_APP_URL}/app/typeform/connections/add',
+ placeholder: null,
+ description:
+ 'When asked to input an OAuth callback or redirect URL in Typeform OAuth, enter the URL above.',
+ clickToCopy: true,
+ },
+ {
+ key: 'clientId',
+ label: 'Client ID',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ {
+ key: 'clientSecret',
+ label: 'Client Secret',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ ],
+
+ generateAuthUrl,
+ verifyCredentials,
+ isStillVerified,
+ refreshToken,
+ verifyWebhook,
+};
diff --git a/packages/backend/src/apps/typeform/auth/is-still-verified.ts b/packages/backend/src/apps/typeform/auth/is-still-verified.ts
new file mode 100644
index 00000000..4fb0c3e3
--- /dev/null
+++ b/packages/backend/src/apps/typeform/auth/is-still-verified.ts
@@ -0,0 +1,9 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+const isStillVerified = async ($: IGlobalVariable) => {
+ await $.http.get('/me');
+
+ return true;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/typeform/auth/refresh-token.ts b/packages/backend/src/apps/typeform/auth/refresh-token.ts
new file mode 100644
index 00000000..7da4e118
--- /dev/null
+++ b/packages/backend/src/apps/typeform/auth/refresh-token.ts
@@ -0,0 +1,24 @@
+import { IGlobalVariable } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+import authScope from '../common/auth-scope';
+
+const refreshToken = async ($: IGlobalVariable) => {
+ const params = new URLSearchParams({
+ grant_type: 'refresh_token',
+ client_id: $.auth.data.clientId as string,
+ client_secret: $.auth.data.clientSecret as string,
+ refresh_token: $.auth.data.refreshToken as string,
+ scope: authScope.join(' '),
+ });
+
+ const { data } = await $.http.post('/oauth/token', params.toString());
+
+ await $.auth.set({
+ accessToken: data.access_token,
+ expiresIn: data.expires_in,
+ tokenType: data.token_type,
+ refreshToken: data.refresh_token,
+ });
+};
+
+export default refreshToken;
diff --git a/packages/backend/src/apps/typeform/auth/verify-credentials.ts b/packages/backend/src/apps/typeform/auth/verify-credentials.ts
new file mode 100644
index 00000000..2d1e55ea
--- /dev/null
+++ b/packages/backend/src/apps/typeform/auth/verify-credentials.ts
@@ -0,0 +1,46 @@
+import { IField, IGlobalVariable } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+
+const verifyCredentials = async ($: IGlobalVariable) => {
+ const oauthRedirectUrl = $.app.auth.fields.find(
+ (field: IField) => field.key == 'oAuthRedirectUrl'
+ ).value;
+
+ const params = new URLSearchParams({
+ grant_type: 'authorization_code',
+ code: $.auth.data.code as string,
+ client_id: $.auth.data.clientId as string,
+ client_secret: $.auth.data.clientSecret as string,
+ redirect_uri: oauthRedirectUrl as string,
+ });
+
+ const { data: verifiedCredentials } = await $.http.post(
+ '/oauth/token',
+ params.toString()
+ );
+
+ const {
+ access_token: accessToken,
+ expires_in: expiresIn,
+ token_type: tokenType,
+ refresh_token: refreshToken,
+ } = verifiedCredentials;
+
+ const { data: user } = await $.http.get('/me', {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ await $.auth.set({
+ accessToken,
+ expiresIn,
+ tokenType,
+ userId: user.user_id,
+ screenName: user.alias,
+ email: user.email,
+ refreshToken,
+ });
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/typeform/auth/verify-webhook.ts b/packages/backend/src/apps/typeform/auth/verify-webhook.ts
new file mode 100644
index 00000000..f5ba0d46
--- /dev/null
+++ b/packages/backend/src/apps/typeform/auth/verify-webhook.ts
@@ -0,0 +1,20 @@
+import crypto from 'crypto';
+import { IGlobalVariable } from '@automatisch/types';
+import appConfig from '../../../config/app';
+
+const verifyWebhook = async ($: IGlobalVariable) => {
+ const signature = $.request.headers['typeform-signature'] as string;
+ const isValid = verifySignature(signature, $.request.rawBody.toString());
+
+ return isValid;
+};
+
+const verifySignature = function (receivedSignature: string, payload: string) {
+ const hash = crypto
+ .createHmac('sha256', appConfig.appSecretKey)
+ .update(payload)
+ .digest('base64');
+ return receivedSignature === `sha256=${hash}`;
+};
+
+export default verifyWebhook;
diff --git a/packages/backend/src/apps/typeform/common/add-auth-header.ts b/packages/backend/src/apps/typeform/common/add-auth-header.ts
new file mode 100644
index 00000000..d650c915
--- /dev/null
+++ b/packages/backend/src/apps/typeform/common/add-auth-header.ts
@@ -0,0 +1,12 @@
+import { TBeforeRequest } from '@automatisch/types';
+
+const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
+ if ($.auth.data?.accessToken) {
+ const authorizationHeader = `Bearer ${$.auth.data.accessToken}`;
+ requestConfig.headers.Authorization = authorizationHeader;
+ }
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/typeform/common/auth-scope.ts b/packages/backend/src/apps/typeform/common/auth-scope.ts
new file mode 100644
index 00000000..7b3154c8
--- /dev/null
+++ b/packages/backend/src/apps/typeform/common/auth-scope.ts
@@ -0,0 +1,12 @@
+const authScope: string[] = [
+ 'forms:read',
+ 'forms:write',
+ 'webhooks:read',
+ 'webhooks:write',
+ 'responses:read',
+ 'accounts:read',
+ 'workspaces:read',
+ 'offline',
+];
+
+export default authScope;
diff --git a/packages/backend/src/apps/typeform/dynamic-data/index.ts b/packages/backend/src/apps/typeform/dynamic-data/index.ts
new file mode 100644
index 00000000..cb6cd053
--- /dev/null
+++ b/packages/backend/src/apps/typeform/dynamic-data/index.ts
@@ -0,0 +1,3 @@
+import listForms from './list-forms';
+
+export default [listForms];
diff --git a/packages/backend/src/apps/typeform/dynamic-data/list-forms/index.ts b/packages/backend/src/apps/typeform/dynamic-data/list-forms/index.ts
new file mode 100644
index 00000000..409ec1a3
--- /dev/null
+++ b/packages/backend/src/apps/typeform/dynamic-data/list-forms/index.ts
@@ -0,0 +1,25 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+
+export default {
+ name: 'List forms',
+ key: 'listForms',
+
+ async run($: IGlobalVariable) {
+ const forms: {
+ data: IJSONObject[];
+ } = {
+ data: [],
+ };
+
+ const response = await $.http.get('/forms');
+
+ forms.data = response.data.items.map((form: IJSONObject) => {
+ return {
+ value: form.id,
+ name: form.title,
+ };
+ });
+
+ return forms;
+ },
+};
diff --git a/packages/backend/src/apps/typeform/index.d.ts b/packages/backend/src/apps/typeform/index.d.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/backend/src/apps/typeform/index.ts b/packages/backend/src/apps/typeform/index.ts
new file mode 100644
index 00000000..f0f6ffcb
--- /dev/null
+++ b/packages/backend/src/apps/typeform/index.ts
@@ -0,0 +1,20 @@
+import defineApp from '../../helpers/define-app';
+import addAuthHeader from './common/add-auth-header';
+import auth from './auth';
+import triggers from './triggers';
+import dynamicData from './dynamic-data';
+
+export default defineApp({
+ name: 'Typeform',
+ key: 'typeform',
+ iconUrl: '{BASE_URL}/apps/typeform/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/apps/typeform/connection',
+ supportsConnections: true,
+ baseUrl: 'https://typeform.com',
+ apiBaseUrl: 'https://api.typeform.com',
+ primaryColor: '262627',
+ beforeRequest: [addAuthHeader],
+ auth,
+ triggers,
+ dynamicData,
+});
diff --git a/packages/backend/src/apps/typeform/triggers/index.ts b/packages/backend/src/apps/typeform/triggers/index.ts
new file mode 100644
index 00000000..c49828b1
--- /dev/null
+++ b/packages/backend/src/apps/typeform/triggers/index.ts
@@ -0,0 +1,3 @@
+import newEntry from './new-entry';
+
+export default [newEntry];
diff --git a/packages/backend/src/apps/typeform/triggers/new-entry/index.ts b/packages/backend/src/apps/typeform/triggers/new-entry/index.ts
new file mode 100644
index 00000000..5d718439
--- /dev/null
+++ b/packages/backend/src/apps/typeform/triggers/new-entry/index.ts
@@ -0,0 +1,89 @@
+import appConfig from '../../../../config/app';
+import defineTrigger from '../../../../helpers/define-trigger';
+
+export default defineTrigger({
+ name: 'New entry',
+ key: 'newEntry',
+ type: 'webhook',
+ description: 'Triggers when a new form submitted.',
+ arguments: [
+ {
+ label: 'Form',
+ key: 'formId',
+ type: 'dropdown' as const,
+ required: true,
+ description: 'Pick a form to receive submissions.',
+ variables: false,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listForms',
+ },
+ ],
+ },
+ },
+ ],
+
+ async testRun($) {
+ const { data: form } = await $.http.get(
+ `/forms/${$.step.parameters.formId}`
+ );
+
+ const { data: responses } = await $.http.get(
+ `/forms/${$.step.parameters.formId}/responses`
+ );
+
+ const lastResponse = responses.items[0];
+
+ if (!lastResponse) {
+ return;
+ }
+
+ const computedWebhookEvent = {
+ event_type: 'form_response',
+ form_response: {
+ form_id: form.id,
+ token: lastResponse.token,
+ landed_at: lastResponse.landed_at,
+ submitted_at: lastResponse.submitted_at,
+ definition: {
+ id: $.step.parameters.formId,
+ title: form.title,
+ fields: form?.fields,
+ },
+ answers: lastResponse.answers,
+ },
+ };
+
+ const dataItem = {
+ raw: computedWebhookEvent,
+ meta: {
+ internalId: computedWebhookEvent.form_response.token,
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async registerHook($) {
+ const subscriptionPayload = {
+ enabled: true,
+ url: $.webhookUrl,
+ secret: appConfig.appSecretKey,
+ };
+
+ await $.http.put(
+ `/forms/${$.step.parameters.formId}/webhooks/${$.flow.id}`,
+ subscriptionPayload
+ );
+ },
+
+ async unregisterHook($) {
+ await $.http.delete(
+ `/forms/${$.step.parameters.formId}/webhooks/${$.flow.id}`
+ );
+ },
+});
diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts
index 6d41956b..7403421e 100644
--- a/packages/backend/src/config/app.ts
+++ b/packages/backend/src/config/app.ts
@@ -6,6 +6,7 @@ type AppConfig = {
protocol: string;
port: string;
webAppUrl: string;
+ webhookUrl: string;
appEnv: string;
isDev: boolean;
postgresDatabase: string;
@@ -37,6 +38,8 @@ const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let webAppUrl = `${protocol}://${host}:${port}`;
+const webhookUrl = process.env.WEBHOOK_URL || webAppUrl;
+
if (serveWebAppSeparately) {
webAppUrl = process.env.WEB_APP_URL || 'http://localhost:3001';
}
@@ -73,6 +76,7 @@ const appConfig: AppConfig = {
bullMQDashboardPassword: process.env.BULLMQ_DASHBOARD_PASSWORD,
baseUrl,
webAppUrl,
+ webhookUrl,
telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true,
};
diff --git a/packages/backend/src/controllers/webhooks/handler.ts b/packages/backend/src/controllers/webhooks/handler.ts
new file mode 100644
index 00000000..72329adc
--- /dev/null
+++ b/packages/backend/src/controllers/webhooks/handler.ts
@@ -0,0 +1,75 @@
+import { Response } from 'express';
+import bcrypt from 'bcrypt';
+import { IRequest, ITriggerItem } from '@automatisch/types';
+
+import Flow from '../../models/flow';
+import { processTrigger } from '../../services/trigger';
+import actionQueue from '../../queues/action';
+import globalVariable from '../../helpers/global-variable';
+import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration';
+
+export default async (request: IRequest, response: Response) => {
+ const flow = await Flow.query()
+ .findById(request.params.flowId)
+ .throwIfNotFound();
+
+ if (!flow.active) {
+ return response.send(404);
+ }
+
+ const triggerStep = await flow.getTriggerStep();
+ const triggerCommand = await triggerStep.getTriggerCommand();
+
+ if (triggerCommand.type !== 'webhook') {
+ return response.send(404);
+ }
+
+ const app = await triggerStep.getApp();
+
+ if (app.auth.verifyWebhook) {
+ const $ = await globalVariable({
+ flow,
+ connection: await triggerStep.$relatedQuery('connection'),
+ app,
+ step: triggerStep,
+ request,
+ });
+
+ const verified = await app.auth.verifyWebhook($);
+
+ if (!verified) {
+ return response.sendStatus(401);
+ }
+ }
+
+ const triggerItem: ITriggerItem = {
+ raw: request.body,
+ meta: {
+ internalId: await bcrypt.hash(request.rawBody, 1),
+ },
+ };
+
+ const { flowId, executionId } = await processTrigger({
+ flowId: flow.id,
+ stepId: triggerStep.id,
+ triggerItem,
+ });
+
+ const nextStep = await triggerStep.getNextStep();
+ const jobName = `${executionId}-${nextStep.id}`;
+
+ const jobPayload = {
+ flowId,
+ executionId,
+ stepId: nextStep.id,
+ };
+
+ const jobOptions = {
+ removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
+ removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
+ }
+
+ await actionQueue.add(jobName, jobPayload, jobOptions);
+
+ return response.sendStatus(200);
+};
diff --git a/packages/backend/src/graphql/mutations/update-flow-status.ts b/packages/backend/src/graphql/mutations/update-flow-status.ts
index 10e2f9f5..1cb80d5a 100644
--- a/packages/backend/src/graphql/mutations/update-flow-status.ts
+++ b/packages/backend/src/graphql/mutations/update-flow-status.ts
@@ -1,6 +1,7 @@
import Context from '../../types/express/context';
import flowQueue from '../../queues/flow';
import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration';
+import globalVariable from '../../helpers/global-variable';
type Params = {
input: {
@@ -39,28 +40,44 @@ const updateFlowStatus = async (
pattern: interval || EVERY_15_MINUTES_CRON,
};
- if (flow.active) {
- flow = await flow.$query().patchAndFetch({
- published_at: new Date().toISOString(),
+ if (trigger.type === 'webhook') {
+ const $ = await globalVariable({
+ flow,
+ connection: await triggerStep.$relatedQuery('connection'),
+ app: await triggerStep.getApp(),
+ step: triggerStep,
+ testRun: false,
});
- const jobName = `${JOB_NAME}-${flow.id}`;
-
- await flowQueue.add(
- jobName,
- { flowId: flow.id },
- {
- repeat: repeatOptions,
- jobId: flow.id,
- removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
- removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS
- }
- );
+ if (flow.active) {
+ await trigger.registerHook($);
+ } else {
+ await trigger.unregisterHook($);
+ }
} else {
- const repeatableJobs = await flowQueue.getRepeatableJobs();
- const job = repeatableJobs.find((job) => job.id === flow.id);
+ if (flow.active) {
+ flow = await flow.$query().patchAndFetch({
+ published_at: new Date().toISOString(),
+ });
- await flowQueue.removeRepeatableByKey(job.key);
+ const jobName = `${JOB_NAME}-${flow.id}`;
+
+ await flowQueue.add(
+ jobName,
+ { flowId: flow.id },
+ {
+ repeat: repeatOptions,
+ jobId: flow.id,
+ removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
+ removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS
+ }
+ );
+ } else {
+ const repeatableJobs = await flowQueue.getRepeatableJobs();
+ const job = repeatableJobs.find((job) => job.id === flow.id);
+
+ await flowQueue.removeRepeatableByKey(job.key);
+ }
}
return flow;
diff --git a/packages/backend/src/helpers/global-variable.ts b/packages/backend/src/helpers/global-variable.ts
index 9fbb4446..de9a09e8 100644
--- a/packages/backend/src/helpers/global-variable.ts
+++ b/packages/backend/src/helpers/global-variable.ts
@@ -3,12 +3,14 @@ import Connection from '../models/connection';
import Flow from '../models/flow';
import Step from '../models/step';
import Execution from '../models/execution';
+import appConfig from '../config/app';
import {
IJSONObject,
IApp,
IGlobalVariable,
ITriggerItem,
IActionItem,
+ IRequest,
} from '@automatisch/types';
import EarlyExitError from '../errors/early-exit';
@@ -19,12 +21,21 @@ type GlobalVariableOptions = {
step?: Step;
execution?: Execution;
testRun?: boolean;
+ request?: IRequest;
};
const globalVariable = async (
options: GlobalVariableOptions
): Promise => {
- const { connection, app, flow, step, execution, testRun = false } = options;
+ const {
+ connection,
+ app,
+ flow,
+ step,
+ execution,
+ request,
+ testRun = false,
+ } = options;
const lastInternalId = testRun ? undefined : await flow?.lastInternalId();
const nextStep = await step?.getNextStep();
@@ -95,12 +106,22 @@ const globalVariable = async (
},
};
+ if (request) {
+ $.request = request;
+ }
+
$.http = createHttpClient({
$,
baseURL: app.apiBaseUrl,
beforeRequest: app.beforeRequest,
});
+ if (flow) {
+ const webhookUrl = appConfig.webhookUrl + '/webhooks/' + flow.id;
+
+ $.webhookUrl = webhookUrl;
+ }
+
const lastInternalIds =
testRun || (flow && step.isAction) ? [] : await flow?.lastInternalIds(2000);
diff --git a/packages/backend/src/helpers/graphql-instance.ts b/packages/backend/src/helpers/graphql-instance.ts
index d65fd83c..dac70079 100644
--- a/packages/backend/src/helpers/graphql-instance.ts
+++ b/packages/backend/src/helpers/graphql-instance.ts
@@ -1,12 +1,13 @@
-import { graphqlHTTP } from 'express-graphql';
-import logger from '../helpers/logger';
-import { applyMiddleware } from 'graphql-middleware';
-import authentication from '../helpers/authentication';
import { join } from 'path';
+import { graphqlHTTP } from 'express-graphql';
import { loadSchemaSync } from '@graphql-tools/load';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { addResolversToSchema } from '@graphql-tools/schema';
+import { applyMiddleware } from 'graphql-middleware';
+import logger from '../helpers/logger';
+import authentication from '../helpers/authentication';
import resolvers from '../graphql/resolvers';
+import HttpError from '../errors/http';
const schema = loadSchemaSync(join(__dirname, '../graphql/schema.graphql'), {
loaders: [new GraphQLFileLoader()],
@@ -23,6 +24,10 @@ const graphQLInstance = graphqlHTTP({
customFormatErrorFn: (error) => {
logger.error(error.path + ' : ' + error.message + '\n' + error.stack);
+ if (error.originalError instanceof HttpError) {
+ delete (error.originalError as HttpError).response;
+ }
+
return error.originalError;
},
});
diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts
new file mode 100644
index 00000000..1cb26316
--- /dev/null
+++ b/packages/backend/src/routes/index.ts
@@ -0,0 +1,10 @@
+import { Router } from 'express';
+import graphQLInstance from '../helpers/graphql-instance';
+import webhooksRouter from './webhooks';
+
+const router = Router();
+
+router.use('/graphql', graphQLInstance);
+router.use('/webhooks', webhooksRouter);
+
+export default router;
diff --git a/packages/backend/src/routes/webhooks.ts b/packages/backend/src/routes/webhooks.ts
new file mode 100644
index 00000000..c26f4add
--- /dev/null
+++ b/packages/backend/src/routes/webhooks.ts
@@ -0,0 +1,8 @@
+import { Router } from 'express';
+import webhookHandler from '../controllers/webhooks/handler';
+
+const router = Router();
+
+router.post('/:flowId', webhookHandler);
+
+export default router;
diff --git a/packages/backend/src/services/flow.ts b/packages/backend/src/services/flow.ts
index f6a256fe..862066f7 100644
--- a/packages/backend/src/services/flow.ts
+++ b/packages/backend/src/services/flow.ts
@@ -23,7 +23,11 @@ export const processFlow = async (options: ProcessFlowOptions) => {
});
try {
- await triggerCommand.run($);
+ if (triggerCommand.type === 'webhook' && !flow.active) {
+ await triggerCommand.testRun($);
+ } else {
+ await triggerCommand.run($);
+ }
} catch (error) {
if (error instanceof EarlyExitError === false) {
if (error instanceof HttpError) {
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index df2521dd..0b2fb1f8 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -1,5 +1,6 @@
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
export type IHttpClient = AxiosInstance;
+import type { Request } from 'express';
// Type definitions for automatisch
@@ -182,6 +183,7 @@ export interface IAuth {
verifyCredentials($: IGlobalVariable): Promise;
isStillVerified($: IGlobalVariable): Promise;
refreshToken?($: IGlobalVariable): Promise;
+ verifyWebhook?($: IGlobalVariable): Promise;
isRefreshTokenRequested?: boolean;
fields: IField[];
authenticationSteps?: IAuthenticationStep[];
@@ -210,10 +212,14 @@ export interface ITriggerItem {
export interface IBaseTrigger {
name: string;
key: string;
+ type?: 'webhook' | 'polling';
pollInterval?: number;
description: string;
getInterval?(parameters: IStep['parameters']): string;
- run($: IGlobalVariable): Promise;
+ run?($: IGlobalVariable): Promise;
+ testRun?($: IGlobalVariable): Promise;
+ registerHook?($: IGlobalVariable): Promise;
+ unregisterHook?($: IGlobalVariable): Promise;
sort?(item: ITriggerItem, nextItem: ITriggerItem): number;
}
@@ -238,7 +244,7 @@ export interface IBaseAction {
name: string;
key: string;
description: string;
- run($: IGlobalVariable): Promise;
+ run?($: IGlobalVariable): Promise;
}
export interface IRawAction extends IBaseAction {
@@ -274,6 +280,7 @@ export type IGlobalVariable = {
};
app: IApp;
http?: IHttpClient;
+ request?: IRequest;
flow?: {
id: string;
lastInternalId: string;
@@ -293,6 +300,7 @@ export type IGlobalVariable = {
id: string;
testRun: boolean;
};
+ webhookUrl?: string;
triggerOutput?: ITriggerOutput;
actionOutput?: IActionOutput;
pushTriggerItem?: (triggerItem: ITriggerItem) => void;
@@ -308,3 +316,8 @@ declare module 'axios' {
additionalProperties?: Record;
}
}
+
+export interface IRequest extends Request {
+ rawBody?: Buffer;
+}
+