Merge pull request #632 from automatisch/feature/extended-error-handler

Extend error handling logic to capture errors
This commit is contained in:
Ömer Faruk Aydın
2022-10-22 23:11:02 +02:00
committed by GitHub
40 changed files with 254 additions and 609 deletions

View File

@@ -27,25 +27,25 @@ export default defineAction({
arguments: [ arguments: [
{ {
name: 'key', name: 'key',
value: 'listRepos' value: 'listRepos',
} },
] ],
} },
}, },
{ {
label: 'Title', label: 'Title',
key: 'title', key: 'title',
type: 'string', type: 'string',
required: true, required: true,
variables: true variables: true,
}, },
{ {
label: 'Body', label: 'Body',
key: 'body', key: 'body',
type: 'string', type: 'string',
required: true, required: true,
variables: true variables: true,
} },
], ],
}, },
{ {
@@ -59,7 +59,7 @@ export default defineAction({
const title = $.step.parameters.title as string; const title = $.step.parameters.title as string;
const body = $.step.parameters.body as string; const body = $.step.parameters.body as string;
if (!repoParameter) throw new Error('A repo must be set!') if (!repoParameter) throw new Error('A repo must be set!');
if (!title) throw new Error('A title must be set!'); if (!title) throw new Error('A title must be set!');
const { repoOwner, repo } = getRepoOwnerAndRepo(repoParameter); const { repoOwner, repo } = getRepoOwnerAndRepo(repoParameter);
@@ -68,13 +68,6 @@ export default defineAction({
body, body,
}); });
const issue: IActionOutput = { $.setActionItem({ raw: response.data });
data: {
raw: response.data,
},
error: response?.integrationError,
};
return issue;
}, },
}); });

View File

@@ -1,13 +1,16 @@
import { IGlobalVariable, IJSONObject } from "@automatisch/types"; import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import type { AxiosResponse } from 'axios'; import type { AxiosResponse } from 'axios';
import parseLinkHeader from '../../../helpers/parse-header-link'; import parseLinkHeader from '../../../helpers/parse-header-link';
type TResponse = { type TResponse = {
data: IJSONObject[], data: IJSONObject[];
error?: IJSONObject, error?: IJSONObject;
} };
export default async function paginateAll($: IGlobalVariable, request: Promise<AxiosResponse>) { export default async function paginateAll(
$: IGlobalVariable,
request: Promise<AxiosResponse>
) {
const response = await request; const response = await request;
const aggregatedResponse: TResponse = { const aggregatedResponse: TResponse = {
data: [...response.data], data: [...response.data],
@@ -21,15 +24,8 @@ export default async function paginateAll($: IGlobalVariable, request: Promise<A
url: links.next.uri, url: links.next.uri,
}); });
if (nextPageResponse.integrationError) { aggregatedResponse.data.push(...nextPageResponse.data);
aggregatedResponse.error = nextPageResponse.integrationError; links = parseLinkHeader(nextPageResponse.headers.link);
links = null;
} else {
aggregatedResponse.data.push(...nextPageResponse.data);
links = parseLinkHeader(nextPageResponse.headers.link);
}
} }
return aggregatedResponse; return aggregatedResponse;

View File

@@ -9,7 +9,7 @@ export default defineTrigger({
substeps: [ substeps: [
{ {
key: 'chooseConnection', key: 'chooseConnection',
name: 'Choose connection' name: 'Choose connection',
}, },
{ {
key: 'chooseTrigger', key: 'chooseTrigger',
@@ -27,10 +27,10 @@ export default defineTrigger({
arguments: [ arguments: [
{ {
name: 'key', name: 'key',
value: 'listRepos' value: 'listRepos',
} },
] ],
} },
}, },
{ {
label: 'Which types of issues should this trigger on?', label: 'Which types of issues should this trigger on?',
@@ -43,25 +43,25 @@ export default defineTrigger({
options: [ options: [
{ {
label: 'Any issue you can see', label: 'Any issue you can see',
value: 'all' value: 'all',
}, },
{ {
label: 'Only issues assigned to you', label: 'Only issues assigned to you',
value: 'assigned' value: 'assigned',
}, },
{ {
label: 'Only issues created by you', label: 'Only issues created by you',
value: 'created' value: 'created',
}, },
{ {
label: `Only issues you're mentioned in`, label: `Only issues you're mentioned in`,
value: 'mentioned' value: 'mentioned',
}, },
{ {
label: `Only issues you're subscribed to`, label: `Only issues you're subscribed to`,
value: 'subscribed' value: 'subscribed',
} },
] ],
}, },
{ {
label: 'Label', label: 'Label',
@@ -77,24 +77,24 @@ export default defineTrigger({
arguments: [ arguments: [
{ {
name: 'key', name: 'key',
value: 'listLabels' value: 'listLabels',
}, },
{ {
name: 'parameters.repo', name: 'parameters.repo',
value: '{parameters.repo}' value: '{parameters.repo}',
} },
] ],
} },
} },
] ],
}, },
{ {
key: 'testStep', key: 'testStep',
name: 'Test trigger' name: 'Test trigger',
} },
], ],
async run($) { async run($) {
return await newIssues($); await newIssues($);
}, },
}); });

View File

@@ -1,12 +1,11 @@
import { import { IGlobalVariable } from '@automatisch/types';
IGlobalVariable,
ITriggerOutput,
} from '@automatisch/types';
import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo';
import parseLinkHeader from '../../../../helpers/parse-header-link'; import parseLinkHeader from '../../../../helpers/parse-header-link';
function getPathname($: IGlobalVariable) { function getPathname($: IGlobalVariable) {
const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo as string); const { repoOwner, repo } = getRepoOwnerAndRepo(
$.step.parameters.repo as string
);
if (repoOwner && repo) { if (repoOwner && repo) {
return `/repos/${repoOwner}/${repo}/issues`; return `/repos/${repoOwner}/${repo}/issues`;
@@ -26,25 +25,17 @@ const newIssues = async ($: IGlobalVariable) => {
per_page: 100, per_page: 100,
}; };
const issues: ITriggerOutput = {
data: [],
};
let links; let links;
do { do {
const response = await $.http.get(pathname, { params }); const response = await $.http.get(pathname, { params });
links = parseLinkHeader(response.headers.link); links = parseLinkHeader(response.headers.link);
if (response.integrationError) {
issues.error = response.integrationError;
return issues;
}
if (response.data.length) { if (response.data.length) {
for (const issue of response.data) { for (const issue of response.data) {
const issueId = issue.id; const issueId = issue.id;
if (issueId <= Number($.flow.lastInternalId) && !$.execution.testRun) return issues; if (issueId <= Number($.flow.lastInternalId) && !$.execution.testRun)
return;
const dataItem = { const dataItem = {
raw: issue, raw: issue,
@@ -53,12 +44,10 @@ const newIssues = async ($: IGlobalVariable) => {
}, },
}; };
issues.data.push(dataItem); $.triggerOutput.data.push(dataItem);
} }
} }
} while (links.next && !$.execution.testRun); } while (links.next && !$.execution.testRun);
return issues;
}; };
export default newIssues; export default newIssues;

View File

@@ -9,7 +9,7 @@ export default defineTrigger({
substeps: [ substeps: [
{ {
key: 'chooseConnection', key: 'chooseConnection',
name: 'Choose connection' name: 'Choose connection',
}, },
{ {
key: 'chooseTrigger', key: 'chooseTrigger',
@@ -27,20 +27,20 @@ export default defineTrigger({
arguments: [ arguments: [
{ {
name: 'key', name: 'key',
value: 'listRepos' value: 'listRepos',
} },
] ],
} },
} },
] ],
}, },
{ {
key: 'testStep', key: 'testStep',
name: 'Test trigger' name: 'Test trigger',
} },
], ],
async run($) { async run($) {
return await newPullRequests($); await newPullRequests($);
}, },
}); });

View File

@@ -1,11 +1,8 @@
import { import { IGlobalVariable } from '@automatisch/types';
IGlobalVariable,
ITriggerOutput,
} from '@automatisch/types';
import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo';
import parseLinkHeader from '../../../../helpers/parse-header-link'; import parseLinkHeader from '../../../../helpers/parse-header-link';
const fetchPullRequests = async ($: IGlobalVariable) => { const newPullRequests = async ($: IGlobalVariable) => {
const repoParameter = $.step.parameters.repo as string; const repoParameter = $.step.parameters.repo as string;
if (!repoParameter) throw new Error('A repo must be set!'); if (!repoParameter) throw new Error('A repo must be set!');
@@ -20,25 +17,20 @@ const fetchPullRequests = async ($: IGlobalVariable) => {
per_page: 100, per_page: 100,
}; };
const pullRequests: ITriggerOutput = {
data: [],
};
let links; let links;
do { do {
const response = await $.http.get(pathname, { params }); const response = await $.http.get(pathname, { params });
links = parseLinkHeader(response.headers.link); links = parseLinkHeader(response.headers.link);
if (response.integrationError) {
pullRequests.error = response.integrationError;
return pullRequests;
}
if (response.data.length) { if (response.data.length) {
for (const pullRequest of response.data) { for (const pullRequest of response.data) {
const pullRequestId = pullRequest.id; const pullRequestId = pullRequest.id;
if (pullRequestId <= Number($.flow.lastInternalId) && !$.execution.testRun) return pullRequests; if (
pullRequestId <= Number($.flow.lastInternalId) &&
!$.execution.testRun
)
return;
const dataItem = { const dataItem = {
raw: pullRequest, raw: pullRequest,
@@ -47,20 +39,10 @@ const fetchPullRequests = async ($: IGlobalVariable) => {
}, },
}; };
pullRequests.data.push(dataItem); $.pushTriggerItem(dataItem);
} }
} }
} while (links.next && !$.execution.testRun); } while (links.next && !$.execution.testRun);
return pullRequests;
}
const newPullRequests = async ($: IGlobalVariable) => {
const pullRequests = await fetchPullRequests($);
pullRequests.data.reverse();
return pullRequests;
}; };
export default newPullRequests; export default newPullRequests;

View File

@@ -9,7 +9,7 @@ export default defineTrigger({
substeps: [ substeps: [
{ {
key: 'chooseConnection', key: 'chooseConnection',
name: 'Choose connection' name: 'Choose connection',
}, },
{ {
key: 'chooseTrigger', key: 'chooseTrigger',
@@ -27,20 +27,26 @@ export default defineTrigger({
arguments: [ arguments: [
{ {
name: 'key', name: 'key',
value: 'listRepos' value: 'listRepos',
} },
] ],
} },
}, },
] ],
}, },
{ {
key: 'testStep', key: 'testStep',
name: 'Test trigger' name: 'Test trigger',
} },
], ],
async run($) { async run($) {
return await newStargazers($); await newStargazers($);
},
sort(stargazerA, stargazerB) {
return (
Number(stargazerB.meta.internalId) - Number(stargazerA.meta.internalId)
);
}, },
}); });

View File

@@ -1,19 +1,17 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { import { IGlobalVariable, IJSONObject } from '@automatisch/types';
IGlobalVariable,
IJSONObject,
ITriggerOutput,
} from '@automatisch/types';
import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo';
import parseLinkHeader from '../../../../helpers/parse-header-link'; import parseLinkHeader from '../../../../helpers/parse-header-link';
type TResponseDataItem = { type TResponseDataItem = {
starred_at: string; starred_at: string;
user: IJSONObject; user: IJSONObject;
} };
const fetchStargazers = async ($: IGlobalVariable) => { const newStargazers = async ($: IGlobalVariable) => {
const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo as string); const { repoOwner, repo } = getRepoOwnerAndRepo(
$.step.parameters.repo as string
);
const firstPagePathname = `/repos/${repoOwner}/${repo}/stargazers`; const firstPagePathname = `/repos/${repoOwner}/${repo}/stargazers`;
const requestConfig = { const requestConfig = {
params: { params: {
@@ -22,35 +20,33 @@ const fetchStargazers = async ($: IGlobalVariable) => {
headers: { headers: {
// needed to get `starred_at` time // needed to get `starred_at` time
Accept: 'application/vnd.github.star+json', Accept: 'application/vnd.github.star+json',
} },
} };
const firstPageResponse = await $.http.get<TResponseDataItem[]>(firstPagePathname, requestConfig); const firstPageResponse = await $.http.get<TResponseDataItem[]>(
firstPagePathname,
requestConfig
);
const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link); const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link);
// in case there is only single page to fetch // in case there is only single page to fetch
let pathname = firstPageLinks.last?.uri || firstPagePathname; let pathname = firstPageLinks.last?.uri || firstPagePathname;
const stargazers: ITriggerOutput = {
data: [],
};
do { do {
const response = await $.http.get<TResponseDataItem[]>(pathname, requestConfig); const response = await $.http.get<TResponseDataItem[]>(
pathname,
requestConfig
);
const links = parseLinkHeader(response.headers.link); const links = parseLinkHeader(response.headers.link);
pathname = links.prev?.uri; pathname = links.prev?.uri;
if (response.integrationError) {
stargazers.error = response.integrationError;
return stargazers;
}
if (response.data.length) { if (response.data.length) {
for (const starEntry of response.data) { for (const starEntry of response.data) {
const { starred_at, user } = starEntry; const { starred_at, user } = starEntry;
const timestamp = DateTime.fromISO(starred_at).toMillis(); const timestamp = DateTime.fromISO(starred_at).toMillis();
if (timestamp <= Number($.flow.lastInternalId) && !$.execution.testRun) return stargazers; if (timestamp <= Number($.flow.lastInternalId) && !$.execution.testRun)
return;
const dataItem = { const dataItem = {
raw: user, raw: user,
@@ -59,22 +55,10 @@ const fetchStargazers = async ($: IGlobalVariable) => {
}, },
}; };
stargazers.data.push(dataItem); $.triggerOutput.data.push(dataItem);
} }
} }
} while (pathname && !$.execution.testRun); } while (pathname && !$.execution.testRun);
return stargazers;
}
const newStargazers = async ($: IGlobalVariable) => {
const stargazers = await fetchStargazers($);
stargazers.data.sort((stargazerA, stargazerB) => {
return Number(stargazerA.meta.internalId) - Number(stargazerB.meta.internalId);
});
return stargazers;
}; };
export default newStargazers; export default newStargazers;

View File

@@ -10,7 +10,7 @@ export default defineTrigger({
substeps: [ substeps: [
{ {
key: 'chooseConnection', key: 'chooseConnection',
name: 'Choose connection' name: 'Choose connection',
}, },
{ {
key: 'chooseTrigger', key: 'chooseTrigger',
@@ -28,20 +28,24 @@ export default defineTrigger({
arguments: [ arguments: [
{ {
name: 'key', name: 'key',
value: 'listRepos' value: 'listRepos',
} },
] ],
} },
}, },
] ],
}, },
{ {
key: 'testStep', key: 'testStep',
name: 'Test trigger' name: 'Test trigger',
} },
], ],
async run($) { async run($) {
return await newWatchers($); await newWatchers($);
},
sort() {
return -1;
}, },
}); });

View File

@@ -1,11 +1,8 @@
import { import { IGlobalVariable, ITriggerOutput } from '@automatisch/types';
IGlobalVariable,
ITriggerOutput,
} from '@automatisch/types';
import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo'; import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo';
import parseLinkHeader from '../../../../helpers/parse-header-link'; import parseLinkHeader from '../../../../helpers/parse-header-link';
const fetchWatchers = async ($: IGlobalVariable) => { const newWatchers = async ($: IGlobalVariable) => {
const repoParameter = $.step.parameters.repo as string; const repoParameter = $.step.parameters.repo as string;
if (!repoParameter) throw new Error('A repo must be set!'); if (!repoParameter) throw new Error('A repo must be set!');
@@ -15,9 +12,9 @@ const fetchWatchers = async ($: IGlobalVariable) => {
const firstPagePathname = `/repos/${repoOwner}/${repo}/subscribers`; const firstPagePathname = `/repos/${repoOwner}/${repo}/subscribers`;
const requestConfig = { const requestConfig = {
params: { params: {
per_page: 100 per_page: 100,
}, },
} };
const firstPageResponse = await $.http.get(firstPagePathname, requestConfig); const firstPageResponse = await $.http.get(firstPagePathname, requestConfig);
const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link); const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link);
@@ -25,20 +22,11 @@ const fetchWatchers = async ($: IGlobalVariable) => {
// in case there is only single page to fetch // in case there is only single page to fetch
let pathname = firstPageLinks.last?.uri || firstPagePathname; let pathname = firstPageLinks.last?.uri || firstPagePathname;
const watchers: ITriggerOutput = {
data: [],
};
do { do {
const response = await $.http.get(pathname, requestConfig); const response = await $.http.get(pathname, requestConfig);
const links = parseLinkHeader(response.headers.link); const links = parseLinkHeader(response.headers.link);
pathname = links.prev?.uri; pathname = links.prev?.uri;
if (response.integrationError) {
watchers.error = response.integrationError;
return watchers;
}
if (response.data.length) { if (response.data.length) {
// to iterate reverse-chronologically // to iterate reverse-chronologically
response.data.reverse(); response.data.reverse();
@@ -46,7 +34,8 @@ const fetchWatchers = async ($: IGlobalVariable) => {
for (const watcher of response.data) { for (const watcher of response.data) {
const watcherId = watcher.id.toString(); const watcherId = watcher.id.toString();
if ($.flow.isAlreadyProcessed(watcherId) && !$.execution.testRun) return watchers; if ($.flow.isAlreadyProcessed(watcherId) && !$.execution.testRun)
return;
const dataItem = { const dataItem = {
raw: watcher, raw: watcher,
@@ -55,21 +44,10 @@ const fetchWatchers = async ($: IGlobalVariable) => {
}, },
}; };
watchers.data.push(dataItem); $.pushTriggerItem(dataItem);
} }
} }
} while (pathname && !$.execution.testRun === false); } while (pathname && !$.execution.testRun === false);
return watchers;
}
const newWatchers = async ($: IGlobalVariable) => {
const watchers = await fetchWatchers($);
// to process chronologically
watchers.data.reverse();
return watchers;
}; };
export default newWatchers; export default newWatchers;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,39 +0,0 @@
import type { IAuthentication, IApp, IJSONObject } from '@automatisch/types';
import { Client } from 'pg';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: Client;
constructor(appData: IApp, connectionData: IJSONObject) {
this.client = new Client({
host: connectionData.host as string,
port: connectionData.port as number,
database: connectionData.database as string,
user: connectionData.username as string,
password: connectionData.password as string,
ssl: connectionData.ssl as boolean,
});
this.connectionData = connectionData;
this.appData = appData;
}
async verifyCredentials() {
await this.client.connect();
return {
screenName: this.connectionData.database,
};
}
async isStillVerified() {
try {
await this.client.connect();
return true;
} catch (error) {
return false;
}
}
}

View File

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

View File

@@ -1,201 +0,0 @@
{
"name": "PostgreSQL",
"key": "postgresql",
"iconUrl": "{BASE_URL}/apps/postgresql/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/postgresql",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "host",
"label": "Host",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": "The host information Automatisch will connect to.",
"docUrl": "https://automatisch.io/docs/postgresql#host",
"clickToCopy": false
},
{
"key": "port",
"label": "Port",
"type": "integer",
"required": true,
"readOnly": false,
"value": 5432,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/postgresql#port",
"clickToCopy": false
},
{
"key": "database",
"label": "Database",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": "The name of the database.",
"docUrl": "https://automatisch.io/docs/postgresql#password",
"clickToCopy": false
},
{
"key": "username",
"label": "Username",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/postgresql#username",
"clickToCopy": false
},
{
"key": "password",
"label": "Password",
"type": "string",
"required": false,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/postgresql#password",
"clickToCopy": false
},
{
"key": "ssl",
"label": "Use SSL?",
"type": "boolean",
"required": true,
"readOnly": false,
"value": false,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/postgresql#ssl",
"clickToCopy": false
}
],
"authenticationSteps": [
{
"step": 1,
"type": "mutation",
"name": "createConnection",
"arguments": [
{
"name": "key",
"value": "{key}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "host",
"value": "{fields.host}"
},
{
"name": "port",
"value": "{fields.port}"
},
{
"name": "database",
"value": "{fields.database}"
},
{
"name": "username",
"value": "{fields.username}"
},
{
"name": "password",
"value": "{fields.password}"
},
{
"name": "ssl",
"value": "{fields.ssl}"
}
]
}
]
},
{
"step": 2,
"type": "mutation",
"name": "verifyConnection",
"arguments": [
{
"name": "id",
"value": "{createConnection.id}"
}
]
}
],
"reconnectionSteps": [
{
"step": 1,
"type": "mutation",
"name": "resetConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
},
{
"step": 2,
"type": "mutation",
"name": "updateConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "host",
"value": "{fields.host}"
},
{
"name": "port",
"value": "{fields.port}"
},
{
"name": "database",
"value": "{fields.database}"
},
{
"name": "username",
"value": "{fields.username}"
},
{
"name": "password",
"value": "{fields.password}"
},
{
"name": "ssl",
"value": "{fields.ssl}"
}
]
}
]
},
{
"step": 3,
"type": "mutation",
"name": "verifyConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
}
]
}

View File

@@ -171,6 +171,6 @@ export default defineTrigger({
}, },
}; };
return { data: [dataItem] }; $.triggerOutput.data.push(dataItem);
}, },
}); });

View File

@@ -65,6 +65,6 @@ export default defineTrigger({
}, },
}; };
return { data: [dataItem] }; $.triggerOutput.data.push(dataItem);
}, },
}); });

View File

@@ -287,6 +287,6 @@ export default defineTrigger({
}, },
}; };
return { data: [dataItem] }; $.triggerOutput.data.push(dataItem);
}, },
}); });

View File

@@ -191,6 +191,6 @@ export default defineTrigger({
}, },
}; };
return { data: [dataItem] }; $.triggerOutput.data.push(dataItem);
}, },
}); });

View File

@@ -21,14 +21,11 @@ const findMessage = async ($: IGlobalVariable, options: FindMessageOptions) => {
const data = response.data; const data = response.data;
const message: IActionOutput = { if (!data.ok && data) {
data: { throw new Error(JSON.stringify(response.data));
raw: data?.messages.matches[0], }
},
error: response?.integrationError || (!data.ok && data),
};
return message; $.actionOutput.data.raw = data?.messages.matches[0];
}; };
export default findMessage; export default findMessage;

View File

@@ -1,4 +1,4 @@
import { IGlobalVariable, IActionOutput } from '@automatisch/types'; import { IGlobalVariable } from '@automatisch/types';
const postMessage = async ( const postMessage = async (
$: IGlobalVariable, $: IGlobalVariable,
@@ -12,18 +12,15 @@ const postMessage = async (
const response = await $.http.post('/chat.postMessage', params); const response = await $.http.post('/chat.postMessage', params);
const message: IActionOutput = {
data: {
raw: response?.data?.message,
},
error: response?.integrationError,
};
if (response.data.ok === false) { if (response.data.ok === false) {
message.error = response.data; throw new Error(JSON.stringify(response.data));
} }
return message; const message = {
raw: response?.data?.message,
};
$.setActionItem(message);
}; };
export default postMessage; export default postMessage;

View File

@@ -15,14 +15,8 @@ export default {
const response = await $.http.get('/conversations.list'); const response = await $.http.get('/conversations.list');
if (response.integrationError) {
channels.error = response.integrationError;
return channels;
}
if (response.data.ok === false) { if (response.data.ok === false) {
channels.error = response.data; throw new Error(response.data);
return channels;
} }
channels.data = response.data.channels.map((channel: IJSONObject) => { channels.data = response.data.channels.map((channel: IJSONObject) => {

View File

@@ -36,13 +36,6 @@ export default defineAction({
text, text,
}); });
const tweet: IActionOutput = { $.actionOutput.data.raw = response.data;
data: {
raw: response.data,
},
error: response?.integrationError,
};
return tweet;
}, },
}); });

View File

@@ -1,8 +1,4 @@
import { import { IGlobalVariable, IJSONObject } from '@automatisch/types';
IGlobalVariable,
IJSONObject,
ITriggerOutput,
} from '@automatisch/types';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import { omitBy, isEmpty } from 'lodash'; import { omitBy, isEmpty } from 'lodash';
@@ -16,10 +12,6 @@ const getUserFollowers = async (
) => { ) => {
let response; let response;
const followers: ITriggerOutput = {
data: [],
};
do { do {
const params: IJSONObject = { const params: IJSONObject = {
pagination_token: response?.data?.meta?.next_token, pagination_token: response?.data?.meta?.next_token,
@@ -33,31 +25,23 @@ const getUserFollowers = async (
response = await $.http.get(requestPath); response = await $.http.get(requestPath);
if (response.integrationError) {
followers.error = response.integrationError;
return followers;
}
if (response.data?.errors) { if (response.data?.errors) {
followers.error = response.data.errors; throw new Error(response.data.errors);
return followers;
} }
if (response.data.meta.result_count > 0) { if (response.data.meta.result_count > 0) {
for (const follower of response.data.data) { for (const follower of response.data.data) {
if ($.flow.isAlreadyProcessed(follower.id as string)) { if ($.flow.isAlreadyProcessed(follower.id as string)) {
return followers; return;
} }
followers.data.push({ $.pushTriggerItem({
raw: follower, raw: follower,
meta: { internalId: follower.id as string }, meta: { internalId: follower.id as string },
}); });
} }
} }
} while (response.data.meta.next_token && !$.execution.testRun); } while (response.data.meta.next_token && !$.execution.testRun);
return followers;
}; };
export default getUserFollowers; export default getUserFollowers;

View File

@@ -1,8 +1,4 @@
import { import { IGlobalVariable, IJSONObject } from '@automatisch/types';
IGlobalVariable,
IJSONObject,
ITriggerOutput,
} from '@automatisch/types';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import omitBy from 'lodash/omitBy'; import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
@@ -18,10 +14,6 @@ const fetchTweets = async ($: IGlobalVariable, username: string) => {
let response; let response;
const tweets: ITriggerOutput = {
data: [],
};
do { do {
const params: IJSONObject = { const params: IJSONObject = {
since_id: $.execution.testRun ? null : $.flow.lastInternalId, since_id: $.execution.testRun ? null : $.flow.lastInternalId,
@@ -36,14 +28,9 @@ const fetchTweets = async ($: IGlobalVariable, username: string) => {
response = await $.http.get(requestPath); response = await $.http.get(requestPath);
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.meta.result_count > 0) { if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => { response.data.data.forEach((tweet: IJSONObject) => {
tweets.data.push({ $.triggerOutput.data.push({
raw: tweet, raw: tweet,
meta: { meta: {
internalId: tweet.id as string, internalId: tweet.id as string,
@@ -53,7 +40,7 @@ const fetchTweets = async ($: IGlobalVariable, username: string) => {
} }
} while (response.data.meta.next_token && !$.execution.testRun); } while (response.data.meta.next_token && !$.execution.testRun);
return tweets; return $.triggerOutput;
}; };
const getUserTweets = async ( const getUserTweets = async (
@@ -69,13 +56,7 @@ const getUserTweets = async (
username = $.step.parameters.username as string; username = $.step.parameters.username as string;
} }
const tweets = await fetchTweets($, username); await fetchTweets($, username);
tweets.data.sort((tweet, nextTweet) => {
return Number(tweet.meta.internalId) - Number(nextTweet.meta.internalId);
});
return tweets;
}; };
export default getUserTweets; export default getUserTweets;

View File

@@ -18,8 +18,10 @@ export default defineTrigger({
], ],
async run($) { async run($) {
return await getUserTweets($, { await getUserTweets($, { currentUser: true });
currentUser: true, },
});
sort(tweet, nextTweet) {
return Number(nextTweet.meta.internalId) - Number(tweet.meta.internalId);
}, },
}); });

View File

@@ -19,6 +19,6 @@ export default defineTrigger({
], ],
async run($) { async run($) {
return await myFollowers($); await myFollowers($);
}, },
}); });

View File

@@ -31,6 +31,10 @@ export default defineTrigger({
], ],
async run($) { async run($) {
return await searchTweets($); await searchTweets($);
},
sort(tweet, nextTweet) {
return Number(nextTweet.meta.internalId) - Number(tweet.meta.internalId);
}, },
}); });

View File

@@ -1,20 +1,12 @@
import { import { IGlobalVariable, IJSONObject } from '@automatisch/types';
IGlobalVariable,
IJSONObject,
ITriggerOutput,
} from '@automatisch/types';
import qs from 'qs'; import qs from 'qs';
import { omitBy, isEmpty } from 'lodash'; import { omitBy, isEmpty } from 'lodash';
const fetchTweets = async ($: IGlobalVariable) => { const searchTweets = async ($: IGlobalVariable) => {
const searchTerm = $.step.parameters.searchTerm as string; const searchTerm = $.step.parameters.searchTerm as string;
let response; let response;
const tweets: ITriggerOutput = {
data: [],
};
do { do {
const params: IJSONObject = { const params: IJSONObject = {
query: searchTerm, query: searchTerm,
@@ -30,14 +22,8 @@ const fetchTweets = async ($: IGlobalVariable) => {
response = await $.http.get(requestPath); response = await $.http.get(requestPath);
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.errors) { if (response.data.errors) {
tweets.error = response.data.errors; throw new Error(JSON.stringify(response.data.errors));
return tweets;
} }
if (response.data.meta.result_count > 0) { if (response.data.meta.result_count > 0) {
@@ -49,22 +35,10 @@ const fetchTweets = async ($: IGlobalVariable) => {
}, },
}; };
tweets.data.push(dataItem); $.triggerOutput.data.push(dataItem);
}); });
} }
} while (response.data.meta.next_token && !$.execution.testRun); } while (response.data.meta.next_token && !$.execution.testRun);
return tweets;
};
const searchTweets = async ($: IGlobalVariable) => {
const tweets = await fetchTweets($);
tweets.data.sort((tweet, nextTweet) => {
return Number(tweet.meta.internalId) - Number(nextTweet.meta.internalId);
});
return tweets;
}; };
export default searchTweets; export default searchTweets;

View File

@@ -30,8 +30,10 @@ export default defineTrigger({
], ],
async run($) { async run($) {
return await getUserTweets($, { await getUserTweets($, { currentUser: false });
currentUser: false, },
});
sort(tweet, nextTweet) {
return Number(nextTweet.meta.internalId) - Number(tweet.meta.internalId);
}, },
}); });

View File

@@ -3,7 +3,13 @@ import Connection from '../models/connection';
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 { IJSONObject, IApp, IGlobalVariable } from '@automatisch/types'; import {
IJSONObject,
IApp,
IGlobalVariable,
ITriggerItem,
IActionItem,
} from '@automatisch/types';
type GlobalVariableOptions = { type GlobalVariableOptions = {
connection?: Connection; connection?: Connection;
@@ -59,6 +65,20 @@ const globalVariable = async (
id: execution?.id, id: execution?.id,
testRun, testRun,
}, },
triggerOutput: {
data: [],
},
actionOutput: {
data: {
raw: null,
},
},
pushTriggerItem: (triggerItem: ITriggerItem) => {
$.triggerOutput.data.push(triggerItem);
},
setActionItem: (actionItem: IActionItem) => {
$.actionOutput.data = actionItem;
},
}; };
$.http = createHttpClient({ $.http = createHttpClient({

View File

@@ -39,8 +39,8 @@ export default function createHttpClient({
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
error.response.integrationError = error.response.data; error.response.httpError = error.response.data;
return error.response; throw error;
} }
); );

View File

@@ -10,12 +10,7 @@ class App {
// Temporaryly restrict the apps we expose until // Temporaryly restrict the apps we expose until
// their actions/triggers are implemented! // their actions/triggers are implemented!
static temporaryList = [ static temporaryList = ['github', 'scheduler', 'slack', 'twitter'];
'github',
'scheduler',
'slack',
'twitter',
];
static async findAll(name?: string, stripFuncs = true): Promise<IApp[]> { static async findAll(name?: string, stripFuncs = true): Promise<IApp[]> {
if (!name) if (!name)

View File

@@ -39,16 +39,29 @@ export const processAction = async (options: ProcessActionOptions) => {
const actionCommand = await step.getActionCommand(); const actionCommand = await step.getActionCommand();
$.step.parameters = computedParameters; $.step.parameters = computedParameters;
const actionOutput = await actionCommand.run($);
try {
await actionCommand.run($);
} catch (error) {
if (error?.response?.httpError) {
$.actionOutput.error = error.response.httpError;
} else {
try {
$.actionOutput.error = JSON.parse(error.message);
} catch {
$.actionOutput.error = { error: error.message };
}
}
}
const executionStep = await execution const executionStep = await execution
.$relatedQuery('executionSteps') .$relatedQuery('executionSteps')
.insertAndFetch({ .insertAndFetch({
stepId: $.step.id, stepId: $.step.id,
status: actionOutput.error ? 'failure' : 'success', status: $.actionOutput.error ? 'failure' : 'success',
dataIn: computedParameters, dataIn: computedParameters,
dataOut: actionOutput.error ? null : actionOutput.data?.raw, dataOut: $.actionOutput.error ? null : $.actionOutput.data?.raw,
errorDetails: actionOutput.error ? actionOutput.error : null, errorDetails: $.actionOutput.error ? $.actionOutput.error : null,
}); });
return { flowId, stepId, executionId, executionStep }; return { flowId, stepId, executionId, executionStep };

View File

@@ -20,5 +20,23 @@ export const processFlow = async (options: ProcessFlowOptions) => {
testRun: options.testRun, testRun: options.testRun,
}); });
return await triggerCommand.run($); try {
await triggerCommand.run($);
} catch (error) {
if (error?.response?.httpError) {
$.triggerOutput.error = error.response.httpError;
} else {
try {
$.triggerOutput.error = JSON.parse(error.message);
} catch {
$.triggerOutput.error = { error: error.message };
}
}
}
if (triggerCommand?.sort) {
$.triggerOutput.data.sort(triggerCommand.sort);
}
return $.triggerOutput;
}; };

View File

@@ -35,13 +35,13 @@ const testRun = async (options: TestRunOptions) => {
return { executionStep: triggerExecutionStepWithError }; return { executionStep: triggerExecutionStepWithError };
} }
const firstTriggerDataItem = data[0]; const firstTriggerItem = data[0];
const { executionId, executionStep: triggerExecutionStep } = const { executionId, executionStep: triggerExecutionStep } =
await processTrigger({ await processTrigger({
flowId: flow.id, flowId: flow.id,
stepId: triggerStep.id, stepId: triggerStep.id,
triggerDataItem: firstTriggerDataItem, triggerItem: firstTriggerItem,
testRun: true, testRun: true,
}); });

View File

@@ -1,4 +1,4 @@
import { IJSONObject, ITriggerDataItem } from '@automatisch/types'; import { IJSONObject, ITriggerItem } from '@automatisch/types';
import Step from '../models/step'; import Step from '../models/step';
import Flow from '../models/flow'; import Flow from '../models/flow';
import Execution from '../models/execution'; import Execution from '../models/execution';
@@ -7,13 +7,13 @@ import globalVariable from '../helpers/global-variable';
type ProcessTriggerOptions = { type ProcessTriggerOptions = {
flowId: string; flowId: string;
stepId: string; stepId: string;
triggerDataItem?: ITriggerDataItem; triggerItem?: ITriggerItem;
error?: IJSONObject; error?: IJSONObject;
testRun?: boolean; testRun?: boolean;
}; };
export const processTrigger = async (options: ProcessTriggerOptions) => { export const processTrigger = async (options: ProcessTriggerOptions) => {
const { flowId, stepId, triggerDataItem, error, testRun } = options; const { flowId, stepId, triggerItem, error, testRun } = options;
const step = await Step.query().findById(stepId).throwIfNotFound(); const step = await Step.query().findById(stepId).throwIfNotFound();
@@ -29,7 +29,7 @@ export const processTrigger = async (options: ProcessTriggerOptions) => {
const execution = await Execution.query().insert({ const execution = await Execution.query().insert({
flowId: $.flow.id, flowId: $.flow.id,
testRun, testRun,
internalId: triggerDataItem?.meta.internalId, internalId: triggerItem?.meta.internalId,
}); });
const executionStep = await execution const executionStep = await execution
@@ -38,7 +38,7 @@ export const processTrigger = async (options: ProcessTriggerOptions) => {
stepId: $.step.id, stepId: $.step.id,
status: error ? 'failure' : 'success', status: error ? 'failure' : 'success',
dataIn: $.step.parameters, dataIn: $.step.parameters,
dataOut: !error ? triggerDataItem?.raw : null, dataOut: !error ? triggerItem?.raw : null,
errorDetails: error, errorDetails: error,
}); });

View File

@@ -15,13 +15,15 @@ export const worker = new Worker(
const { data, error } = await processFlow({ flowId }); const { data, error } = await processFlow({ flowId });
for (const triggerDataItem of data) { const reversedData = data.reverse();
const jobName = `${triggerStep.id}-${triggerDataItem.meta.internalId}`;
for (const triggerItem of reversedData) {
const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`;
const jobPayload = { const jobPayload = {
flowId, flowId,
stepId: triggerStep.id, stepId: triggerStep.id,
triggerDataItem, triggerItem,
}; };
await triggerQueue.add(jobName, jobPayload); await triggerQueue.add(jobName, jobPayload);

View File

@@ -1,7 +1,7 @@
import { Worker } from 'bullmq'; import { Worker } from 'bullmq';
import redisConfig from '../config/redis'; import redisConfig from '../config/redis';
import logger from '../helpers/logger'; import logger from '../helpers/logger';
import { IJSONObject, ITriggerDataItem } from '@automatisch/types'; import { IJSONObject, ITriggerItem } from '@automatisch/types';
import actionQueue from '../queues/action'; import actionQueue from '../queues/action';
import Step from '../models/step'; import Step from '../models/step';
import { processTrigger } from '../services/trigger'; import { processTrigger } from '../services/trigger';
@@ -9,7 +9,7 @@ import { processTrigger } from '../services/trigger';
type JobData = { type JobData = {
flowId: string; flowId: string;
stepId: string; stepId: string;
triggerDataItem?: ITriggerDataItem; triggerItem?: ITriggerItem;
error?: IJSONObject; error?: IJSONObject;
}; };

View File

@@ -171,7 +171,7 @@ export interface IApp {
export type TBeforeRequest = { export type TBeforeRequest = {
($: IGlobalVariable, requestConfig: AxiosRequestConfig): AxiosRequestConfig; ($: IGlobalVariable, requestConfig: AxiosRequestConfig): AxiosRequestConfig;
} };
export interface IData { export interface IData {
[index: string]: any; [index: string]: any;
@@ -194,11 +194,11 @@ export interface IService {
} }
export interface ITriggerOutput { export interface ITriggerOutput {
data: ITriggerDataItem[]; data: ITriggerItem[];
error?: IJSONObject; error?: IJSONObject;
} }
export interface ITriggerDataItem { export interface ITriggerItem {
raw: IJSONObject; raw: IJSONObject;
meta: { meta: {
internalId: string; internalId: string;
@@ -212,19 +212,18 @@ export interface ITrigger {
description: string; description: string;
dedupeStrategy?: 'greatest' | 'unique' | 'last'; dedupeStrategy?: 'greatest' | 'unique' | 'last';
substeps: ISubstep[]; substeps: ISubstep[];
getInterval?(parameters: IGlobalVariable['step']['parameters']): string; getInterval?(parameters: IStep['parameters']): string;
run($: IGlobalVariable): Promise<ITriggerOutput>; run($: IGlobalVariable): Promise<void>;
sort?(item: ITriggerItem, nextItem: ITriggerItem): number;
} }
export interface IActionOutput { export interface IActionOutput {
data: IActionDataItem; data: IActionItem;
error?: IJSONObject; error?: IJSONObject;
} }
export interface IActionDataItem { export interface IActionItem {
raw: { raw: IJSONObject;
data?: IJSONObject;
};
} }
export interface IAction { export interface IAction {
@@ -232,7 +231,7 @@ export interface IAction {
key: string; key: string;
description: string; description: string;
substeps: ISubstep[]; substeps: ISubstep[];
run($: IGlobalVariable): Promise<IActionOutput>; run($: IGlobalVariable): Promise<void>;
} }
export interface IAuthentication { export interface IAuthentication {
@@ -279,11 +278,14 @@ export type IGlobalVariable = {
id: string; id: string;
testRun: boolean; testRun: boolean;
}; };
process?: (triggerDataItem: ITriggerDataItem) => Promise<void>; triggerOutput?: ITriggerOutput;
actionOutput?: IActionOutput;
pushTriggerItem?: (triggerItem: ITriggerItem) => void;
setActionItem?: (actionItem: IActionItem) => void;
}; };
declare module 'axios' { declare module 'axios' {
interface AxiosResponse { interface AxiosResponse {
integrationError?: IJSONObject; httpError?: IJSONObject;
} }
} }