Compare commits

..

1 Commits

Author SHA1 Message Date
Ali BARIN
69ae448d64 feat: new find project merge requests action in GitLab 2022-05-07 20:47:37 +02:00
251 changed files with 4678 additions and 4365 deletions

View File

@@ -1,47 +0,0 @@
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:

View File

@@ -1,11 +0,0 @@
# 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

View File

@@ -1,21 +0,0 @@
# 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

View File

@@ -1,11 +0,0 @@
#!/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 "$@"

View File

@@ -13,4 +13,3 @@ ENCRYPTION_KEY=sample-encryption-key
APP_SECRET_KEY=sample-app-secret-key
REDIS_PORT=6379
REDIS_HOST=127.0.0.1
ENABLE_BULLMQ_DASHBOARD=false

View File

@@ -12,14 +12,8 @@ export async function createUser(email = 'user@automatisch.io', password = 'samp
};
try {
const userCount = await User.query().resultSize();
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.');
}
const user = await User.query().insertAndFetch(userParams);
logger.info(`User has been saved: ${user.email}`);
} catch (err) {
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
throw err;

View File

@@ -1 +0,0 @@
export * from './dist/bin/database/utils';

View File

@@ -1,2 +0,0 @@
/* eslint-disable */
module.exports = require('./dist/bin/database/utils');

View File

@@ -1 +1,2 @@
export * from './dist/src/config/database';
export * as utils from './dist/bin/database/utils';
export * as database from './dist/src/config/database';

View File

@@ -1,2 +1,3 @@
/* eslint-disable */
module.exports = require('./dist/src/config/database');
module.exports.utils = require('./dist/bin/database/utils');
module.exports.database = require('./dist/src/config/database');

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "> TODO: description",
"scripts": {
"dev": "ts-node-dev src/server.ts",
"dev": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec 'ts-node' src/server.ts --ext ts,json",
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
"build": "tsc && yarn copy-statics",
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
@@ -16,8 +16,7 @@
"db:migration:create": "knex migrate:make",
"db:rollback": "knex migrate:rollback",
"db:migrate": "knex migrate:latest",
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
"prepack": "yarn build"
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist"
},
"dependencies": {
"@automatisch/web": "0.1.0",
@@ -25,6 +24,7 @@
"@gitbeaker/node": "^35.6.0",
"@graphql-tools/graphql-file-loader": "^7.3.4",
"@graphql-tools/load": "^7.5.2",
"@octokit/oauth-methods": "^1.2.6",
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@slack/bolt": "3.10.0",
"@types/luxon": "^2.3.1",
@@ -53,7 +53,6 @@
"luxon": "2.3.1",
"morgan": "^1.10.0",
"nodemailer": "6.7.0",
"oauth-1.0a": "^2.2.6",
"objection": "^3.0.0",
"octokit": "^1.7.1",
"pg": "^8.7.1",
@@ -76,19 +75,14 @@
"test": "__tests__"
},
"files": [
"dist",
"bin",
"src",
"server.js",
"server.d.ts",
"worker.js",
"worker.d.ts",
"logger.js",
"logger.d.ts",
"database.js",
"database.d.ts",
"database-utils.js",
"database-utils.d.ts"
"database.d.ts"
],
"repository": {
"type": "git",
@@ -115,8 +109,7 @@
"ava": "^3.15.0",
"nodemon": "^2.0.13",
"sinon": "^11.1.2",
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8"
"ts-node": "^10.2.1"
},
"ava": {
"files": [

View File

@@ -15,13 +15,13 @@ import {
} from './helpers/create-bull-board-handler';
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
if (appConfig.enableBullMQDashboard) {
if (appConfig.appEnv === 'development') {
createBullBoardHandler(serverAdapter);
}
const app = express();
if (appConfig.enableBullMQDashboard) {
if (appConfig.appEnv === 'development') {
injectBullBoardHandler(app, serverAdapter);
}

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/discord/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/discord",
"primaryColor": "5865f2",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/firebase/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/firebase",
"primaryColor": "ffca28",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/flickr/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/flickr",
"primaryColor": "000000",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",
@@ -223,8 +222,8 @@
"description": "Triggers when you favorite a photo.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",
@@ -238,8 +237,8 @@
"description": "Triggers when you add a new photo in an album.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -276,8 +275,8 @@
"description": "Triggers when you add a new photo.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",
@@ -291,8 +290,8 @@
"description": "Triggers when you create a new album.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",

View File

@@ -4,19 +4,35 @@ import type {
IField,
IJSONObject,
} from '@automatisch/types';
import HttpClient from '../../helpers/http-client';
import { URLSearchParams } from 'url';
import {
getWebFlowAuthorizationUrl,
exchangeWebFlowCode,
checkToken,
} from '@octokit/oauth-methods';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
scopes: string[] = ['read:org', 'repo', 'user'];
client: HttpClient;
scopes: string[] = [
'read:org',
'repo',
'user',
];
client: {
getWebFlowAuthorizationUrl: typeof getWebFlowAuthorizationUrl;
exchangeWebFlowCode: typeof exchangeWebFlowCode;
checkToken: typeof checkToken;
};
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
this.client = new HttpClient({ baseURL: 'https://github.com' });
this.client = {
getWebFlowAuthorizationUrl,
exchangeWebFlowCode,
checkToken,
};
}
get oauthRedirectUrl(): string {
@@ -26,28 +42,26 @@ export default class Authentication implements IAuthentication {
}
async createAuthData(): Promise<{ url: string }> {
const searchParams = new URLSearchParams({
client_id: this.connectionData.consumerKey as string,
redirect_uri: this.oauthRedirectUrl,
scope: this.scopes.join(','),
const { url } = await this.client.getWebFlowAuthorizationUrl({
clientType: 'oauth-app',
clientId: this.connectionData.consumerKey as string,
redirectUrl: this.oauthRedirectUrl,
scopes: this.scopes,
});
const url = `https://github.com/login/oauth/authorize?${searchParams.toString()}`;
return {
url,
url: url,
};
}
async verifyCredentials() {
const response = await this.client.post('/login/oauth/access_token', {
client_id: this.connectionData.consumerKey,
client_secret: this.connectionData.consumerSecret,
code: this.connectionData.oauthVerifier,
const { data } = await this.client.exchangeWebFlowCode({
clientType: 'oauth-app',
clientId: this.connectionData.consumerKey as string,
clientSecret: this.connectionData.consumerSecret as string,
code: this.connectionData.oauthVerifier as string,
});
const data = Object.fromEntries(new URLSearchParams(response.data));
this.connectionData.accessToken = data.access_token;
const tokenInfo = await this.getTokenInfo();
@@ -64,23 +78,12 @@ export default class Authentication implements IAuthentication {
}
async getTokenInfo() {
const basicAuthToken = Buffer.from(
this.connectionData.consumerKey + ':' + this.connectionData.consumerSecret
).toString('base64');
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 }
);
return this.client.checkToken({
clientType: 'oauth-app',
clientId: this.connectionData.consumerKey as string,
clientSecret: this.connectionData.consumerSecret as string,
token: this.connectionData.accessToken as string,
});
}
async isStillVerified() {

View File

@@ -1,16 +1,13 @@
import { IJSONObject } from '@automatisch/types';
import ListRepos from './data/list-repos';
import ListBranches from './data/list-branches';
import ListLabels from './data/list-labels';
export default class Data {
listRepos: ListRepos;
listBranches: ListBranches;
listLabels: ListLabels;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.listRepos = new ListRepos(connectionData);
this.listBranches = new ListBranches(connectionData, parameters);
this.listLabels = new ListLabels(connectionData, parameters);
}
}

View File

@@ -1,36 +0,0 @@
import { Octokit } from 'octokit';
import type { IJSONObject } from '@automatisch/types';
import { assignOwnerAndRepo } from '../utils';
export default class ListLabels {
client?: Octokit;
repoOwner?: string;
repo?: string;
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
if (connectionData.accessToken) {
this.client = new Octokit({
auth: connectionData.accessToken as string,
});
}
assignOwnerAndRepo(this, parameters?.repo as string);
}
get options() {
return {
owner: this.repoOwner,
repo: this.repo,
};
}
async run() {
const labels = await this.client.paginate(this.client.rest.issues.listLabelsForRepo, this.options);
return labels.map((label) => ({
value: label.name,
name: label.name,
}));
}
}

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/github/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/github",
"primaryColor": "000000",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",
@@ -20,26 +19,26 @@
},
{
"key": "consumerKey",
"label": "Client ID",
"label": "Consumer Key",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/github#client-id",
"docUrl": "https://automatisch.io/docs/github#consumer-key",
"clickToCopy": false
},
{
"key": "consumerSecret",
"label": "Client Secret",
"label": "Consumer Secret",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/github#client-secret",
"docUrl": "https://automatisch.io/docs/github#consumer-secret",
"clickToCopy": false
}
],
@@ -223,8 +222,8 @@
"description": "Triggers when a new repository is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",
@@ -238,8 +237,8 @@
"description": "Triggers when a new organization is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",
@@ -253,8 +252,8 @@
"description": "Triggers when a new branch is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -291,8 +290,8 @@
"description": "Triggers when a new notification is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -330,8 +329,8 @@
"description": "Triggers when a new pull request is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -368,8 +367,8 @@
"description": "Triggers when a new watcher is added to a repo",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -406,8 +405,8 @@
"description": "Triggers when a new milestone is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -444,8 +443,8 @@
"description": "Triggers when a new commit comment is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -482,8 +481,8 @@
"description": "Triggers when a new label is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -520,8 +519,8 @@
"description": "Triggers when a new collaborator is added to a repo",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -558,8 +557,8 @@
"description": "Triggers when a new release is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -596,8 +595,8 @@
"description": "Triggers when a new commit is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -650,98 +649,6 @@
"name": "Test trigger"
}
]
},
{
"name": "New issue",
"key": "newIssue",
"description": "Triggers when a new issue is created",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Repo",
"key": "repo",
"type": "dropdown",
"required": false,
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listRepos"
}
]
}
},
{
"label": "Which types of issues should this trigger on?",
"key": "issueType",
"type": "dropdown",
"description": "Defaults to any issue you can see.",
"required": true,
"variables": false,
"value": "all",
"options": [
{
"label": "Any issue you can see",
"value": "all"
},
{
"label": "Only issues assigned to you",
"value": "assigned"
},
{
"label": "Only issues created by you",
"value": "created"
},
{
"label": "Only issues you're mentioned in",
"value": "mentioned"
},
{
"label": "Only issues you're subscribed to",
"value": "subscribed"
}
]
},
{
"label": "Label",
"key": "label",
"type": "dropdown",
"description": "Only trigger on issues when this label is added.",
"required": false,
"variables": false,
"dependsOn": ["parameters.repo"],
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listLabels"
},
{
"name": "parameters.repo",
"value": "{parameters.repo}"
}
]
}
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
]
}

View File

@@ -11,7 +11,6 @@ import NewCommitComment from './triggers/new-commit-comment';
import NewLabel from './triggers/new-label';
import NewCollaborator from './triggers/new-collaborator';
import NewRelease from './triggers/new-release';
import NewIssue from './triggers/new-issue';
export default class Triggers {
newRepository: NewRepository;
@@ -26,7 +25,6 @@ export default class Triggers {
newLabel: NewLabel;
newCollaborator: NewCollaborator;
newRelease: NewRelease;
newIssue: NewIssue;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.newRepository = new NewRepository(connectionData);
@@ -41,6 +39,5 @@ export default class Triggers {
this.newLabel = new NewLabel(connectionData, parameters);
this.newCollaborator = new NewCollaborator(connectionData, parameters);
this.newRelease = new NewRelease(connectionData, parameters);
this.newIssue = new NewIssue(connectionData, parameters);
}
}

View File

@@ -1,88 +0,0 @@
import { Octokit } from 'octokit';
import { DateTime } from 'luxon';
import { IJSONObject } from '@automatisch/types';
import { assignOwnerAndRepo } from '../utils';
export default class NewIssue {
client?: Octokit;
connectionData?: IJSONObject;
repoOwner?: string;
repo?: string;
hasRepo?: boolean;
label?: string;
issueType?: string;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
if (connectionData.accessToken) {
this.client = new Octokit({
auth: connectionData.accessToken as string,
});
}
assignOwnerAndRepo(this, parameters?.repo as string);
}
get options() {
return {
labels: this.label,
}
}
async listRepoIssues(options = {}, paginate = false) {
const listRepoIssues = this.client.rest.issues.listForRepo;
const extendedOptions = {
...this.options,
repo: this.repo,
owner: this.repoOwner,
filter: this.issueType,
...options,
};
if (paginate) {
return await this.client.paginate(listRepoIssues, extendedOptions);
}
return (await listRepoIssues(extendedOptions)).data;
}
async listIssues(options = {}, paginate = false) {
const listIssues = this.client.rest.issues.listForAuthenticatedUser;
const extendedOptions = {
...this.options,
...options,
};
if (paginate) {
return await this.client.paginate(listIssues, extendedOptions);
}
return (await listIssues(extendedOptions)).data;
}
async run(startTime: Date) {
const options = {
since: DateTime.fromJSDate(startTime).toISO(),
};
if (this.hasRepo) {
return await this.listRepoIssues(options, true);
}
return await this.listIssues(options, true);
}
async testRun() {
const options = {
per_page: 1,
};
if (this.hasRepo) {
return await this.listRepoIssues(options, false);
}
return await this.listIssues(options, false);
}
}

View File

@@ -9,7 +9,6 @@ export default class NewNotification {
connectionData?: IJSONObject;
repoOwner?: string;
repo?: string;
hasRepo?: boolean;
baseOptions = {
all: true,
participating: false,
@@ -25,6 +24,10 @@ export default class NewNotification {
assignOwnerAndRepo(this, parameters?.repo as string);
}
get hasRepo() {
return this.repoOwner && this.repo;
}
async listRepoNotifications(options = {}, paginate = false) {
const listRepoNotifications = this.client.rest.activity.listRepoNotificationsForAuthenticatedUser;

View File

@@ -1,9 +1,8 @@
export function assignOwnerAndRepo<T extends { repoOwner?: string; repo?: string; hasRepo?: boolean; }>(object: T, repoFullName: string): T {
export function assignOwnerAndRepo<T extends { repoOwner?: string; repo?: string; }>(object: T, repoFullName: string): T {
if (object && repoFullName) {
const [repoOwner, repo] = repoFullName.split('/');
object.repoOwner = repoOwner;
object.repo = repo;
object.hasRepo = true;
}
return object;

View File

@@ -0,0 +1,13 @@
import { IJSONObject } from '@automatisch/types';
import FindProjectMergeRequests from './actions/find-project-merge-requests';
export default class Actions {
findProjectMergeRequests: FindProjectMergeRequests;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.findProjectMergeRequests = new FindProjectMergeRequests(
connectionData,
parameters
);
}
}

View File

@@ -0,0 +1,35 @@
import { Gitlab } from '@gitbeaker/node';
import { IJSONObject } from '@automatisch/types';
export default class FindProjectMergeRequests {
client: any;
projectId: number;
state: string;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
if (connectionData?.accessToken) {
this.client = new Gitlab({
host: `https://${connectionData.host}`,
oauthToken: connectionData?.accessToken as string,
});
}
if (parameters.project) {
this.projectId = parameters.project as number;
}
if (parameters.state) {
this.state = parameters.state as string;
}
}
async run() {
const mergeRequests = await this.client.MergeRequests.all({
state: this.state,
projectId: this.projectId,
maxPages: 1,
});
return { data: mergeRequests };
}
}

View File

@@ -0,0 +1,10 @@
import { IJSONObject } from '@automatisch/types';
import ListProjects from './data/list-projects';
export default class Data {
listProjects: ListProjects;
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
this.listProjects = new ListProjects(connectionData, parameters);
}
}

View File

@@ -0,0 +1,26 @@
import { Gitlab } from '@gitbeaker/node';
import type { IJSONObject } from '@automatisch/types';
export default class ListProjects {
client?: any;
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
if (connectionData?.accessToken) {
this.client = new Gitlab({
host: `https://${connectionData.host}`,
oauthToken: connectionData?.accessToken as string,
});
}
}
async run() {
const projects = await this.client.Projects.all({
membership: true,
});
return projects.map((project: any) => ({
value: project.id,
name: project.name_with_namespace,
}));
}
}

View File

@@ -1,15 +1,25 @@
import Authentication from './authentication';
import {
IService,
IAuthentication,
IApp,
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Actions from './actions';
import Data from './data';
export default class Gitlab implements IService {
authenticationClient: IAuthentication;
actions: Actions;
data: Data;
constructor(appData: IApp, connectionData: IJSONObject) {
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
this.authenticationClient = new Authentication(appData, connectionData);
this.actions = new Actions(connectionData, parameters);
this.data = new Data(connectionData, parameters);
}
}

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/gitlab/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/gitlab",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",
@@ -235,5 +234,76 @@
}
]
}
],
"actions": [
{
"name": "Find project merge requests",
"key": "findProjectMergeRequests",
"description": "Find merge requests for a project.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "setupAction",
"name": "Set up action",
"arguments": [
{
"label": "Project",
"key": "project",
"type": "dropdown",
"required": true,
"description": "Search for merge requests in this project.",
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listProjects"
}
]
}
},
{
"label": "State",
"key": "state",
"type": "dropdown",
"required": true,
"description": "Filter merge requests by their state.",
"variables": false,
"options": [
{
"label": "All",
"value": "all"
},
{
"label": "Opened",
"value": "opened"
},
{
"label": "Closed",
"value": "closed"
},
{
"label": "Locked",
"value": "locked"
},
{
"label": "Merged",
"value": "merged"
}
]
}
]
},
{
"key": "testStep",
"name": "Test action"
}
]
}
]
}

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/postgresql/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/postgresql",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "host",

View File

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -5,7 +5,7 @@ import {
IJSONObject,
} from '@automatisch/types';
export default class Scheduler implements IService {
export default class Schedule implements IService {
triggers: Triggers;
constructor(

View File

@@ -1,11 +1,9 @@
{
"name": "Scheduler",
"key": "scheduler",
"iconUrl": "{BASE_URL}/apps/scheduler/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/scheduler",
"authDocUrl": "https://automatisch.io/docs/connections/scheduler",
"name": "Schedule",
"key": "schedule",
"iconUrl": "{BASE_URL}/apps/schedule/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/schedule",
"primaryColor": "0059F7",
"supportsConnections": false,
"requiresAuthentication": false,
"triggers": [
{

View File

@@ -1,15 +1,13 @@
import SendMessageToChannel from './actions/send-message-to-channel';
import FindMessage from './actions/find-message';
import SlackClient from './client';
import { IJSONObject } from '@automatisch/types';
export default class Actions {
client: SlackClient;
sendMessageToChannel: SendMessageToChannel;
findMessage: FindMessage;
constructor(client: SlackClient) {
this.client = client;
this.sendMessageToChannel = new SendMessageToChannel(client);
this.findMessage = new FindMessage(client);
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.sendMessageToChannel = new SendMessageToChannel(
connectionData,
parameters
);
}
}

View File

@@ -1,26 +0,0 @@
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;
}
}

View File

@@ -1,18 +1,21 @@
import SlackClient from '../client';
import { WebClient } from '@slack/web-api';
import { IJSONObject } from '@automatisch/types';
export default class SendMessageToChannel {
client: SlackClient;
client: WebClient;
parameters: IJSONObject;
constructor(client: SlackClient) {
this.client = client;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.client = new WebClient(connectionData.accessToken as string);
this.parameters = parameters;
}
async run() {
const channelId = this.client.step.parameters.channel as string;
const text = this.client.step.parameters.message as string;
const result = await this.client.chat.postMessage({
channel: this.parameters.channel as string,
text: this.parameters.message as string,
});
const message = await this.client.postMessageToChannel.run(channelId, text);
return message;
return result;
}
}

View File

@@ -1,33 +1,36 @@
import type { IAuthentication, IJSONObject } from '@automatisch/types';
import SlackClient from './client';
import type { IAuthentication, IApp, IJSONObject } from '@automatisch/types';
import { WebClient } from '@slack/web-api';
export default class Authentication implements IAuthentication {
client: SlackClient;
appData: IApp;
connectionData: IJSONObject;
client: WebClient;
static requestOptions: IJSONObject = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
constructor(appData: IApp, connectionData: IJSONObject) {
this.client = new WebClient();
constructor(client: SlackClient) {
this.client = client;
this.connectionData = connectionData;
this.appData = appData;
}
async verifyCredentials() {
const { bot_id: botId, user: screenName } =
await this.client.verifyAccessToken.run();
const { bot_id: botId, user: screenName } = await this.client.auth.test({
token: this.connectionData.accessToken as string,
});
return {
botId,
screenName,
token: this.client.connection.formattedData.accessToken,
token: this.connectionData.accessToken,
};
}
async isStillVerified() {
try {
await this.client.verifyAccessToken.run();
await this.client.auth.test({
token: this.connectionData.accessToken as string,
});
return true;
} catch (error) {
return false;

View File

@@ -1,44 +0,0 @@
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;
}
}

View File

@@ -1,34 +0,0 @@
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;
}
}

View File

@@ -1,35 +0,0 @@
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;
}
}

View File

@@ -1,29 +0,0 @@
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);
}
}

View File

@@ -1,12 +1,10 @@
import ListChannels from './data/list-channels';
import SlackClient from './client';
import { IJSONObject } from '@automatisch/types';
export default class Data {
client: SlackClient;
listChannels: ListChannels;
constructor(client: SlackClient) {
this.client = client;
this.listChannels = new ListChannels(client);
constructor(connectionData: IJSONObject) {
this.listChannels = new ListChannels(connectionData);
}
}

View File

@@ -1,27 +1,17 @@
import { IJSONObject } from '@automatisch/types';
import SlackClient from '../client';
import type { IJSONObject } from '@automatisch/types';
import { WebClient } from '@slack/web-api';
export default class ListChannels {
client: SlackClient;
client: WebClient;
constructor(client: SlackClient) {
this.client = client;
constructor(connectionData: IJSONObject) {
this.client = new WebClient(connectionData.accessToken as string);
}
async run() {
const response = await this.client.httpClient.get('/conversations.list', {
headers: {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
},
});
const { channels } = await this.client.conversations.list();
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 channels.map((channel) => {
return {
value: channel.id,
name: channel.name,

View File

@@ -1,30 +1,25 @@
import {
IService,
IAuthentication,
IConnection,
IFlow,
IStep,
IApp,
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Triggers from './triggers';
import Actions from './actions';
import Data from './data';
import SlackClient from './client';
export default class Slack implements IService {
client: SlackClient;
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
data: Data;
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
this.client = new SlackClient(connection, flow, step);
this.authenticationClient = new Authentication(this.client);
// this.triggers = new Triggers(this.client);
this.actions = new Actions(this.client);
this.data = new Data(this.client);
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
this.authenticationClient = new Authentication(appData, connectionData);
this.data = new Data(connectionData);
this.actions = new Actions(connectionData, parameters);
}
}

View File

@@ -3,9 +3,7 @@
"key": "slack",
"iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/slack",
"authDocUrl": "https://automatisch.io/docs/connections/slack",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "accessToken",
@@ -16,6 +14,7 @@
"value": null,
"placeholder": null,
"description": "Access token of slack that Automatisch will connect to.",
"docUrl": "https://automatisch.io/docs/slack#access-token",
"clickToCopy": false
}
],
@@ -98,66 +97,6 @@
]
}
],
"triggers": [
{
"name": "New message posted to a channel",
"key": "newMessageToChannel",
"pollInterval": 15,
"description": "Triggers when a new message is posted to a channel",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Channel",
"key": "channel",
"type": "dropdown",
"required": true,
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listChannels"
}
]
}
},
{
"label": "Trigger for Bot Messages?",
"key": "triggerForBotMessages",
"type": "dropdown",
"description": "Should this flow trigger for bot messages?",
"required": true,
"value": true,
"variables": false,
"options": [
{
"label": "Yes",
"value": true
},
{
"label": "No",
"value": false
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
],
"actions": [
{
"name": "Send a message to channel",
@@ -165,8 +104,8 @@
"description": "Send a message to a specific channel you specify.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "setupAction",
@@ -205,73 +144,6 @@
"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"
}
]
}
]
}

View File

@@ -1,13 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import NewMessageToChannel from './triggers/new-message-to-channel';
export default class Triggers {
newMessageToChannel: NewMessageToChannel;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.newMessageToChannel = new NewMessageToChannel(
connectionData,
parameters
);
}
}

View File

@@ -1,47 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import axios, { AxiosInstance } from 'axios';
export default class NewMessageToChannel {
httpClient: AxiosInstance;
parameters: IJSONObject;
connectionData: IJSONObject;
BASE_URL = 'https://slack.com/api';
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.httpClient = axios.create({ baseURL: this.BASE_URL });
this.connectionData = connectionData;
this.parameters = parameters;
}
async run() {
// TODO: Fix after webhook implementation.
}
async testRun() {
const headers = {
Authorization: `Bearer ${this.connectionData.accessToken}`,
};
const params = {
channel: this.parameters.channel,
};
const response = await this.httpClient.get('/conversations.history', {
headers,
params,
});
let lastMessage;
if (this.parameters.triggerForBotMessages) {
lastMessage = response.data.messages[0];
} else {
lastMessage = response.data.messages.find(
(message: IJSONObject) =>
!Object.prototype.hasOwnProperty.call(message, 'bot_id')
);
}
return [lastMessage];
}
}

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/smtp/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/smtp",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "host",

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/twilio/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twilio",
"primaryColor": "f22f46",
"supportsConnections": true,
"fields": [
{
"key": "accountSid",

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/twitch/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twitch",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -1,12 +1,10 @@
import TwitterClient from './client';
import CreateTweet from './actions/create-tweet';
import { IJSONObject } from '@automatisch/types';
export default class Actions {
client: TwitterClient;
createTweet: CreateTweet;
constructor(client: TwitterClient) {
this.client = client;
this.createTweet = new CreateTweet(client);
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.createTweet = new CreateTweet(connectionData, parameters);
}
}

View File

@@ -1,17 +1,23 @@
import TwitterClient from '../client';
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
import { IJSONObject } from '@automatisch/types';
export default class CreateTweet {
client: TwitterClient;
client: TwitterApi;
parameters: IJSONObject;
constructor(client: TwitterClient) {
this.client = client;
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() {
const tweet = await this.client.createTweet.run(
this.client.step.parameters.tweet as string
);
const tweet = await this.client.v1.tweet(this.parameters.tweet as string);
return tweet;
}
}

View File

@@ -1,50 +1,65 @@
import type { IAuthentication, IField } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from './client';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
export default class Authentication implements IAuthentication {
client: TwitterClient;
appData: IApp;
connectionData: IJSONObject;
client: TwitterApi;
constructor(client: TwitterClient) {
this.client = client;
constructor(appData: IApp, connectionData: IJSONObject) {
this.appData = appData;
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() {
const appFields = this.client.connection.appData.fields.find(
const appFields = this.appData.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = appFields.value;
const response = await this.client.oauthRequestToken.run(callbackUrl);
const responseData = Object.fromEntries(new URLSearchParams(response.data));
const authLink = await this.client.generateAuthLink(callbackUrl);
return {
url: `${TwitterClient.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
url: authLink.url,
accessToken: authLink.oauth_token,
accessSecret: authLink.oauth_token_secret,
};
}
async verifyCredentials() {
const response = await this.client.verifyAccessToken.run();
const responseData = Object.fromEntries(new URLSearchParams(response.data));
const verifiedCredentials = await this.client.login(
this.connectionData.oauthVerifier as string
);
return {
consumerKey: this.client.connection.formattedData.consumerKey as string,
consumerSecret: this.client.connection.formattedData
.consumerSecret as string,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
userId: responseData.user_id,
screenName: responseData.screen_name,
consumerKey: this.connectionData.consumerKey,
consumerSecret: this.connectionData.consumerSecret,
accessToken: verifiedCredentials.accessToken,
accessSecret: verifiedCredentials.accessSecret,
userId: verifiedCredentials.userId,
screenName: verifiedCredentials.screenName,
};
}
async isStillVerified() {
try {
await this.client.getCurrentUser.run();
await this.client.currentUser();
return true;
} catch (error) {
} catch {
return false;
}
}

View File

@@ -1,40 +0,0 @@
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}`);
}
}
}

View File

@@ -1,35 +0,0 @@
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;
}
}

View File

@@ -1,45 +0,0 @@
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;
}
}

View File

@@ -1,70 +0,0 @@
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;
}
}

View File

@@ -1,71 +0,0 @@
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;
}
}

View File

@@ -1,42 +0,0 @@
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}`
);
}
}
}

View File

@@ -1,70 +0,0 @@
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 };
}
}

View File

@@ -1,20 +0,0 @@
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);
}
}
}

View File

@@ -1,64 +0,0 @@
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);
}
}

View File

@@ -1,27 +1,25 @@
import {
IService,
IAuthentication,
IFlow,
IStep,
IConnection,
IApp,
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Triggers from './triggers';
import Actions from './actions';
import TwitterClient from './client';
export default class Twitter implements IService {
client: TwitterClient;
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
this.client = new TwitterClient(connection, flow, step);
this.authenticationClient = new Authentication(this.client);
this.triggers = new Triggers(this.client);
this.actions = new Actions(this.client);
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
this.authenticationClient = new Authentication(appData, connectionData);
this.triggers = new Triggers(connectionData, parameters);
this.actions = new Actions(connectionData, parameters);
}
}

View File

@@ -3,9 +3,7 @@
"key": "twitter",
"iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twitter",
"authDocUrl": "https://automatisch.io/docs/connections/twitter",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",
@@ -16,28 +14,31 @@
"value": "{WEB_APP_URL}/app/twitter/connections/add",
"placeholder": null,
"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
},
{
"key": "consumerKey",
"label": "API Key",
"label": "Consumer Key",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/twitter#consumer-key",
"clickToCopy": false
},
{
"key": "consumerSecret",
"label": "API Secret",
"label": "Consumer Secret",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/twitter#consumer-secret",
"clickToCopy": false
}
],
@@ -216,14 +217,13 @@
],
"triggers": [
{
"name": "My Tweets",
"key": "myTweets",
"pollInterval": 15,
"name": "My Tweet",
"key": "myTweet",
"description": "Will be triggered when you tweet something new.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",
@@ -232,14 +232,13 @@
]
},
{
"name": "User Tweets",
"key": "userTweets",
"pollInterval": 15,
"name": "User Tweet",
"key": "userTweet",
"description": "Will be triggered when a specific user tweet something new.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -260,14 +259,13 @@
]
},
{
"name": "Search Tweets",
"key": "searchTweets",
"pollInterval": 15,
"name": "Search Tweet",
"key": "searchTweet",
"description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
@@ -286,22 +284,6 @@
"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": [
@@ -311,8 +293,8 @@
"description": "Will create a tweet.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseAction",

View File

@@ -1,21 +1,13 @@
import TwitterClient from './client';
import UserTweets from './triggers/user-tweets';
import SearchTweets from './triggers/search-tweets';
import MyTweets from './triggers/my-tweets';
import MyFollowers from './triggers/my-followers';
import { IJSONObject } from '@automatisch/types';
import MyTweet from './triggers/my-tweet';
import SearchTweet from './triggers/search-tweet';
export default class Triggers {
client: TwitterClient;
userTweets: UserTweets;
searchTweets: SearchTweets;
myTweets: MyTweets;
myFollowers: MyFollowers;
myTweet: MyTweet;
searchTweet: SearchTweet;
constructor(client: TwitterClient) {
this.client = client;
this.userTweets = new UserTweets(client);
this.searchTweets = new SearchTweets(client);
this.myTweets = new MyTweets(client);
this.myFollowers = new MyFollowers(client);
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.myTweet = new MyTweet(connectionData);
this.searchTweet = new SearchTweet(connectionData, parameters);
}
}

View File

@@ -1,28 +0,0 @@
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;
}
}

View File

@@ -0,0 +1,25 @@
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;
}
}

View File

@@ -1,25 +0,0 @@
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;
}
}

View File

@@ -0,0 +1,58 @@
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];
}
}

View File

@@ -1,26 +0,0 @@
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;
}
}

View File

@@ -1,27 +0,0 @@
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;
}
}

View File

@@ -5,12 +5,14 @@ import type {
IJSONObject,
} from '@automatisch/types';
import { URLSearchParams } from 'url';
import HttpClient from '../../helpers/http-client';
import axios, { AxiosInstance } from 'axios';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: HttpClient;
client: AxiosInstance = axios.create({
baseURL: 'https://api.typeform.com',
});
scope: string[] = [
'forms:read',
@@ -25,7 +27,6 @@ export default class Authentication implements IAuthentication {
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
this.client = new HttpClient({ baseURL: 'https://api.typeform.com' });
}
get oauthRedirectUrl() {

View File

@@ -4,7 +4,6 @@
"iconUrl": "{BASE_URL}/apps/typeform/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/typeform",
"primaryColor": "5865f2",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -13,7 +13,6 @@ type AppConfig = {
postgresHost: string;
postgresUsername: string;
postgresPassword?: string;
version: string;
postgresEnableSsl: boolean;
baseUrl: string;
encryptionKey: string;
@@ -21,14 +20,12 @@ type AppConfig = {
serveWebAppSeparately: boolean;
redisHost: string;
redisPort: number;
enableBullMQDashboard: boolean;
};
const host = process.env.HOST || 'localhost';
const protocol = process.env.PROTOCOL || 'http';
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}`;
if (serveWebAppSeparately) {
@@ -45,9 +42,8 @@ const appConfig: AppConfig = {
port,
appEnv: appEnv,
isDev: appEnv === 'development',
version: process.env.npm_package_version,
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',
postgresUsername:
process.env.POSTGRES_USERNAME || 'automatisch_development_user',
@@ -58,8 +54,6 @@ const appConfig: AppConfig = {
serveWebAppSeparately,
redisHost: process.env.REDIS_HOST || '127.0.0.1',
redisPort: parseInt(process.env.REDIS_PORT || '6379'),
enableBullMQDashboard:
process.env.ENABLE_BULLMQ_DASHBOARD === 'true' ? true : false,
baseUrl,
webAppUrl,
};

View File

@@ -1,8 +1,4 @@
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 type { Knex } from 'knex';
import knexConfig from '../../knexfile';
@@ -12,12 +8,10 @@ export const client: Knex = knex(knexConfig);
const CONNECTION_REFUSED = 'ECONNREFUSED';
client.raw('SELECT 1').catch((err) => {
if (err.code === CONNECTION_REFUSED) {
logger.error(
'Make sure you have installed PostgreSQL and it is running.',
err
);
process.exit();
}
});
client.raw('SELECT 1')
.catch((err) => {
if (err.code === CONNECTION_REFUSED) {
logger.error('Make sure you have installed PostgreSQL and it is running.', err);
process.exit();
}
});

View File

@@ -1,13 +0,0 @@
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');
});
}

View File

@@ -1,13 +0,0 @@
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');
});
}

View File

@@ -1,13 +0,0 @@
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');
});
}

View File

@@ -1,13 +0,0 @@
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');
});
}

View File

@@ -1,5 +1,5 @@
import Context from '../../types/express/context';
import axios from 'axios';
import App from '../../models/app';
type Params = {
input: {
@@ -20,20 +20,13 @@ const createAuthData = async (
.throwIfNotFound();
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(connection);
const appInstance = new appClass(appData, connection.formattedData);
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({
formattedData: {
...connection.formattedData,

View File

@@ -13,12 +13,19 @@ const createConnection = async (
params: Params,
context: Context
) => {
App.findOneByKey(params.input.key);
const app = App.findOneByKey(params.input.key);
return await context.currentUser.$relatedQuery('connections').insert({
key: params.input.key,
formattedData: params.input.formattedData,
});
const connection = await context.currentUser
.$relatedQuery('connections')
.insert({
key: params.input.key,
formattedData: params.input.formattedData,
});
return {
...connection,
app,
};
};
export default createConnection;

View File

@@ -4,7 +4,6 @@ import Context from '../../types/express/context';
type Params = {
input: {
triggerAppKey: string;
connectionId: string;
};
};
@@ -13,32 +12,17 @@ const createFlow = async (
params: Params,
context: Context
) => {
const connectionId = params?.input?.connectionId;
const appKey = params?.input?.triggerAppKey;
const flow = await context.currentUser.$relatedQuery('flows').insert({
name: 'Name your flow',
});
if (connectionId) {
await context.currentUser
.$relatedQuery('connections')
.findById(connectionId)
.throwIfNotFound();
}
await Step.query().insert({
flowId: flow.id,
type: 'trigger',
position: 1,
appKey,
connectionId
});
await Step.query().insert({
flowId: flow.id,
type: 'action',
position: 2
});
return flow;

View File

@@ -36,13 +36,9 @@ const updateFlowStatus = async (
const interval = trigger.interval;
const repeatOptions = {
cron: interval || EVERY_15_MINUTES_CRON,
};
}
if (flow.active) {
flow = await flow.$query().patchAndFetch({
published_at: new Date().toISOString(),
});
await processorQueue.add(
JOB_NAME,
{ flowId: flow.id },
@@ -53,7 +49,7 @@ const updateFlowStatus = async (
);
} else {
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);
}

View File

@@ -20,9 +20,9 @@ const verifyConnection = async (
.throwIfNotFound();
const appClass = (await import(`../../apps/${connection.key}`)).default;
const app = App.findOneByKey(connection.key);
const appData = App.findOneByKey(connection.key);
const appInstance = new appClass(connection);
const appInstance = new appClass(appData, connection.formattedData);
const verifiedCredentials =
await appInstance.authenticationClient.verifyCredentials();
@@ -32,13 +32,9 @@ const verifyConnection = async (
...verifiedCredentials,
},
verified: true,
draft: false,
});
return {
...connection,
app,
};
return connection;
};
export default verifyConnection;

View File

@@ -0,0 +1,27 @@
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;

View File

@@ -11,15 +11,9 @@ const getApp = async (_parent: unknown, params: Params, context: Context) => {
if (context.currentUser) {
const connections = await context.currentUser
.$relatedQuery('connections')
.select('connections.*')
.fullOuterJoinRelated('steps')
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
key: params.key,
});
return {
...app,

View File

@@ -16,42 +16,22 @@ const getConnectedApps = async (
const connections = await context.currentUser
.$relatedQuery('connections')
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.where({ verified: true })
.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 usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
apps = apps
.filter((app: IApp) => {
return usedApps.includes(app.key);
})
.filter((app: IApp) => connectionKeys.includes(app.key))
.map((app: IApp) => {
const connection = connections.find(
(connection) => (connection as IConnection).key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
if (connection) {
app.connectionCount = connection.count;
}
return app;
});

View File

@@ -1,4 +1,5 @@
import { IJSONObject } from '@automatisch/types';
import App from '../../models/app';
import Context from '../../types/express/context';
type Params = {
@@ -10,10 +11,7 @@ type Params = {
const getData = async (_parent: unknown, params: Params, context: Context) => {
const step = await context.currentUser
.$relatedQuery('steps')
.withGraphFetched({
connection: true,
flow: true,
})
.withGraphFetched('connection')
.findById(params.stepId);
if (!step) return null;
@@ -22,9 +20,10 @@ const getData = async (_parent: unknown, params: Params, context: Context) => {
if (!connection || !step.appKey) return null;
const appData = App.findOneByKey(step.appKey);
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 fetchedData = await command.run();

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -13,11 +13,7 @@ const getExecutions = async (
) => {
const executions = context.currentUser
.$relatedQuery('executions')
.withGraphFetched({
flow: {
steps: true
}
})
.withGraphFetched('flow')
.orderBy('created_at', 'desc');
return paginate(executions, params.limit, params.offset);

View File

@@ -1,42 +1,16 @@
import Context from '../../types/express/context';
import paginate from '../../helpers/pagination';
type Params = {
appKey?: string;
connectionId?: string;
name?: string;
limit: number;
offset: number;
};
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
const flowsQuery = context.currentUser
const getFlows = async (
_parent: unknown,
_params: unknown,
context: Context
) => {
const flows = await context.currentUser
.$relatedQuery('flows')
.joinRelated({
steps: true
})
.withGraphFetched({
steps: {
connection: true
}
})
.where((builder) => {
if (params.connectionId) {
builder.where('steps.connection_id', params.connectionId);
}
.withGraphJoined('[steps.[connection]]')
.orderBy('created_at', 'desc');
if (params.name) {
builder.where('flows.name', 'ilike', `%${params.name}%`);
}
if (params.appKey) {
builder.where('steps.app_key', params.appKey);
}
})
.groupBy('flows.id')
.orderBy('updated_at', 'desc');
return paginate(flowsQuery, params.limit, params.offset);
return flows;
};
export default getFlows;

View File

@@ -1,9 +0,0 @@
import appConfig from '../../config/app';
const healthcheck = () => {
return {
version: appConfig.version,
}
};
export default healthcheck;

View File

@@ -1,4 +1,5 @@
import Context from '../../types/express/context';
import App from '../../models/app';
type Params = {
id: string;
@@ -18,8 +19,9 @@ const testConnection = async (
.throwIfNotFound();
const appClass = (await import(`../../apps/${connection.key}`)).default;
const appInstance = new appClass(connection);
const appData = App.findOneByKey(connection.key);
const appInstance = new appClass(appData, connection.formattedData);
const isStillVerified =
await appInstance.authenticationClient.isStillVerified();

Some files were not shown because too many files have changed in this diff Show More