Compare commits

..

1 Commits

Author SHA1 Message Date
Rıdvan Akca
a650e3beaa feat(amazon-s3): add amazon s3 integration 2023-11-21 16:28:47 +03:00
204 changed files with 1630 additions and 2037 deletions

View File

@@ -4,11 +4,6 @@ on:
branches:
- main
pull_request:
paths:
- 'packages/backend/**'
- 'packages/e2e-tests/**'
- 'packages/web/**'
- '!packages/backend/src/apps/**'
workflow_dispatch:
env:

View File

@@ -1,28 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
overrides: [
{
files: ['**/*.test.ts', '**/test/**/*.ts'],
rules: {
'@typescript-eslint/ban-ts-comment': ['off'],
'@typescript-eslint/no-explicit-any': ['off'],
},
},
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': ['off'],
},
},
],
};

View File

@@ -6,9 +6,10 @@ import Role from '../../src/models/role';
import '../../src/config/orm';
async function fetchAdminRole() {
const role = await Role.query()
const role = await Role
.query()
.where({
key: 'admin',
key: 'admin'
})
.limit(1)
.first();
@@ -40,7 +41,7 @@ export async function createUser(
logger.info('No need to seed a user.');
}
} catch (err) {
if (err.nativeError.code !== UNIQUE_VIOLATION_CODE) {
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
throw err;
}
@@ -67,7 +68,7 @@ export const createDatabase = async (database = appConfig.postgresDatabase) => {
await client.query(`CREATE DATABASE ${database}`);
logger.info(`Database: ${database} created!`);
} catch (err) {
if (err.code !== DUPLICATE_DB_CODE) {
if ((err as any).code !== DUPLICATE_DB_CODE) {
throw err;
}
@@ -84,7 +85,7 @@ export const createDatabaseUser = async (user = appConfig.postgresUsername) => {
return result;
} catch (err) {
if (err.code !== DUPLICATE_OBJECT_CODE) {
if ((err as any).code !== DUPLICATE_OBJECT_CODE) {
throw err;
}

View File

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" width="428" height="512" viewBox="0 0 428 512">
<defs>
<style>
.cls-1 {
fill: #e25444;
}
.cls-1, .cls-2, .cls-3 {
fill-rule: evenodd;
}
.cls-2 {
fill: #7b1d13;
}
.cls-3 {
fill: #58150d;
}
</style>
</defs>
<path class="cls-1" d="M378,99L295,257l83,158,34-19V118Z"/>
<path class="cls-2" d="M378,99L212,118,127.5,257,212,396l166,19V99Z"/>
<path class="cls-3" d="M43,99L16,111V403l27,12L212,257Z"/>
<path class="cls-1" d="M42.637,98.667l169.587,47.111V372.444L42.637,415.111V98.667Z"/>
<path class="cls-3" d="M212.313,170.667l-72.008-11.556,72.008-81.778,71.83,81.778Z"/>
<path class="cls-3" d="M284.143,159.111l-71.919,11.733-71.919-11.733V77.333"/>
<path class="cls-3" d="M212.313,342.222l-72.008,13.334,72.008,70.222,71.83-70.222Z"/>
<path class="cls-2" d="M212,16L140,54V159l72.224-20.333Z"/>
<path class="cls-2" d="M212.224,196.444l-71.919,7.823V309.105l71.919,8.228V196.444Z"/>
<path class="cls-2" d="M212.224,373.333L140.305,355.3V458.363L212.224,496V373.333Z"/>
<path class="cls-1" d="M284.143,355.3l-71.919,18.038V496l71.919-37.637V355.3Z"/>
<path class="cls-1" d="M212.224,196.444l71.919,7.823V309.105l-71.919,8.228V196.444Z"/>
<path class="cls-1" d="M212,16l72,38V159l-72-20V16Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,56 @@
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string' as const,
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/amazon-s3/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in AWS, enter the URL above.',
clickToCopy: true,
},
{
key: 'accessKeyId',
label: 'Access Key ID',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'secretAccessKey',
label: 'Secret Access Key',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'screenName',
label: 'Screen Name',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,9 @@
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
const isStillVerified = async ($: IGlobalVariable) => {
const currentUser = await getCurrentUser($);
return !!currentUser.resourceName;
};
export default isStillVerified;

View File

@@ -0,0 +1,9 @@
import { IGlobalVariable } from '@automatisch/types';
const verifyCredentials = async ($: IGlobalVariable) => {
const { data } = await $.http.get('/');
console.log('data:', data);
};
export default verifyCredentials;

View File

@@ -0,0 +1,153 @@
import { IJSONObject, TBeforeRequest } from '@automatisch/types';
import crypto from 'crypto';
import { getISODate, getYYYYMMDD } from './get-current-date';
function hmac(key: string | Buffer, data: string) {
return crypto.createHmac('sha256', key).update(data).digest('hex');
}
function hmacWoHex(key: Buffer | string, data: string) {
return crypto.createHmac('sha256', key).update(data).digest();
}
function hash(data: string) {
return crypto.createHash('sha256').update(data).digest('hex');
}
function prepareCanonicalRequest(
method: string,
path: string,
queryParams: IJSONObject | string,
headers: IJSONObject,
payload: string
) {
const canonicalRequest = [method, encodeURIComponent(path)];
// Step 3: Canonical Query String
if (typeof queryParams === 'string') {
canonicalRequest.push('');
} else {
const sortedQueryParams = Object.keys(queryParams)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
queryParams[key] as string
)}`
)
.sort();
canonicalRequest.push(sortedQueryParams.join('&'));
}
// Step 4: Canonical Headers
const sortedHeaders = Object.keys(headers)
.sort()
.map((key) => `${key.toLowerCase()}:${(headers[key] as string).trim()}`);
canonicalRequest.push(sortedHeaders.join('\n'));
// Step 5: Signed Headers
const signedHeaders = Object.keys(headers)
.sort()
.map((key) => key.toLowerCase())
.join(';');
canonicalRequest.push(signedHeaders);
const hashedPayload = hash(payload);
canonicalRequest.push(hashedPayload);
return canonicalRequest.join('\n');
}
function prepareStringToSign(
datetime: string,
credentialScope: string,
hashedCanonicalRequest: string
) {
const stringToSign = [
'AWS4-HMAC-SHA256',
datetime,
credentialScope,
hashedCanonicalRequest,
];
return stringToSign.join('\n');
}
function calculateSigningKey(
secretKey: string,
date: string,
region: string,
service: string
) {
const dateKey = hmacWoHex('AWS4' + secretKey, date);
const dateRegionKey = hmacWoHex(dateKey, region);
const dateRegionServiceKey = hmacWoHex(dateRegionKey, service);
const signingKey = hmacWoHex(dateRegionServiceKey, 'aws4_request');
return signingKey;
}
function createAuthorizationHeader(
accessKey: string,
credentialScope: string,
signedHeaders: string,
signature: string
) {
return `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
const accessKeyId = $.auth.data.accessKeyId as string;
const secretAccessKey = $.auth.data.secretAccessKey as string;
const date = getYYYYMMDD();
const formattedDate = getISODate();
const region = 'us-east-1';
const method = 'GET';
const path = '/';
const queryParams = '';
const payload = '';
const headers = {
Host: 's3.amazonaws.com',
'X-Amz-Content-Sha256': hash(payload),
'X-Amz-Date': formattedDate,
};
const headerKeys = Object.keys(headers)
.sort()
.map((header) => header.toLowerCase())
.join(';');
const canonicalRequest = prepareCanonicalRequest(
method,
path,
queryParams,
headers,
payload
);
const stringToSign = prepareStringToSign(
formattedDate,
`${date}/${region}/s3/aws4_request`,
hash(canonicalRequest)
);
const signingKey = calculateSigningKey(secretAccessKey, date, region, 's3');
const signature = hmac(signingKey, stringToSign);
const authorizationHeader = createAuthorizationHeader(
accessKeyId,
`${date}/${region}/s3/aws4_request`,
headerKeys,
signature
);
if ($.auth.data?.secretAccessKey && $.auth.data?.accessKeyId) {
requestConfig.headers.Authorization = authorizationHeader;
requestConfig.headers['Host'] = 's3.amazonaws.com';
requestConfig.headers['X-Amz-Content-Sha256'] = hash(payload);
requestConfig.headers['X-Amz-Date'] = formattedDate;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,13 @@
export const getYYYYMMDD = () => {
const today = new Date();
const year = today.getFullYear();
const month = (today.getMonth() + 1).toString().padStart(2, '0');
const day = today.getDate().toString().padStart(2, '0');
const formattedDate = `${year}${month}${day}`;
return formattedDate;
};
export const getISODate = () => {
return new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
};

View File

@@ -1,9 +1,7 @@
import { IGlobalVariable } from '@automatisch/types';
const getCurrentUser = async ($: IGlobalVariable) => {
const { data: currentUser } = await $.http.get(
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
);
const { data: currentUser } = await $.http.get('/');
return currentUser;
};

View File

View File

@@ -0,0 +1,16 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-header';
import auth from './auth';
export default defineApp({
name: 'Amazon S3',
key: 'amazon-s3',
baseUrl: '',
apiBaseUrl: 'https://s3.amazonaws.com',
iconUrl: '{BASE_URL}/apps/amazon-s3/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/amazon-s3/connection',
primaryColor: '7B1D13',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,
});

View File

@@ -1,102 +0,0 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Create a scheduled event',
key: 'createScheduledEvent',
description: 'Creates a scheduled event',
arguments: [
{
label: 'Type',
key: 'entityType',
type: 'dropdown' as const,
required: true,
variables: true,
options: [
{ label: 'Stage channel', value: 1 },
{ label: 'Voice channel', value: 2 },
{ label: 'External', value: 3 }
],
additionalFields: {
type: 'query',
name: 'getDynamicFields',
arguments: [
{
name: 'key',
value: 'listExternalScheduledEventFields',
},
{
name: 'parameters.entityType',
value: '{parameters.entityType}',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: true,
variables: true,
},
{
label: 'Description',
key: 'description',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Image',
key: 'image',
type: 'string' as const,
required: false,
description: 'Image as DataURI scheme [data:image/<jpeg/png/gif>;base64,BASE64_ENCODED_<JPEG/PNG/GIF>_IMAGE_DATA]',
variables: true,
},
],
async run($) {
type entity_metadata = {
location: string
}
type guild_event = {
channel_id: number,
name: string,
privacy_level: number,
scheduled_start_time: string,
scheduled_end_time?: string,
description?: string,
entity_type?: number,
entity_metadata?: entity_metadata,
image?: string, //data:image/jpeg;base64,BASE64_ENCODED_JPEG_IMAGE_DATA
}
const data: guild_event = {
channel_id: $.step.parameters.channel_id as number,
name: $.step.parameters.name as string,
privacy_level: 2,
scheduled_start_time: $.step.parameters.scheduledStartTime as string,
scheduled_end_time: $.step.parameters.scheduledEndTime as string,
description: $.step.parameters.description as string,
entity_type: $.step.parameters.entityType as number,
image: $.step.parameters.image as string,
};
const isExternal = $.step.parameters.entityType === 3;
if (isExternal) {
data.entity_metadata = {
location: $.step.parameters.location as string,
};
data.channel_id = null;
}
const response = await $.http?.post(
`/guilds/${$.auth.data.guildId}/scheduled-events`,
data
);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,4 +1,3 @@
import sendMessageToChannel from './send-message-to-channel';
import createScheduledEvent from './create-scheduled-event';
export default [sendMessageToChannel, createScheduledEvent];
export default [sendMessageToChannel];

View File

@@ -1,4 +1,3 @@
import listChannels from './list-channels';
import listVoiceChannels from './list-voice-channels';
export default [listChannels, listVoiceChannels];
export default [listChannels];

View File

@@ -1,34 +0,0 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List voice channels',
key: 'listVoiceChannels',
async run($: IGlobalVariable) {
const channels: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const response = await $.http.get(
`/guilds/${$.auth.data.guildId}/channels`
);
channels.data = response.data
.filter((channel: IJSONObject) => {
// filter in voice and stage channels only
return channel.type === 2 || channel.type === 13;
})
.map((channel: IJSONObject) => {
return {
value: channel.id,
name: channel.name,
};
});
return channels;
},
};

View File

@@ -1,3 +0,0 @@
import listExternalScheduledEventFields from './list-external-scheduled-event-fields';
export default [listExternalScheduledEventFields];

View File

@@ -1,83 +0,0 @@
import { IGlobalVariable } from '@automatisch/types';
export default {
name: 'List external scheduled event fields',
key: 'listExternalScheduledEventFields',
async run($: IGlobalVariable) {
const isExternal = $.step.parameters.entityType === 3;
if (isExternal) {
return [
{
label: 'Location',
key: 'location',
type: 'string' as const,
required: true,
description: 'The location of the event (1-100 characters). This will be omitted if type is NOT EXTERNAL',
variables: true,
},
{
label: 'Start-Time',
key: 'scheduledStartTime',
type: 'string' as const,
required: true,
description: 'The time the event will start [ISO8601]',
variables: true,
},
{
label: 'End-Time',
key: 'scheduledEndTime',
type: 'string' as const,
required: true,
description: 'The time the event will end [ISO8601]. This will be omitted if type is NOT EXTERNAL',
variables: true,
},
];
}
return [
{
label: 'Channel',
key: 'channel_id',
type: 'dropdown' as const,
required: true,
description: 'Pick a voice or stage channel to link the event to. This will be omitted if type is EXTERNAL',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listVoiceChannels',
},
],
},
},
{
label: 'Location',
key: 'location',
type: 'string' as const,
required: false,
description: 'The location of the event (1-100 characters). This will be omitted if type is NOT EXTERNAL',
variables: true,
},
{
label: 'Start-Time',
key: 'scheduledStartTime',
type: 'string' as const,
required: true,
description: 'The time the event will start [ISO8601]',
variables: true,
},
{
label: 'End-Time',
key: 'scheduledEndTime',
type: 'string' as const,
required: false,
description: 'The time the event will end [ISO8601]. This will be omitted if type is NOT EXTERNAL',
variables: true,
},
];
},
};

View File

@@ -4,7 +4,6 @@ import auth from './auth';
import dynamicData from './dynamic-data';
import actions from './actions';
import triggers from './triggers';
import dynamicFields from './dynamic-fields';
export default defineApp({
name: 'Discord',
@@ -18,7 +17,6 @@ export default defineApp({
beforeRequest: [addAuthHeader],
auth,
dynamicData,
dynamicFields,
triggers,
actions,
});

View File

@@ -1,5 +1,5 @@
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
import getCurrentUser from '../../amazon-s3/common/get-current-user';
const isStillVerified = async ($: IGlobalVariable) => {
const currentUser = await getCurrentUser($);

View File

@@ -1,5 +1,5 @@
import { IField, IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
import getCurrentUser from '../../amazon-s3/common/get-current-user';
type TUser = {
displayName: string;

View File

@@ -1,4 +1,3 @@
import newDatabaseItems from './new-database-items';
import updatedDatabaseItems from './updated-database-items';
export default [newDatabaseItems, updatedDatabaseItems];
export default [newDatabaseItems];

View File

@@ -1,33 +0,0 @@
import defineTrigger from '../../../../helpers/define-trigger';
import updatedDatabaseItems from './updated-database-items';
export default defineTrigger({
name: 'Updated database items',
key: 'updatedDatabaseItems',
pollInterval: 15,
description:
'Triggers when there is an update to an item in a chosen database',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: false,
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
],
async run($) {
await updatedDatabaseItems($);
},
});

View File

@@ -1,51 +0,0 @@
import { IGlobalVariable } from '@automatisch/types';
type DatabaseItem = {
id: string;
last_edited_time: string;
};
type ResponseData = {
results: DatabaseItem[];
next_cursor?: string;
};
type Payload = {
sorts: [
{
timestamp: 'created_time' | 'last_edited_time';
direction: 'ascending' | 'descending';
}
];
start_cursor?: string;
};
const updatedDatabaseItems = async ($: IGlobalVariable) => {
const payload: Payload = {
sorts: [
{
timestamp: 'last_edited_time',
direction: 'descending',
},
],
};
const databaseId = $.step.parameters.databaseId as string;
const path = `/v1/databases/${databaseId}/query`;
do {
const response = await $.http.post<ResponseData>(path, payload);
payload.start_cursor = response.data.next_cursor;
for (const databaseItem of response.data.results) {
$.pushTriggerItem({
raw: databaseItem,
meta: {
internalId: `${databaseItem.id}-${databaseItem.last_edited_time}`,
},
});
}
} while (payload.start_cursor);
};
export default updatedDatabaseItems;

View File

@@ -1,3 +1,4 @@
import qs from 'qs';
import defineAction from '../../../../helpers/define-action';
export default defineAction({
@@ -18,8 +19,7 @@ export default defineAction({
key: 'message',
type: 'string' as const,
required: true,
description:
'Message body to be sent, set to triggered if empty or not passed.',
description: 'Message body to be sent, set to triggered if empty or not passed.',
variables: true,
},
{
@@ -67,15 +67,22 @@ export default defineAction({
key: 'delay',
type: 'string' as const,
required: false,
description:
'Timestamp or duration for delayed delivery. For example, 30min or 9am.',
description: 'Timestamp or duration for delayed delivery. For example, 30min or 9am.',
variables: true,
},
],
async run($) {
const { topic, message, title, email, click, attach, filename, delay } =
$.step.parameters;
const {
topic,
message,
title,
email,
click,
attach,
filename,
delay
} = $.step.parameters;
const payload = {
topic,
message,
@@ -84,7 +91,7 @@ export default defineAction({
click,
attach,
filename,
delay,
delay
};
const response = await $.http.post('/', payload);

View File

@@ -11,7 +11,7 @@ export default {
readOnly: false,
value: null,
placeholder: null,
description: 'Host name of your Odoo Server (e.g. sub.domain.com without the protocol)',
description: 'Host name of your Odoo Server',
clickToCopy: false,
},
{
@@ -25,27 +25,6 @@ export default {
description: 'Port that the host is running on, defaults to 443 (HTTPS)',
clickToCopy: false,
},
{
key: 'secure',
label: 'Secure',
type: 'dropdown' as const,
required: true,
readOnly: false,
value: 'true',
description: 'True if the host communicates via secure protocol.',
variables: false,
clickToCopy: false,
options: [
{
label: 'True',
value: 'true',
},
{
label: 'False',
value: 'false',
},
],
},
{
key: 'databaseName',
label: 'Database Name',
@@ -61,7 +40,7 @@ export default {
key: 'email',
label: 'Email Address',
type: 'string' as const,
required: true,
requires: true,
readOnly: false,
value: null,
placeholder: null,

View File

@@ -32,10 +32,8 @@ export const asyncMethodCall = async <T = number>($: IGlobalVariable, { method,
export const getClient = ($: IGlobalVariable, { path = 'common' }) => {
const host = $.auth.data.host as string;
const port = Number($.auth.data.port as string);
const secure = $.auth.data.secure === 'true';
const createClientFunction = secure ? xmlrpc.createSecureClient : xmlrpc.createClient;
return createClientFunction(
return xmlrpc.createClient(
{
host,
port,

View File

@@ -1,22 +1,10 @@
import { TBeforeRequest } from '@automatisch/types';
import appConfig from '../../../config/app';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
const screenName = $.auth.data?.screenName as string;
if ($.auth.data?.accessToken) {
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
}
if (screenName) {
requestConfig.headers[
'User-Agent'
] = `web:automatisch:${appConfig.version} (by /u/${screenName})`;
} else {
requestConfig.headers[
'User-Agent'
] = `web:automatisch:${appConfig.version}`;
}
return requestConfig;
};

View File

@@ -1,3 +0,0 @@
import removeImageBackground from './remove-image-background';
export default [removeImageBackground];

View File

@@ -1,82 +0,0 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Remove image background',
key: 'removeImageBackground',
description:
'Removes the background of an image.',
arguments: [
{
label: 'Image file',
key: 'imageFileB64',
type: 'string' as const,
required: true,
variables: true,
description: 'Provide a JPG or PNG file in Base64 format, up to 12 MB (see remove.bg/supported-images)',
},
{
label: 'Size',
key: 'size',
type: 'dropdown' as const,
required: true,
value: 'auto',
options: [
{ label: 'Auto', value: 'auto' },
{ label: 'Preview (up to 0.25 megapixels)', value: 'preview' },
{ label: 'Full (up to 10 megapixels)', value: 'full' },
]
},
{
label: 'Background color',
key: 'bgColor',
type: 'string' as const,
description: 'Adds a solid color background. Can be a hex color code (e.g. 81d4fa, fff) or a color name (e.g. green)',
required: false,
},
{
label: 'Background image URL',
key: 'bgImageUrl',
type: 'string' as const,
description: 'Adds a background image from a URL.',
required: false,
},
{
label: 'Output image format',
key: 'outputFormat',
type: 'dropdown' as const,
description: 'Note: Use PNG to preserve transparency',
required: true,
value: 'auto',
options: [
{ label: 'Auto', value: 'auto' },
{ label: 'PNG', value: 'png' },
{ label: 'JPG', value: 'jpg' },
{ label: 'ZIP', value: 'zip' }
]
}
],
async run($) {
const imageFileB64 = $.step.parameters.imageFileB64 as string;
const size = $.step.parameters.size as string;
const bgColor = $.step.parameters.bgColor as string;
const bgImageUrl = $.step.parameters.bgImageUrl as string;
const outputFormat = $.step.parameters.outputFormat as string;
const body = JSON.stringify({
image_file_b64: imageFileB64,
size: size,
bg_color: bgColor,
bg_image_url: bgImageUrl,
format: outputFormat
});
const response = await $.http.post('/removebg', body, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
$.setActionItem({ raw: response.data });
}
});

View File

@@ -1,7 +1,6 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-header';
import auth from './auth';
import actions from './actions';
export default defineApp({
name: 'Remove.bg',
@@ -14,5 +13,4 @@ export default defineApp({
primaryColor: '55636c',
beforeRequest: [addAuthHeader],
auth,
actions,
});

View File

@@ -1,3 +1,4 @@
import qs from 'qs';
import defineAction from '../../../../helpers/define-action';
export default defineAction({
@@ -10,8 +11,7 @@ export default defineAction({
key: 'chatId',
type: 'string' as const,
required: true,
description:
'Unique identifier for the target chat or username of the target channel (in the format @channelusername).',
description: 'Unique identifier for the target chat or username of the target channel (in the format @channelusername).',
variables: true,
},
{
@@ -28,8 +28,7 @@ export default defineAction({
type: 'dropdown' as const,
required: false,
value: false,
description:
'Sends the message silently. Users will receive a notification with no sound.',
description: 'Sends the message silently. Users will receive a notification with no sound.',
variables: true,
options: [
{

View File

@@ -1,102 +0,0 @@
export const fields = [
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: true,
variables: true,
description: '',
},
{
label: 'Email',
key: 'email',
type: 'string' as const,
required: true,
variables: true,
description:
'It is essential to be distinctive. Zendesk prohibits the existence of identical users sharing the same email address.',
},
{
label: 'Details',
key: 'details',
type: 'string' as const,
required: false,
variables: true,
description: '',
},
{
label: 'Notes',
key: 'notes',
type: 'string' as const,
required: false,
variables: true,
description:
'Within this field, you have the capability to save any remarks or comments you may have concerning the user.',
},
{
label: 'Phone',
key: 'phone',
type: 'string' as const,
required: false,
variables: true,
description:
"The user's contact number should be entered in the following format: +1 (555) 123-4567.",
},
{
label: 'Tags',
key: 'tags',
type: 'string' as const,
required: false,
variables: true,
description: 'A comma separated list of tags.',
},
{
label: 'Role',
key: 'role',
type: 'string' as const,
required: false,
variables: true,
description:
"It can take on one of the designated roles: 'end-user', 'agent', or 'admin'. If a different value is set or none is specified, the default is 'end-user.'",
},
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown' as const,
required: false,
variables: true,
description: 'Assign this user to a specific organization.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'External Id',
key: 'externalId',
type: 'string' as const,
required: false,
variables: true,
description:
'An exclusive external identifier; you can utilize this to link organizations with an external record.',
},
{
label: 'Verified',
key: 'verified',
type: 'dropdown' as const,
required: false,
description:
"Specify if you can verify that the user's assertion of their identity is accurate.",
variables: true,
options: [
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' },
],
},
];

View File

@@ -1,53 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
import { fields } from './fields';
type Payload = {
user: IJSONObject;
};
export default defineAction({
name: 'Create user',
key: 'createUser',
description: 'Creates a new user.',
arguments: fields,
async run($) {
const {
name,
email,
details,
notes,
phone,
role,
organizationId,
externalId,
verified,
} = $.step.parameters;
const tags = $.step.parameters.tags as string;
const formattedTags = tags.split(',');
const payload: Payload = {
user: {
name,
email,
details,
notes,
phone,
organization_id: organizationId,
external_id: externalId,
verified: verified || 'false',
tags: formattedTags,
},
};
if (role) {
payload.user.role = role;
}
const response = await $.http.post('/api/v2/users', payload);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,35 +0,0 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Delete ticket',
key: 'deleteTicket',
description: 'Deletes an existing ticket.',
arguments: [
{
label: 'Ticket',
key: 'ticketId',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Select the ticket you want to delete.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirstPageOfTickets',
},
],
},
},
],
async run($) {
const ticketId = $.step.parameters.ticketId;
const response = await $.http.delete(`/api/v2/tickets/${ticketId}`);
$.setActionItem({ raw: { data: response.data } });
},
});

View File

@@ -1,43 +0,0 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Delete user',
key: 'deleteUser',
description: 'Deletes an existing user.',
arguments: [
{
label: 'User',
key: 'userId',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Select the user you want to modify.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
{
name: 'parameters.showUserRole',
value: 'true',
},
{
name: 'parameters.includeAllUsers',
value: 'true',
},
],
},
},
],
async run($) {
const userId = $.step.parameters.userId;
const response = await $.http.delete(`/api/v2/users/${userId}`);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,32 +0,0 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Find ticket',
key: 'findTicket',
description: 'Finds an existing ticket.',
arguments: [
{
label: 'Query',
key: 'query',
type: 'string' as const,
required: true,
variables: true,
description:
'Write a search string that specifies the way we will search for the ticket in Zendesk.',
},
],
async run($) {
const query = $.step.parameters.query;
const params = {
query: `type:ticket ${query}`,
sort_by: 'created_at',
sort_order: 'desc',
};
const response = await $.http.get('/api/v2/search', { params });
$.setActionItem({ raw: response.data.results[0] });
},
});

View File

@@ -1,15 +1,3 @@
import createTicket from './create-ticket';
import createUser from './create-user';
import deleteTicket from './delete-ticket';
import deleteUser from './delete-user';
import findTicket from './find-ticket';
import updateTicket from './update-ticket';
export default [
createTicket,
createUser,
deleteTicket,
deleteUser,
findTicket,
updateTicket,
];
export default [createTicket];

View File

@@ -1,167 +0,0 @@
export const fields = [
{
label: 'Ticket',
key: 'ticketId',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Select the ticket you want to change.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirstPageOfTickets',
},
],
},
},
{
label: 'Subject',
key: 'subject',
type: 'string' as const,
required: false,
variables: true,
description: '',
},
{
label: 'Assignee',
key: 'assigneeId',
type: 'dropdown' as const,
required: false,
variables: true,
description:
'Note: An error occurs if the assignee is not in the default group (or the specific group chosen below).',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
{
name: 'parameters.showUserRole',
value: 'true',
},
{
name: 'parameters.includeAdmins',
value: 'true',
},
],
},
},
{
label: 'Group',
key: 'groupId',
type: 'dropdown' as const,
required: false,
variables: true,
description: 'Allocate this ticket to a specific group.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listGroups',
},
],
},
},
{
label: 'New Status',
key: 'status',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'New', value: 'new' },
{ label: 'Open', value: 'open' },
{ label: 'Pending', value: 'pending' },
{ label: 'Hold', value: 'hold' },
{ label: 'Solved', value: 'solved' },
{ label: 'Closed', value: 'closed' },
],
},
{
label: 'New comment to add to the ticket',
key: 'comment',
type: 'string' as const,
required: false,
variables: true,
description: '',
},
{
label: 'Should the first comment be public?',
key: 'publicOrNot',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
],
},
{
label: 'Tags',
key: 'tags',
type: 'string' as const,
required: false,
variables: true,
description: 'A comma separated list of tags.',
},
{
label: 'Type',
key: 'type',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'Problem', value: 'problem' },
{ label: 'Incident', value: 'incident' },
{ label: 'Question', value: 'question' },
{ label: 'Task', value: 'task' },
],
},
{
label: 'Priority',
key: 'priority',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
options: [
{ label: 'Urgent', value: 'urgent' },
{ label: 'High', value: 'high' },
{ label: 'Normal', value: 'normal' },
{ label: 'Low', value: 'low' },
],
},
{
label: 'Submitter',
key: 'submitterId',
type: 'dropdown' as const,
required: false,
variables: true,
description: '',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
{
name: 'parameters.includeAdmins',
value: 'false',
},
],
},
},
];

View File

@@ -1,57 +0,0 @@
import defineAction from '../../../../helpers/define-action';
import { fields } from './fields';
import isEmpty from 'lodash/isEmpty';
import omitBy from 'lodash/omitBy';
export default defineAction({
name: 'Update ticket',
key: 'updateTicket',
description: 'Modify the status of an existing ticket or append comments.',
arguments: fields,
async run($) {
const {
ticketId,
subject,
assigneeId,
groupId,
status,
comment,
publicOrNot,
type,
priority,
submitterId,
} = $.step.parameters;
const tags = $.step.parameters.tags as string;
const formattedTags = tags.split(',');
const payload = {
subject,
assignee_id: assigneeId,
group_id: groupId,
status,
comment: {
body: comment,
public: publicOrNot,
},
tags: formattedTags,
type,
priority,
submitter_id: submitterId,
};
const fieldsToRemoveIfEmpty = ['group_id', 'status', 'type', 'priority'];
const filteredPayload = omitBy(
payload,
(value, key) => fieldsToRemoveIfEmpty.includes(key) && isEmpty(value)
);
const response = await $.http.put(`/api/v2/tickets/${ticketId}`, {
ticket: filteredPayload,
});
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,20 +1,13 @@
import listUsers from './list-users';
import listBrands from './list-brands';
import listFirstPageOfTickets from './list-first-page-of-tickets';
import listGroups from './list-groups';
import listOrganizations from './list-organizations';
import listSharingAgreements from './list-sharing-agreements';
import listTicketForms from './list-ticket-forms';
import listViews from './list-views';
export default [
listUsers,
listBrands,
listFirstPageOfTickets,
listGroups,
listOrganizations,
listSharingAgreements,
listFirstPageOfTickets,
listTicketForms,
listViews,
];

View File

@@ -1,33 +0,0 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List first page of tickets',
key: 'listFirstPageOfTickets',
async run($: IGlobalVariable) {
const tickets: {
data: IJSONObject[];
} = {
data: [],
};
const params = {
'page[size]': 100,
sort: '-id',
};
const response = await $.http.get('/api/v2/tickets', { params });
const allTickets = response.data.tickets;
if (allTickets?.length) {
for (const ticket of allTickets) {
tickets.data.push({
value: ticket.id,
name: ticket.subject,
});
}
}
return tickets;
},
};

View File

@@ -21,7 +21,7 @@ export default {
const response = await $.http.get('/api/v2/groups', { params });
const allGroups = response?.data?.groups;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
params['page[after]'] = response.data.links?.after_cursor;
if (allGroups?.length) {
for (const group of allGroups) {

View File

@@ -1,38 +0,0 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List organizations',
key: 'listOrganizations',
async run($: IGlobalVariable) {
const organizations: {
data: IJSONObject[];
} = {
data: [],
};
let hasMore;
const params = {
'page[size]': 100,
'page[after]': undefined as unknown as string,
};
do {
const response = await $.http.get('/api/v2/organizations', { params });
const allOrganizations = response?.data?.organizations;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
if (allOrganizations?.length) {
for (const organization of allOrganizations) {
organizations.data.push({
value: organization.id,
name: organization.name,
});
}
}
} while (hasMore);
return organizations;
},
};

View File

@@ -25,7 +25,7 @@ export default {
const response = await $.http.get('/api/v2/users', { params });
const allUsers = response?.data?.users;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
params['page[after]'] = response.data.links?.after_cursor;
if (allUsers?.length) {
for (const user of allUsers) {

View File

@@ -1,38 +0,0 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List views',
key: 'listViews',
async run($: IGlobalVariable) {
const views: {
data: IJSONObject[];
} = {
data: [],
};
let hasMore;
const params = {
'page[size]': 100,
'page[after]': undefined as unknown as string,
};
do {
const response = await $.http.get('/api/v2/views', { params });
const allViews = response?.data?.views;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
if (allViews?.length) {
for (const view of allViews) {
views.data.push({
value: view.id,
name: view.title,
});
}
}
} while (hasMore);
return views;
},
};

View File

@@ -1,7 +1,6 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-headers';
import auth from './auth';
import triggers from './triggers';
import actions from './actions';
import dynamicData from './dynamic-data';
@@ -16,7 +15,6 @@ export default defineApp({
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,
triggers,
actions,
dynamicData,
});

View File

@@ -1,4 +0,0 @@
import newTickets from './new-tickets';
import newUsers from './new-users';
export default [newTickets, newUsers];

View File

@@ -1,59 +0,0 @@
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'New tickets',
key: 'newTickets',
pollInterval: 15,
description: 'Triggers when a new ticket is created in a specific view.',
arguments: [
{
label: 'View',
key: 'viewId',
type: 'dropdown' as const,
required: true,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listViews',
},
],
},
},
],
async run($) {
const viewId = $.step.parameters.viewId;
const params = {
'page[size]': 100,
'page[after]': undefined as unknown as string,
sort_by: 'nice_id',
sort_order: 'desc',
};
let hasMore;
do {
const response = await $.http.get(`/api/v2/views/${viewId}/tickets`, {
params,
});
const allTickets = response?.data?.tickets;
hasMore = response?.data?.meta?.has_more;
params['page[after]'] = response.data.meta?.after_cursor;
if (allTickets?.length) {
for (const ticket of allTickets) {
$.pushTriggerItem({
raw: ticket,
meta: {
internalId: ticket.id.toString(),
},
});
}
}
} while (hasMore);
},
});

View File

@@ -1,83 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'New users',
key: 'newUsers',
type: 'webhook',
description: 'Triggers upon the creation of a new user.',
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const params = {
query: 'type:user',
sort_by: 'created_at',
sort_order: 'desc',
};
const response = await $.http.get('/api/v2/search', { params });
const lastUser = response.data.results[0];
const computedWebhookEvent = {
id: Crypto.randomUUID(),
time: lastUser.created_at,
type: 'zen:event-type:user.created',
event: {},
detail: {
id: lastUser.id,
role: lastUser.role,
email: lastUser.email,
created_at: lastUser.created_at,
updated_at: lastUser.updated_at,
external_id: lastUser.external_id,
organization_id: lastUser.organization_id,
default_group_id: lastUser.default_group_id,
},
subject: `zen:user:${lastUser.id}`,
account_id: '',
zendesk_event_version: '2022-11-06',
};
const dataItem = {
raw: computedWebhookEvent,
meta: {
internalId: computedWebhookEvent.id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const payload = {
webhook: {
name: `Flow ID: ${$.flow.id}`,
status: 'active',
subscriptions: ['zen:event-type:user.created'],
endpoint: $.webhookUrl,
http_method: 'POST',
request_format: 'json',
},
};
const response = await $.http.post('/api/v2/webhooks', payload);
const id = response.data.webhook.id;
await $.flow.setRemoteWebhookId(id);
},
async unregisterHook($) {
await $.http.delete(`/api/v2/webhooks/${$.flow.remoteWebhookId}`);
},
});

View File

@@ -1,7 +1,6 @@
import { URL } from 'node:url';
import * as dotenv from 'dotenv';
import path from 'path';
import process from 'node:process';
if (process.env.APP_ENV === 'test') {
dotenv.config({ path: path.resolve(__dirname, '../../.env.test') });
@@ -9,6 +8,56 @@ if (process.env.APP_ENV === 'test') {
dotenv.config();
}
type AppConfig = {
host: string;
protocol: string;
port: string;
webAppUrl: string;
webhookUrl: string;
appEnv: string;
logLevel: string;
isDev: boolean;
isTest: boolean;
isProd: boolean;
postgresDatabase: string;
postgresSchema: string;
postgresPort: number;
postgresHost: string;
postgresUsername: string;
postgresPassword?: string;
version: string;
postgresEnableSsl: boolean;
baseUrl: string;
encryptionKey: string;
webhookSecretKey: string;
appSecretKey: string;
serveWebAppSeparately: boolean;
redisHost: string;
redisPort: number;
redisUsername: string;
redisPassword: string;
redisTls: boolean;
enableBullMQDashboard: boolean;
bullMQDashboardUsername: string;
bullMQDashboardPassword: string;
telemetryEnabled: boolean;
requestBodySizeLimit: string;
smtpHost: string;
smtpPort: number;
smtpSecure: boolean;
smtpUser: string;
smtpPassword: string;
fromEmail: string;
isCloud: boolean;
isSelfHosted: boolean;
paddleVendorId: number;
paddleVendorAuthCode: string;
paddlePublicKey: string;
licenseKey: string;
sentryDsn: string;
CI: boolean;
};
const host = process.env.HOST || 'localhost';
const protocol = process.env.PROTOCOL || 'http';
const port = process.env.PORT || '3000';
@@ -35,7 +84,7 @@ webhookUrl = webhookUrl.substring(0, webhookUrl.length - 1);
const appEnv = process.env.APP_ENV || 'development';
const appConfig = {
const appConfig: AppConfig = {
host,
protocol,
port,
@@ -78,7 +127,6 @@ const appConfig = {
fromEmail: process.env.FROM_EMAIL,
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
isSelfHosted: process.env.AUTOMATISCH_CLOUD !== 'true',
isMation: process.env.MATION === 'true',
paddleVendorId: Number(process.env.PADDLE_VENDOR_ID),
paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE,
paddlePublicKey: process.env.PADDLE_PUBLIC_KEY,

View File

@@ -4,10 +4,11 @@ import process from 'process';
import pg from 'pg';
pg.types.setTypeParser(20, 'text', parseInt);
import knex from 'knex';
import type { Knex } from 'knex';
import knexConfig from '../../knexfile';
import logger from '../helpers/logger';
export const client = knex(knexConfig);
export const client: Knex = knex(knexConfig);
const CONNECTION_REFUSED = 'ECONNREFUSED';

View File

@@ -1,6 +1,16 @@
import appConfig from './app';
const redisConfig = {
type TRedisConfig = {
host: string,
port: number,
username?: string,
password?: string,
tls?: Record<string, unknown>,
enableReadyCheck?: boolean,
enableOfflineQueue: boolean,
}
const redisConfig: TRedisConfig = {
host: appConfig.redisHost,
port: appConfig.redisPort,
username: appConfig.redisUsername,

View File

@@ -1,9 +1,11 @@
import { Response } from 'express';
import { IJSONObject, IRequest } from '@automatisch/types';
import crypto from 'crypto';
import { serialize } from 'php-serialize';
import Billing from '../../helpers/billing/index.ee';
import appConfig from '../../config/app';
export default async (request, response) => {
export default async (request: IRequest, response: Response) => {
if (!verifyWebhook(request)) {
return response.sendStatus(401);
}
@@ -21,14 +23,14 @@ export default async (request, response) => {
return response.sendStatus(200);
};
const verifyWebhook = (request) => {
const verifyWebhook = (request: IRequest) => {
const signature = request.body.p_signature;
const keys = Object.keys(request.body)
.filter((key) => key !== 'p_signature')
.sort();
const sorted = {};
const sorted: IJSONObject = {};
keys.forEach((key) => {
sorted[key] = request.body[key];
});

View File

@@ -1,10 +1,12 @@
import path from 'node:path';
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import Connection from '../../models/connection';
import logger from '../../helpers/logger';
import handler from '../../helpers/webhook-handler';
export default async (request, response) => {
export default async (request: IRequest, response: Response) => {
const computedRequestPayload = {
headers: request.headers,
body: request.body,
@@ -20,7 +22,7 @@ export default async (request, response) => {
.findById(connectionId)
.throwIfNotFound();
if (!(await connection.verifyWebhook(request))) {
if (!await connection.verifyWebhook(request)) {
return response.sendStatus(401);
}

View File

@@ -1,8 +1,11 @@
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import Flow from '../../models/flow';
import logger from '../../helpers/logger';
import handler from '../../helpers/webhook-handler';
export default async (request, response) => {
export default async (request: IRequest, response: Response) => {
const computedRequestPayload = {
headers: request.headers,
body: request.body,

View File

@@ -1,22 +1,22 @@
import { IJSONObject } from '@automatisch/types';
export default class BaseError extends Error {
details = {};
statusCode?: number;
constructor(error) {
let computedError;
constructor(error?: string | IJSONObject) {
let computedError: Record<string, unknown>;
try {
computedError = JSON.parse(error);
computedError = JSON.parse(error as string);
} catch {
computedError =
typeof error === 'string' || Array.isArray(error) ? { error } : error;
computedError = (typeof error === 'string' || Array.isArray(error)) ? { error } : error;
}
let computedMessage;
let computedMessage: string;
try {
// challenge to input to see if it is stringified JSON
JSON.parse(error);
computedMessage = error;
JSON.parse(error as string);
computedMessage = error as string;
} catch {
if (typeof error === 'string') {
computedMessage = error;

View File

@@ -1,10 +0,0 @@
import BaseError from './base';
export default class GenerateAuthUrlError extends BaseError {
constructor(error) {
const computedError = error.response?.data || error.message;
super(computedError);
this.message = `Error occured while creating authorization URL!`;
}
}

View File

@@ -0,0 +1,14 @@
import { IJSONObject } from '@automatisch/types';
import BaseError from './base';
export default class GenerateAuthUrlError extends BaseError {
constructor(error: IJSONObject) {
const computedError =
((error.response as IJSONObject)?.data as IJSONObject) ||
(error.message as string);
super(computedError);
this.message = `Error occured while creating authorization URL!`;
}
}

View File

@@ -1,10 +0,0 @@
import BaseError from './base';
export default class HttpError extends BaseError {
constructor(error) {
const computedError = error.response?.data || error.message;
super(computedError);
this.response = error.response;
}
}

View File

@@ -0,0 +1,17 @@
import type { AxiosResponse, AxiosError } from 'axios';
import { IJSONObject } from '@automatisch/types';
import BaseError from './base';
export default class HttpError extends BaseError {
response: AxiosResponse;
constructor(error: AxiosError) {
const computedError =
error.response?.data as IJSONObject ||
error.message as string;
super(computedError);
this.response = error.response;
}
}

View File

@@ -1,17 +0,0 @@
import AppConfig from '../../models/app-config';
const createAppAuthClient = async (_parent, params, context) => {
context.currentUser.can('update', 'App');
const appConfig = await AppConfig.query()
.findById(params.input.appConfigId)
.throwIfNotFound();
const appAuthClient = await appConfig
.$relatedQuery('appAuthClients')
.insert(params.input);
return appAuthClient;
};
export default createAppAuthClient;

View File

@@ -0,0 +1,35 @@
import { IJSONObject } from '@automatisch/types';
import AppConfig from '../../models/app-config';
import Context from '../../types/express/context';
type Params = {
input: {
appConfigId: string;
name: string;
formattedAuthDefaults?: IJSONObject;
active?: boolean;
};
};
const createAppAuthClient = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'App');
const appConfig = await AppConfig
.query()
.findById(params.input.appConfigId)
.throwIfNotFound();
const appAuthClient = await appConfig
.$relatedQuery('appAuthClients')
.insert(
params.input
);
return appAuthClient;
};
export default createAppAuthClient;

View File

@@ -1,18 +0,0 @@
import App from '../../models/app';
import AppConfig from '../../models/app-config';
const createAppConfig = async (_parent, params, context) => {
context.currentUser.can('update', 'App');
const key = params.input.key;
const app = await App.findOneByKey(key);
if (!app) throw new Error('The app cannot be found!');
const appConfig = await AppConfig.query().insert(params.input);
return appConfig;
};
export default createAppConfig;

View File

@@ -0,0 +1,36 @@
import App from '../../models/app';
import AppConfig from '../../models/app-config';
import Context from '../../types/express/context';
type Params = {
input: {
key: string;
allowCustomConnection?: boolean;
shared?: boolean;
disabled?: boolean;
};
};
const createAppConfig = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'App');
const key = params.input.key;
const app = await App.findOneByKey(key);
if (!app) throw new Error('The app cannot be found!');
const appConfig = await AppConfig
.query()
.insert(
params.input
);
return appConfig;
};
export default createAppConfig;

View File

@@ -1,7 +1,21 @@
import { IJSONObject } from '@automatisch/types';
import App from '../../models/app';
import AppConfig from '../../models/app-config';
import Context from '../../types/express/context';
const createConnection = async (_parent, params, context) => {
type Params = {
input: {
key: string;
appAuthClientId: string;
formattedData: IJSONObject;
};
};
const createConnection = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'Connection');
const { key, appAuthClientId } = params.input;
@@ -12,20 +26,16 @@ const createConnection = async (_parent, params, context) => {
let formattedData = params.input.formattedData;
if (appConfig) {
if (appConfig.disabled)
throw new Error(
'This application has been disabled for new connections!'
);
if (appConfig.disabled) throw new Error('This application has been disabled for new connections!');
if (!appConfig.allowCustomConnection && formattedData)
throw new Error(`Custom connections cannot be created for ${app.name}!`);
if (!appConfig.allowCustomConnection && formattedData) throw new Error(`Custom connections cannot be created for ${app.name}!`);
if (appConfig.shared && !formattedData) {
const authClient = await appConfig
.$relatedQuery('appAuthClients')
.findById(appAuthClientId)
.where({
active: true,
active: true
})
.throwIfNotFound();
@@ -33,7 +43,8 @@ const createConnection = async (_parent, params, context) => {
}
}
const createdConnection = await context.currentUser
const createdConnection = await context
.currentUser
.$relatedQuery('connections')
.insert({
key,

View File

@@ -1,7 +1,19 @@
import App from '../../models/app';
import Step from '../../models/step';
import Context from '../../types/express/context';
const createFlow = async (_parent, params, context) => {
type Params = {
input: {
triggerAppKey: string;
connectionId: string;
};
};
const createFlow = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'Flow');
const connectionId = params?.input?.connectionId;

View File

@@ -1,29 +0,0 @@
import kebabCase from 'lodash/kebabCase';
import Role from '../../models/role';
const createRole = async (_parent, params, context) => {
context.currentUser.can('create', 'Role');
const { name, description, permissions } = params.input;
const key = kebabCase(name);
const existingRole = await Role.query().findOne({ key });
if (existingRole) {
throw new Error('Role already exists!');
}
return await Role.query()
.insertGraph(
{
key,
name,
description,
permissions,
},
{ relate: ['permissions'] }
)
.returning('*');
};
export default createRole;

View File

@@ -0,0 +1,34 @@
import kebabCase from 'lodash/kebabCase';
import Permission from '../../models/permission';
import Role from '../../models/role';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
description: string;
permissions: Permission[];
};
};
const createRole = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('create', 'Role');
const { name, description, permissions } = params.input;
const key = kebabCase(name);
const existingRole = await Role.query().findOne({ key });
if (existingRole) {
throw new Error('Role already exists!');
}
return await Role.query().insertGraph({
key,
name,
description,
permissions,
}, { relate: ['permissions'] }).returning('*');
};
export default createRole;

View File

@@ -1,7 +1,28 @@
import App from '../../models/app';
import Flow from '../../models/flow';
import Context from '../../types/express/context';
const createStep = async (_parent, params, context) => {
type Params = {
input: {
key: string;
appKey: string;
flow: {
id: string;
};
connection: {
id: string;
};
previousStep: {
id: string;
};
};
};
const createStep = async (
_parent: unknown,
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();

View File

@@ -1,7 +1,23 @@
import User from '../../models/user';
import Role from '../../models/role';
import Context from '../../types/express/context';
const createUser = async (_parent, params, context) => {
type Params = {
input: {
fullName: string;
email: string;
password: string;
role: {
id: string;
};
};
};
const createUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'User');
const { fullName, email, password } = params.input;
@@ -14,7 +30,7 @@ const createUser = async (_parent, params, context) => {
throw new Error('User already exists!');
}
const userPayload = {
const userPayload: Partial<User> = {
fullName,
email,
password,

View File

@@ -1,9 +1,21 @@
import Context from '../../types/express/context';
import AppAuthClient from '../../models/app-auth-client';
const deleteAppAuthClient = async (_parent, params, context) => {
type Params = {
input: {
id: string;
};
};
const deleteAppAuthClient = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'App');
await AppAuthClient.query()
await AppAuthClient
.query()
.delete()
.findOne({
id: params.input.id,

View File

@@ -1,4 +1,16 @@
const deleteConnection = async (_parent, params, context) => {
import Context from '../../types/express/context';
type Params = {
input: {
id: string;
};
};
const deleteConnection = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'Connection');
await context.currentUser

View File

@@ -1,11 +1,17 @@
import { Duration } from 'luxon';
import Context from '../../types/express/context';
import deleteUserQueue from '../../queues/delete-user.ee';
import flowQueue from '../../queues/flow';
import Flow from '../../models/flow';
import Execution from '../../models/execution';
import ExecutionStep from '../../models/execution-step';
import appConfig from '../../config/app';
const deleteCurrentUser = async (_parent, params, context) => {
const deleteCurrentUser = async (
_parent: unknown,
params: never,
context: Context
) => {
const id = context.currentUser.id;
const flows = await context.currentUser.$relatedQuery('flows').where({
@@ -26,7 +32,7 @@ const deleteCurrentUser = async (_parent, params, context) => {
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution) => execution.id);
).map((execution: Execution) => execution.id);
const flowIds = flows.map((flow) => flow.id);
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);

View File

@@ -1,9 +1,21 @@
import Context from '../../types/express/context';
import Flow from '../../models/flow';
import Execution from '../../models/execution';
import ExecutionStep from '../../models/execution-step';
import globalVariable from '../../helpers/global-variable';
import logger from '../../helpers/logger';
const deleteFlow = async (_parent, params, context) => {
type Params = {
input: {
id: string;
};
};
const deleteFlow = async (
_parent: unknown,
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('delete', 'Flow');
const isCreator = conditions.isCreator;
const allFlows = Flow.query();
@@ -31,15 +43,13 @@ const deleteFlow = async (_parent, params, context) => {
await trigger.unregisterHook($);
} catch (error) {
// suppress error as the remote resource might have been already deleted
logger.debug(
`Failed to unregister webhook for flow ${flow.id}: ${error.message}`
);
logger.debug(`Failed to unregister webhook for flow ${flow.id}: ${error.message}`);
}
}
const executionIds = (
await flow.$relatedQuery('executions').select('executions.id')
).map((execution) => execution.id);
).map((execution: Execution) => execution.id);
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);

View File

@@ -1,7 +1,18 @@
import Role from '../../models/role';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
const deleteRole = async (_parent, params, context) => {
type Params = {
input: {
id: string;
};
};
const deleteRole = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'Role');
const role = await Role.query().findById(params.input.id).throwIfNotFound();

View File

@@ -1,4 +1,16 @@
const deleteStep = async (_parent, params, context) => {
import Context from '../../types/express/context';
type Params = {
input: {
id: string;
};
};
const deleteStep = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'Flow');
const step = await context.currentUser

View File

@@ -1,8 +1,19 @@
import { Duration } from 'luxon';
import Context from '../../types/express/context';
import User from '../../models/user';
import deleteUserQueue from '../../queues/delete-user.ee';
const deleteUser = async (_parent, params, context) => {
type Params = {
input: {
id: string;
};
};
const deleteUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'User');
const id = params.input.id;
@@ -13,7 +24,7 @@ const deleteUser = async (_parent, params, context) => {
const jobPayload = { id };
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobOptions = {
delay: millisecondsFor30Days,
delay: millisecondsFor30Days
};
await deleteUserQueue.add(jobName, jobPayload, jobOptions);

View File

@@ -1,4 +1,15 @@
function updateStepId(value, newStepIds) {
import Context from '../../types/express/context';
import Step from '../../models/step';
type Params = {
input: {
id: string;
};
};
type NewStepIds = Record<string, string>;
function updateStepId(value: string, newStepIds: NewStepIds) {
let newValue = value;
const stepIdEntries = Object.entries(newStepIds);
@@ -13,9 +24,9 @@ function updateStepId(value, newStepIds) {
return newValue;
}
function updateStepVariables(parameters, newStepIds) {
function updateStepVariables(parameters: Step['parameters'], newStepIds: NewStepIds): Step['parameters'] {
const entries = Object.entries(parameters);
return entries.reduce((result, [key, value]) => {
return entries.reduce((result, [key, value]: [string, unknown]) => {
if (typeof value === 'string') {
return {
...result,
@@ -26,7 +37,7 @@ function updateStepVariables(parameters, newStepIds) {
if (Array.isArray(value)) {
return {
...result,
[key]: value.map((item) => updateStepVariables(item, newStepIds)),
[key]: value.map(item => updateStepVariables(item, newStepIds)),
};
}
@@ -37,7 +48,11 @@ function updateStepVariables(parameters, newStepIds) {
}, {});
}
const duplicateFlow = async (_parent, params, context) => {
const duplicateFlow = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'Flow');
const flow = await context.currentUser
@@ -54,16 +69,17 @@ const duplicateFlow = async (_parent, params, context) => {
active: false,
});
const newStepIds = {};
const newStepIds: NewStepIds = {};
for (const step of flow.steps) {
const duplicatedStep = await duplicatedFlow.$relatedQuery('steps').insert({
key: step.key,
appKey: step.appKey,
type: step.type,
connectionId: step.connectionId,
position: step.position,
parameters: updateStepVariables(step.parameters, newStepIds),
});
const duplicatedStep = await duplicatedFlow.$relatedQuery('steps')
.insert({
key: step.key,
appKey: step.appKey,
type: step.type,
connectionId: step.connectionId,
position: step.position,
parameters: updateStepVariables(step.parameters, newStepIds),
});
if (duplicatedStep.isTrigger) {
await duplicatedStep.updateWebhookUrl();

View File

@@ -1,7 +1,18 @@
import Context from '../../types/express/context';
import testRun from '../../services/test-run';
import Step from '../../models/step';
const executeFlow = async (_parent, params, context) => {
type Params = {
input: {
stepId: string;
};
};
const executeFlow = async (
_parent: unknown,
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Flow');
const isCreator = conditions.isCreator;
const allSteps = Step.query();
@@ -10,7 +21,10 @@ const executeFlow = async (_parent, params, context) => {
const { stepId } = params.input;
const untilStep = await baseQuery.clone().findById(stepId).throwIfNotFound();
const untilStep = await baseQuery
.clone()
.findById(stepId)
.throwIfNotFound();
const { executionStep } = await testRun({ stepId });

View File

@@ -6,7 +6,13 @@ import {
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../../helpers/remove-job-configuration';
const forgotPassword = async (_parent, params) => {
type Params = {
input: {
email: string;
};
};
const forgotPassword = async (_parent: unknown, params: Params) => {
const { email } = params.input;
const user = await User.query().findOne({ email: email.toLowerCase() });

View File

@@ -1,7 +1,18 @@
import Context from '../../types/express/context';
import globalVariable from '../../helpers/global-variable';
import App from '../../models/app';
const generateAuthUrl = async (_parent, params, context) => {
type Params = {
input: {
id: string;
};
};
const generateAuthUrl = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'Connection');
const connection = await context.currentUser

View File

@@ -1,7 +1,14 @@
import User from '../../models/user';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
const login = async (_parent, params) => {
type Params = {
input: {
email: string;
password: string;
};
};
const login = async (_parent: unknown, params: Params) => {
const user = await User.query().findOne({
email: params.input.email.toLowerCase(),
});

View File

@@ -1,7 +1,15 @@
import User from '../../models/user';
import Role from '../../models/role';
const registerUser = async (_parent, params) => {
type Params = {
input: {
fullName: string;
email: string;
password: string;
};
};
const registerUser = async (_parent: unknown, params: Params) => {
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({

View File

@@ -1,4 +1,16 @@
const resetConnection = async (_parent, params, context) => {
import Context from '../../types/express/context';
type Params = {
input: {
id: string;
};
};
const resetConnection = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser

View File

@@ -1,6 +1,13 @@
import User from '../../models/user';
const resetPassword = async (_parent, params) => {
type Params = {
input: {
token: string;
password: string;
};
};
const resetPassword = async (_parent: unknown, params: Params) => {
const { token, password } = params.input;
if (!token) {

View File

@@ -1,17 +0,0 @@
import AppAuthClient from '../../models/app-auth-client';
const updateAppAuthClient = async (_parent, params, context) => {
context.currentUser.can('update', 'App');
const { id, ...appAuthClientData } = params.input;
const appAuthClient = await AppAuthClient.query()
.findById(id)
.throwIfNotFound();
await appAuthClient.$query().patch(appAuthClientData);
return appAuthClient;
};
export default updateAppAuthClient;

View File

@@ -0,0 +1,38 @@
import { IJSONObject } from '@automatisch/types';
import AppAuthClient from '../../models/app-auth-client';
import Context from '../../types/express/context';
type Params = {
input: {
id: string;
name: string;
formattedAuthDefaults?: IJSONObject;
active?: boolean;
};
};
const updateAppAuthClient = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'App');
const {
id,
...appAuthClientData
} = params.input;
const appAuthClient = await AppAuthClient
.query()
.findById(id)
.throwIfNotFound();
await appAuthClient
.$query()
.patch(appAuthClientData);
return appAuthClient;
};
export default updateAppAuthClient;

View File

@@ -1,15 +0,0 @@
import AppConfig from '../../models/app-config';
const updateAppConfig = async (_parent, params, context) => {
context.currentUser.can('update', 'App');
const { id, ...appConfigToUpdate } = params.input;
const appConfig = await AppConfig.query().findById(id).throwIfNotFound();
await appConfig.$query().patch(appConfigToUpdate);
return appConfig;
};
export default updateAppConfig;

View File

@@ -0,0 +1,39 @@
import AppConfig from '../../models/app-config';
import Context from '../../types/express/context';
type Params = {
input: {
id: string;
allowCustomConnection?: boolean;
shared?: boolean;
disabled?: boolean;
};
};
const updateAppConfig = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'App');
const {
id,
...appConfigToUpdate
} = params.input;
const appConfig = await AppConfig
.query()
.findById(id)
.throwIfNotFound();
await appConfig
.$query()
.patch(
appConfigToUpdate
);
return appConfig;
};
export default updateAppConfig;

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