Compare commits
221 Commits
debug/memo
...
feature/er
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d86dbf7bd9 | ||
![]() |
8c12d2dc85 | ||
![]() |
b8970fc3f8 | ||
![]() |
dc8a32b0b4 | ||
![]() |
02deed3e3a | ||
![]() |
e50f641533 | ||
![]() |
bd754da1ed | ||
![]() |
23c568c87c | ||
![]() |
cc9b047300 | ||
![]() |
eb3ac23c01 | ||
![]() |
5358d6ce5d | ||
![]() |
c0eaab3254 | ||
![]() |
2a39435413 | ||
![]() |
01ea316c1f | ||
![]() |
b253040d0a | ||
![]() |
57748a3541 | ||
![]() |
486eb088cf | ||
![]() |
c07a741970 | ||
![]() |
d1c7d5ef70 | ||
![]() |
4b15dad5ea | ||
![]() |
577e3fc669 | ||
![]() |
b5c7c6d88b | ||
![]() |
d812bb431f | ||
![]() |
3593727d29 | ||
![]() |
75b536959e | ||
![]() |
29a9338ad7 | ||
![]() |
3755a3ce4c | ||
![]() |
ec0d0203ae | ||
![]() |
72e3a69bd9 | ||
![]() |
2c61fb0c8b | ||
![]() |
56352560a5 | ||
![]() |
2ac7c17514 | ||
![]() |
a5b3a68588 | ||
![]() |
10dcccf99f | ||
![]() |
d0922d85b3 | ||
![]() |
fc330e25cf | ||
![]() |
dc79d623be | ||
![]() |
91f5f1d628 | ||
![]() |
e0f055a375 | ||
![]() |
7f3098362a | ||
![]() |
26eee1bb63 | ||
![]() |
0246d48584 | ||
![]() |
92053ea25a | ||
![]() |
d12984f324 | ||
![]() |
0eb28ec1a5 | ||
![]() |
20f8cca07d | ||
![]() |
9613e142c9 | ||
![]() |
1cc752b5c7 | ||
![]() |
163629b990 | ||
![]() |
770e115be9 | ||
![]() |
0eb5f3d3e6 | ||
![]() |
6e9a9992c0 | ||
![]() |
827fef8a05 | ||
![]() |
b53fcdebe3 | ||
![]() |
6537a1c649 | ||
![]() |
abaad7cb82 | ||
![]() |
ba140f05d3 | ||
![]() |
81a444e056 | ||
![]() |
db2c3556de | ||
![]() |
9bd1447bcf | ||
![]() |
fda957b1f6 | ||
![]() |
29abf702bd | ||
![]() |
5ddb5ab6fa | ||
![]() |
e997aa6c81 | ||
![]() |
6271cedc25 | ||
![]() |
743b6d6587 | ||
![]() |
ee4303a676 | ||
![]() |
c361b9af8d | ||
![]() |
f66656fd4e | ||
![]() |
be141e55a9 | ||
![]() |
5ed3b9230e | ||
![]() |
44e3de8534 | ||
![]() |
cd6c5216ff | ||
![]() |
17010f9283 | ||
![]() |
5fb988ae2d | ||
![]() |
c4a3f19bba | ||
![]() |
347c9c9455 | ||
![]() |
731443ab7d | ||
![]() |
898ab41167 | ||
![]() |
cf4f5bd084 | ||
![]() |
c7e55fe3e0 | ||
![]() |
b737cf68ba | ||
![]() |
c95622affe | ||
![]() |
c1b637b284 | ||
![]() |
bb37299c5b | ||
![]() |
2e9d5fb2fc | ||
![]() |
2aeabd4be3 | ||
![]() |
a4a9d60d68 | ||
![]() |
dac9276b9e | ||
![]() |
38700256b0 | ||
![]() |
a5f391d2dc | ||
![]() |
77d7260698 | ||
![]() |
782b2bafaa | ||
![]() |
a35bee0bc9 | ||
![]() |
c7d5584cd9 | ||
![]() |
bb68b2dea1 | ||
![]() |
3b587de138 | ||
![]() |
c827ce4270 | ||
![]() |
c6c3cbb1d3 | ||
![]() |
ad97fae883 | ||
![]() |
a5b6e66e22 | ||
![]() |
5d7daa8886 | ||
![]() |
47ba42f9f8 | ||
![]() |
04f8a71244 | ||
![]() |
3148118784 | ||
![]() |
b7c4a63d2b | ||
![]() |
a4abd7e1cd | ||
![]() |
0b01a6386d | ||
![]() |
fa75f11eaf | ||
![]() |
a1b360a172 | ||
![]() |
e0a76ea918 | ||
![]() |
7990c68d65 | ||
![]() |
8c5d95796f | ||
![]() |
b901a396bf | ||
![]() |
fa92375e33 | ||
![]() |
04b3d93d5d | ||
![]() |
1f8bc9cfbd | ||
![]() |
71939f9163 | ||
![]() |
0ea8f3a01a | ||
![]() |
ffa49149c9 | ||
![]() |
40a35691fe | ||
![]() |
908a3126f1 | ||
![]() |
726707afe8 | ||
![]() |
3c926adeca | ||
![]() |
fc681d9ebc | ||
![]() |
364f53142c | ||
![]() |
2764aa2c06 | ||
![]() |
ca141b1076 | ||
![]() |
2e980664ac | ||
![]() |
03cfa8b904 | ||
![]() |
c5bb07a768 | ||
![]() |
3a1e8b4bbd | ||
![]() |
0e749936a6 | ||
![]() |
744b31aad6 | ||
![]() |
82bdf9d3b1 | ||
![]() |
8c59bd664e | ||
![]() |
15aaada3fe | ||
![]() |
0fdbe4d39b | ||
![]() |
280c7832ae | ||
![]() |
1f9dd6a3bc | ||
![]() |
e7f12f4a06 | ||
![]() |
5a24b9ec8a | ||
![]() |
1d8a72e03b | ||
![]() |
ef987eae36 | ||
![]() |
dc4899c240 | ||
![]() |
88a780f008 | ||
![]() |
6fc13f3707 | ||
![]() |
6f1b9b8fe7 | ||
![]() |
533d73d718 | ||
![]() |
63241d2438 | ||
![]() |
5a177b330a | ||
![]() |
ae8f701e5c | ||
![]() |
744c927b69 | ||
![]() |
6ca1b99947 | ||
![]() |
cdb018dcd2 | ||
![]() |
03efe3f0b3 | ||
![]() |
913a2773c1 | ||
![]() |
10c64167d7 | ||
![]() |
a624a8d8b5 | ||
![]() |
ff09a836b4 | ||
![]() |
e7c734c55e | ||
![]() |
5271af8b94 | ||
![]() |
df55d9fdd9 | ||
![]() |
c7ff9dc162 | ||
![]() |
f7ab2b667c | ||
![]() |
b89086d3b8 | ||
![]() |
448a1a49b4 | ||
![]() |
98d7969a1e | ||
![]() |
d513e03138 | ||
![]() |
a5367c3770 | ||
![]() |
58d5847eed | ||
![]() |
02b22740b2 | ||
![]() |
70d59c6c64 | ||
![]() |
095948e737 | ||
![]() |
e6cec355cc | ||
![]() |
351f152664 | ||
![]() |
04450b8793 | ||
![]() |
76612e81b2 | ||
![]() |
cb26948db6 | ||
![]() |
18cdd226bb | ||
![]() |
2199970e50 | ||
![]() |
8389c9fdad | ||
![]() |
0d06ec9a22 | ||
![]() |
ce56f166cc | ||
![]() |
a07938d517 | ||
![]() |
7d81a4bdd2 | ||
![]() |
c4bffe7f9d | ||
![]() |
0c2caccc7c | ||
![]() |
d3c9f7a491 | ||
![]() |
f09fa0fe7c | ||
![]() |
8f1fbf086f | ||
![]() |
272c666d23 | ||
![]() |
a1db89700b | ||
![]() |
311f0a747d | ||
![]() |
69298857a9 | ||
![]() |
c98680fa59 | ||
![]() |
5f357afcd6 | ||
![]() |
997775e54b | ||
![]() |
2cf79e27de | ||
![]() |
95d03e00da | ||
![]() |
c85aadf006 | ||
![]() |
e9ffb7ef82 | ||
![]() |
4237972c86 | ||
![]() |
b69009f8b6 | ||
![]() |
6fa6ee7a1b | ||
![]() |
7cf1bfffbc | ||
![]() |
903b0f52b9 | ||
![]() |
16e299a12d | ||
![]() |
e950d73742 | ||
![]() |
b44cccb972 | ||
![]() |
5d4ef1bbe8 | ||
![]() |
12a6912d97 | ||
![]() |
2740f8e23d | ||
![]() |
d0814477eb | ||
![]() |
cdb256390c | ||
![]() |
9895cc1488 | ||
![]() |
fad2495941 | ||
![]() |
f9fa7c4094 | ||
![]() |
a5538a07f1 | ||
![]() |
d1b46df78a | ||
![]() |
57186bf85d |
47
docker/compose/docker-compose.yml
Normal file
47
docker/compose/docker-compose.yml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
automatisch:
|
||||||
|
build:
|
||||||
|
context: ../images/wait-for-postgres
|
||||||
|
network: host
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- HOST=localhost
|
||||||
|
- PROTOCOL=http
|
||||||
|
- PORT=3000
|
||||||
|
- APP_ENV=production
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- POSTGRES_HOST=postgres
|
||||||
|
- POSTGRES_DATABASE=automatisch
|
||||||
|
- POSTGRES_USERNAME=automatisch_user
|
||||||
|
volumes:
|
||||||
|
- automatisch_storage:/automatisch/storage
|
||||||
|
worker:
|
||||||
|
build:
|
||||||
|
context: ../images/plain
|
||||||
|
network: host
|
||||||
|
depends_on:
|
||||||
|
- automatisch
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- POSTGRES_HOST=postgres
|
||||||
|
- POSTGRES_DATABASE=automatisch
|
||||||
|
- POSTGRES_USERNAME=automatisch_user
|
||||||
|
command: automatisch start-worker --env-file /automatisch/storage/.env
|
||||||
|
volumes:
|
||||||
|
- automatisch_storage:/automatisch/storage
|
||||||
|
postgres:
|
||||||
|
image: "postgres:14.5"
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
POSTGRES_DB: automatisch
|
||||||
|
POSTGRES_USER: automatisch_user
|
||||||
|
redis:
|
||||||
|
image: "redis:7.0.4"
|
||||||
|
volumes:
|
||||||
|
automatisch_storage:
|
11
docker/images/plain/Dockerfile
Normal file
11
docker/images/plain/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM node:16
|
||||||
|
WORKDIR /automatisch
|
||||||
|
|
||||||
|
# npm registry for dev purposes
|
||||||
|
RUN npm config set fetch-retry-maxtimeout 5000
|
||||||
|
RUN npm config set fetch-retry-mintimeout 3000
|
||||||
|
RUN npm set registry http://localhost:5000
|
||||||
|
# npm registry for dev purposes
|
||||||
|
|
||||||
|
RUN yarn global add @automatisch/cli
|
21
docker/images/wait-for-postgres/Dockerfile
Normal file
21
docker/images/wait-for-postgres/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM node:16
|
||||||
|
WORKDIR /automatisch
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y postgresql-client
|
||||||
|
COPY ./wait-for-postgres.sh /automatisch/wait-for-postgres.sh
|
||||||
|
|
||||||
|
# npm registry for dev purposes
|
||||||
|
RUN npm config set fetch-retry-maxtimeout 5000
|
||||||
|
RUN npm config set fetch-retry-mintimeout 3000
|
||||||
|
RUN npm set registry http://localhost:5000
|
||||||
|
# npm registry for dev purposes
|
||||||
|
|
||||||
|
RUN mkdir -p /automatisch/storage
|
||||||
|
RUN touch /automatisch/storage/.env
|
||||||
|
RUN echo "ENCRYPTION_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
|
||||||
|
RUN echo "APP_SECRET_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
|
||||||
|
RUN yarn global add @automatisch/cli
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD sh /automatisch/wait-for-postgres.sh automatisch start --env-file=/automatisch/storage/.env
|
11
docker/images/wait-for-postgres/wait-for-postgres.sh
Normal file
11
docker/images/wait-for-postgres/wait-for-postgres.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
until psql -h "$POSTGRES_HOST" -U "$POSTGRES_USERNAME" -d "$POSTGRES_HOST" -c '\q'; do
|
||||||
|
>&2 echo "Postgres is unavailable - sleeping"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
>&2 echo "Postgres is up - executing command"
|
||||||
|
exec "$@"
|
@@ -13,3 +13,4 @@ ENCRYPTION_KEY=sample-encryption-key
|
|||||||
APP_SECRET_KEY=sample-app-secret-key
|
APP_SECRET_KEY=sample-app-secret-key
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
|
ENABLE_BULLMQ_DASHBOARD=false
|
||||||
|
@@ -12,8 +12,14 @@ export async function createUser(email = 'user@automatisch.io', password = 'samp
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await User.query().insertAndFetch(userParams);
|
const userCount = await User.query().resultSize();
|
||||||
logger.info(`User has been saved: ${user.email}`);
|
|
||||||
|
if (userCount === 0) {
|
||||||
|
const user = await User.query().insertAndFetch(userParams);
|
||||||
|
logger.info(`User has been saved: ${user.email}`);
|
||||||
|
} else {
|
||||||
|
logger.info('No need to seed a user.');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
|
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
|
||||||
throw err;
|
throw err;
|
||||||
|
1
packages/backend/database-utils.d.ts
vendored
Normal file
1
packages/backend/database-utils.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dist/bin/database/utils';
|
2
packages/backend/database-utils.js
Normal file
2
packages/backend/database-utils.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = require('./dist/bin/database/utils');
|
3
packages/backend/database.d.ts
vendored
3
packages/backend/database.d.ts
vendored
@@ -1,2 +1 @@
|
|||||||
export * as utils from './dist/bin/database/utils';
|
export * from './dist/src/config/database';
|
||||||
export * as database from './dist/src/config/database';
|
|
||||||
|
@@ -1,3 +1,2 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
module.exports.utils = require('./dist/bin/database/utils');
|
module.exports = require('./dist/src/config/database');
|
||||||
module.exports.database = require('./dist/src/config/database');
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "> TODO: description",
|
"description": "> TODO: description",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec 'ts-node' src/server.ts --ext ts,json",
|
"dev": "ts-node-dev src/server.ts",
|
||||||
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
|
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
|
||||||
"build": "tsc && yarn copy-statics",
|
"build": "tsc && yarn copy-statics",
|
||||||
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
|
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
"db:migration:create": "knex migrate:make",
|
"db:migration:create": "knex migrate:make",
|
||||||
"db:rollback": "knex migrate:rollback",
|
"db:rollback": "knex migrate:rollback",
|
||||||
"db:migrate": "knex migrate:latest",
|
"db:migrate": "knex migrate:latest",
|
||||||
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist"
|
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
|
||||||
|
"prepack": "yarn build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automatisch/web": "0.1.0",
|
"@automatisch/web": "0.1.0",
|
||||||
@@ -24,7 +25,6 @@
|
|||||||
"@gitbeaker/node": "^35.6.0",
|
"@gitbeaker/node": "^35.6.0",
|
||||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||||
"@graphql-tools/load": "^7.5.2",
|
"@graphql-tools/load": "^7.5.2",
|
||||||
"@octokit/oauth-methods": "^1.2.6",
|
|
||||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||||
"@slack/bolt": "3.10.0",
|
"@slack/bolt": "3.10.0",
|
||||||
"@types/luxon": "^2.3.1",
|
"@types/luxon": "^2.3.1",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"luxon": "2.3.1",
|
"luxon": "2.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nodemailer": "6.7.0",
|
"nodemailer": "6.7.0",
|
||||||
|
"oauth-1.0a": "^2.2.6",
|
||||||
"objection": "^3.0.0",
|
"objection": "^3.0.0",
|
||||||
"octokit": "^1.7.1",
|
"octokit": "^1.7.1",
|
||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
@@ -75,14 +76,19 @@
|
|||||||
"test": "__tests__"
|
"test": "__tests__"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"dist",
|
||||||
"bin",
|
"bin",
|
||||||
"src",
|
"src",
|
||||||
"server.js",
|
"server.js",
|
||||||
"server.d.ts",
|
"server.d.ts",
|
||||||
|
"worker.js",
|
||||||
|
"worker.d.ts",
|
||||||
"logger.js",
|
"logger.js",
|
||||||
"logger.d.ts",
|
"logger.d.ts",
|
||||||
"database.js",
|
"database.js",
|
||||||
"database.d.ts"
|
"database.d.ts",
|
||||||
|
"database-utils.js",
|
||||||
|
"database-utils.d.ts"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -109,7 +115,8 @@
|
|||||||
"ava": "^3.15.0",
|
"ava": "^3.15.0",
|
||||||
"nodemon": "^2.0.13",
|
"nodemon": "^2.0.13",
|
||||||
"sinon": "^11.1.2",
|
"sinon": "^11.1.2",
|
||||||
"ts-node": "^10.2.1"
|
"ts-node": "^10.2.1",
|
||||||
|
"ts-node-dev": "^1.1.8"
|
||||||
},
|
},
|
||||||
"ava": {
|
"ava": {
|
||||||
"files": [
|
"files": [
|
||||||
|
@@ -15,13 +15,13 @@ import {
|
|||||||
} from './helpers/create-bull-board-handler';
|
} from './helpers/create-bull-board-handler';
|
||||||
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
||||||
|
|
||||||
if (appConfig.appEnv === 'development') {
|
if (appConfig.enableBullMQDashboard) {
|
||||||
createBullBoardHandler(serverAdapter);
|
createBullBoardHandler(serverAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
if (appConfig.appEnv === 'development') {
|
if (appConfig.enableBullMQDashboard) {
|
||||||
injectBullBoardHandler(app, serverAdapter);
|
injectBullBoardHandler(app, serverAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/discord/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/discord/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/discord",
|
"docUrl": "https://automatisch.io/docs/discord",
|
||||||
"primaryColor": "5865f2",
|
"primaryColor": "5865f2",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/firebase/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/firebase/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/firebase",
|
"docUrl": "https://automatisch.io/docs/firebase",
|
||||||
"primaryColor": "ffca28",
|
"primaryColor": "ffca28",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/flickr/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/flickr/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/flickr",
|
"docUrl": "https://automatisch.io/docs/flickr",
|
||||||
"primaryColor": "000000",
|
"primaryColor": "000000",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
@@ -222,8 +223,8 @@
|
|||||||
"description": "Triggers when you favorite a photo.",
|
"description": "Triggers when you favorite a photo.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "testStep",
|
"key": "testStep",
|
||||||
@@ -237,8 +238,8 @@
|
|||||||
"description": "Triggers when you add a new photo in an album.",
|
"description": "Triggers when you add a new photo in an album.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -275,8 +276,8 @@
|
|||||||
"description": "Triggers when you add a new photo.",
|
"description": "Triggers when you add a new photo.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "testStep",
|
"key": "testStep",
|
||||||
@@ -290,8 +291,8 @@
|
|||||||
"description": "Triggers when you create a new album.",
|
"description": "Triggers when you create a new album.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "testStep",
|
"key": "testStep",
|
||||||
|
@@ -4,35 +4,19 @@ import type {
|
|||||||
IField,
|
IField,
|
||||||
IJSONObject,
|
IJSONObject,
|
||||||
} from '@automatisch/types';
|
} from '@automatisch/types';
|
||||||
import {
|
import HttpClient from '../../helpers/http-client';
|
||||||
getWebFlowAuthorizationUrl,
|
import { URLSearchParams } from 'url';
|
||||||
exchangeWebFlowCode,
|
|
||||||
checkToken,
|
|
||||||
} from '@octokit/oauth-methods';
|
|
||||||
|
|
||||||
export default class Authentication implements IAuthentication {
|
export default class Authentication implements IAuthentication {
|
||||||
appData: IApp;
|
appData: IApp;
|
||||||
connectionData: IJSONObject;
|
connectionData: IJSONObject;
|
||||||
scopes: string[] = [
|
scopes: string[] = ['read:org', 'repo', 'user'];
|
||||||
'read:org',
|
client: HttpClient;
|
||||||
'repo',
|
|
||||||
'user',
|
|
||||||
];
|
|
||||||
client: {
|
|
||||||
getWebFlowAuthorizationUrl: typeof getWebFlowAuthorizationUrl;
|
|
||||||
exchangeWebFlowCode: typeof exchangeWebFlowCode;
|
|
||||||
checkToken: typeof checkToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
constructor(appData: IApp, connectionData: IJSONObject) {
|
||||||
this.connectionData = connectionData;
|
this.connectionData = connectionData;
|
||||||
this.appData = appData;
|
this.appData = appData;
|
||||||
|
this.client = new HttpClient({ baseURL: 'https://github.com' });
|
||||||
this.client = {
|
|
||||||
getWebFlowAuthorizationUrl,
|
|
||||||
exchangeWebFlowCode,
|
|
||||||
checkToken,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get oauthRedirectUrl(): string {
|
get oauthRedirectUrl(): string {
|
||||||
@@ -42,26 +26,28 @@ export default class Authentication implements IAuthentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createAuthData(): Promise<{ url: string }> {
|
async createAuthData(): Promise<{ url: string }> {
|
||||||
const { url } = await this.client.getWebFlowAuthorizationUrl({
|
const searchParams = new URLSearchParams({
|
||||||
clientType: 'oauth-app',
|
client_id: this.connectionData.consumerKey as string,
|
||||||
clientId: this.connectionData.consumerKey as string,
|
redirect_uri: this.oauthRedirectUrl,
|
||||||
redirectUrl: this.oauthRedirectUrl,
|
scope: this.scopes.join(','),
|
||||||
scopes: this.scopes,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const url = `https://github.com/login/oauth/authorize?${searchParams.toString()}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: url,
|
url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyCredentials() {
|
async verifyCredentials() {
|
||||||
const { data } = await this.client.exchangeWebFlowCode({
|
const response = await this.client.post('/login/oauth/access_token', {
|
||||||
clientType: 'oauth-app',
|
client_id: this.connectionData.consumerKey,
|
||||||
clientId: this.connectionData.consumerKey as string,
|
client_secret: this.connectionData.consumerSecret,
|
||||||
clientSecret: this.connectionData.consumerSecret as string,
|
code: this.connectionData.oauthVerifier,
|
||||||
code: this.connectionData.oauthVerifier as string,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = Object.fromEntries(new URLSearchParams(response.data));
|
||||||
|
|
||||||
this.connectionData.accessToken = data.access_token;
|
this.connectionData.accessToken = data.access_token;
|
||||||
|
|
||||||
const tokenInfo = await this.getTokenInfo();
|
const tokenInfo = await this.getTokenInfo();
|
||||||
@@ -78,12 +64,23 @@ export default class Authentication implements IAuthentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTokenInfo() {
|
async getTokenInfo() {
|
||||||
return this.client.checkToken({
|
const basicAuthToken = Buffer.from(
|
||||||
clientType: 'oauth-app',
|
this.connectionData.consumerKey + ':' + this.connectionData.consumerSecret
|
||||||
clientId: this.connectionData.consumerKey as string,
|
).toString('base64');
|
||||||
clientSecret: this.connectionData.consumerSecret as string,
|
|
||||||
token: this.connectionData.accessToken as string,
|
const headers = {
|
||||||
});
|
Authorization: `Basic ${basicAuthToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
access_token: this.connectionData.accessToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.client.post(
|
||||||
|
`https://api.github.com/applications/${this.connectionData.consumerKey}/token`,
|
||||||
|
body,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isStillVerified() {
|
async isStillVerified() {
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/github/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/github/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/github",
|
"docUrl": "https://automatisch.io/docs/github",
|
||||||
"primaryColor": "000000",
|
"primaryColor": "000000",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
@@ -19,26 +20,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "consumerKey",
|
"key": "consumerKey",
|
||||||
"label": "Consumer Key",
|
"label": "Client ID",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": true,
|
"required": true,
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"value": null,
|
"value": null,
|
||||||
"placeholder": null,
|
"placeholder": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"docUrl": "https://automatisch.io/docs/github#consumer-key",
|
"docUrl": "https://automatisch.io/docs/github#client-id",
|
||||||
"clickToCopy": false
|
"clickToCopy": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "consumerSecret",
|
"key": "consumerSecret",
|
||||||
"label": "Consumer Secret",
|
"label": "Client Secret",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": true,
|
"required": true,
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"value": null,
|
"value": null,
|
||||||
"placeholder": null,
|
"placeholder": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"docUrl": "https://automatisch.io/docs/github#consumer-secret",
|
"docUrl": "https://automatisch.io/docs/github#client-secret",
|
||||||
"clickToCopy": false
|
"clickToCopy": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -222,8 +223,8 @@
|
|||||||
"description": "Triggers when a new repository is created",
|
"description": "Triggers when a new repository is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "testStep",
|
"key": "testStep",
|
||||||
@@ -237,8 +238,8 @@
|
|||||||
"description": "Triggers when a new organization is created",
|
"description": "Triggers when a new organization is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "testStep",
|
"key": "testStep",
|
||||||
@@ -252,8 +253,8 @@
|
|||||||
"description": "Triggers when a new branch is created",
|
"description": "Triggers when a new branch is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -290,8 +291,8 @@
|
|||||||
"description": "Triggers when a new notification is created",
|
"description": "Triggers when a new notification is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -329,8 +330,8 @@
|
|||||||
"description": "Triggers when a new pull request is created",
|
"description": "Triggers when a new pull request is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -367,8 +368,8 @@
|
|||||||
"description": "Triggers when a new watcher is added to a repo",
|
"description": "Triggers when a new watcher is added to a repo",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -405,8 +406,8 @@
|
|||||||
"description": "Triggers when a new milestone is created",
|
"description": "Triggers when a new milestone is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -443,8 +444,8 @@
|
|||||||
"description": "Triggers when a new commit comment is created",
|
"description": "Triggers when a new commit comment is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -481,8 +482,8 @@
|
|||||||
"description": "Triggers when a new label is created",
|
"description": "Triggers when a new label is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -519,8 +520,8 @@
|
|||||||
"description": "Triggers when a new collaborator is added to a repo",
|
"description": "Triggers when a new collaborator is added to a repo",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -557,8 +558,8 @@
|
|||||||
"description": "Triggers when a new release is created",
|
"description": "Triggers when a new release is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -595,8 +596,8 @@
|
|||||||
"description": "Triggers when a new commit is created",
|
"description": "Triggers when a new commit is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -656,8 +657,8 @@
|
|||||||
"description": "Triggers when a new issue is created",
|
"description": "Triggers when a new issue is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/gitlab/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/gitlab/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/gitlab",
|
"docUrl": "https://automatisch.io/docs/gitlab",
|
||||||
"primaryColor": "2DAAE1",
|
"primaryColor": "2DAAE1",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/postgresql/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/postgresql/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/postgresql",
|
"docUrl": "https://automatisch.io/docs/postgresql",
|
||||||
"primaryColor": "2DAAE1",
|
"primaryColor": "2DAAE1",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "host",
|
"key": "host",
|
||||||
|
@@ -3,7 +3,9 @@
|
|||||||
"key": "scheduler",
|
"key": "scheduler",
|
||||||
"iconUrl": "{BASE_URL}/apps/scheduler/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/scheduler/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/scheduler",
|
"docUrl": "https://automatisch.io/docs/scheduler",
|
||||||
|
"authDocUrl": "https://automatisch.io/docs/connections/scheduler",
|
||||||
"primaryColor": "0059F7",
|
"primaryColor": "0059F7",
|
||||||
|
"supportsConnections": false,
|
||||||
"requiresAuthentication": false,
|
"requiresAuthentication": false,
|
||||||
"triggers": [
|
"triggers": [
|
||||||
{
|
{
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import SendMessageToChannel from './actions/send-message-to-channel';
|
import SendMessageToChannel from './actions/send-message-to-channel';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import FindMessage from './actions/find-message';
|
||||||
|
import SlackClient from './client';
|
||||||
|
|
||||||
export default class Actions {
|
export default class Actions {
|
||||||
|
client: SlackClient;
|
||||||
sendMessageToChannel: SendMessageToChannel;
|
sendMessageToChannel: SendMessageToChannel;
|
||||||
|
findMessage: FindMessage;
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
constructor(client: SlackClient) {
|
||||||
this.sendMessageToChannel = new SendMessageToChannel(
|
this.client = client;
|
||||||
connectionData,
|
this.sendMessageToChannel = new SendMessageToChannel(client);
|
||||||
parameters
|
this.findMessage = new FindMessage(client);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
packages/backend/src/apps/slack/actions/find-message.ts
Normal file
26
packages/backend/src/apps/slack/actions/find-message.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import SlackClient from '../client';
|
||||||
|
|
||||||
|
export default class FindMessage {
|
||||||
|
client: SlackClient;
|
||||||
|
|
||||||
|
constructor(client: SlackClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const parameters = this.client.step.parameters;
|
||||||
|
const query = parameters.query as string;
|
||||||
|
const sortBy = parameters.sortBy as string;
|
||||||
|
const sortDirection = parameters.sortDirection as string;
|
||||||
|
const count = 1;
|
||||||
|
|
||||||
|
const messages = await this.client.findMessages.run(
|
||||||
|
query,
|
||||||
|
sortBy,
|
||||||
|
sortDirection,
|
||||||
|
count,
|
||||||
|
);
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,21 +1,18 @@
|
|||||||
import { WebClient } from '@slack/web-api';
|
import SlackClient from '../client';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
|
|
||||||
export default class SendMessageToChannel {
|
export default class SendMessageToChannel {
|
||||||
client: WebClient;
|
client: SlackClient;
|
||||||
parameters: IJSONObject;
|
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
constructor(client: SlackClient) {
|
||||||
this.client = new WebClient(connectionData.accessToken as string);
|
this.client = client;
|
||||||
this.parameters = parameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const result = await this.client.chat.postMessage({
|
const channelId = this.client.step.parameters.channel as string;
|
||||||
channel: this.parameters.channel as string,
|
const text = this.client.step.parameters.message as string;
|
||||||
text: this.parameters.message as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
const message = await this.client.postMessageToChannel.run(channelId, text);
|
||||||
|
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,36 +1,33 @@
|
|||||||
import type { IAuthentication, IApp, IJSONObject } from '@automatisch/types';
|
import type { IAuthentication, IJSONObject } from '@automatisch/types';
|
||||||
import { WebClient } from '@slack/web-api';
|
import SlackClient from './client';
|
||||||
|
|
||||||
export default class Authentication implements IAuthentication {
|
export default class Authentication implements IAuthentication {
|
||||||
appData: IApp;
|
client: SlackClient;
|
||||||
connectionData: IJSONObject;
|
|
||||||
client: WebClient;
|
|
||||||
|
|
||||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
static requestOptions: IJSONObject = {
|
||||||
this.client = new WebClient();
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
this.connectionData = connectionData;
|
constructor(client: SlackClient) {
|
||||||
this.appData = appData;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyCredentials() {
|
async verifyCredentials() {
|
||||||
const { bot_id: botId, user: screenName } = await this.client.auth.test({
|
const { bot_id: botId, user: screenName } =
|
||||||
token: this.connectionData.accessToken as string,
|
await this.client.verifyAccessToken.run();
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
botId,
|
botId,
|
||||||
screenName,
|
screenName,
|
||||||
token: this.connectionData.accessToken,
|
token: this.client.connection.formattedData.accessToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async isStillVerified() {
|
async isStillVerified() {
|
||||||
try {
|
try {
|
||||||
await this.client.auth.test({
|
await this.client.verifyAccessToken.run();
|
||||||
token: this.connectionData.accessToken as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
import SlackClient from '../index';
|
||||||
|
|
||||||
|
export default class FindMessages {
|
||||||
|
client: SlackClient;
|
||||||
|
|
||||||
|
constructor(client: SlackClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(query: string, sortBy: string, sortDirection: string, count = 1) {
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
query,
|
||||||
|
sort: sortBy,
|
||||||
|
sort_dir: sortDirection,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.client.httpClient.get('/search.messages', {
|
||||||
|
headers,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (!data.ok) {
|
||||||
|
if (data.error === 'missing_scope') {
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while finding messages; ${data.error}: ${data.needed}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Error occured while finding messages; ${data.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = data.messages.matches;
|
||||||
|
const message = messages?.[0];
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
import SlackClient from '../index';
|
||||||
|
|
||||||
|
export default class PostMessageToChannel {
|
||||||
|
client: SlackClient;
|
||||||
|
|
||||||
|
constructor(client: SlackClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(channelId: string, text: string) {
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
channel: channelId,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.client.httpClient.post(
|
||||||
|
'/chat.postMessage',
|
||||||
|
params,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.ok === 'false') {
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while posting a message to channel: ${response.data.error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.message;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import qs from 'qs';
|
||||||
|
import SlackClient from '../index';
|
||||||
|
|
||||||
|
export default class VerifyAccessToken {
|
||||||
|
client: SlackClient;
|
||||||
|
|
||||||
|
static requestOptions: IJSONObject = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(client: SlackClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const response = await this.client.httpClient.post(
|
||||||
|
'/auth.test',
|
||||||
|
qs.stringify({
|
||||||
|
token: this.client.connection.formattedData.accessToken,
|
||||||
|
}),
|
||||||
|
VerifyAccessToken.requestOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.ok === false) {
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
29
packages/backend/src/apps/slack/client/index.ts
Normal file
29
packages/backend/src/apps/slack/client/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { IFlow, IStep, IConnection } from '@automatisch/types';
|
||||||
|
import HttpClient from '../../../helpers/http-client';
|
||||||
|
import VerifyAccessToken from './endpoints/verify-access-token';
|
||||||
|
import PostMessageToChannel from './endpoints/post-message-to-channel';
|
||||||
|
import FindMessages from './endpoints/find-messages';
|
||||||
|
|
||||||
|
export default class SlackClient {
|
||||||
|
flow: IFlow;
|
||||||
|
step: IStep;
|
||||||
|
connection: IConnection;
|
||||||
|
httpClient: HttpClient;
|
||||||
|
|
||||||
|
verifyAccessToken: VerifyAccessToken;
|
||||||
|
postMessageToChannel: PostMessageToChannel;
|
||||||
|
findMessages: FindMessages;
|
||||||
|
|
||||||
|
static baseUrl = 'https://slack.com/api';
|
||||||
|
|
||||||
|
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
|
||||||
|
this.connection = connection;
|
||||||
|
this.flow = flow;
|
||||||
|
this.step = step;
|
||||||
|
|
||||||
|
this.httpClient = new HttpClient({ baseURL: SlackClient.baseUrl });
|
||||||
|
this.verifyAccessToken = new VerifyAccessToken(this);
|
||||||
|
this.postMessageToChannel = new PostMessageToChannel(this);
|
||||||
|
this.findMessages = new FindMessages(this);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,12 @@
|
|||||||
import ListChannels from './data/list-channels';
|
import ListChannels from './data/list-channels';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import SlackClient from './client';
|
||||||
|
|
||||||
export default class Data {
|
export default class Data {
|
||||||
|
client: SlackClient;
|
||||||
listChannels: ListChannels;
|
listChannels: ListChannels;
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject) {
|
constructor(client: SlackClient) {
|
||||||
this.listChannels = new ListChannels(connectionData);
|
this.client = client;
|
||||||
|
this.listChannels = new ListChannels(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,27 @@
|
|||||||
import type { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
import { WebClient } from '@slack/web-api';
|
import SlackClient from '../client';
|
||||||
|
|
||||||
export default class ListChannels {
|
export default class ListChannels {
|
||||||
client: WebClient;
|
client: SlackClient;
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject) {
|
constructor(client: SlackClient) {
|
||||||
this.client = new WebClient(connectionData.accessToken as string);
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const { channels } = await this.client.conversations.list();
|
const response = await this.client.httpClient.get('/conversations.list', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return channels.map((channel) => {
|
if (response.data.ok === 'false') {
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while fetching slack channels: ${response.data.error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.channels.map((channel: IJSONObject) => {
|
||||||
return {
|
return {
|
||||||
value: channel.id,
|
value: channel.id,
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
|
@@ -1,28 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
IService,
|
IService,
|
||||||
IAuthentication,
|
IAuthentication,
|
||||||
IApp,
|
IConnection,
|
||||||
IJSONObject,
|
IFlow,
|
||||||
|
IStep,
|
||||||
} from '@automatisch/types';
|
} from '@automatisch/types';
|
||||||
import Authentication from './authentication';
|
import Authentication from './authentication';
|
||||||
import Triggers from './triggers';
|
import Triggers from './triggers';
|
||||||
import Actions from './actions';
|
import Actions from './actions';
|
||||||
import Data from './data';
|
import Data from './data';
|
||||||
|
import SlackClient from './client';
|
||||||
|
|
||||||
export default class Slack implements IService {
|
export default class Slack implements IService {
|
||||||
|
client: SlackClient;
|
||||||
|
|
||||||
authenticationClient: IAuthentication;
|
authenticationClient: IAuthentication;
|
||||||
triggers: Triggers;
|
triggers: Triggers;
|
||||||
actions: Actions;
|
actions: Actions;
|
||||||
data: Data;
|
data: Data;
|
||||||
|
|
||||||
constructor(
|
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
|
||||||
appData: IApp,
|
this.client = new SlackClient(connection, flow, step);
|
||||||
connectionData: IJSONObject,
|
|
||||||
parameters: IJSONObject
|
this.authenticationClient = new Authentication(this.client);
|
||||||
) {
|
// this.triggers = new Triggers(this.client);
|
||||||
this.authenticationClient = new Authentication(appData, connectionData);
|
this.actions = new Actions(this.client);
|
||||||
this.data = new Data(connectionData);
|
this.data = new Data(this.client);
|
||||||
this.triggers = new Triggers(connectionData, parameters);
|
|
||||||
this.actions = new Actions(connectionData, parameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,9 @@
|
|||||||
"key": "slack",
|
"key": "slack",
|
||||||
"iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/slack",
|
"docUrl": "https://automatisch.io/docs/slack",
|
||||||
|
"authDocUrl": "https://automatisch.io/docs/connections/slack",
|
||||||
"primaryColor": "2DAAE1",
|
"primaryColor": "2DAAE1",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "accessToken",
|
"key": "accessToken",
|
||||||
@@ -14,7 +16,6 @@
|
|||||||
"value": null,
|
"value": null,
|
||||||
"placeholder": null,
|
"placeholder": null,
|
||||||
"description": "Access token of slack that Automatisch will connect to.",
|
"description": "Access token of slack that Automatisch will connect to.",
|
||||||
"docUrl": "https://automatisch.io/docs/slack#access-token",
|
|
||||||
"clickToCopy": false
|
"clickToCopy": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -101,11 +102,12 @@
|
|||||||
{
|
{
|
||||||
"name": "New message posted to a channel",
|
"name": "New message posted to a channel",
|
||||||
"key": "newMessageToChannel",
|
"key": "newMessageToChannel",
|
||||||
|
"pollInterval": 15,
|
||||||
"description": "Triggers when a new message is posted to a channel",
|
"description": "Triggers when a new message is posted to a channel",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -163,8 +165,8 @@
|
|||||||
"description": "Send a message to a specific channel you specify.",
|
"description": "Send a message to a specific channel you specify.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "setupAction",
|
"key": "setupAction",
|
||||||
@@ -203,6 +205,73 @@
|
|||||||
"name": "Test action"
|
"name": "Test action"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Find message",
|
||||||
|
"key": "findMessage",
|
||||||
|
"description": "Find a Slack message using the Slack Search feature.",
|
||||||
|
"substeps": [
|
||||||
|
{
|
||||||
|
"key": "chooseConnection",
|
||||||
|
"name": "Choose connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "setupAction",
|
||||||
|
"name": "Set up action",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"label": "Search Query",
|
||||||
|
"key": "query",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "Search query to use for finding matching messages. See the Slack Search Documentation for more information on constructing a query.",
|
||||||
|
"variables": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Sort by",
|
||||||
|
"key": "sortBy",
|
||||||
|
"type": "dropdown",
|
||||||
|
"description": "Sort messages by their match strength or by their date. Default is score.",
|
||||||
|
"required": true,
|
||||||
|
"value": "score",
|
||||||
|
"variables": false,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Match strength",
|
||||||
|
"value": "score"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Message date time",
|
||||||
|
"value": "timestamp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Sort direction",
|
||||||
|
"key": "sortDirection",
|
||||||
|
"type": "dropdown",
|
||||||
|
"description": "Sort matching messages in ascending or descending order. Default is descending.",
|
||||||
|
"required": true,
|
||||||
|
"value": "desc",
|
||||||
|
"variables": false,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Descending (newest or best match first)",
|
||||||
|
"value": "desc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ascending (oldest or worst match first)",
|
||||||
|
"value": "asc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "testStep",
|
||||||
|
"name": "Test action"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/smtp/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/smtp/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/smtp",
|
"docUrl": "https://automatisch.io/docs/smtp",
|
||||||
"primaryColor": "2DAAE1",
|
"primaryColor": "2DAAE1",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "host",
|
"key": "host",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/twilio/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/twilio/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/twilio",
|
"docUrl": "https://automatisch.io/docs/twilio",
|
||||||
"primaryColor": "f22f46",
|
"primaryColor": "f22f46",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "accountSid",
|
"key": "accountSid",
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/twitch/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/twitch/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/twitch",
|
"docUrl": "https://automatisch.io/docs/twitch",
|
||||||
"primaryColor": "2DAAE1",
|
"primaryColor": "2DAAE1",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
|
import TwitterClient from './client';
|
||||||
import CreateTweet from './actions/create-tweet';
|
import CreateTweet from './actions/create-tweet';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
|
|
||||||
export default class Actions {
|
export default class Actions {
|
||||||
|
client: TwitterClient;
|
||||||
createTweet: CreateTweet;
|
createTweet: CreateTweet;
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
constructor(client: TwitterClient) {
|
||||||
this.createTweet = new CreateTweet(connectionData, parameters);
|
this.client = client;
|
||||||
|
this.createTweet = new CreateTweet(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,17 @@
|
|||||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
import TwitterClient from '../client';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
|
|
||||||
export default class CreateTweet {
|
export default class CreateTweet {
|
||||||
client: TwitterApi;
|
client: TwitterClient;
|
||||||
parameters: IJSONObject;
|
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
constructor(client: TwitterClient) {
|
||||||
this.client = new TwitterApi({
|
this.client = client;
|
||||||
appKey: connectionData.consumerKey,
|
|
||||||
appSecret: connectionData.consumerSecret,
|
|
||||||
accessToken: connectionData.accessToken,
|
|
||||||
accessSecret: connectionData.accessSecret,
|
|
||||||
} as TwitterApiTokens);
|
|
||||||
|
|
||||||
this.parameters = parameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const tweet = await this.client.v1.tweet(this.parameters.tweet as string);
|
const tweet = await this.client.createTweet.run(
|
||||||
|
this.client.step.parameters.tweet as string
|
||||||
|
);
|
||||||
|
|
||||||
return tweet;
|
return tweet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,65 +1,50 @@
|
|||||||
import type {
|
import type { IAuthentication, IField } from '@automatisch/types';
|
||||||
IAuthentication,
|
import { URLSearchParams } from 'url';
|
||||||
IApp,
|
import TwitterClient from './client';
|
||||||
IField,
|
|
||||||
IJSONObject,
|
|
||||||
} from '@automatisch/types';
|
|
||||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
|
||||||
|
|
||||||
export default class Authentication implements IAuthentication {
|
export default class Authentication implements IAuthentication {
|
||||||
appData: IApp;
|
client: TwitterClient;
|
||||||
connectionData: IJSONObject;
|
|
||||||
client: TwitterApi;
|
|
||||||
|
|
||||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
constructor(client: TwitterClient) {
|
||||||
this.appData = appData;
|
this.client = client;
|
||||||
this.connectionData = connectionData;
|
|
||||||
|
|
||||||
const clientParams = {
|
|
||||||
appKey: connectionData.consumerKey,
|
|
||||||
appSecret: connectionData.consumerSecret,
|
|
||||||
accessToken: connectionData.accessToken,
|
|
||||||
accessSecret: connectionData.accessSecret,
|
|
||||||
} as TwitterApiTokens;
|
|
||||||
|
|
||||||
this.client = new TwitterApi(clientParams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAuthData() {
|
async createAuthData() {
|
||||||
const appFields = this.appData.fields.find(
|
const appFields = this.client.connection.appData.fields.find(
|
||||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||||
);
|
);
|
||||||
const callbackUrl = appFields.value;
|
const callbackUrl = appFields.value;
|
||||||
|
|
||||||
const authLink = await this.client.generateAuthLink(callbackUrl);
|
const response = await this.client.oauthRequestToken.run(callbackUrl);
|
||||||
|
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: authLink.url,
|
url: `${TwitterClient.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
|
||||||
accessToken: authLink.oauth_token,
|
accessToken: responseData.oauth_token,
|
||||||
accessSecret: authLink.oauth_token_secret,
|
accessSecret: responseData.oauth_token_secret,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyCredentials() {
|
async verifyCredentials() {
|
||||||
const verifiedCredentials = await this.client.login(
|
const response = await this.client.verifyAccessToken.run();
|
||||||
this.connectionData.oauthVerifier as string
|
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
consumerKey: this.connectionData.consumerKey,
|
consumerKey: this.client.connection.formattedData.consumerKey as string,
|
||||||
consumerSecret: this.connectionData.consumerSecret,
|
consumerSecret: this.client.connection.formattedData
|
||||||
accessToken: verifiedCredentials.accessToken,
|
.consumerSecret as string,
|
||||||
accessSecret: verifiedCredentials.accessSecret,
|
accessToken: responseData.oauth_token,
|
||||||
userId: verifiedCredentials.userId,
|
accessSecret: responseData.oauth_token_secret,
|
||||||
screenName: verifiedCredentials.screenName,
|
userId: responseData.user_id,
|
||||||
|
screenName: responseData.screen_name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async isStillVerified() {
|
async isStillVerified() {
|
||||||
try {
|
try {
|
||||||
await this.client.currentUser();
|
await this.client.getCurrentUser.run();
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,40 @@
|
|||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class CreateTweet {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(text: string) {
|
||||||
|
try {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connection.formattedData.accessToken as string,
|
||||||
|
secret: this.client.connection.formattedData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}/2/tweets`,
|
||||||
|
method: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await this.client.httpClient.post(
|
||||||
|
`/2/tweets`,
|
||||||
|
{ text },
|
||||||
|
{ headers: { ...authHeader } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const tweet = response.data.data;
|
||||||
|
|
||||||
|
return tweet;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response.data.detail;
|
||||||
|
throw new Error(`Error occured while creating a tweet: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class GetCurrentUser {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connection.formattedData.accessToken as string,
|
||||||
|
secret: this.client.connection.formattedData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPath = '/2/users/me';
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}${requestPath}`,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await this.client.httpClient.get(requestPath, {
|
||||||
|
headers: { ...authHeader },
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUser = response.data.data;
|
||||||
|
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class GetUserByUsername {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(username: string) {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connection.formattedData.accessToken as string,
|
||||||
|
secret: this.client.connection.formattedData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPath = `/2/users/by/username/${username}`;
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}${requestPath}`,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await this.client.httpClient.get(requestPath, {
|
||||||
|
headers: { ...authHeader },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data?.errors) {
|
||||||
|
const errorMessages = response.data.errors
|
||||||
|
.map((error: IJSONObject) => error.detail)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while fetching user data: ${errorMessages}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = response.data.data;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import TwitterClient from '../index';
|
||||||
|
import omitBy from 'lodash/omitBy';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
|
||||||
|
export default class GetUserFollowers {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(userId: string, lastInternalId?: string) {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connection.formattedData.accessToken as string,
|
||||||
|
secret: this.client.connection.formattedData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
const followers: IJSONObject[] = [];
|
||||||
|
|
||||||
|
do {
|
||||||
|
const params: IJSONObject = {
|
||||||
|
pagination_token: response?.data?.meta?.next_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
|
||||||
|
|
||||||
|
const requestPath = `/2/users/${userId}/followers${
|
||||||
|
queryParams.toString() ? `?${queryParams.toString()}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}${requestPath}`,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
response = await this.client.httpClient.get(requestPath, {
|
||||||
|
headers: { ...authHeader },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.meta.result_count > 0) {
|
||||||
|
response.data.data.forEach((tweet: IJSONObject) => {
|
||||||
|
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
|
||||||
|
followers.push(tweet);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (response.data.meta.next_token && lastInternalId);
|
||||||
|
|
||||||
|
if (response.data?.errors) {
|
||||||
|
const errorMessages = response.data.errors
|
||||||
|
.map((error: IJSONObject) => error.detail)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while fetching user data: ${errorMessages}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import TwitterClient from '../index';
|
||||||
|
import omitBy from 'lodash/omitBy';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
|
||||||
|
export default class GetUserTweets {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(userId: string, lastInternalId?: string) {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connection.formattedData.accessToken as string,
|
||||||
|
secret: this.client.connection.formattedData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
const tweets: IJSONObject[] = [];
|
||||||
|
|
||||||
|
do {
|
||||||
|
const params: IJSONObject = {
|
||||||
|
since_id: lastInternalId,
|
||||||
|
pagination_token: response?.data?.meta?.next_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
|
||||||
|
|
||||||
|
const requestPath = `/2/users/${userId}/tweets${
|
||||||
|
queryParams.toString() ? `?${queryParams.toString()}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}${requestPath}`,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
response = await this.client.httpClient.get(requestPath, {
|
||||||
|
headers: { ...authHeader },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.meta.result_count > 0) {
|
||||||
|
response.data.data.forEach((tweet: IJSONObject) => {
|
||||||
|
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
|
||||||
|
tweets.push(tweet);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (response.data.meta.next_token && lastInternalId);
|
||||||
|
|
||||||
|
if (response.data?.errors) {
|
||||||
|
const errorMessages = response.data.errors
|
||||||
|
.map((error: IJSONObject) => error.detail)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while fetching user data: ${errorMessages}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class OAuthRequestToken {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(callbackUrl: string) {
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}/oauth/request_token`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { oauth_callback: callbackUrl },
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData)
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await this.client.httpClient.post(
|
||||||
|
`/oauth/request_token`,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
headers: { ...authHeader },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessages = error.response.data.errors
|
||||||
|
.map((error: IJSONObject) => error.message)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Error occured while verifying credentials: ${errorMessages}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import TwitterClient from '../index';
|
||||||
|
import omitBy from 'lodash/omitBy';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import qs from 'qs';
|
||||||
|
|
||||||
|
export default class SearchTweets {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(searchTerm: string, lastInternalId?: string) {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connection.formattedData.accessToken as string,
|
||||||
|
secret: this.client.connection.formattedData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
const tweets: IJSONObject[] = [];
|
||||||
|
|
||||||
|
do {
|
||||||
|
const params: IJSONObject = {
|
||||||
|
query: searchTerm,
|
||||||
|
since_id: lastInternalId,
|
||||||
|
pagination_token: response?.data?.meta?.next_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = qs.stringify(omitBy(params, isEmpty));
|
||||||
|
|
||||||
|
const requestPath = `/2/tweets/search/recent${
|
||||||
|
queryParams.toString() ? `?${queryParams.toString()}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
url: `${TwitterClient.baseUrl}${requestPath}`,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = this.client.oauthClient.toHeader(
|
||||||
|
this.client.oauthClient.authorize(requestData, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
response = await this.client.httpClient.get(requestPath, {
|
||||||
|
headers: { ...authHeader },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
if (response.data.meta.result_count > 0) {
|
||||||
|
response.data.data.forEach((tweet: IJSONObject) => {
|
||||||
|
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
|
||||||
|
tweets.push(tweet);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (response.data.meta.next_token && lastInternalId);
|
||||||
|
|
||||||
|
if (response.data?.errors) {
|
||||||
|
const errors = response.data.errors;
|
||||||
|
return { errors, data: tweets };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: tweets };
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class VerifyAccessToken {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
try {
|
||||||
|
return await this.client.httpClient.post(
|
||||||
|
`/oauth/access_token?oauth_verifier=${this.client.connection.formattedData.oauthVerifier}&oauth_token=${this.client.connection.formattedData.accessToken}`,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
packages/backend/src/apps/twitter/client/index.ts
Normal file
64
packages/backend/src/apps/twitter/client/index.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { IFlow, IStep, IConnection } from '@automatisch/types';
|
||||||
|
import OAuth from 'oauth-1.0a';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import HttpClient from '../../../helpers/http-client';
|
||||||
|
import OAuthRequestToken from './endpoints/oauth-request-token';
|
||||||
|
import VerifyAccessToken from './endpoints/verify-access-token';
|
||||||
|
import GetCurrentUser from './endpoints/get-current-user';
|
||||||
|
import GetUserByUsername from './endpoints/get-user-by-username';
|
||||||
|
import GetUserTweets from './endpoints/get-user-tweets';
|
||||||
|
import CreateTweet from './endpoints/create-tweet';
|
||||||
|
import SearchTweets from './endpoints/search-tweets';
|
||||||
|
import GetUserFollowers from './endpoints/get-user-followers';
|
||||||
|
|
||||||
|
export default class TwitterClient {
|
||||||
|
flow: IFlow;
|
||||||
|
step: IStep;
|
||||||
|
connection: IConnection;
|
||||||
|
oauthClient: OAuth;
|
||||||
|
httpClient: HttpClient;
|
||||||
|
|
||||||
|
oauthRequestToken: OAuthRequestToken;
|
||||||
|
verifyAccessToken: VerifyAccessToken;
|
||||||
|
getCurrentUser: GetCurrentUser;
|
||||||
|
getUserByUsername: GetUserByUsername;
|
||||||
|
getUserTweets: GetUserTweets;
|
||||||
|
createTweet: CreateTweet;
|
||||||
|
searchTweets: SearchTweets;
|
||||||
|
getUserFollowers: GetUserFollowers;
|
||||||
|
|
||||||
|
static baseUrl = 'https://api.twitter.com';
|
||||||
|
|
||||||
|
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
|
||||||
|
this.connection = connection;
|
||||||
|
this.flow = flow;
|
||||||
|
this.step = step;
|
||||||
|
|
||||||
|
this.httpClient = new HttpClient({ baseURL: TwitterClient.baseUrl });
|
||||||
|
|
||||||
|
const consumerData = {
|
||||||
|
key: this.connection.formattedData.consumerKey as string,
|
||||||
|
secret: this.connection.formattedData.consumerSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.oauthClient = new OAuth({
|
||||||
|
consumer: consumerData,
|
||||||
|
signature_method: 'HMAC-SHA1',
|
||||||
|
hash_function(base_string, key) {
|
||||||
|
return crypto
|
||||||
|
.createHmac('sha1', key)
|
||||||
|
.update(base_string)
|
||||||
|
.digest('base64');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.oauthRequestToken = new OAuthRequestToken(this);
|
||||||
|
this.verifyAccessToken = new VerifyAccessToken(this);
|
||||||
|
this.getCurrentUser = new GetCurrentUser(this);
|
||||||
|
this.getUserByUsername = new GetUserByUsername(this);
|
||||||
|
this.getUserTweets = new GetUserTweets(this);
|
||||||
|
this.createTweet = new CreateTweet(this);
|
||||||
|
this.searchTweets = new SearchTweets(this);
|
||||||
|
this.getUserFollowers = new GetUserFollowers(this);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,25 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
IService,
|
IService,
|
||||||
IAuthentication,
|
IAuthentication,
|
||||||
IApp,
|
IFlow,
|
||||||
IJSONObject,
|
IStep,
|
||||||
|
IConnection,
|
||||||
} from '@automatisch/types';
|
} from '@automatisch/types';
|
||||||
import Authentication from './authentication';
|
import Authentication from './authentication';
|
||||||
import Triggers from './triggers';
|
import Triggers from './triggers';
|
||||||
import Actions from './actions';
|
import Actions from './actions';
|
||||||
|
import TwitterClient from './client';
|
||||||
|
|
||||||
export default class Twitter implements IService {
|
export default class Twitter implements IService {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
authenticationClient: IAuthentication;
|
authenticationClient: IAuthentication;
|
||||||
triggers: Triggers;
|
triggers: Triggers;
|
||||||
actions: Actions;
|
actions: Actions;
|
||||||
|
|
||||||
constructor(
|
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
|
||||||
appData: IApp,
|
this.client = new TwitterClient(connection, flow, step);
|
||||||
connectionData: IJSONObject,
|
|
||||||
parameters: IJSONObject
|
this.authenticationClient = new Authentication(this.client);
|
||||||
) {
|
this.triggers = new Triggers(this.client);
|
||||||
this.authenticationClient = new Authentication(appData, connectionData);
|
this.actions = new Actions(this.client);
|
||||||
this.triggers = new Triggers(connectionData, parameters);
|
|
||||||
this.actions = new Actions(connectionData, parameters);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,9 @@
|
|||||||
"key": "twitter",
|
"key": "twitter",
|
||||||
"iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/twitter",
|
"docUrl": "https://automatisch.io/docs/twitter",
|
||||||
|
"authDocUrl": "https://automatisch.io/docs/connections/twitter",
|
||||||
"primaryColor": "2DAAE1",
|
"primaryColor": "2DAAE1",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
@@ -14,31 +16,28 @@
|
|||||||
"value": "{WEB_APP_URL}/app/twitter/connections/add",
|
"value": "{WEB_APP_URL}/app/twitter/connections/add",
|
||||||
"placeholder": null,
|
"placeholder": null,
|
||||||
"description": "When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.",
|
"description": "When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.",
|
||||||
"docUrl": "https://automatisch.io/docs/twitter#oauth-redirect-url",
|
|
||||||
"clickToCopy": true
|
"clickToCopy": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "consumerKey",
|
"key": "consumerKey",
|
||||||
"label": "Consumer Key",
|
"label": "API Key",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": true,
|
"required": true,
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"value": null,
|
"value": null,
|
||||||
"placeholder": null,
|
"placeholder": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"docUrl": "https://automatisch.io/docs/twitter#consumer-key",
|
|
||||||
"clickToCopy": false
|
"clickToCopy": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "consumerSecret",
|
"key": "consumerSecret",
|
||||||
"label": "Consumer Secret",
|
"label": "API Secret",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": true,
|
"required": true,
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"value": null,
|
"value": null,
|
||||||
"placeholder": null,
|
"placeholder": null,
|
||||||
"description": null,
|
"description": null,
|
||||||
"docUrl": "https://automatisch.io/docs/twitter#consumer-secret",
|
|
||||||
"clickToCopy": false
|
"clickToCopy": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -217,13 +216,14 @@
|
|||||||
],
|
],
|
||||||
"triggers": [
|
"triggers": [
|
||||||
{
|
{
|
||||||
"name": "My Tweet",
|
"name": "My Tweets",
|
||||||
"key": "myTweet",
|
"key": "myTweets",
|
||||||
|
"pollInterval": 15,
|
||||||
"description": "Will be triggered when you tweet something new.",
|
"description": "Will be triggered when you tweet something new.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "testStep",
|
"key": "testStep",
|
||||||
@@ -232,13 +232,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "User Tweet",
|
"name": "User Tweets",
|
||||||
"key": "userTweet",
|
"key": "userTweets",
|
||||||
|
"pollInterval": 15,
|
||||||
"description": "Will be triggered when a specific user tweet something new.",
|
"description": "Will be triggered when a specific user tweet something new.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -259,13 +260,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Search Tweet",
|
"name": "Search Tweets",
|
||||||
"key": "searchTweet",
|
"key": "searchTweets",
|
||||||
|
"pollInterval": 15,
|
||||||
"description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.",
|
"description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseTrigger",
|
"key": "chooseTrigger",
|
||||||
@@ -284,6 +286,22 @@
|
|||||||
"name": "Test trigger"
|
"name": "Test trigger"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "New follower of me",
|
||||||
|
"key": "myFollowers",
|
||||||
|
"pollInterval": 15,
|
||||||
|
"description": "Will be triggered when you have a new follower.",
|
||||||
|
"substeps": [
|
||||||
|
{
|
||||||
|
"key": "chooseConnection",
|
||||||
|
"name": "Choose connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "testStep",
|
||||||
|
"name": "Test trigger"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
@@ -293,8 +311,8 @@
|
|||||||
"description": "Will create a tweet.",
|
"description": "Will create a tweet.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
"key": "chooseAccount",
|
"key": "chooseConnection",
|
||||||
"name": "Choose account"
|
"name": "Choose connection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "chooseAction",
|
"key": "chooseAction",
|
||||||
|
@@ -1,13 +1,21 @@
|
|||||||
import { IJSONObject } from '@automatisch/types';
|
import TwitterClient from './client';
|
||||||
import MyTweet from './triggers/my-tweet';
|
import UserTweets from './triggers/user-tweets';
|
||||||
import SearchTweet from './triggers/search-tweet';
|
import SearchTweets from './triggers/search-tweets';
|
||||||
|
import MyTweets from './triggers/my-tweets';
|
||||||
|
import MyFollowers from './triggers/my-followers';
|
||||||
|
|
||||||
export default class Triggers {
|
export default class Triggers {
|
||||||
myTweet: MyTweet;
|
client: TwitterClient;
|
||||||
searchTweet: SearchTweet;
|
userTweets: UserTweets;
|
||||||
|
searchTweets: SearchTweets;
|
||||||
|
myTweets: MyTweets;
|
||||||
|
myFollowers: MyFollowers;
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
constructor(client: TwitterClient) {
|
||||||
this.myTweet = new MyTweet(connectionData);
|
this.client = client;
|
||||||
this.searchTweet = new SearchTweet(connectionData, parameters);
|
this.userTweets = new UserTweets(client);
|
||||||
|
this.searchTweets = new SearchTweets(client);
|
||||||
|
this.myTweets = new MyTweets(client);
|
||||||
|
this.myFollowers = new MyFollowers(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
packages/backend/src/apps/twitter/triggers/my-followers.ts
Normal file
28
packages/backend/src/apps/twitter/triggers/my-followers.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import TwitterClient from '../client';
|
||||||
|
|
||||||
|
export default class MyFollowers {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(lastInternalId: string) {
|
||||||
|
return this.getFollowers(lastInternalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testRun() {
|
||||||
|
return this.getFollowers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFollowers(lastInternalId?: string) {
|
||||||
|
const { username } = await this.client.getCurrentUser.run();
|
||||||
|
const user = await this.client.getUserByUsername.run(username as string);
|
||||||
|
|
||||||
|
const tweets = await this.client.getUserFollowers.run(
|
||||||
|
user.id,
|
||||||
|
lastInternalId
|
||||||
|
);
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,25 +0,0 @@
|
|||||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
|
|
||||||
export default class MyTweet {
|
|
||||||
client: TwitterApi;
|
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject) {
|
|
||||||
this.client = new TwitterApi({
|
|
||||||
appKey: connectionData.consumerKey,
|
|
||||||
appSecret: connectionData.consumerSecret,
|
|
||||||
accessToken: connectionData.accessToken,
|
|
||||||
accessSecret: connectionData.accessSecret,
|
|
||||||
} as TwitterApiTokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
const response = await this.client.currentUser();
|
|
||||||
const username = response.screen_name;
|
|
||||||
|
|
||||||
const userTimeline = await this.client.v1.userTimelineByUsername(username);
|
|
||||||
const fetchedTweets = userTimeline.tweets;
|
|
||||||
|
|
||||||
return fetchedTweets;
|
|
||||||
}
|
|
||||||
}
|
|
25
packages/backend/src/apps/twitter/triggers/my-tweets.ts
Normal file
25
packages/backend/src/apps/twitter/triggers/my-tweets.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import TwitterClient from '../client';
|
||||||
|
|
||||||
|
export default class MyTweets {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(lastInternalId: string) {
|
||||||
|
return this.getTweets(lastInternalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testRun() {
|
||||||
|
return this.getTweets();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTweets(lastInternalId?: string) {
|
||||||
|
const { username } = await this.client.getCurrentUser.run();
|
||||||
|
const user = await this.client.getUserByUsername.run(username as string);
|
||||||
|
|
||||||
|
const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,58 +0,0 @@
|
|||||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
|
||||||
import { IJSONObject } from '@automatisch/types';
|
|
||||||
|
|
||||||
export default class SearchTweet {
|
|
||||||
client: TwitterApi;
|
|
||||||
parameters: IJSONObject;
|
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
|
||||||
this.client = new TwitterApi({
|
|
||||||
appKey: connectionData.consumerKey,
|
|
||||||
appSecret: connectionData.consumerSecret,
|
|
||||||
accessToken: connectionData.accessToken,
|
|
||||||
accessSecret: connectionData.accessSecret,
|
|
||||||
} as TwitterApiTokens);
|
|
||||||
|
|
||||||
this.parameters = parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(startTime: Date) {
|
|
||||||
const tweets = [];
|
|
||||||
|
|
||||||
const response = await this.client.v2.search(
|
|
||||||
this.parameters.searchTerm as string,
|
|
||||||
{
|
|
||||||
max_results: 50,
|
|
||||||
'tweet.fields': 'created_at',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
for await (const tweet of response.data.data) {
|
|
||||||
if (new Date(tweet.created_at).getTime() <= startTime.getTime()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
tweets.push(tweet);
|
|
||||||
|
|
||||||
if (response.data.meta.next_token) {
|
|
||||||
await response.fetchNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tweets;
|
|
||||||
}
|
|
||||||
|
|
||||||
async testRun() {
|
|
||||||
const response = await this.client.v2.search(
|
|
||||||
this.parameters.searchTerm as string,
|
|
||||||
{
|
|
||||||
max_results: 10,
|
|
||||||
'tweet.fields': 'created_at',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const mostRecentTweet = response.data.data[0];
|
|
||||||
|
|
||||||
return [mostRecentTweet];
|
|
||||||
}
|
|
||||||
}
|
|
26
packages/backend/src/apps/twitter/triggers/search-tweets.ts
Normal file
26
packages/backend/src/apps/twitter/triggers/search-tweets.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import TwitterClient from '../client';
|
||||||
|
|
||||||
|
export default class SearchTweets {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(lastInternalId: string) {
|
||||||
|
return this.getTweets(lastInternalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testRun() {
|
||||||
|
return this.getTweets();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTweets(lastInternalId?: string) {
|
||||||
|
const tweets = await this.client.searchTweets.run(
|
||||||
|
this.client.step.parameters.searchTerm as string,
|
||||||
|
lastInternalId
|
||||||
|
);
|
||||||
|
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
}
|
27
packages/backend/src/apps/twitter/triggers/user-tweets.ts
Normal file
27
packages/backend/src/apps/twitter/triggers/user-tweets.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import TwitterClient from '../client';
|
||||||
|
|
||||||
|
export default class UserTweets {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(lastInternalId: string) {
|
||||||
|
return this.getTweets(lastInternalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testRun() {
|
||||||
|
return this.getTweets();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTweets(lastInternalId?: string) {
|
||||||
|
const user = await this.client.getUserByUsername.run(
|
||||||
|
this.client.step.parameters.username as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
|
||||||
|
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
}
|
@@ -5,14 +5,12 @@ import type {
|
|||||||
IJSONObject,
|
IJSONObject,
|
||||||
} from '@automatisch/types';
|
} from '@automatisch/types';
|
||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import HttpClient from '../../helpers/http-client';
|
||||||
|
|
||||||
export default class Authentication implements IAuthentication {
|
export default class Authentication implements IAuthentication {
|
||||||
appData: IApp;
|
appData: IApp;
|
||||||
connectionData: IJSONObject;
|
connectionData: IJSONObject;
|
||||||
client: AxiosInstance = axios.create({
|
client: HttpClient;
|
||||||
baseURL: 'https://api.typeform.com',
|
|
||||||
});
|
|
||||||
|
|
||||||
scope: string[] = [
|
scope: string[] = [
|
||||||
'forms:read',
|
'forms:read',
|
||||||
@@ -27,6 +25,7 @@ export default class Authentication implements IAuthentication {
|
|||||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
constructor(appData: IApp, connectionData: IJSONObject) {
|
||||||
this.connectionData = connectionData;
|
this.connectionData = connectionData;
|
||||||
this.appData = appData;
|
this.appData = appData;
|
||||||
|
this.client = new HttpClient({ baseURL: 'https://api.typeform.com' });
|
||||||
}
|
}
|
||||||
|
|
||||||
get oauthRedirectUrl() {
|
get oauthRedirectUrl() {
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
"iconUrl": "{BASE_URL}/apps/typeform/assets/favicon.svg",
|
"iconUrl": "{BASE_URL}/apps/typeform/assets/favicon.svg",
|
||||||
"docUrl": "https://automatisch.io/docs/typeform",
|
"docUrl": "https://automatisch.io/docs/typeform",
|
||||||
"primaryColor": "5865f2",
|
"primaryColor": "5865f2",
|
||||||
|
"supportsConnections": true,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "oAuthRedirectUrl",
|
"key": "oAuthRedirectUrl",
|
||||||
|
@@ -13,6 +13,7 @@ type AppConfig = {
|
|||||||
postgresHost: string;
|
postgresHost: string;
|
||||||
postgresUsername: string;
|
postgresUsername: string;
|
||||||
postgresPassword?: string;
|
postgresPassword?: string;
|
||||||
|
version: string;
|
||||||
postgresEnableSsl: boolean;
|
postgresEnableSsl: boolean;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
encryptionKey: string;
|
encryptionKey: string;
|
||||||
@@ -20,12 +21,14 @@ type AppConfig = {
|
|||||||
serveWebAppSeparately: boolean;
|
serveWebAppSeparately: boolean;
|
||||||
redisHost: string;
|
redisHost: string;
|
||||||
redisPort: number;
|
redisPort: number;
|
||||||
|
enableBullMQDashboard: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const host = process.env.HOST || 'localhost';
|
const host = process.env.HOST || 'localhost';
|
||||||
const protocol = process.env.PROTOCOL || 'http';
|
const protocol = process.env.PROTOCOL || 'http';
|
||||||
const port = process.env.PORT || '3000';
|
const port = process.env.PORT || '3000';
|
||||||
const serveWebAppSeparately = process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
const serveWebAppSeparately =
|
||||||
|
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
||||||
|
|
||||||
let webAppUrl = `${protocol}://${host}:${port}`;
|
let webAppUrl = `${protocol}://${host}:${port}`;
|
||||||
if (serveWebAppSeparately) {
|
if (serveWebAppSeparately) {
|
||||||
@@ -42,8 +45,9 @@ const appConfig: AppConfig = {
|
|||||||
port,
|
port,
|
||||||
appEnv: appEnv,
|
appEnv: appEnv,
|
||||||
isDev: appEnv === 'development',
|
isDev: appEnv === 'development',
|
||||||
|
version: process.env.npm_package_version,
|
||||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||||
postgresPort: parseInt(process.env.POSTGRES_PORT|| '5432'),
|
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||||
postgresHost: process.env.POSTGRES_HOST || 'localhost',
|
postgresHost: process.env.POSTGRES_HOST || 'localhost',
|
||||||
postgresUsername:
|
postgresUsername:
|
||||||
process.env.POSTGRES_USERNAME || 'automatisch_development_user',
|
process.env.POSTGRES_USERNAME || 'automatisch_development_user',
|
||||||
@@ -54,6 +58,8 @@ const appConfig: AppConfig = {
|
|||||||
serveWebAppSeparately,
|
serveWebAppSeparately,
|
||||||
redisHost: process.env.REDIS_HOST || '127.0.0.1',
|
redisHost: process.env.REDIS_HOST || '127.0.0.1',
|
||||||
redisPort: parseInt(process.env.REDIS_PORT || '6379'),
|
redisPort: parseInt(process.env.REDIS_PORT || '6379'),
|
||||||
|
enableBullMQDashboard:
|
||||||
|
process.env.ENABLE_BULLMQ_DASHBOARD === 'true' ? true : false,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
webAppUrl,
|
webAppUrl,
|
||||||
};
|
};
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
// The following two lines are required to get count values as number.
|
||||||
|
// More info: https://github.com/knex/knex/issues/387#issuecomment-51554522
|
||||||
|
import pg from 'pg';
|
||||||
|
pg.types.setTypeParser(20, 'text', parseInt);
|
||||||
import knex from 'knex';
|
import knex from 'knex';
|
||||||
import type { Knex } from 'knex';
|
import type { Knex } from 'knex';
|
||||||
import knexConfig from '../../knexfile';
|
import knexConfig from '../../knexfile';
|
||||||
@@ -8,10 +12,12 @@ export const client: Knex = knex(knexConfig);
|
|||||||
|
|
||||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
||||||
|
|
||||||
client.raw('SELECT 1')
|
client.raw('SELECT 1').catch((err) => {
|
||||||
.catch((err) => {
|
if (err.code === CONNECTION_REFUSED) {
|
||||||
if (err.code === CONNECTION_REFUSED) {
|
logger.error(
|
||||||
logger.error('Make sure you have installed PostgreSQL and it is running.', err);
|
'Make sure you have installed PostgreSQL and it is running.',
|
||||||
process.exit();
|
err
|
||||||
}
|
);
|
||||||
});
|
process.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('connections', (table) => {
|
||||||
|
table.boolean('draft').defaultTo(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('connections', (table) => {
|
||||||
|
table.dropColumn('draft');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('flows', (table) => {
|
||||||
|
table.timestamp('published_at').nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('flows', (table) => {
|
||||||
|
table.dropColumn('published_at');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('executions', (table) => {
|
||||||
|
table.string('internal_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('executions', (table) => {
|
||||||
|
table.dropColumn('internal_id');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('execution_steps', (table) => {
|
||||||
|
table.jsonb('error_details');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.table('execution_steps', (table) => {
|
||||||
|
table.dropColumn('error_details');
|
||||||
|
});
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
import App from '../../models/app';
|
import axios from 'axios';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
@@ -20,13 +20,20 @@ const createAuthData = async (
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const appClass = (await import(`../../apps/${connection.key}`)).default;
|
const appClass = (await import(`../../apps/${connection.key}`)).default;
|
||||||
const appData = App.findOneByKey(connection.key);
|
|
||||||
|
|
||||||
if (!connection.formattedData) { return null; }
|
if (!connection.formattedData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const appInstance = new appClass(appData, connection.formattedData);
|
const appInstance = new appClass(connection);
|
||||||
const authLink = await appInstance.authenticationClient.createAuthData();
|
const authLink = await appInstance.authenticationClient.createAuthData();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.get(authLink.url);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Error occured while creating authorization URL!');
|
||||||
|
}
|
||||||
|
|
||||||
await connection.$query().patch({
|
await connection.$query().patch({
|
||||||
formattedData: {
|
formattedData: {
|
||||||
...connection.formattedData,
|
...connection.formattedData,
|
||||||
|
@@ -13,19 +13,12 @@ const createConnection = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
const app = App.findOneByKey(params.input.key);
|
App.findOneByKey(params.input.key);
|
||||||
|
|
||||||
const connection = await context.currentUser
|
return await context.currentUser.$relatedQuery('connections').insert({
|
||||||
.$relatedQuery('connections')
|
key: params.input.key,
|
||||||
.insert({
|
formattedData: params.input.formattedData,
|
||||||
key: params.input.key,
|
});
|
||||||
formattedData: params.input.formattedData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...connection,
|
|
||||||
app,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createConnection;
|
export default createConnection;
|
||||||
|
@@ -4,6 +4,7 @@ import Context from '../../types/express/context';
|
|||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
triggerAppKey: string;
|
triggerAppKey: string;
|
||||||
|
connectionId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12,17 +13,32 @@ const createFlow = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
const connectionId = params?.input?.connectionId;
|
||||||
const appKey = params?.input?.triggerAppKey;
|
const appKey = params?.input?.triggerAppKey;
|
||||||
|
|
||||||
const flow = await context.currentUser.$relatedQuery('flows').insert({
|
const flow = await context.currentUser.$relatedQuery('flows').insert({
|
||||||
name: 'Name your flow',
|
name: 'Name your flow',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
await context.currentUser
|
||||||
|
.$relatedQuery('connections')
|
||||||
|
.findById(connectionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
await Step.query().insert({
|
await Step.query().insert({
|
||||||
flowId: flow.id,
|
flowId: flow.id,
|
||||||
type: 'trigger',
|
type: 'trigger',
|
||||||
position: 1,
|
position: 1,
|
||||||
appKey,
|
appKey,
|
||||||
|
connectionId
|
||||||
|
});
|
||||||
|
|
||||||
|
await Step.query().insert({
|
||||||
|
flowId: flow.id,
|
||||||
|
type: 'action',
|
||||||
|
position: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
return flow;
|
return flow;
|
||||||
|
@@ -36,9 +36,13 @@ const updateFlowStatus = async (
|
|||||||
const interval = trigger.interval;
|
const interval = trigger.interval;
|
||||||
const repeatOptions = {
|
const repeatOptions = {
|
||||||
cron: interval || EVERY_15_MINUTES_CRON,
|
cron: interval || EVERY_15_MINUTES_CRON,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (flow.active) {
|
if (flow.active) {
|
||||||
|
flow = await flow.$query().patchAndFetch({
|
||||||
|
published_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
await processorQueue.add(
|
await processorQueue.add(
|
||||||
JOB_NAME,
|
JOB_NAME,
|
||||||
{ flowId: flow.id },
|
{ flowId: flow.id },
|
||||||
@@ -49,7 +53,7 @@ const updateFlowStatus = async (
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const repeatableJobs = await processorQueue.getRepeatableJobs();
|
const repeatableJobs = await processorQueue.getRepeatableJobs();
|
||||||
const job = repeatableJobs.find(job => job.id === flow.id);
|
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||||
|
|
||||||
await processorQueue.removeRepeatableByKey(job.key);
|
await processorQueue.removeRepeatableByKey(job.key);
|
||||||
}
|
}
|
||||||
|
@@ -20,9 +20,9 @@ const verifyConnection = async (
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const appClass = (await import(`../../apps/${connection.key}`)).default;
|
const appClass = (await import(`../../apps/${connection.key}`)).default;
|
||||||
const appData = App.findOneByKey(connection.key);
|
const app = App.findOneByKey(connection.key);
|
||||||
|
|
||||||
const appInstance = new appClass(appData, connection.formattedData);
|
const appInstance = new appClass(connection);
|
||||||
const verifiedCredentials =
|
const verifiedCredentials =
|
||||||
await appInstance.authenticationClient.verifyCredentials();
|
await appInstance.authenticationClient.verifyCredentials();
|
||||||
|
|
||||||
@@ -32,9 +32,13 @@ const verifyConnection = async (
|
|||||||
...verifiedCredentials,
|
...verifiedCredentials,
|
||||||
},
|
},
|
||||||
verified: true,
|
verified: true,
|
||||||
|
draft: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return connection;
|
return {
|
||||||
|
...connection,
|
||||||
|
app,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default verifyConnection;
|
export default verifyConnection;
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
import App from '../../models/app';
|
|
||||||
import Context from '../../types/express/context';
|
|
||||||
|
|
||||||
type Params = {
|
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAppConnections = async (
|
|
||||||
_parent: unknown,
|
|
||||||
params: Params,
|
|
||||||
context: Context
|
|
||||||
) => {
|
|
||||||
const app = App.findOneByKey(params.key);
|
|
||||||
|
|
||||||
const connections = await context.currentUser
|
|
||||||
.$relatedQuery('connections')
|
|
||||||
.where({
|
|
||||||
key: params.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
return connections.map((connection) => ({
|
|
||||||
...connection,
|
|
||||||
app,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getAppConnections;
|
|
@@ -11,9 +11,15 @@ const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
|||||||
if (context.currentUser) {
|
if (context.currentUser) {
|
||||||
const connections = await context.currentUser
|
const connections = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
|
.select('connections.*')
|
||||||
|
.fullOuterJoinRelated('steps')
|
||||||
.where({
|
.where({
|
||||||
key: params.key,
|
'connections.key': params.key,
|
||||||
});
|
'connections.draft': false,
|
||||||
|
})
|
||||||
|
.countDistinct('steps.flow_id as flowCount')
|
||||||
|
.groupBy('connections.id')
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
|
@@ -16,22 +16,42 @@ const getConnectedApps = async (
|
|||||||
const connections = await context.currentUser
|
const connections = await context.currentUser
|
||||||
.$relatedQuery('connections')
|
.$relatedQuery('connections')
|
||||||
.select('connections.key')
|
.select('connections.key')
|
||||||
|
.where({ draft: false })
|
||||||
.count('connections.id as count')
|
.count('connections.id as count')
|
||||||
.where({ verified: true })
|
|
||||||
.groupBy('connections.key');
|
.groupBy('connections.key');
|
||||||
|
|
||||||
|
const flows = await context.currentUser
|
||||||
|
.$relatedQuery('flows')
|
||||||
|
.withGraphJoined('steps')
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
|
|
||||||
|
const duplicatedUsedApps = flows
|
||||||
|
.map((flow) => flow.steps.map((step) => step.appKey))
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const connectionKeys = connections.map((connection) => connection.key);
|
const connectionKeys = connections.map((connection) => connection.key);
|
||||||
|
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
|
||||||
|
|
||||||
apps = apps
|
apps = apps
|
||||||
.filter((app: IApp) => connectionKeys.includes(app.key))
|
.filter((app: IApp) => {
|
||||||
|
return usedApps.includes(app.key);
|
||||||
|
})
|
||||||
.map((app: IApp) => {
|
.map((app: IApp) => {
|
||||||
const connection = connections.find(
|
const connection = connections.find(
|
||||||
(connection) => (connection as IConnection).key === app.key
|
(connection) => (connection as IConnection).key === app.key
|
||||||
);
|
);
|
||||||
|
|
||||||
if (connection) {
|
app.connectionCount = connection?.count || 0;
|
||||||
app.connectionCount = connection.count;
|
app.flowCount = 0;
|
||||||
}
|
|
||||||
|
flows.forEach((flow) => {
|
||||||
|
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
|
||||||
|
|
||||||
|
if (usedFlow) {
|
||||||
|
app.flowCount += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
import App from '../../models/app';
|
|
||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
@@ -11,7 +10,10 @@ type Params = {
|
|||||||
const getData = async (_parent: unknown, params: Params, context: Context) => {
|
const getData = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
const step = await context.currentUser
|
const step = await context.currentUser
|
||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.withGraphFetched('connection')
|
.withGraphFetched({
|
||||||
|
connection: true,
|
||||||
|
flow: true,
|
||||||
|
})
|
||||||
.findById(params.stepId);
|
.findById(params.stepId);
|
||||||
|
|
||||||
if (!step) return null;
|
if (!step) return null;
|
||||||
@@ -20,10 +22,9 @@ const getData = async (_parent: unknown, params: Params, context: Context) => {
|
|||||||
|
|
||||||
if (!connection || !step.appKey) return null;
|
if (!connection || !step.appKey) return null;
|
||||||
|
|
||||||
const appData = App.findOneByKey(step.appKey);
|
|
||||||
const AppClass = (await import(`../../apps/${step.appKey}`)).default;
|
const AppClass = (await import(`../../apps/${step.appKey}`)).default;
|
||||||
|
const appInstance = new AppClass(connection, step.flow, step);
|
||||||
|
|
||||||
const appInstance = new AppClass(appData, connection.formattedData, params.parameters);
|
|
||||||
const command = appInstance.data[params.key];
|
const command = appInstance.data[params.key];
|
||||||
const fetchedData = await command.run();
|
const fetchedData = await command.run();
|
||||||
|
|
||||||
|
25
packages/backend/src/graphql/queries/get-execution.ts
Normal file
25
packages/backend/src/graphql/queries/get-execution.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Context from '../../types/express/context';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExecution = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
const execution = await context.currentUser
|
||||||
|
.$relatedQuery('executions')
|
||||||
|
.withGraphFetched({
|
||||||
|
flow: {
|
||||||
|
steps: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.findById(params.executionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return execution;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getExecution;
|
@@ -13,7 +13,11 @@ const getExecutions = async (
|
|||||||
) => {
|
) => {
|
||||||
const executions = context.currentUser
|
const executions = context.currentUser
|
||||||
.$relatedQuery('executions')
|
.$relatedQuery('executions')
|
||||||
.withGraphFetched('flow')
|
.withGraphFetched({
|
||||||
|
flow: {
|
||||||
|
steps: true
|
||||||
|
}
|
||||||
|
})
|
||||||
.orderBy('created_at', 'desc');
|
.orderBy('created_at', 'desc');
|
||||||
|
|
||||||
return paginate(executions, params.limit, params.offset);
|
return paginate(executions, params.limit, params.offset);
|
||||||
|
@@ -1,22 +1,42 @@
|
|||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
|
import paginate from '../../helpers/pagination';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
appKey?: string;
|
appKey?: string;
|
||||||
|
connectionId?: string;
|
||||||
|
name?: string;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
const flowsQuery = context.currentUser
|
const flowsQuery = context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.withGraphJoined('[steps.[connection]]')
|
.joinRelated({
|
||||||
.orderBy('created_at', 'desc');
|
steps: true
|
||||||
|
})
|
||||||
|
.withGraphFetched({
|
||||||
|
steps: {
|
||||||
|
connection: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((builder) => {
|
||||||
|
if (params.connectionId) {
|
||||||
|
builder.where('steps.connection_id', params.connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
if (params.appKey) {
|
if (params.name) {
|
||||||
flowsQuery.where('steps.app_key', params.appKey);
|
builder.where('flows.name', 'ilike', `%${params.name}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const flows = await flowsQuery;
|
if (params.appKey) {
|
||||||
|
builder.where('steps.app_key', params.appKey);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.groupBy('flows.id')
|
||||||
|
.orderBy('updated_at', 'desc');
|
||||||
|
|
||||||
return flows;
|
return paginate(flowsQuery, params.limit, params.offset);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getFlows;
|
export default getFlows;
|
||||||
|
9
packages/backend/src/graphql/queries/healthcheck.ts
Normal file
9
packages/backend/src/graphql/queries/healthcheck.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
const healthcheck = () => {
|
||||||
|
return {
|
||||||
|
version: appConfig.version,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default healthcheck;
|
@@ -1,5 +1,4 @@
|
|||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
import App from '../../models/app';
|
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,9 +18,8 @@ const testConnection = async (
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const appClass = (await import(`../../apps/${connection.key}`)).default;
|
const appClass = (await import(`../../apps/${connection.key}`)).default;
|
||||||
const appData = App.findOneByKey(connection.key);
|
const appInstance = new appClass(connection);
|
||||||
|
|
||||||
const appInstance = new appClass(appData, connection.formattedData);
|
|
||||||
const isStillVerified =
|
const isStillVerified =
|
||||||
await appInstance.authenticationClient.isStillVerified();
|
await appInstance.authenticationClient.isStillVerified();
|
||||||
|
|
||||||
|
@@ -1,29 +1,31 @@
|
|||||||
import getApps from './queries/get-apps';
|
import getApps from './queries/get-apps';
|
||||||
import getApp from './queries/get-app';
|
import getApp from './queries/get-app';
|
||||||
import getConnectedApps from './queries/get-connected-apps';
|
import getConnectedApps from './queries/get-connected-apps';
|
||||||
import getAppConnections from './queries/get-app-connections';
|
|
||||||
import testConnection from './queries/test-connection';
|
import testConnection from './queries/test-connection';
|
||||||
import getFlow from './queries/get-flow';
|
import getFlow from './queries/get-flow';
|
||||||
import getFlows from './queries/get-flows';
|
import getFlows from './queries/get-flows';
|
||||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||||
|
import getExecution from './queries/get-execution';
|
||||||
import getExecutions from './queries/get-executions';
|
import getExecutions from './queries/get-executions';
|
||||||
import getExecutionSteps from './queries/get-execution-steps';
|
import getExecutionSteps from './queries/get-execution-steps';
|
||||||
import getData from './queries/get-data';
|
import getData from './queries/get-data';
|
||||||
import getCurrentUser from './queries/get-current-user';
|
import getCurrentUser from './queries/get-current-user';
|
||||||
|
import healthcheck from './queries/healthcheck';
|
||||||
|
|
||||||
const queryResolvers = {
|
const queryResolvers = {
|
||||||
getApps,
|
getApps,
|
||||||
getApp,
|
getApp,
|
||||||
getConnectedApps,
|
getConnectedApps,
|
||||||
getAppConnections,
|
|
||||||
testConnection,
|
testConnection,
|
||||||
getFlow,
|
getFlow,
|
||||||
getFlows,
|
getFlows,
|
||||||
getStepWithTestExecutions,
|
getStepWithTestExecutions,
|
||||||
|
getExecution,
|
||||||
getExecutions,
|
getExecutions,
|
||||||
getExecutionSteps,
|
getExecutionSteps,
|
||||||
getData,
|
getData,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
|
healthcheck,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default queryResolvers;
|
export default queryResolvers;
|
||||||
|
@@ -2,11 +2,17 @@ type Query {
|
|||||||
getApps(name: String, onlyWithTriggers: Boolean): [App]
|
getApps(name: String, onlyWithTriggers: Boolean): [App]
|
||||||
getApp(key: AvailableAppsEnumType!): App
|
getApp(key: AvailableAppsEnumType!): App
|
||||||
getConnectedApps(name: String): [App]
|
getConnectedApps(name: String): [App]
|
||||||
getAppConnections(key: AvailableAppsEnumType!): [Connection]
|
|
||||||
testConnection(id: String!): Connection
|
testConnection(id: String!): Connection
|
||||||
getFlow(id: String!): Flow
|
getFlow(id: String!): Flow
|
||||||
getFlows(appKey: String): [Flow]
|
getFlows(
|
||||||
|
limit: Int!
|
||||||
|
offset: Int!
|
||||||
|
appKey: String
|
||||||
|
connectionId: String
|
||||||
|
name: String
|
||||||
|
): FlowConnection
|
||||||
getStepWithTestExecutions(stepId: String!): [Step]
|
getStepWithTestExecutions(stepId: String!): [Step]
|
||||||
|
getExecution(executionId: String!): Execution
|
||||||
getExecutions(limit: Int!, offset: Int!): ExecutionConnection
|
getExecutions(limit: Int!, offset: Int!): ExecutionConnection
|
||||||
getExecutionSteps(
|
getExecutionSteps(
|
||||||
executionId: String!
|
executionId: String!
|
||||||
@@ -15,6 +21,7 @@ type Query {
|
|||||||
): ExecutionStepConnection
|
): ExecutionStepConnection
|
||||||
getData(stepId: String!, key: String!, parameters: JSONObject): JSONObject
|
getData(stepId: String!, key: String!, parameters: JSONObject): JSONObject
|
||||||
getCurrentUser: User
|
getCurrentUser: User
|
||||||
|
healthcheck: AppHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@@ -66,6 +73,7 @@ type ActionSubstepArgument {
|
|||||||
description: String
|
description: String
|
||||||
required: Boolean
|
required: Boolean
|
||||||
variables: Boolean
|
variables: Boolean
|
||||||
|
options: [ActionSubstepArgumentOption]
|
||||||
source: ActionSubstepArgumentSource
|
source: ActionSubstepArgumentSource
|
||||||
dependsOn: [String]
|
dependsOn: [String]
|
||||||
}
|
}
|
||||||
@@ -76,6 +84,11 @@ type ActionSubstepArgumentSource {
|
|||||||
arguments: [ActionSubstepArgumentSourceArgument]
|
arguments: [ActionSubstepArgumentSourceArgument]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActionSubstepArgumentOption {
|
||||||
|
label: String
|
||||||
|
value: JSONObject
|
||||||
|
}
|
||||||
|
|
||||||
type ActionSubstepArgumentSourceArgument {
|
type ActionSubstepArgumentSourceArgument {
|
||||||
name: String
|
name: String
|
||||||
value: String
|
value: String
|
||||||
@@ -85,9 +98,12 @@ type App {
|
|||||||
name: String
|
name: String
|
||||||
key: String
|
key: String
|
||||||
connectionCount: Int
|
connectionCount: Int
|
||||||
|
flowCount: Int
|
||||||
iconUrl: String
|
iconUrl: String
|
||||||
docUrl: String
|
docUrl: String
|
||||||
|
authDocUrl: String
|
||||||
primaryColor: String
|
primaryColor: String
|
||||||
|
supportsConnections: Boolean
|
||||||
fields: [Field]
|
fields: [Field]
|
||||||
authenticationSteps: [AuthenticationStep]
|
authenticationSteps: [AuthenticationStep]
|
||||||
reconnectionSteps: [ReconnectionStep]
|
reconnectionSteps: [ReconnectionStep]
|
||||||
@@ -142,6 +158,7 @@ enum AvailableAppsEnumType {
|
|||||||
twitter
|
twitter
|
||||||
typeform
|
typeform
|
||||||
slack
|
slack
|
||||||
|
scheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connection {
|
type Connection {
|
||||||
@@ -151,6 +168,7 @@ type Connection {
|
|||||||
verified: Boolean
|
verified: Boolean
|
||||||
app: App
|
app: App
|
||||||
createdAt: String
|
createdAt: String
|
||||||
|
flowCount: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionData {
|
type ConnectionData {
|
||||||
@@ -187,11 +205,22 @@ type Field {
|
|||||||
clickToCopy: Boolean
|
clickToCopy: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FlowConnection {
|
||||||
|
edges: [FlowEdge]
|
||||||
|
pageInfo: PageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlowEdge {
|
||||||
|
node: Flow
|
||||||
|
}
|
||||||
|
|
||||||
type Flow {
|
type Flow {
|
||||||
id: String
|
id: String
|
||||||
name: String
|
name: String
|
||||||
active: Boolean
|
active: Boolean
|
||||||
steps: [Step]
|
steps: [Step]
|
||||||
|
createdAt: String
|
||||||
|
updatedAt: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Execution {
|
type Execution {
|
||||||
@@ -230,6 +259,7 @@ input DeleteConnectionInput {
|
|||||||
|
|
||||||
input CreateFlowInput {
|
input CreateFlowInput {
|
||||||
triggerAppKey: String
|
triggerAppKey: String
|
||||||
|
connectionId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateFlowInput {
|
input UpdateFlowInput {
|
||||||
@@ -319,6 +349,7 @@ type Step {
|
|||||||
previousStepId: String
|
previousStepId: String
|
||||||
key: String
|
key: String
|
||||||
appKey: String
|
appKey: String
|
||||||
|
iconUrl: String
|
||||||
type: StepEnumType
|
type: StepEnumType
|
||||||
parameters: JSONObject
|
parameters: JSONObject
|
||||||
connection: Connection
|
connection: Connection
|
||||||
@@ -356,6 +387,7 @@ type Trigger {
|
|||||||
name: String
|
name: String
|
||||||
key: String
|
key: String
|
||||||
description: String
|
description: String
|
||||||
|
pollInterval: Int
|
||||||
substeps: [TriggerSubstep]
|
substeps: [TriggerSubstep]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +455,10 @@ type ExecutionStepConnection {
|
|||||||
pageInfo: PageInfo
|
pageInfo: PageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppHealth {
|
||||||
|
version: String
|
||||||
|
}
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
mutation: Mutation
|
mutation: Mutation
|
||||||
|
@@ -24,6 +24,7 @@ const authentication = shield(
|
|||||||
{
|
{
|
||||||
Query: {
|
Query: {
|
||||||
'*': isAuthenticated,
|
'*': isAuthenticated,
|
||||||
|
healthcheck: allow,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': isAuthenticated,
|
'*': isAuthenticated,
|
||||||
|
21
packages/backend/src/helpers/http-client/index.ts
Normal file
21
packages/backend/src/helpers/http-client/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { IJSONObject, IHttpClientParams } from '@automatisch/types';
|
||||||
|
|
||||||
|
export default class HttpClient {
|
||||||
|
instance: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(params: IHttpClientParams) {
|
||||||
|
this.instance = axios.create({
|
||||||
|
baseURL: params.baseURL,
|
||||||
|
validateStatus: () => true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(path: string, options?: IJSONObject) {
|
||||||
|
return await this.instance.get(path, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(path: string, body: IJSONObject | string, options?: IJSONObject) {
|
||||||
|
return await this.instance.post(path, body, options);
|
||||||
|
}
|
||||||
|
}
|
@@ -125,7 +125,7 @@ class Telemetry {
|
|||||||
|
|
||||||
diagnosticInfo() {
|
diagnosticInfo() {
|
||||||
this.track('diagnosticInfo', {
|
this.track('diagnosticInfo', {
|
||||||
automatischVersion: process.env.npm_package_version,
|
automatischVersion: appConfig.version,
|
||||||
serveWebAppSeparately: appConfig.serveWebAppSeparately,
|
serveWebAppSeparately: appConfig.serveWebAppSeparately,
|
||||||
operatingSystem: {
|
operatingSystem: {
|
||||||
type: os.type(),
|
type: os.type(),
|
||||||
@@ -139,7 +139,7 @@ class Telemetry {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(this.diagnosticInfo, SIX_HOURS_IN_MILLISECONDS);
|
setTimeout(() => this.diagnosticInfo(), SIX_HOURS_IN_MILLISECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,10 +7,15 @@ class App {
|
|||||||
static folderPath = join(__dirname, '../apps');
|
static folderPath = join(__dirname, '../apps');
|
||||||
static list = fs.readdirSync(this.folderPath);
|
static list = fs.readdirSync(this.folderPath);
|
||||||
|
|
||||||
static findAll(name?: string): IApp[] {
|
// Temporaryly restrict the apps we expose until
|
||||||
if (!name) return this.list.map((name) => this.findOneByName(name));
|
// their actions/triggers are implemented!
|
||||||
|
static temporaryList = ['slack', 'twitter', 'scheduler'];
|
||||||
|
|
||||||
return this.list
|
static findAll(name?: string): IApp[] {
|
||||||
|
if (!name)
|
||||||
|
return this.temporaryList.map((name) => this.findOneByName(name));
|
||||||
|
|
||||||
|
return this.temporaryList
|
||||||
.filter((app) => app.includes(name.toLowerCase()))
|
.filter((app) => app.includes(name.toLowerCase()))
|
||||||
.map((name) => this.findOneByName(name));
|
.map((name) => this.findOneByName(name));
|
||||||
}
|
}
|
||||||
|
@@ -31,9 +31,9 @@ class Base extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise<void> {
|
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise<void> {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
this.updatedAt = new Date().toISOString();
|
this.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,8 @@ import type { RelationMappings } from 'objection';
|
|||||||
import { AES, enc } from 'crypto-js';
|
import { AES, enc } from 'crypto-js';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import User from './user';
|
import User from './user';
|
||||||
|
import Step from './step';
|
||||||
|
import App from './app';
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
import Telemetry from '../helpers/telemetry';
|
import Telemetry from '../helpers/telemetry';
|
||||||
@@ -14,7 +16,9 @@ class Connection extends Base {
|
|||||||
formattedData?: IJSONObject;
|
formattedData?: IJSONObject;
|
||||||
userId!: string;
|
userId!: string;
|
||||||
verified = false;
|
verified = false;
|
||||||
|
draft: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
flowCount?: number;
|
||||||
|
|
||||||
static tableName = 'connections';
|
static tableName = 'connections';
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ class Connection extends Base {
|
|||||||
formattedData: { type: 'object' },
|
formattedData: { type: 'object' },
|
||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
verified: { type: 'boolean' },
|
verified: { type: 'boolean' },
|
||||||
|
draft: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,8 +46,20 @@ class Connection extends Base {
|
|||||||
to: 'users.id',
|
to: 'users.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
steps: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: Step,
|
||||||
|
join: {
|
||||||
|
from: 'connections.id',
|
||||||
|
to: 'steps.connection_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get appData() {
|
||||||
|
return App.findOneByKey(this.key);
|
||||||
|
}
|
||||||
|
|
||||||
encryptData(): void {
|
encryptData(): void {
|
||||||
if (!this.eligibleForEncryption()) return;
|
if (!this.eligibleForEncryption()) return;
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ class ExecutionStep extends Base {
|
|||||||
stepId!: string;
|
stepId!: string;
|
||||||
dataIn!: Record<string, unknown>;
|
dataIn!: Record<string, unknown>;
|
||||||
dataOut!: Record<string, unknown>;
|
dataOut!: Record<string, unknown>;
|
||||||
|
errorDetails: Record<string, unknown>;
|
||||||
status = 'failure';
|
status = 'failure';
|
||||||
step: Step;
|
step: Step;
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ class ExecutionStep extends Base {
|
|||||||
dataIn: { type: 'object' },
|
dataIn: { type: 'object' },
|
||||||
dataOut: { type: 'object' },
|
dataOut: { type: 'object' },
|
||||||
status: { type: 'string', enum: ['success', 'failure'] },
|
status: { type: 'string', enum: ['success', 'failure'] },
|
||||||
|
errorDetails: { type: ['object', 'null'] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ class Execution extends Base {
|
|||||||
id!: string;
|
id!: string;
|
||||||
flowId!: string;
|
flowId!: string;
|
||||||
testRun = false;
|
testRun = false;
|
||||||
|
internalId: string;
|
||||||
executionSteps: ExecutionStep[] = [];
|
executionSteps: ExecutionStep[] = [];
|
||||||
|
|
||||||
static tableName = 'executions';
|
static tableName = 'executions';
|
||||||
@@ -19,6 +20,7 @@ class Execution extends Base {
|
|||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
flowId: { type: 'string', format: 'uuid' },
|
flowId: { type: 'string', format: 'uuid' },
|
||||||
testRun: { type: 'boolean' },
|
testRun: { type: 'boolean' },
|
||||||
|
internalId: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ValidationError } from 'objection';
|
import { ValidationError } from 'objection';
|
||||||
import type { ModelOptions, QueryContext } from 'objection';
|
import type { ModelOptions, QueryContext, QueryBuilder } from 'objection';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
@@ -9,17 +9,19 @@ class Flow extends Base {
|
|||||||
id!: string;
|
id!: string;
|
||||||
name!: string;
|
name!: string;
|
||||||
userId!: string;
|
userId!: string;
|
||||||
active = false;
|
active: boolean;
|
||||||
steps?: [Step];
|
steps?: [Step];
|
||||||
|
published_at: string;
|
||||||
|
|
||||||
static tableName = 'flows';
|
static tableName = 'flows';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
required: ['name'],
|
||||||
|
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
name: { type: 'string' },
|
name: { type: 'string', minLength: 1 },
|
||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
active: { type: 'boolean' },
|
active: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
@@ -33,6 +35,9 @@ class Flow extends Base {
|
|||||||
from: 'flows.id',
|
from: 'flows.id',
|
||||||
to: 'steps.flow_id',
|
to: 'steps.flow_id',
|
||||||
},
|
},
|
||||||
|
filter(builder: QueryBuilder<Step>) {
|
||||||
|
builder.orderBy('position', 'asc');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
executions: {
|
executions: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
@@ -44,7 +49,20 @@ class Flow extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async $beforeUpdate(opt: ModelOptions): Promise<void> {
|
async lastInternalId() {
|
||||||
|
const lastExecution = await this.$relatedQuery('executions')
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return lastExecution ? (lastExecution as Execution).internalId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeUpdate(
|
||||||
|
opt: ModelOptions,
|
||||||
|
queryContext: QueryContext
|
||||||
|
): Promise<void> {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
if (!this.active) return;
|
if (!this.active) return;
|
||||||
|
|
||||||
const oldFlow = opt.old as Flow;
|
const oldFlow = opt.old as Flow;
|
||||||
|
@@ -6,6 +6,7 @@ import Connection from './connection';
|
|||||||
import ExecutionStep from './execution-step';
|
import ExecutionStep from './execution-step';
|
||||||
import type { IStep } from '@automatisch/types';
|
import type { IStep } from '@automatisch/types';
|
||||||
import Telemetry from '../helpers/telemetry';
|
import Telemetry from '../helpers/telemetry';
|
||||||
|
import appConfig from '../config/app';
|
||||||
|
|
||||||
class Step extends Base {
|
class Step extends Base {
|
||||||
id!: string;
|
id!: string;
|
||||||
@@ -40,6 +41,10 @@ class Step extends Base {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['iconUrl'];
|
||||||
|
}
|
||||||
|
|
||||||
static relationMappings = () => ({
|
static relationMappings = () => ({
|
||||||
flow: {
|
flow: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
@@ -67,6 +72,16 @@ class Step extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get iconUrl() {
|
||||||
|
if (!this.appKey) return null;
|
||||||
|
|
||||||
|
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get appData() {
|
||||||
|
return App.findOneByKey(this.appKey);
|
||||||
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext: QueryContext) {
|
async $afterInsert(queryContext: QueryContext) {
|
||||||
await super.$afterInsert(queryContext);
|
await super.$afterInsert(queryContext);
|
||||||
Telemetry.stepCreated(this);
|
Telemetry.stepCreated(this);
|
||||||
@@ -84,15 +99,13 @@ class Step extends Base {
|
|||||||
async getTrigger() {
|
async getTrigger() {
|
||||||
if (!this.isTrigger) return null;
|
if (!this.isTrigger) return null;
|
||||||
|
|
||||||
const { appKey, connection, key, parameters = {} } = this;
|
const { appKey, key } = this;
|
||||||
|
|
||||||
|
const connection = await this.$relatedQuery('connection');
|
||||||
|
const flow = await this.$relatedQuery('flow');
|
||||||
|
|
||||||
const appData = App.findOneByKey(appKey);
|
|
||||||
const AppClass = (await import(`../apps/${appKey}`)).default;
|
const AppClass = (await import(`../apps/${appKey}`)).default;
|
||||||
const appInstance = new AppClass(
|
const appInstance = new AppClass(connection, flow, this);
|
||||||
appData,
|
|
||||||
connection?.formattedData,
|
|
||||||
parameters,
|
|
||||||
);
|
|
||||||
const command = appInstance.triggers[key];
|
const command = appInstance.triggers[key];
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
|
@@ -10,7 +10,11 @@ const redisConnection = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processorQueue = new Queue('processor', redisConnection);
|
const processorQueue = new Queue('processor', redisConnection);
|
||||||
new QueueScheduler('processor', redisConnection);
|
const queueScheduler = new QueueScheduler('processor', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await queueScheduler.close();
|
||||||
|
});
|
||||||
|
|
||||||
processorQueue.on('error', (err) => {
|
processorQueue.on('error', (err) => {
|
||||||
if ((err as any).code === CONNECTION_REFUSED) {
|
if ((err as any).code === CONNECTION_REFUSED) {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
import App from '../models/app';
|
|
||||||
import Flow from '../models/flow';
|
import Flow from '../models/flow';
|
||||||
import Step from '../models/step';
|
import Step from '../models/step';
|
||||||
import Execution from '../models/execution';
|
import Execution from '../models/execution';
|
||||||
import ExecutionStep from '../models/execution-step';
|
import ExecutionStep from '../models/execution-step';
|
||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
|
||||||
type ExecutionSteps = Record<string, ExecutionStep>;
|
type ExecutionSteps = Record<string, ExecutionStep>;
|
||||||
|
|
||||||
@@ -33,18 +33,44 @@ class Processor {
|
|||||||
|
|
||||||
const triggerStep = steps.find((step) => step.type === 'trigger');
|
const triggerStep = steps.find((step) => step.type === 'trigger');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
let initialTriggerData = await this.getInitialTriggerData(triggerStep!);
|
const initialTriggerData = await this.getInitialTriggerData(triggerStep!);
|
||||||
|
|
||||||
|
if (initialTriggerData.data.length === 0) {
|
||||||
|
const lastInternalId = await this.flow.lastInternalId();
|
||||||
|
|
||||||
|
const executionData: Partial<Execution> = {
|
||||||
|
flowId: this.flow.id,
|
||||||
|
testRun: this.testRun,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastInternalId) {
|
||||||
|
executionData.internalId = lastInternalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Execution.query().insert(executionData);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.testRun) {
|
if (this.testRun) {
|
||||||
initialTriggerData = [initialTriggerData[0]];
|
initialTriggerData.data = [initialTriggerData.data[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialTriggerData.data.length > 1) {
|
||||||
|
initialTriggerData.data = initialTriggerData.data.sort(
|
||||||
|
(item: IJSONObject, nextItem: IJSONObject) => {
|
||||||
|
return (item.id as number) - (nextItem.id as number);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const executions: Execution[] = [];
|
const executions: Execution[] = [];
|
||||||
|
|
||||||
for await (const data of initialTriggerData) {
|
for await (const data of initialTriggerData.data) {
|
||||||
const execution = await Execution.query().insert({
|
const execution = await Execution.query().insert({
|
||||||
flowId: this.flow.id,
|
flowId: this.flow.id,
|
||||||
testRun: this.testRun,
|
testRun: this.testRun,
|
||||||
|
internalId: data.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
executions.push(execution);
|
executions.push(execution);
|
||||||
@@ -56,16 +82,7 @@ class Processor {
|
|||||||
for await (const step of steps) {
|
for await (const step of steps) {
|
||||||
if (!step.appKey) continue;
|
if (!step.appKey) continue;
|
||||||
|
|
||||||
const appData = App.findOneByKey(step.appKey);
|
const { appKey, key, type, parameters: rawParameters = {}, id } = step;
|
||||||
|
|
||||||
const {
|
|
||||||
appKey,
|
|
||||||
connection,
|
|
||||||
key,
|
|
||||||
type,
|
|
||||||
parameters: rawParameters = {},
|
|
||||||
id,
|
|
||||||
} = step;
|
|
||||||
|
|
||||||
const isTrigger = type === 'trigger';
|
const isTrigger = type === 'trigger';
|
||||||
const AppClass = (await import(`../apps/${appKey}`)).default;
|
const AppClass = (await import(`../apps/${appKey}`)).default;
|
||||||
@@ -75,11 +92,9 @@ class Processor {
|
|||||||
priorExecutionSteps
|
priorExecutionSteps
|
||||||
);
|
);
|
||||||
|
|
||||||
const appInstance = new AppClass(
|
step.parameters = computedParameters;
|
||||||
appData,
|
|
||||||
connection?.formattedData,
|
const appInstance = new AppClass(step.connection, this.flow, step);
|
||||||
computedParameters
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isTrigger && key) {
|
if (!isTrigger && key) {
|
||||||
const command = appInstance.actions[key];
|
const command = appInstance.actions[key];
|
||||||
@@ -103,6 +118,23 @@ class Processor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialTriggerData.errors) {
|
||||||
|
const execution = await Execution.query().insert({
|
||||||
|
flowId: this.flow.id,
|
||||||
|
testRun: this.testRun,
|
||||||
|
internalId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ExecutionStep.query().insert({
|
||||||
|
executionId: execution.id,
|
||||||
|
stepId: steps[0].id,
|
||||||
|
status: 'failure',
|
||||||
|
dataIn: steps[0].parameters,
|
||||||
|
dataOut: null,
|
||||||
|
errorDetails: initialTriggerData.errors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.testRun) return;
|
if (!this.testRun) return;
|
||||||
|
|
||||||
const lastExecutionStepFromFirstExecution = await executions[0]
|
const lastExecutionStepFromFirstExecution = await executions[0]
|
||||||
@@ -114,37 +146,21 @@ class Processor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getInitialTriggerData(step: Step) {
|
async getInitialTriggerData(step: Step) {
|
||||||
if (!step.appKey) return null;
|
if (!step.appKey || !step.key) return null;
|
||||||
|
|
||||||
const appData = App.findOneByKey(step.appKey);
|
const AppClass = (await import(`../apps/${step.appKey}`)).default;
|
||||||
const { appKey, connection, key, parameters: rawParameters = {} } = step;
|
const appInstance = new AppClass(step.connection, this.flow, step);
|
||||||
|
|
||||||
if (!key) return null;
|
const command = appInstance.triggers[step.key];
|
||||||
|
|
||||||
const AppClass = (await import(`../apps/${appKey}`)).default;
|
|
||||||
const appInstance = new AppClass(
|
|
||||||
appData,
|
|
||||||
connection?.formattedData,
|
|
||||||
rawParameters
|
|
||||||
);
|
|
||||||
|
|
||||||
const lastExecutionStep = await step
|
|
||||||
.$relatedQuery('executionSteps')
|
|
||||||
.orderBy('created_at', 'desc')
|
|
||||||
.first();
|
|
||||||
|
|
||||||
const lastExecutionStepCreatedAt = lastExecutionStep?.createdAt as string;
|
|
||||||
const flow = (await step.$relatedQuery('flow')) as Flow;
|
|
||||||
|
|
||||||
const command = appInstance.triggers[key];
|
|
||||||
|
|
||||||
const startTime = new Date(lastExecutionStepCreatedAt || flow.updatedAt);
|
|
||||||
let fetchedData;
|
let fetchedData;
|
||||||
|
|
||||||
|
const lastInternalId = await this.flow.lastInternalId();
|
||||||
|
|
||||||
if (this.testRun) {
|
if (this.testRun) {
|
||||||
fetchedData = await command.testRun(startTime);
|
fetchedData = await command.testRun();
|
||||||
} else {
|
} else {
|
||||||
fetchedData = await command.run(startTime);
|
fetchedData = await command.run(lastInternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchedData;
|
return fetchedData;
|
||||||
@@ -163,7 +179,10 @@ class Processor {
|
|||||||
.map((part: string) => {
|
.map((part: string) => {
|
||||||
const isVariable = part.match(Processor.variableRegExp);
|
const isVariable = part.match(Processor.variableRegExp);
|
||||||
if (isVariable) {
|
if (isVariable) {
|
||||||
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '') as string;
|
const stepIdAndKeyPath = part.replace(
|
||||||
|
/{{step.|}}/g,
|
||||||
|
''
|
||||||
|
) as string;
|
||||||
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
|
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
|
||||||
const keyPath = keyPaths.join('.');
|
const keyPath = keyPaths.join('.');
|
||||||
const executionStep = executionSteps[stepId.toString() as string];
|
const executionStep = executionSteps[stepId.toString() as string];
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
import './config/orm';
|
import './config/orm';
|
||||||
import './workers/processor';
|
export { worker } from './workers/processor';
|
||||||
|
@@ -4,7 +4,7 @@ import redisConfig from '../config/redis';
|
|||||||
import Flow from '../models/flow';
|
import Flow from '../models/flow';
|
||||||
import logger from '../helpers/logger';
|
import logger from '../helpers/logger';
|
||||||
|
|
||||||
const worker = new Worker(
|
export const worker = new Worker(
|
||||||
'processor',
|
'processor',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const flow = await Flow.query().findById(job.data.flowId).throwIfNotFound();
|
const flow = await Flow.query().findById(job.data.flowId).throwIfNotFound();
|
||||||
@@ -24,3 +24,7 @@ worker.on('failed', (job, err) => {
|
|||||||
`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed with ${err.message}`
|
`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed with ${err.message}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
1
packages/backend/worker.d.ts
vendored
Normal file
1
packages/backend/worker.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dist/src/worker';
|
2
packages/backend/worker.js
Normal file
2
packages/backend/worker.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = require('./dist/src/worker.js');
|
49
packages/cli/src/commands/start-worker.ts
Normal file
49
packages/cli/src/commands/start-worker.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { Command, Flags } from '@oclif/core';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
export default class StartWorker extends Command {
|
||||||
|
static description = 'Run automatisch worker';
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
env: Flags.string({
|
||||||
|
multiple: true,
|
||||||
|
char: 'e',
|
||||||
|
}),
|
||||||
|
'env-file': Flags.string(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareEnvVars(): Promise<void> {
|
||||||
|
const { flags } = await this.parse(StartWorker);
|
||||||
|
|
||||||
|
if (flags['env-file']) {
|
||||||
|
const envFile = readFileSync(flags['env-file'], 'utf8');
|
||||||
|
const envConfig = dotenv.parse(envFile);
|
||||||
|
|
||||||
|
for (const key in envConfig) {
|
||||||
|
const value = envConfig[key];
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.env) {
|
||||||
|
for (const env of flags.env) {
|
||||||
|
const [key, value] = env.split('=');
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// must serve until more customization is introduced
|
||||||
|
delete process.env.SERVE_WEB_APP_SEPARATELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runWorker(): Promise<void> {
|
||||||
|
await import('@automatisch/backend/worker');
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
await this.prepareEnvVars();
|
||||||
|
|
||||||
|
await this.runWorker();
|
||||||
|
}
|
||||||
|
}
|
@@ -13,6 +13,10 @@ export default class Start extends Command {
|
|||||||
'env-file': Flags.string(),
|
'env-file': Flags.string(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isProduction() {
|
||||||
|
return process.env.APP_ENV === 'production';
|
||||||
|
}
|
||||||
|
|
||||||
async prepareEnvVars(): Promise<void> {
|
async prepareEnvVars(): Promise<void> {
|
||||||
const { flags } = await this.parse(Start);
|
const { flags } = await this.parse(Start);
|
||||||
|
|
||||||
@@ -38,7 +42,7 @@ export default class Start extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createDatabaseAndUser(): Promise<void> {
|
async createDatabaseAndUser(): Promise<void> {
|
||||||
const { utils } = await import('@automatisch/backend/database');
|
const utils = await import('@automatisch/backend/database-utils');
|
||||||
|
|
||||||
await utils.createDatabaseAndUser(
|
await utils.createDatabaseAndUser(
|
||||||
process.env.POSTGRES_DATABASE,
|
process.env.POSTGRES_DATABASE,
|
||||||
@@ -48,7 +52,7 @@ export default class Start extends Command {
|
|||||||
|
|
||||||
async runMigrationsIfNeeded(): Promise<void> {
|
async runMigrationsIfNeeded(): Promise<void> {
|
||||||
const { logger } = await import('@automatisch/backend/logger');
|
const { logger } = await import('@automatisch/backend/logger');
|
||||||
const { database } = await import('@automatisch/backend/database');
|
const database = await import('@automatisch/backend/database');
|
||||||
const migrator = database.client.migrate;
|
const migrator = database.client.migrate;
|
||||||
|
|
||||||
const [, pendingMigrations] = await migrator.list();
|
const [, pendingMigrations] = await migrator.list();
|
||||||
@@ -66,7 +70,7 @@ export default class Start extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async seedUser(): Promise<void> {
|
async seedUser(): Promise<void> {
|
||||||
const { utils } = await import('@automatisch/backend/database');
|
const utils = await import('@automatisch/backend/database-utils');
|
||||||
|
|
||||||
await utils.createUser();
|
await utils.createUser();
|
||||||
}
|
}
|
||||||
@@ -78,7 +82,9 @@ export default class Start extends Command {
|
|||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
await this.prepareEnvVars();
|
await this.prepareEnvVars();
|
||||||
|
|
||||||
await this.createDatabaseAndUser();
|
if (!this.isProduction) {
|
||||||
|
await this.createDatabaseAndUser();
|
||||||
|
}
|
||||||
|
|
||||||
await this.runMigrationsIfNeeded();
|
await this.runMigrationsIfNeeded();
|
||||||
|
|
||||||
|
20
packages/docs/.gitignore
vendored
20
packages/docs/.gitignore
vendored
@@ -1,20 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# Production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.docusaurus
|
|
||||||
.cache-loader
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
@@ -1,33 +0,0 @@
|
|||||||
# Website
|
|
||||||
|
|
||||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
|
||||||
|
|
||||||
### Deployment
|
|
||||||
|
|
||||||
```
|
|
||||||
$ GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user