Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b9d89b040f | ||
![]() |
41421b849a | ||
![]() |
324375da93 | ||
![]() |
536446faf6 | ||
![]() |
d026ac09f3 | ||
![]() |
88c93ac992 | ||
![]() |
d540322d8b | ||
![]() |
ad4db5e936 | ||
![]() |
25cb4d90f3 | ||
![]() |
6c14a353ef | ||
![]() |
74d7d1aa98 | ||
![]() |
43b0d9ed29 | ||
![]() |
3572e6f65a | ||
![]() |
d23d5d2da0 | ||
![]() |
183b9b0d88 | ||
![]() |
7a1af268ae | ||
![]() |
f879b3c5b0 | ||
![]() |
40be72cf65 | ||
![]() |
a8886571d1 | ||
![]() |
1fcd51ea26 | ||
![]() |
89752138be | ||
![]() |
f29ccace2a | ||
![]() |
0c8343e76f | ||
![]() |
9776c9f5a4 | ||
![]() |
a5dbac9817 | ||
![]() |
bad5e0b855 | ||
![]() |
8e4ca55560 | ||
![]() |
f52afc1fe0 | ||
![]() |
815e64302e | ||
![]() |
07b2b18a4e | ||
![]() |
69eca33de7 | ||
![]() |
ec76a480d0 | ||
![]() |
a8823c3ed0 | ||
![]() |
1f1b3a341c | ||
![]() |
8c164a3852 | ||
![]() |
dcf526d810 | ||
![]() |
2fc6d680a0 | ||
![]() |
f414972f33 | ||
![]() |
69d192d989 | ||
![]() |
6c8769e598 | ||
![]() |
c12703422c | ||
![]() |
c0171e1cd1 | ||
![]() |
920a983146 | ||
![]() |
7ec86bfef1 | ||
![]() |
d8bc318688 | ||
![]() |
600ea1848f | ||
![]() |
a3ce9c7662 | ||
![]() |
44ce7577c6 | ||
![]() |
8c3e42f7eb | ||
![]() |
b8887c506c | ||
![]() |
6c4228b7b8 | ||
![]() |
9b1da98386 | ||
![]() |
1615169a3d | ||
![]() |
df83aa4d15 | ||
![]() |
142b96beb0 | ||
![]() |
18d07dd3b9 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
**/node_modules/
|
||||||
|
**/dist/
|
||||||
|
**/logs/
|
||||||
|
**/.devcontainer
|
||||||
|
**/.github
|
||||||
|
**/.vscode
|
||||||
|
packages/docs
|
||||||
|
packages/e2e-test
|
@@ -1,5 +1,5 @@
|
|||||||
The Automatisch Enterprise license (the “Enterprise License”)
|
The Automatisch Enterprise license (the “Enterprise License”)
|
||||||
Copyright (c) 2023 Ömer Faruk Aydın, Ali Barın.
|
Copyright (c) 2023-present AB Software GmbH.
|
||||||
|
|
||||||
With regard to the Automatisch Software:
|
With regard to the Automatisch Software:
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ WORKDIR /automatisch
|
|||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk --no-cache add --virtual build-dependencies python3 build-base && \
|
apk --no-cache add --virtual build-dependencies python3 build-base && \
|
||||||
yarn global add @automatisch/cli@0.5.0 --network-timeout 1000000 && \
|
yarn global add @automatisch/cli@0.7.0 --network-timeout 1000000 && \
|
||||||
rm -rf /usr/local/share/.cache/ && \
|
rm -rf /usr/local/share/.cache/ && \
|
||||||
apk del build-dependencies
|
apk del build-dependencies
|
||||||
|
|
||||||
|
19
docker/Dockerfile.cloud
Normal file
19
docker/Dockerfile.cloud
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM node:16-alpine
|
||||||
|
WORKDIR /automatisch
|
||||||
|
|
||||||
|
ENV PORT 3000
|
||||||
|
|
||||||
|
RUN ls -lna
|
||||||
|
|
||||||
|
# copy the app, note .dockerignore
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
RUN yarn
|
||||||
|
RUN yarn lerna bootstrap
|
||||||
|
RUN yarn lerna run --scope=@*/{web,backend,cli} build
|
||||||
|
|
||||||
|
COPY ./docker/entrypoint-cloud.sh /entrypoint-cloud.sh
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
ENTRYPOINT ["sh", "/entrypoint-cloud.sh"]
|
@@ -1,5 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM automatischio/automatisch:0.5.0
|
FROM automatischio/automatisch:0.7.0
|
||||||
WORKDIR /automatisch
|
WORKDIR /automatisch
|
||||||
|
|
||||||
RUN apk add --no-cache openssl dos2unix
|
RUN apk add --no-cache openssl dos2unix
|
||||||
|
9
docker/entrypoint-cloud.sh
Executable file
9
docker/entrypoint-cloud.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "$WORKER" ]; then
|
||||||
|
yarn automatisch start-worker
|
||||||
|
else
|
||||||
|
yarn automatisch start
|
||||||
|
fi
|
@@ -2,7 +2,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"command": {
|
"command": {
|
||||||
|
@@ -12,6 +12,7 @@ const knexConfig = {
|
|||||||
database: appConfig.postgresDatabase,
|
database: appConfig.postgresDatabase,
|
||||||
ssl: appConfig.postgresEnableSsl,
|
ssl: appConfig.postgresEnableSsl,
|
||||||
},
|
},
|
||||||
|
searchPath: [appConfig.postgresSchema],
|
||||||
pool: { min: 0, max: 20 },
|
pool: { min: 0, max: 20 },
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/src/db/migrations',
|
directory: __dirname + '/src/db/migrations',
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@automatisch/backend",
|
"name": "@automatisch/backend",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
"db:migration:create": "knex migrate:make",
|
"db:migration:create": "knex migrate:make",
|
||||||
"db:rollback": "knex migrate:rollback",
|
"db:rollback": "knex migrate:rollback",
|
||||||
"db:migrate": "knex migrate:latest",
|
"db:migrate": "knex migrate:latest",
|
||||||
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
|
"copy-statics": "copyfiles src/**/*.{graphql,json,svg,hbs} dist",
|
||||||
"prepack": "yarn build",
|
"prepack": "yarn build",
|
||||||
"prebuild": "rm -rf ./dist"
|
"prebuild": "rm -rf ./dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automatisch/web": "^0.5.0",
|
"@automatisch/web": "^0.7.0",
|
||||||
"@bull-board/express": "^3.10.1",
|
"@bull-board/express": "^3.10.1",
|
||||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||||
"@graphql-tools/load": "^7.5.2",
|
"@graphql-tools/load": "^7.5.2",
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
"url": "https://github.com/automatisch/automatisch/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@automatisch/types": "^0.5.0",
|
"@automatisch/types": "^0.7.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bull": "^3.15.8",
|
"@types/bull": "^3.15.8",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
|
@@ -10,7 +10,7 @@ type TGroupItem = {
|
|||||||
type TGroup = Record<'and', TGroupItem[]>;
|
type TGroup = Record<'and', TGroupItem[]>;
|
||||||
|
|
||||||
const isEqual = (a: string, b: string) => a === b;
|
const isEqual = (a: string, b: string) => a === b;
|
||||||
const isNotEqual = (a: string, b: string) => !isEqual(a, b)
|
const isNotEqual = (a: string, b: string) => !isEqual(a, b);
|
||||||
const isGreaterThan = (a: string, b: string) => Number(a) > Number(b);
|
const isGreaterThan = (a: string, b: string) => Number(a) > Number(b);
|
||||||
const isLessThan = (a: string, b: string) => Number(a) < Number(b);
|
const isLessThan = (a: string, b: string) => Number(a) < Number(b);
|
||||||
const isGreaterThanOrEqual = (a: string, b: string) => Number(a) >= Number(b);
|
const isGreaterThanOrEqual = (a: string, b: string) => Number(a) >= Number(b);
|
||||||
@@ -18,6 +18,36 @@ const isLessThanOrEqual = (a: string, b: string) => Number(a) <= Number(b);
|
|||||||
const contains = (a: string, b: string) => a.includes(b);
|
const contains = (a: string, b: string) => a.includes(b);
|
||||||
const doesNotContain = (a: string, b: string) => !contains(a, b);
|
const doesNotContain = (a: string, b: string) => !contains(a, b);
|
||||||
|
|
||||||
|
const shouldContinue = (orGroups: TGroup[]) => {
|
||||||
|
let atLeastOneGroupMatches = false;
|
||||||
|
|
||||||
|
for (const group of orGroups) {
|
||||||
|
let groupMatches = true;
|
||||||
|
|
||||||
|
for (const condition of group.and) {
|
||||||
|
const conditionMatches = operate(
|
||||||
|
condition.operator,
|
||||||
|
condition.key,
|
||||||
|
condition.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!conditionMatches) {
|
||||||
|
groupMatches = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupMatches) {
|
||||||
|
atLeastOneGroupMatches = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return atLeastOneGroupMatches;
|
||||||
|
}
|
||||||
|
|
||||||
type TOperatorFunc = (a: string, b: string) => boolean;
|
type TOperatorFunc = (a: string, b: string) => boolean;
|
||||||
|
|
||||||
type TOperators = {
|
type TOperators = {
|
||||||
@@ -66,7 +96,7 @@ export default defineAction({
|
|||||||
return groups;
|
return groups;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (matchingGroups.length === 0) {
|
if (!shouldContinue(orGroups)) {
|
||||||
$.execution.exit();
|
$.execution.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,13 +11,19 @@ export default {
|
|||||||
data: [],
|
data: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const params = {
|
const params: Record<string, unknown> = {
|
||||||
q: `mimeType='application/vnd.google-apps.folder'`,
|
q: `mimeType='application/vnd.google-apps.folder'`,
|
||||||
orderBy: 'createdTime desc',
|
orderBy: 'createdTime desc',
|
||||||
pageToken: undefined as unknown as string,
|
pageToken: undefined as unknown as string,
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const { data } = await $.http.get(
|
const { data } = await $.http.get(
|
||||||
`https://www.googleapis.com/drive/v3/files`,
|
`https://www.googleapis.com/drive/v3/files`,
|
||||||
|
@@ -32,6 +32,7 @@ export default defineTrigger({
|
|||||||
key: 'folderId',
|
key: 'folderId',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: false,
|
required: false,
|
||||||
|
dependsOn: ['parameters.driveId'],
|
||||||
description:
|
description:
|
||||||
'Check a specific folder for new files. Please note: new files added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
'Check a specific folder for new files. Please note: new files added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
||||||
variables: false,
|
variables: false,
|
||||||
@@ -43,6 +44,10 @@ export default defineTrigger({
|
|||||||
name: 'key',
|
name: 'key',
|
||||||
value: 'listFolders',
|
value: 'listFolders',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.driveId',
|
||||||
|
value: '{parameters.driveId}',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -7,15 +7,20 @@ const newFilesInFolder = async ($: IGlobalVariable) => {
|
|||||||
} else {
|
} else {
|
||||||
q += ` and parents in 'root'`;
|
q += ` and parents in 'root'`;
|
||||||
}
|
}
|
||||||
const params = {
|
const params: Record<string, unknown> = {
|
||||||
pageToken: undefined as unknown as string,
|
pageToken: undefined as unknown as string,
|
||||||
orderBy: 'createdTime desc',
|
orderBy: 'createdTime desc',
|
||||||
fields: '*',
|
fields: '*',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
q,
|
q,
|
||||||
driveId: $.step.parameters.driveId,
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const { data } = await $.http.get(`/v3/files`, { params });
|
const { data } = await $.http.get(`/v3/files`, { params });
|
||||||
params.pageToken = data.nextPageToken;
|
params.pageToken = data.nextPageToken;
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
const newFiles = async ($: IGlobalVariable) => {
|
const newFiles = async ($: IGlobalVariable) => {
|
||||||
const params = {
|
const params: Record<string, unknown> = {
|
||||||
pageToken: undefined as unknown as string,
|
pageToken: undefined as unknown as string,
|
||||||
orderBy: 'createdTime desc',
|
orderBy: 'createdTime desc',
|
||||||
fields: '*',
|
fields: '*',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
q: `mimeType!='application/vnd.google-apps.folder'`,
|
q: `mimeType!='application/vnd.google-apps.folder'`,
|
||||||
driveId: $.step.parameters.driveId,
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const { data } = await $.http.get('/v3/files', { params });
|
const { data } = await $.http.get('/v3/files', { params });
|
||||||
params.pageToken = data.nextPageToken;
|
params.pageToken = data.nextPageToken;
|
||||||
|
@@ -32,6 +32,7 @@ export default defineTrigger({
|
|||||||
key: 'folderId',
|
key: 'folderId',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: false,
|
required: false,
|
||||||
|
dependsOn: ['parameters.driveId'],
|
||||||
description:
|
description:
|
||||||
'Check a specific folder for new subfolders. Please note: new folders added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
'Check a specific folder for new subfolders. Please note: new folders added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
||||||
variables: false,
|
variables: false,
|
||||||
@@ -43,6 +44,10 @@ export default defineTrigger({
|
|||||||
name: 'key',
|
name: 'key',
|
||||||
value: 'listFolders',
|
value: 'listFolders',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.driveId',
|
||||||
|
value: '{parameters.driveId}',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -8,15 +8,20 @@ const newFolders = async ($: IGlobalVariable) => {
|
|||||||
q += ` and parents in 'root'`;
|
q += ` and parents in 'root'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params: Record<string, unknown> = {
|
||||||
pageToken: undefined as unknown as string,
|
pageToken: undefined as unknown as string,
|
||||||
orderBy: 'createdTime desc',
|
orderBy: 'createdTime desc',
|
||||||
fields: '*',
|
fields: '*',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
q,
|
q,
|
||||||
driveId: $.step.parameters.driveId,
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const { data } = await $.http.get(`/v3/files`, { params });
|
const { data } = await $.http.get(`/v3/files`, { params });
|
||||||
params.pageToken = data.nextPageToken;
|
params.pageToken = data.nextPageToken;
|
||||||
|
@@ -32,6 +32,7 @@ export default defineTrigger({
|
|||||||
key: 'folderId',
|
key: 'folderId',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: false,
|
required: false,
|
||||||
|
dependsOn: ['parameters.driveId'],
|
||||||
description:
|
description:
|
||||||
'Check a specific folder for updated files. Please note: files located in subfolders of the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
'Check a specific folder for updated files. Please note: files located in subfolders of the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
||||||
source: {
|
source: {
|
||||||
@@ -42,6 +43,10 @@ export default defineTrigger({
|
|||||||
name: 'key',
|
name: 'key',
|
||||||
value: 'listFolders',
|
value: 'listFolders',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.driveId',
|
||||||
|
value: '{parameters.driveId}',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -12,15 +12,20 @@ const updatedFiles = async ($: IGlobalVariable) => {
|
|||||||
q += ` and parents in 'root'`;
|
q += ` and parents in 'root'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params: Record<string, unknown> = {
|
||||||
pageToken: undefined as unknown as string,
|
pageToken: undefined as unknown as string,
|
||||||
orderBy: 'modifiedTime desc',
|
orderBy: 'modifiedTime desc',
|
||||||
fields: '*',
|
fields: '*',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
q,
|
q,
|
||||||
driveId: $.step.parameters.driveId,
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const { data } = await $.http.get(`/v3/files`, { params });
|
const { data } = await $.http.get(`/v3/files`, { params });
|
||||||
params.pageToken = data.nextPageToken;
|
params.pageToken = data.nextPageToken;
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
import listDrives from './list-drives';
|
import listDrives from './list-drives';
|
||||||
|
import listSpreadsheets from './list-spreadsheets';
|
||||||
|
|
||||||
export default [listDrives];
|
export default [listDrives, listSpreadsheets];
|
||||||
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'List spreadsheets',
|
||||||
|
key: 'listSpreadsheets',
|
||||||
|
|
||||||
|
async run($: IGlobalVariable) {
|
||||||
|
const spreadsheets: {
|
||||||
|
data: IJSONObject[];
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const params: Record<string, unknown> = {
|
||||||
|
q: `mimeType='application/vnd.google-apps.spreadsheet'`,
|
||||||
|
pageSize: 100,
|
||||||
|
pageToken: undefined as unknown as string,
|
||||||
|
orderBy: 'createdTime desc',
|
||||||
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(
|
||||||
|
`https://www.googleapis.com/drive/v3/files`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
params.pageToken = data.nextPageToken;
|
||||||
|
|
||||||
|
if (data.files?.length) {
|
||||||
|
for (const file of data.files) {
|
||||||
|
spreadsheets.data.push({
|
||||||
|
value: file.id,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.pageToken);
|
||||||
|
|
||||||
|
return spreadsheets;
|
||||||
|
},
|
||||||
|
};
|
@@ -1,3 +1,4 @@
|
|||||||
import newSpreadsheets from './new-spreadsheets';
|
import newSpreadsheets from './new-spreadsheets';
|
||||||
|
import newWorksheets from './new-worksheets';
|
||||||
|
|
||||||
export default [newSpreadsheets];
|
export default [newSpreadsheets, newWorksheets];
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
const newSpreadsheets = async ($: IGlobalVariable) => {
|
const newSpreadsheets = async ($: IGlobalVariable) => {
|
||||||
const params = {
|
const params: Record<string, unknown> = {
|
||||||
pageToken: undefined as unknown as string,
|
pageToken: undefined as unknown as string,
|
||||||
orderBy: 'createdTime desc',
|
orderBy: 'createdTime desc',
|
||||||
q: `mimeType='application/vnd.google-apps.spreadsheet'`,
|
q: `mimeType='application/vnd.google-apps.spreadsheet'`,
|
||||||
fields: '*',
|
fields: '*',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
driveId: $.step.parameters.driveId,
|
driveId: $.step.parameters.driveId,
|
||||||
|
supportsAllDrives: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ($.step.parameters.driveId) {
|
||||||
|
params.includeItemsFromAllDrives = true;
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const { data } = await $.http.get(
|
const { data } = await $.http.get(
|
||||||
'https://www.googleapis.com/drive/v3/files',
|
'https://www.googleapis.com/drive/v3/files',
|
||||||
|
@@ -0,0 +1,57 @@
|
|||||||
|
import defineTrigger from '../../../../helpers/define-trigger';
|
||||||
|
import newWorksheets from './new-worksheets';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New Worksheets',
|
||||||
|
key: 'newWorksheets',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when you create a new worksheet in a spreadsheet.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Drive',
|
||||||
|
key: 'driveId',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||||
|
variables: false,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listDrives',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Spreadsheet',
|
||||||
|
key: 'spreadsheetId',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
dependsOn: ['parameters.driveId'],
|
||||||
|
description: 'The spreadsheets in your Google Drive.',
|
||||||
|
variables: false,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listSpreadsheets',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.driveId',
|
||||||
|
value: '{parameters.driveId}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
await newWorksheets($);
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const newWorksheets = async ($: IGlobalVariable) => {
|
||||||
|
const params = {
|
||||||
|
pageToken: undefined as unknown as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(
|
||||||
|
`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
params.pageToken = data.nextPageToken;
|
||||||
|
|
||||||
|
if (data.sheets?.length) {
|
||||||
|
for (const sheet of data.sheets.reverse()) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: sheet,
|
||||||
|
meta: {
|
||||||
|
internalId: sheet.properties.sheetId.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.pageToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default newWorksheets;
|
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
import defineAction from '../../../../helpers/define-action';
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
|
||||||
type TMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
type TMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||||
@@ -9,6 +10,23 @@ type THeaderEntry = {
|
|||||||
|
|
||||||
type THeaderEntries = THeaderEntry[];
|
type THeaderEntries = THeaderEntry[];
|
||||||
|
|
||||||
|
function isPossiblyTextBased(contentType: string) {
|
||||||
|
if (!contentType) return false;
|
||||||
|
|
||||||
|
return contentType.startsWith('application/json')
|
||||||
|
|| contentType.startsWith('text/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfFileSizeExceedsLimit(contentLength: string) {
|
||||||
|
const maxFileSize = 25 * 1024 * 1024; // 25MB
|
||||||
|
|
||||||
|
if (Number(contentLength) > maxFileSize) {
|
||||||
|
throw new Error(
|
||||||
|
`Response is too large. Maximum size is 25MB. Actual size is ${contentLength}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineAction({
|
export default defineAction({
|
||||||
name: 'Custom Request',
|
name: 'Custom Request',
|
||||||
key: 'customRequest',
|
key: 'customRequest',
|
||||||
@@ -81,29 +99,51 @@ export default defineAction({
|
|||||||
const data = $.step.parameters.data as string;
|
const data = $.step.parameters.data as string;
|
||||||
const url = $.step.parameters.url as string;
|
const url = $.step.parameters.url as string;
|
||||||
const headers = $.step.parameters.headers as THeaderEntries;
|
const headers = $.step.parameters.headers as THeaderEntries;
|
||||||
const maxFileSize = 25 * 1024 * 1024; // 25MB
|
|
||||||
|
|
||||||
const headersObject = headers.reduce((result, entry) => ({ ...result, [entry.key]: entry.value }), {})
|
const headersObject: Record<string, string> = headers.reduce((result, entry) => {
|
||||||
|
const key = entry.key?.toLowerCase();
|
||||||
|
const value = entry.value;
|
||||||
|
|
||||||
const metadataResponse = await $.http.head(url, { headers: headersObject });
|
if (key && value) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[entry.key?.toLowerCase()]: entry.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Number(metadataResponse.headers['content-length']) > maxFileSize) {
|
return result;
|
||||||
throw new Error(
|
}, {});
|
||||||
`Response is too large. Maximum size is 25MB. Actual size is ${metadataResponse.headers['content-length']}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await $.http.request({
|
let contentType = headersObject['content-type'];
|
||||||
|
|
||||||
|
// in case HEAD request is not supported by the URL
|
||||||
|
try {
|
||||||
|
const metadataResponse = await $.http.head(url, { headers: headersObject });
|
||||||
|
contentType = metadataResponse.headers['content-type'];
|
||||||
|
|
||||||
|
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
const requestData: AxiosRequestConfig = {
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
data,
|
data,
|
||||||
headers: headersObject,
|
headers: headersObject,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (!isPossiblyTextBased(contentType)) {
|
||||||
|
requestData.responseType = 'arraybuffer';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await $.http.request(requestData);
|
||||||
|
|
||||||
|
throwIfFileSizeExceedsLimit(response.headers['content-length']);
|
||||||
|
|
||||||
let responseData = response.data;
|
let responseData = response.data;
|
||||||
|
|
||||||
if (typeof response.data === 'string') {
|
if (!isPossiblyTextBased(contentType)) {
|
||||||
responseData = response.data.replaceAll('\u0000', '');
|
responseData = Buffer.from(responseData as string).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
$.setActionItem({ raw: { data: responseData } });
|
$.setActionItem({ raw: { data: responseData } });
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import checkModeration from './check-moderation';
|
import checkModeration from './check-moderation';
|
||||||
import sendPrompt from './send-prompt';
|
import sendPrompt from './send-prompt';
|
||||||
|
import sendChatPrompt from './send-chat-prompt';
|
||||||
|
|
||||||
export default [checkModeration, sendPrompt];
|
export default [checkModeration, sendChatPrompt, sendPrompt];
|
||||||
|
@@ -0,0 +1,137 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
|
||||||
|
type TMessage = {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const castFloatOrUndefined = (value: string | null) => {
|
||||||
|
return value === '' ? undefined : parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Send chat prompt',
|
||||||
|
key: 'sendChatPrompt',
|
||||||
|
description: 'Creates a completion for the provided prompt and parameters.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Model',
|
||||||
|
key: 'model',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listModels',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Messages',
|
||||||
|
key: 'messages',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: false,
|
||||||
|
description: 'Add or remove messages as needed',
|
||||||
|
value: [{ role: 'system', body: '' }],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Role',
|
||||||
|
key: 'role',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'System',
|
||||||
|
value: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User',
|
||||||
|
value: 'user',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Content',
|
||||||
|
key: 'content',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Temperature',
|
||||||
|
key: 'temperature',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
description: 'What sampling temperature to use. Higher values mean the model will take more risk. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer. We generally recommend altering this or Top P but not both.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Maximum tokens',
|
||||||
|
key: 'maxTokens',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
description: 'The maximum number of tokens to generate in the completion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Stop Sequence',
|
||||||
|
key: 'stopSequence',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
description: 'Single stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Top P',
|
||||||
|
key: 'topP',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
description: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with Top P probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Frequency Penalty',
|
||||||
|
key: 'frequencyPenalty',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'presencePenalty',
|
||||||
|
key: 'presencePenalty',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const payload = {
|
||||||
|
model: $.step.parameters.model as string,
|
||||||
|
temperature: castFloatOrUndefined($.step.parameters.temperature as string),
|
||||||
|
max_tokens: castFloatOrUndefined($.step.parameters.maxTokens as string),
|
||||||
|
stop: ($.step.parameters.stopSequence as string || null),
|
||||||
|
top_p: castFloatOrUndefined($.step.parameters.topP as string),
|
||||||
|
frequency_penalty: castFloatOrUndefined($.step.parameters.frequencyPenalty as string),
|
||||||
|
presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty as string),
|
||||||
|
messages: ($.step.parameters.messages as TMessage[]).map(message => ({
|
||||||
|
role: message.role,
|
||||||
|
content: message.content,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const { data } = await $.http.post('/v1/chat/completions', payload);
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
111
packages/backend/src/apps/postgresql/actions/delete/index.ts
Normal file
111
packages/backend/src/apps/postgresql/actions/delete/index.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { IJSONArray } from '@automatisch/types';
|
||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import getClient from '../../common/postgres-client';
|
||||||
|
import setParams from '../../common/set-run-time-parameters';
|
||||||
|
import whereClauseOperators from '../../common/where-clause-operators';
|
||||||
|
|
||||||
|
type TWhereClauseEntry = { columnName: string, value: string, operator: string };
|
||||||
|
type TWhereClauseEntries = TWhereClauseEntry[];
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Delete',
|
||||||
|
key: 'delete',
|
||||||
|
description: 'Delete rows found based on the given where clause entries.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Schema name',
|
||||||
|
key: 'schema',
|
||||||
|
type: 'string' as const,
|
||||||
|
value: 'public',
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Table name',
|
||||||
|
key: 'table',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Where clause entries',
|
||||||
|
key: 'whereClauseEntries',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Column name',
|
||||||
|
key: 'columnName',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Operator',
|
||||||
|
key: 'operator',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
options: whereClauseOperators
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Run-time parameters',
|
||||||
|
key: 'params',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: false,
|
||||||
|
description: 'Change run-time configuration parameters with SET command',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Parameter name',
|
||||||
|
key: 'parameter',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const client = getClient($);
|
||||||
|
await setParams(client, $.step.parameters.params);
|
||||||
|
|
||||||
|
const whereClauseEntries = $.step.parameters.whereClauseEntries as TWhereClauseEntries;
|
||||||
|
|
||||||
|
const response = await client($.step.parameters.table as string)
|
||||||
|
.withSchema($.step.parameters.schema as string)
|
||||||
|
.returning('*')
|
||||||
|
.where((builder) => {
|
||||||
|
for (const whereClauseEntry of whereClauseEntries) {
|
||||||
|
const { columnName, operator, value } = whereClauseEntry;
|
||||||
|
|
||||||
|
if (columnName) {
|
||||||
|
builder.where(columnName, operator, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.del() as IJSONArray;
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: {
|
||||||
|
rows: response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
6
packages/backend/src/apps/postgresql/actions/index.ts
Normal file
6
packages/backend/src/apps/postgresql/actions/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import insertAction from './insert';
|
||||||
|
import updateAction from './update';
|
||||||
|
import deleteAction from './delete';
|
||||||
|
import SQLQuery from './sql-query'
|
||||||
|
|
||||||
|
export default [insertAction, updateAction, deleteAction, SQLQuery];
|
93
packages/backend/src/apps/postgresql/actions/insert/index.ts
Normal file
93
packages/backend/src/apps/postgresql/actions/insert/index.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import getClient from '../../common/postgres-client';
|
||||||
|
import setParams from '../../common/set-run-time-parameters';
|
||||||
|
|
||||||
|
type TColumnValueEntries = { columnName: string, value: string }[];
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Insert',
|
||||||
|
key: 'insert',
|
||||||
|
description: 'Create a new row in a table in specified schema.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Schema name',
|
||||||
|
key: 'schema',
|
||||||
|
type: 'string' as const,
|
||||||
|
value: 'public',
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Table name',
|
||||||
|
key: 'table',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Column - value entries',
|
||||||
|
key: 'columnValueEntries',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: true,
|
||||||
|
description: 'Table columns with values',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Column name',
|
||||||
|
key: 'columnName',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Run-time parameters',
|
||||||
|
key: 'params',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: false,
|
||||||
|
description: 'Change run-time configuration parameters with SET command',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Parameter name',
|
||||||
|
key: 'parameter',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const client = getClient($);
|
||||||
|
await setParams(client, $.step.parameters.params);
|
||||||
|
|
||||||
|
const fields = $.step.parameters.columnValueEntries as TColumnValueEntries;
|
||||||
|
const data = fields.reduce((result, { columnName, value }) => ({
|
||||||
|
...result,
|
||||||
|
[columnName]: value,
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const response = await client($.step.parameters.table as string)
|
||||||
|
.withSchema($.step.parameters.schema as string)
|
||||||
|
.returning('*')
|
||||||
|
.insert(data) as IJSONObject;
|
||||||
|
|
||||||
|
$.setActionItem({ raw: response[0] as IJSONObject });
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,56 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import getClient from '../../common/postgres-client';
|
||||||
|
import setParams from '../../common/set-run-time-parameters';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'SQL query',
|
||||||
|
key: 'SQLQuery',
|
||||||
|
description: 'Executes the given SQL statement.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'SQL statement',
|
||||||
|
key: 'queryStatement',
|
||||||
|
type: 'string' as const,
|
||||||
|
value: 'public',
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Run-time parameters',
|
||||||
|
key: 'params',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: false,
|
||||||
|
description: 'Change run-time configuration parameters with SET command',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Parameter name',
|
||||||
|
key: 'parameter',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const client = getClient($);
|
||||||
|
await setParams(client, $.step.parameters.params);
|
||||||
|
|
||||||
|
const queryStatemnt = $.step.parameters.queryStatement;
|
||||||
|
const { rows } = await client.raw(queryStatemnt);
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: {
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
141
packages/backend/src/apps/postgresql/actions/update/index.ts
Normal file
141
packages/backend/src/apps/postgresql/actions/update/index.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { IJSONArray } from '@automatisch/types';
|
||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import getClient from '../../common/postgres-client';
|
||||||
|
import setParams from '../../common/set-run-time-parameters';
|
||||||
|
import whereClauseOperators from '../../common/where-clause-operators';
|
||||||
|
|
||||||
|
type TColumnValueEntries = { columnName: string, value: string }[];
|
||||||
|
type TWhereClauseEntry = { columnName: string, value: string, operator: string };
|
||||||
|
type TWhereClauseEntries = TWhereClauseEntry[];
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Update',
|
||||||
|
key: 'update',
|
||||||
|
description: 'Update rows found based on the given where clause entries.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Schema name',
|
||||||
|
key: 'schema',
|
||||||
|
type: 'string' as const,
|
||||||
|
value: 'public',
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Table name',
|
||||||
|
key: 'table',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Where clause entries',
|
||||||
|
key: 'whereClauseEntries',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Column name',
|
||||||
|
key: 'columnName',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Operator',
|
||||||
|
key: 'operator',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
options: whereClauseOperators
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Column - value entries',
|
||||||
|
key: 'columnValueEntries',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: true,
|
||||||
|
description: 'Table columns with values',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Column name',
|
||||||
|
key: 'columnName',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Run-time parameters',
|
||||||
|
key: 'params',
|
||||||
|
type: 'dynamic' as const,
|
||||||
|
required: false,
|
||||||
|
description: 'Change run-time configuration parameters with SET command',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Parameter name',
|
||||||
|
key: 'parameter',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const client = getClient($);
|
||||||
|
await setParams(client, $.step.parameters.params);
|
||||||
|
|
||||||
|
const whereClauseEntries = $.step.parameters.whereClauseEntries as TWhereClauseEntries;
|
||||||
|
|
||||||
|
const fields = $.step.parameters.columnValueEntries as TColumnValueEntries;
|
||||||
|
const data: Record<string, unknown> = fields.reduce((result, { columnName, value }) => ({
|
||||||
|
...result,
|
||||||
|
[columnName]: value,
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const response = await client($.step.parameters.table as string)
|
||||||
|
.withSchema($.step.parameters.schema as string)
|
||||||
|
.returning('*')
|
||||||
|
.where((builder) => {
|
||||||
|
for (const whereClauseEntry of whereClauseEntries) {
|
||||||
|
const { columnName, operator, value } = whereClauseEntry;
|
||||||
|
|
||||||
|
if (columnName) {
|
||||||
|
builder.where(columnName, operator, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.update(data) as IJSONArray;
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: {
|
||||||
|
rows: response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
10
packages/backend/src/apps/postgresql/assets/favicon.svg
Normal file
10
packages/backend/src/apps/postgresql/assets/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
98
packages/backend/src/apps/postgresql/auth/index.ts
Normal file
98
packages/backend/src/apps/postgresql/auth/index.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import verifyCredentials from './verify-credentials';
|
||||||
|
import isStillVerified from './is-still-verified';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'version',
|
||||||
|
label: 'PostgreSQL version',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'The version of PostgreSQL database that user want to connect with.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'host',
|
||||||
|
label: 'Host',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: '127.0.0.1',
|
||||||
|
placeholder: null,
|
||||||
|
description: 'The host of the PostgreSQL database.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'port',
|
||||||
|
label: 'Port',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: '5432',
|
||||||
|
placeholder: null,
|
||||||
|
description: 'The port of the PostgreSQL database.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enableSsl',
|
||||||
|
label: 'Enable SSL',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: 'false',
|
||||||
|
description: 'The port of the PostgreSQL database.',
|
||||||
|
variables: false,
|
||||||
|
clickToCopy: false,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'True',
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'False',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'database',
|
||||||
|
label: 'Database name',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: 'The database name of the PostgreSQL database.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Database username',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: 'The user who has access on postgres database.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'password',
|
||||||
|
label: 'Password',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: 'The password of the PostgreSQL database user.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
};
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import verifyCredentials from './verify-credentials';
|
||||||
|
|
||||||
|
const isStillVerified = async ($: IGlobalVariable) => {
|
||||||
|
await verifyCredentials($);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import logger from '../../../helpers/logger';
|
||||||
|
import getClient from '../common/postgres-client';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||||
|
const client = getClient($);
|
||||||
|
const checkConnection = await client.raw('SELECT 1');
|
||||||
|
|
||||||
|
logger.debug(checkConnection);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
screenName: `${$.auth.data.user}@${$.auth.data.host}:${$.auth.data.port}/${$.auth.data.database}`,
|
||||||
|
client: 'pg',
|
||||||
|
version: $.auth.data.version,
|
||||||
|
host: $.auth.data.host,
|
||||||
|
port: Number($.auth.data.port),
|
||||||
|
enableSsl:
|
||||||
|
$.auth.data.enableSsl === 'true' || $.auth.data.enableSsl === true,
|
||||||
|
user: $.auth.data.user,
|
||||||
|
password: $.auth.data.password,
|
||||||
|
database: $.auth.data.database,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
@@ -0,0 +1,22 @@
|
|||||||
|
import knex, { Knex } from 'knex';
|
||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const getClient = ($: IGlobalVariable): Knex<any, unknown[]> => {
|
||||||
|
const client = knex({
|
||||||
|
client: 'pg',
|
||||||
|
version: $.auth.data.version as string,
|
||||||
|
connection: {
|
||||||
|
host: $.auth.data.host as string,
|
||||||
|
port: Number($.auth.data.port),
|
||||||
|
ssl: ($.auth.data.enableSsl === 'true' ||
|
||||||
|
$.auth.data.enableSsl === true) as boolean,
|
||||||
|
user: $.auth.data.user as string,
|
||||||
|
password: $.auth.data.password as string,
|
||||||
|
database: $.auth.data.database as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getClient;
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { type IJSONValue } from '@automatisch/types';
|
||||||
|
|
||||||
|
type TParams = { parameter: string; value: string; }[];
|
||||||
|
|
||||||
|
const setParams = async (client: Knex<any, unknown[]>, params: IJSONValue = []): Promise<void> => {
|
||||||
|
for (const { parameter, value } of (params as TParams)) {
|
||||||
|
if (parameter) {
|
||||||
|
const bindings = {
|
||||||
|
parameter,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
await client.raw('SET :parameter: = :value:', bindings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setParams;
|
@@ -0,0 +1,60 @@
|
|||||||
|
const whereClauseOperators = [
|
||||||
|
{
|
||||||
|
value: "=",
|
||||||
|
label: "="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ">",
|
||||||
|
label: ">"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "<",
|
||||||
|
label: "<"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ">=",
|
||||||
|
label: ">="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "<=",
|
||||||
|
label: "<="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "<>",
|
||||||
|
label: "<>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "!=",
|
||||||
|
label: "!="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "AND",
|
||||||
|
label: "AND"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "OR",
|
||||||
|
label: "OR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "IN",
|
||||||
|
label: "IN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "BETWEEN",
|
||||||
|
label: "BETWEEN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "LIKE",
|
||||||
|
label: "LIKE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "IS NULL",
|
||||||
|
label: "IS NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "NOT",
|
||||||
|
label: "NOT"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default whereClauseOperators;
|
0
packages/backend/src/apps/postgresql/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/postgresql/index.d.ts
vendored
Normal file
16
packages/backend/src/apps/postgresql/index.ts
Normal file
16
packages/backend/src/apps/postgresql/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import defineApp from '../../helpers/define-app';
|
||||||
|
import auth from './auth';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'PostgreSQL',
|
||||||
|
key: 'postgresql',
|
||||||
|
iconUrl: '{BASE_URL}/apps/postgresql/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/postgresql/connection',
|
||||||
|
supportsConnections: true,
|
||||||
|
baseUrl: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
primaryColor: '336791',
|
||||||
|
auth,
|
||||||
|
actions,
|
||||||
|
});
|
@@ -1,5 +1,6 @@
|
|||||||
import findMessage from './find-message';
|
import findMessage from './find-message';
|
||||||
import findUserByEmail from './find-user-by-email';
|
import findUserByEmail from './find-user-by-email';
|
||||||
import sendMessageToChannel from './send-a-message-to-channel';
|
import sendMessageToChannel from './send-a-message-to-channel';
|
||||||
|
import sendDirectMessage from './send-a-direct-message';
|
||||||
|
|
||||||
export default [findMessage, findUserByEmail, sendMessageToChannel];
|
export default [findMessage, findUserByEmail, sendMessageToChannel, sendDirectMessage];
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import postMessage from './post-message';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Send a direct message',
|
||||||
|
key: 'sendDirectMessage',
|
||||||
|
description: 'Sends a direct message to a user or yourself from the Slackbot.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'To username',
|
||||||
|
key: 'toUsername',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
description: 'Pick a user to send the message to.',
|
||||||
|
variables: false,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listUsers',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Message text',
|
||||||
|
key: 'message',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description: 'The content of your new message.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Send as a bot?',
|
||||||
|
key: 'sendAsBot',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: false,
|
||||||
|
value: false,
|
||||||
|
description:
|
||||||
|
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
|
||||||
|
variables: false,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Yes',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
additionalFields: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicFields',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listFieldsAfterSendAsBot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.sendAsBot',
|
||||||
|
value: '{parameters.sendAsBot}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const message = await postMessage($);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
});
|
@@ -0,0 +1,55 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import { URL } from 'url';
|
||||||
|
|
||||||
|
type TData = {
|
||||||
|
channel: string;
|
||||||
|
text: string;
|
||||||
|
username?: string;
|
||||||
|
icon_url?: string;
|
||||||
|
icon_emoji?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const postMessage = async ($: IGlobalVariable) => {
|
||||||
|
const { parameters } = $.step;
|
||||||
|
const toUsername = parameters.toUsername as string;
|
||||||
|
const text = parameters.message as string;
|
||||||
|
const sendAsBot = parameters.sendAsBot as boolean;
|
||||||
|
const botName = parameters.botName as string;
|
||||||
|
const botIcon = parameters.botIcon as string;
|
||||||
|
|
||||||
|
const data: TData = {
|
||||||
|
channel: toUsername,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sendAsBot) {
|
||||||
|
data.username = botName;
|
||||||
|
try {
|
||||||
|
// challenging the input to check if it is a URL!
|
||||||
|
new URL(botIcon);
|
||||||
|
data.icon_url = botIcon;
|
||||||
|
} catch {
|
||||||
|
data.icon_emoji = botIcon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customConfig = {
|
||||||
|
sendAsBot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await $.http.post('/chat.postMessage', data, {
|
||||||
|
additionalProperties: customConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.ok === false) {
|
||||||
|
throw new Error(JSON.stringify(response.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
raw: response?.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
$.setActionItem(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default postMessage;
|
@@ -1,3 +1,4 @@
|
|||||||
import listChannels from './list-channels';
|
import listChannels from './list-channels';
|
||||||
|
import listUsers from './list-users';
|
||||||
|
|
||||||
export default [listChannels];
|
export default [listChannels, listUsers];
|
||||||
|
@@ -36,7 +36,7 @@ export default {
|
|||||||
do {
|
do {
|
||||||
const response: TResponse = await $.http.get('/conversations.list', {
|
const response: TResponse = await $.http.get('/conversations.list', {
|
||||||
params: {
|
params: {
|
||||||
types: 'public_channel,private_channel,im',
|
types: 'public_channel,private_channel',
|
||||||
cursor: nextCursor,
|
cursor: nextCursor,
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||||
|
|
||||||
|
type TMember = {
|
||||||
|
id: string;
|
||||||
|
profile: {
|
||||||
|
real_name_normalized: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type TUserListResponseData = {
|
||||||
|
members: TMember[],
|
||||||
|
response_metadata?: {
|
||||||
|
next_cursor: string
|
||||||
|
};
|
||||||
|
needed?: string;
|
||||||
|
error?: string;
|
||||||
|
ok: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TResponse = {
|
||||||
|
data: TUserListResponseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'List users',
|
||||||
|
key: 'listUsers',
|
||||||
|
|
||||||
|
async run($: IGlobalVariable) {
|
||||||
|
const users: {
|
||||||
|
data: IJSONObject[];
|
||||||
|
error: IJSONObject | null;
|
||||||
|
} = {
|
||||||
|
data: [],
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let nextCursor;
|
||||||
|
do {
|
||||||
|
const response: TResponse = await $.http.get('/users.list', {
|
||||||
|
params: {
|
||||||
|
cursor: nextCursor,
|
||||||
|
limit: 1000,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nextCursor = response.data.response_metadata?.next_cursor;
|
||||||
|
|
||||||
|
if (response.data.error === 'missing_scope') {
|
||||||
|
throw new Error(`Missing "${response.data.needed}" scope while authorizing. Please, reconnect your connection!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.ok === false) {
|
||||||
|
throw new Error(JSON.stringify(response.data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const member of response.data.members) {
|
||||||
|
users.data.push({
|
||||||
|
value: member.id as string,
|
||||||
|
name: member.profile.real_name_normalized as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (nextCursor);
|
||||||
|
|
||||||
|
return users;
|
||||||
|
},
|
||||||
|
};
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
import defineAction from '../../../../helpers/define-action';
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
|
||||||
export default defineAction({
|
export default defineAction({
|
||||||
@@ -8,11 +9,21 @@ export default defineAction({
|
|||||||
{
|
{
|
||||||
label: 'From Number',
|
label: 'From Number',
|
||||||
key: 'fromNumber',
|
key: 'fromNumber',
|
||||||
type: 'string' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description:
|
description:
|
||||||
'The number to send the SMS from. Include country code. Example: 15551234567',
|
'The number to send the SMS from. Include country code. Example: 15551234567',
|
||||||
variables: true,
|
variables: false,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listIncomingPhoneNumbers',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'To Number',
|
label: 'To Number',
|
||||||
@@ -35,14 +46,20 @@ export default defineAction({
|
|||||||
|
|
||||||
async run($) {
|
async run($) {
|
||||||
const requestPath = `/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages.json`;
|
const requestPath = `/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages.json`;
|
||||||
const messageBody = $.step.parameters.message;
|
const messageBody = $.step.parameters.message as string;
|
||||||
|
|
||||||
const fromNumber = '+' + ($.step.parameters.fromNumber as string).trim();
|
const fromNumber = ($.step.parameters.fromNumber as string).trim();
|
||||||
const toNumber = '+' + ($.step.parameters.toNumber as string).trim();
|
const toNumber = ($.step.parameters.toNumber as string).trim();
|
||||||
|
|
||||||
|
const payload = new URLSearchParams({
|
||||||
|
Body: messageBody,
|
||||||
|
From: fromNumber,
|
||||||
|
To: toNumber,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
const response = await $.http.post(
|
const response = await $.http.post(
|
||||||
requestPath,
|
requestPath,
|
||||||
`Body=${messageBody}&From=${fromNumber}&To=${toNumber}`
|
payload,
|
||||||
);
|
);
|
||||||
|
|
||||||
$.setActionItem({ raw: response.data });
|
$.setActionItem({ raw: response.data });
|
||||||
|
3
packages/backend/src/apps/twilio/dynamic-data/index.ts
Normal file
3
packages/backend/src/apps/twilio/dynamic-data/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import listIncomingPhoneNumbers from './list-incoming-phone-numbers';
|
||||||
|
|
||||||
|
export default [listIncomingPhoneNumbers];
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||||
|
|
||||||
|
type TResponse = {
|
||||||
|
data: IJSONObject[];
|
||||||
|
error?: IJSONObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TIncomingPhoneNumber = {
|
||||||
|
phone_number: string;
|
||||||
|
friendly_name: string;
|
||||||
|
sid: string;
|
||||||
|
capabilities: {
|
||||||
|
sms: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TResponseData = {
|
||||||
|
incoming_phone_numbers: TIncomingPhoneNumber[];
|
||||||
|
next_page_uri: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'List incoming phone numbers',
|
||||||
|
key: 'listIncomingPhoneNumbers',
|
||||||
|
|
||||||
|
async run($: IGlobalVariable) {
|
||||||
|
const valueType = $.step.parameters.valueType as string;
|
||||||
|
const isSid = valueType === 'sid';
|
||||||
|
|
||||||
|
const aggregatedResponse: TResponse = { data: [] };
|
||||||
|
let pathname = `/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers.json`;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const response = await $.http.get<TResponseData>(pathname);
|
||||||
|
|
||||||
|
for (const incomingPhoneNumber of response.data.incoming_phone_numbers) {
|
||||||
|
if (incomingPhoneNumber.capabilities.sms === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendlyName = incomingPhoneNumber.friendly_name;
|
||||||
|
const phoneNumber = incomingPhoneNumber.phone_number;
|
||||||
|
const name = [friendlyName, phoneNumber].filter(Boolean).join(' - ');
|
||||||
|
|
||||||
|
aggregatedResponse.data.push({
|
||||||
|
value: isSid ? incomingPhoneNumber.sid : phoneNumber,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pathname = response.data.next_page_uri;
|
||||||
|
} while (pathname);
|
||||||
|
|
||||||
|
return aggregatedResponse;
|
||||||
|
},
|
||||||
|
};
|
@@ -3,6 +3,7 @@ import addAuthHeader from './common/add-auth-header';
|
|||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import triggers from './triggers';
|
import triggers from './triggers';
|
||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
|
import dynamicData from './dynamic-data';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'Twilio',
|
name: 'Twilio',
|
||||||
@@ -17,4 +18,5 @@ export default defineApp({
|
|||||||
auth,
|
auth,
|
||||||
triggers,
|
triggers,
|
||||||
actions,
|
actions,
|
||||||
|
dynamicData,
|
||||||
});
|
});
|
||||||
|
@@ -1,23 +1,73 @@
|
|||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import defineTrigger from '../../../../helpers/define-trigger';
|
import defineTrigger from '../../../../helpers/define-trigger';
|
||||||
import fetchMessages from './fetch-messages';
|
import fetchMessages from './fetch-messages';
|
||||||
|
|
||||||
export default defineTrigger({
|
export default defineTrigger({
|
||||||
name: 'Receive SMS',
|
name: 'Receive SMS',
|
||||||
key: 'receiveSms',
|
key: 'receiveSms',
|
||||||
pollInterval: 15,
|
type: 'webhook',
|
||||||
description: 'Triggers when a new SMS is received.',
|
description: 'Triggers when a new SMS is received.',
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{
|
||||||
label: 'To Number',
|
label: 'To Number',
|
||||||
key: 'toNumber',
|
key: 'phoneNumberSid',
|
||||||
type: 'string',
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description:
|
description:
|
||||||
'The number to receive the SMS on. It should be a Twilio number.',
|
'The number to receive the SMS on. It should be a Twilio number.',
|
||||||
|
variables: false,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listIncomingPhoneNumbers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.valueType',
|
||||||
|
value: 'sid',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
async run($) {
|
async testRun($) {
|
||||||
await fetchMessages($);
|
await fetchMessages($);
|
||||||
|
|
||||||
|
if (!isEmpty($.lastExecutionStep?.dataOut)) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: $.lastExecutionStep.dataOut,
|
||||||
|
meta: {
|
||||||
|
internalId: '',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async registerHook($) {
|
||||||
|
const phoneNumberSid = $.step.parameters.phoneNumberSid as string;
|
||||||
|
const payload = new URLSearchParams({
|
||||||
|
SmsUrl: $.webhookUrl,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
await $.http.post(
|
||||||
|
`/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers/${phoneNumberSid}.json`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async unregisterHook($) {
|
||||||
|
const phoneNumberSid = $.step.parameters.phoneNumberSid as string;
|
||||||
|
const payload = new URLSearchParams({
|
||||||
|
SmsUrl: '',
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
await $.http.post(
|
||||||
|
`/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers/${phoneNumberSid}.json`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,9 @@ type AppConfig = {
|
|||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
appEnv: string;
|
appEnv: string;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
|
isProd: boolean;
|
||||||
postgresDatabase: string;
|
postgresDatabase: string;
|
||||||
|
postgresSchema: string;
|
||||||
postgresPort: number;
|
postgresPort: number;
|
||||||
postgresHost: string;
|
postgresHost: string;
|
||||||
postgresUsername: string;
|
postgresUsername: string;
|
||||||
@@ -79,8 +81,10 @@ const appConfig: AppConfig = {
|
|||||||
port,
|
port,
|
||||||
appEnv: appEnv,
|
appEnv: appEnv,
|
||||||
isDev: appEnv === 'development',
|
isDev: appEnv === 'development',
|
||||||
|
isProd: appEnv === 'production',
|
||||||
version: process.env.npm_package_version,
|
version: process.env.npm_package_version,
|
||||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||||
|
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||||
postgresHost: process.env.POSTGRES_HOST || 'localhost',
|
postgresHost: process.env.POSTGRES_HOST || 'localhost',
|
||||||
postgresUsername:
|
postgresUsername:
|
||||||
|
@@ -85,7 +85,7 @@ export default async (request: IRequest, response: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (testRun) {
|
if (testRun) {
|
||||||
return response.sendStatus(200);
|
return response.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = await triggerStep.getNextStep();
|
const nextStep = await triggerStep.getNextStep();
|
||||||
@@ -104,5 +104,5 @@ export default async (request: IRequest, response: Response) => {
|
|||||||
|
|
||||||
await actionQueue.add(jobName, jobPayload, jobOptions);
|
await actionQueue.add(jobName, jobPayload, jobOptions);
|
||||||
|
|
||||||
return response.sendStatus(200);
|
return response.sendStatus(204);
|
||||||
};
|
};
|
||||||
|
@@ -9,6 +9,7 @@ import updateFlow from './mutations/update-flow';
|
|||||||
import updateFlowStatus from './mutations/update-flow-status';
|
import updateFlowStatus from './mutations/update-flow-status';
|
||||||
import executeFlow from './mutations/execute-flow';
|
import executeFlow from './mutations/execute-flow';
|
||||||
import deleteFlow from './mutations/delete-flow';
|
import deleteFlow from './mutations/delete-flow';
|
||||||
|
import duplicateFlow from './mutations/duplicate-flow';
|
||||||
import createStep from './mutations/create-step';
|
import createStep from './mutations/create-step';
|
||||||
import updateStep from './mutations/update-step';
|
import updateStep from './mutations/update-step';
|
||||||
import deleteStep from './mutations/delete-step';
|
import deleteStep from './mutations/delete-step';
|
||||||
@@ -31,6 +32,7 @@ const mutationResolvers = {
|
|||||||
updateFlowStatus,
|
updateFlowStatus,
|
||||||
executeFlow,
|
executeFlow,
|
||||||
deleteFlow,
|
deleteFlow,
|
||||||
|
duplicateFlow,
|
||||||
createStep,
|
createStep,
|
||||||
updateStep,
|
updateStep,
|
||||||
deleteStep,
|
deleteStep,
|
||||||
|
88
packages/backend/src/graphql/mutations/duplicate-flow.ts
Normal file
88
packages/backend/src/graphql/mutations/duplicate-flow.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
for (const stepIdEntry of stepIdEntries) {
|
||||||
|
const [oldStepId, newStepId] = stepIdEntry;
|
||||||
|
const partialOldVariable = `{{step.${oldStepId}.`;
|
||||||
|
const partialNewVariable = `{{step.${newStepId}.`;
|
||||||
|
|
||||||
|
newValue = newValue.replace(partialOldVariable, partialNewVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStepVariables(parameters: Step['parameters'], newStepIds: NewStepIds): Step['parameters'] {
|
||||||
|
const entries = Object.entries(parameters);
|
||||||
|
return entries.reduce((result, [key, value]: [string, unknown]) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[key]: updateStepId(value, newStepIds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[key]: value.map(item => updateStepVariables(item, newStepIds)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateFlow = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
const flow = await context.currentUser
|
||||||
|
.$relatedQuery('flows')
|
||||||
|
.withGraphJoined('[steps]')
|
||||||
|
.orderBy('steps.position', 'asc')
|
||||||
|
.findOne({ 'flows.id': params.input.id })
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const duplicatedFlow = await context.currentUser
|
||||||
|
.$relatedQuery('flows')
|
||||||
|
.insert({
|
||||||
|
name: `Copy of ${flow.name}`,
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
newStepIds[step.id] = duplicatedStep.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return duplicatedFlow;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default duplicateFlow;
|
@@ -16,6 +16,7 @@ const getExecution = async (
|
|||||||
steps: true,
|
steps: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.withSoftDeleted()
|
||||||
.findById(params.executionId)
|
.findById(params.executionId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
@@ -32,7 +32,7 @@ const getExecutions = async (
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.groupBy('executions.id')
|
.groupBy('executions.id')
|
||||||
.orderBy('created_at', 'desc');
|
.orderBy('updated_at', 'desc');
|
||||||
|
|
||||||
return paginate(executions, params.limit, params.offset);
|
return paginate(executions, params.limit, params.offset);
|
||||||
};
|
};
|
||||||
|
@@ -56,6 +56,7 @@ type Mutation {
|
|||||||
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
||||||
executeFlow(input: ExecuteFlowInput): executeFlowType
|
executeFlow(input: ExecuteFlowInput): executeFlowType
|
||||||
deleteFlow(input: DeleteFlowInput): Boolean
|
deleteFlow(input: DeleteFlowInput): Boolean
|
||||||
|
duplicateFlow(input: DuplicateFlowInput): Flow
|
||||||
createStep(input: CreateStepInput): Step
|
createStep(input: CreateStepInput): Step
|
||||||
updateStep(input: UpdateStepInput): Step
|
updateStep(input: UpdateStepInput): Step
|
||||||
deleteStep(input: DeleteStepInput): Step
|
deleteStep(input: DeleteStepInput): Step
|
||||||
@@ -324,6 +325,10 @@ input DeleteFlowInput {
|
|||||||
id: String!
|
id: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input DuplicateFlowInput {
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
input CreateStepInput {
|
input CreateStepInput {
|
||||||
id: String
|
id: String
|
||||||
previousStepId: String
|
previousStepId: String
|
||||||
|
@@ -1,13 +1,27 @@
|
|||||||
const plans = [
|
import appConfig from '../../config/app';
|
||||||
|
|
||||||
|
const testPlans = [
|
||||||
{
|
{
|
||||||
name: '10k - monthly',
|
name: '10k - monthly',
|
||||||
limit: '10,000',
|
limit: '10,000',
|
||||||
quota: 10000,
|
quota: 10000,
|
||||||
price: '€20',
|
price: '€20',
|
||||||
productId: '47384',
|
productId: '47384',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const prodPlans = [
|
||||||
|
{
|
||||||
|
name: '10k - monthly',
|
||||||
|
limit: '10,000',
|
||||||
|
quota: 10000,
|
||||||
|
price: '€20',
|
||||||
|
productId: '826658',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const plans = appConfig.isProd ? prodPlans : testPlans;
|
||||||
|
|
||||||
export function getPlanById(id: string) {
|
export function getPlanById(id: string) {
|
||||||
return plans.find((plan) => plan.productId === id);
|
return plans.find((plan) => plan.productId === id);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,8 @@ import Step from '../models/step';
|
|||||||
import ExecutionStep from '../models/execution-step';
|
import ExecutionStep from '../models/execution-step';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_]+)+}})/g;
|
// INFO: don't remove space in allowed character group!
|
||||||
|
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ ]+)+}})/g;
|
||||||
|
|
||||||
export default function computeParameters(
|
export default function computeParameters(
|
||||||
parameters: Step['parameters'],
|
parameters: Step['parameters'],
|
||||||
|
@@ -11,24 +11,32 @@ import appConfig from '../config/app';
|
|||||||
|
|
||||||
const serverAdapter = new ExpressAdapter();
|
const serverAdapter = new ExpressAdapter();
|
||||||
|
|
||||||
|
const queues = [
|
||||||
|
new BullMQAdapter(flowQueue),
|
||||||
|
new BullMQAdapter(triggerQueue),
|
||||||
|
new BullMQAdapter(actionQueue),
|
||||||
|
new BullMQAdapter(emailQueue),
|
||||||
|
new BullMQAdapter(deleteUserQueue),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
queues.push(new BullMQAdapter(removeCancelledSubscriptionsQueue));
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldEnableBullDashboard = () => {
|
||||||
|
return (
|
||||||
|
appConfig.enableBullMQDashboard &&
|
||||||
|
appConfig.bullMQDashboardUsername &&
|
||||||
|
appConfig.bullMQDashboardPassword
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => {
|
const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => {
|
||||||
if (
|
if (!shouldEnableBullDashboard) return;
|
||||||
!appConfig.enableBullMQDashboard ||
|
|
||||||
!appConfig.bullMQDashboardUsername ||
|
|
||||||
!appConfig.bullMQDashboardPassword
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
createBullBoard({
|
createBullBoard({
|
||||||
queues: [
|
queues,
|
||||||
new BullMQAdapter(flowQueue),
|
serverAdapter,
|
||||||
new BullMQAdapter(triggerQueue),
|
|
||||||
new BullMQAdapter(actionQueue),
|
|
||||||
new BullMQAdapter(emailQueue),
|
|
||||||
new BullMQAdapter(deleteUserQueue),
|
|
||||||
new BullMQAdapter(removeCancelledSubscriptionsQueue),
|
|
||||||
],
|
|
||||||
serverAdapter: serverAdapter,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'node:path';
|
||||||
import { graphqlHTTP } from 'express-graphql';
|
import { graphqlHTTP } from 'express-graphql';
|
||||||
import { loadSchemaSync } from '@graphql-tools/load';
|
import { loadSchemaSync } from '@graphql-tools/load';
|
||||||
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
|
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
|
||||||
import { addResolversToSchema } from '@graphql-tools/schema';
|
import { addResolversToSchema } from '@graphql-tools/schema';
|
||||||
import { applyMiddleware } from 'graphql-middleware';
|
import { applyMiddleware } from 'graphql-middleware';
|
||||||
|
|
||||||
|
import appConfig from '../config/app';
|
||||||
import logger from '../helpers/logger';
|
import logger from '../helpers/logger';
|
||||||
import authentication from '../helpers/authentication';
|
import authentication from '../helpers/authentication';
|
||||||
import * as Sentry from '../helpers/sentry.ee';
|
import * as Sentry from '../helpers/sentry.ee';
|
||||||
@@ -22,7 +23,7 @@ const schemaWithResolvers = addResolversToSchema({
|
|||||||
|
|
||||||
const graphQLInstance = graphqlHTTP({
|
const graphQLInstance = graphqlHTTP({
|
||||||
schema: applyMiddleware(schemaWithResolvers, authentication),
|
schema: applyMiddleware(schemaWithResolvers, authentication),
|
||||||
graphiql: true,
|
graphiql: appConfig.isDev,
|
||||||
customFormatErrorFn: (error) => {
|
customFormatErrorFn: (error) => {
|
||||||
logger.error(error.path + ' : ' + error.message + '\n' + error.stack);
|
logger.error(error.path + ' : ' + error.message + '\n' + error.stack);
|
||||||
|
|
||||||
|
@@ -5,8 +5,10 @@ import * as Tracing from '@sentry/tracing';
|
|||||||
|
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
|
|
||||||
|
const isSentryEnabled = !!appConfig.sentryDsn;
|
||||||
|
|
||||||
export function init(app?: Express) {
|
export function init(app?: Express) {
|
||||||
if (!appConfig.isCloud) return;
|
if (!isSentryEnabled) return;
|
||||||
|
|
||||||
return Sentry.init({
|
return Sentry.init({
|
||||||
enabled: !!appConfig.sentryDsn,
|
enabled: !!appConfig.sentryDsn,
|
||||||
@@ -22,19 +24,19 @@ export function init(app?: Express) {
|
|||||||
|
|
||||||
|
|
||||||
export function attachRequestHandler(app: Express) {
|
export function attachRequestHandler(app: Express) {
|
||||||
if (!appConfig.isCloud) return;
|
if (!isSentryEnabled) return;
|
||||||
|
|
||||||
app.use(Sentry.Handlers.requestHandler());
|
app.use(Sentry.Handlers.requestHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachTracingHandler(app: Express) {
|
export function attachTracingHandler(app: Express) {
|
||||||
if (!appConfig.isCloud) return;
|
if (!isSentryEnabled) return;
|
||||||
|
|
||||||
app.use(Sentry.Handlers.tracingHandler());
|
app.use(Sentry.Handlers.tracingHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachErrorHandler(app: Express) {
|
export function attachErrorHandler(app: Express) {
|
||||||
if (!appConfig.isCloud) return;
|
if (!isSentryEnabled) return;
|
||||||
|
|
||||||
app.use(Sentry.Handlers.errorHandler({
|
app.use(Sentry.Handlers.errorHandler({
|
||||||
shouldHandleError() {
|
shouldHandleError() {
|
||||||
@@ -45,7 +47,7 @@ export function attachErrorHandler(app: Express) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function captureException(exception: any, captureContext?: CaptureContext) {
|
export function captureException(exception: any, captureContext?: CaptureContext) {
|
||||||
if (!appConfig.isCloud) return;
|
if (!isSentryEnabled) return;
|
||||||
|
|
||||||
return Sentry.captureException(exception, captureContext);
|
return Sentry.captureException(exception, captureContext);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import * as Sentry from './helpers/sentry.ee';
|
import * as Sentry from './helpers/sentry.ee';
|
||||||
|
import appConfig from './config/app';
|
||||||
|
|
||||||
Sentry.init();
|
Sentry.init();
|
||||||
|
|
||||||
@@ -9,8 +10,12 @@ import './workers/trigger';
|
|||||||
import './workers/action';
|
import './workers/action';
|
||||||
import './workers/email';
|
import './workers/email';
|
||||||
import './workers/delete-user.ee';
|
import './workers/delete-user.ee';
|
||||||
import './workers/remove-cancelled-subscriptions.ee';
|
|
||||||
import './queues/remove-cancelled-subscriptions.ee';
|
if (appConfig.isCloud) {
|
||||||
|
import('./workers/remove-cancelled-subscriptions.ee');
|
||||||
|
import('./queues/remove-cancelled-subscriptions.ee');
|
||||||
|
}
|
||||||
|
|
||||||
import telemetry from './helpers/telemetry';
|
import telemetry from './helpers/telemetry';
|
||||||
|
|
||||||
telemetry.setServiceType('worker');
|
telemetry.setServiceType('worker');
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@automatisch/cli",
|
"name": "@automatisch/cli",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"version": "oclif readme && git add README.md"
|
"version": "oclif readme && git add README.md"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automatisch/backend": "^0.5.0",
|
"@automatisch/backend": "^0.7.0",
|
||||||
"@oclif/core": "^1",
|
"@oclif/core": "^1",
|
||||||
"@oclif/plugin-help": "^5",
|
"@oclif/plugin-help": "^5",
|
||||||
"@oclif/plugin-plugins": "^2.0.1",
|
"@oclif/plugin-plugins": "^2.0.1",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@automatisch/docs",
|
"name": "@automatisch/docs",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@@ -23,7 +23,9 @@ Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment
|
|||||||
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
||||||
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
||||||
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
||||||
|
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
||||||
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
||||||
|
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
|
||||||
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
||||||
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
||||||
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
||||||
|
@@ -3,6 +3,8 @@ favicon: /favicons/google-sheets.svg
|
|||||||
items:
|
items:
|
||||||
- name: New Spreadsheets
|
- name: New Spreadsheets
|
||||||
desc: Triggers when you create a new spreadsheet
|
desc: Triggers when you create a new spreadsheet
|
||||||
|
- name: New Worksheets
|
||||||
|
desc: Triggers when you create a new worksheet in a spreadsheet
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -7,6 +7,8 @@ items:
|
|||||||
desc: Finds a user by email.
|
desc: Finds a user by email.
|
||||||
- name: Send a message to channel
|
- name: Send a message to channel
|
||||||
desc: Sends a message to a channel you specify.
|
desc: Sends a message to a channel you specify.
|
||||||
|
- name: Send a direct message
|
||||||
|
desc: Sends a direct message to a user or yourself from the Slackbot.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -74,6 +74,10 @@ REDIS_TLS=
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
::: info
|
||||||
|
You can use the `openssl rand -base64 36` command in your terminal to generate a random string for the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables.
|
||||||
|
:::
|
||||||
|
|
||||||
## Render
|
## Render
|
||||||
|
|
||||||
<a href="https://render.com/deploy?repo=https://github.com/automatisch/automatisch">
|
<a href="https://render.com/deploy?repo=https://github.com/automatisch/automatisch">
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@automatisch/e2e-tests",
|
"name": "@automatisch/e2e-tests",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@automatisch/types",
|
"name": "@automatisch/types",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "Type definitions for automatisch",
|
"description": "Type definitions for automatisch",
|
||||||
"homepage": "https://github.com/automatisch/automatisch",
|
"homepage": "https://github.com/automatisch/automatisch",
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@automatisch/web",
|
"name": "@automatisch/web",
|
||||||
"version": "0.5.0",
|
"version": "0.7.0",
|
||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.6.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@automatisch/types": "^0.5.0",
|
"@automatisch/types": "^0.7.0",
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@hookform/resolvers": "^2.8.8",
|
"@hookform/resolvers": "^2.8.8",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"notistack": "^2.0.2",
|
"notistack": "^2.0.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-hook-form": "^7.17.2",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-intl": "^5.20.12",
|
"react-intl": "^5.20.12",
|
||||||
"react-json-tree": "^0.16.2",
|
"react-json-tree": "^0.16.2",
|
||||||
"react-router-dom": "^6.0.2",
|
"react-router-dom": "^6.0.2",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete';
|
import Autocomplete, { AutocompleteProps, createFilterOptions } from '@mui/material/Autocomplete';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
|
||||||
@@ -18,6 +18,14 @@ interface ControlledAutocompleteProps
|
|||||||
const getOption = (options: readonly IFieldDropdownOption[], value: string) =>
|
const getOption = (options: readonly IFieldDropdownOption[], value: string) =>
|
||||||
options.find((option) => option.value === value) || null;
|
options.find((option) => option.value === value) || null;
|
||||||
|
|
||||||
|
// Enables filtering by value in autocomplete dropdown
|
||||||
|
const filterOptions = createFilterOptions<IFieldDropdownOption>({
|
||||||
|
stringify: ({ label, value }) => `
|
||||||
|
${label}
|
||||||
|
${value}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
function ControlledAutocomplete(
|
function ControlledAutocomplete(
|
||||||
props: ControlledAutocompleteProps
|
props: ControlledAutocompleteProps
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
@@ -27,7 +35,7 @@ function ControlledAutocomplete(
|
|||||||
required = false,
|
required = false,
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
shouldUnregister = true,
|
shouldUnregister = false,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
description,
|
description,
|
||||||
@@ -75,6 +83,7 @@ function ControlledAutocomplete(
|
|||||||
{...autocompleteProps}
|
{...autocompleteProps}
|
||||||
{...field}
|
{...field}
|
||||||
options={options}
|
options={options}
|
||||||
|
filterOptions={filterOptions}
|
||||||
value={getOption(options, field.value)}
|
value={getOption(options, field.value)}
|
||||||
onChange={(event, selectedOption, reason, details) => {
|
onChange={(event, selectedOption, reason, details) => {
|
||||||
const typedSelectedOption =
|
const typedSelectedOption =
|
||||||
|
@@ -36,7 +36,7 @@ function ExecutionDate(props: Pick<IExecution, 'createdAt'>) {
|
|||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={createdAt.toLocaleString(DateTime.DATE_MED)}>
|
<Tooltip title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}>
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
{relativeCreatedAt}
|
{relativeCreatedAt}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@@ -23,8 +23,8 @@ export default function ExecutionRow(
|
|||||||
const { execution } = props;
|
const { execution } = props;
|
||||||
const { flow } = execution;
|
const { flow } = execution;
|
||||||
|
|
||||||
const createdAt = DateTime.fromMillis(parseInt(execution.createdAt, 10));
|
const updatedAt = DateTime.fromMillis(parseInt(execution.updatedAt, 10));
|
||||||
const relativeCreatedAt = createdAt.toRelative();
|
const relativeUpdatedAt = updatedAt.toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={URLS.EXECUTION(execution.id)} data-test="execution-row">
|
<Link to={URLS.EXECUTION(execution.id)} data-test="execution-row">
|
||||||
@@ -41,8 +41,8 @@ export default function ExecutionRow(
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="caption" noWrap>
|
<Typography variant="caption" noWrap>
|
||||||
{formatMessage('execution.executedAt', {
|
{formatMessage('execution.updatedAt', {
|
||||||
datetime: relativeCreatedAt,
|
datetime: relativeUpdatedAt,
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Title>
|
</Title>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
@@ -6,6 +7,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|||||||
import Tabs from '@mui/material/Tabs';
|
import Tabs from '@mui/material/Tabs';
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { IApp, IExecutionStep, IStep } from '@automatisch/types';
|
import type { IApp, IExecutionStep, IStep } from '@automatisch/types';
|
||||||
|
|
||||||
@@ -29,6 +31,22 @@ type ExecutionStepProps = {
|
|||||||
executionStep: IExecutionStep;
|
executionStep: IExecutionStep;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function ExecutionStepDate(props: Pick<IExecutionStep, 'createdAt'>) {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
|
||||||
|
const relativeCreatedAt = createdAt.toRelative();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}>
|
||||||
|
<Typography variant="caption" gutterBottom>
|
||||||
|
{formatMessage('executionStep.executedAt', {
|
||||||
|
datetime: relativeCreatedAt
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
|
||||||
@@ -56,7 +74,7 @@ export default function ExecutionStep(
|
|||||||
return (
|
return (
|
||||||
<Wrapper elevation={1} data-test="execution-step">
|
<Wrapper elevation={1} data-test="execution-step">
|
||||||
<Header>
|
<Header>
|
||||||
<Stack direction="row" alignItems="center" gap={2}>
|
<Stack direction="row" gap={2}>
|
||||||
<AppIconWrapper>
|
<AppIconWrapper>
|
||||||
<AppIcon url={app?.iconUrl} name={app?.name} />
|
<AppIcon url={app?.iconUrl} name={app?.name} />
|
||||||
|
|
||||||
@@ -65,7 +83,7 @@ export default function ExecutionStep(
|
|||||||
</AppIconStatusIconWrapper>
|
</AppIconStatusIconWrapper>
|
||||||
</AppIconWrapper>
|
</AppIconWrapper>
|
||||||
|
|
||||||
<div>
|
<Box flex="1">
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{isTrigger
|
{isTrigger
|
||||||
? formatMessage('flowStep.triggerType')
|
? formatMessage('flowStep.triggerType')
|
||||||
@@ -75,7 +93,11 @@ export default function ExecutionStep(
|
|||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{step.position}. {app?.name}
|
{step.position}. {app?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</Box>
|
||||||
|
|
||||||
|
<Box alignSelf="flex-end">
|
||||||
|
<ExecutionStepDate createdAt={executionStep.createdAt} />
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
|
|||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
||||||
|
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
@@ -22,8 +23,26 @@ export default function ContextMenu(
|
|||||||
const { flowId, onClose, anchorEl } = props;
|
const { flowId, onClose, anchorEl } = props;
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const [deleteFlow] = useMutation(DELETE_FLOW);
|
const [deleteFlow] = useMutation(DELETE_FLOW);
|
||||||
|
const [duplicateFlow] = useMutation(
|
||||||
|
DUPLICATE_FLOW,
|
||||||
|
{
|
||||||
|
refetchQueries: ['GetFlows'],
|
||||||
|
}
|
||||||
|
);
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
const onFlowDuplicate = React.useCallback(async () => {
|
||||||
|
await duplicateFlow({
|
||||||
|
variables: { input: { id: flowId } },
|
||||||
|
});
|
||||||
|
|
||||||
|
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}, [flowId, onClose, duplicateFlow]);
|
||||||
|
|
||||||
const onFlowDelete = React.useCallback(async () => {
|
const onFlowDelete = React.useCallback(async () => {
|
||||||
await deleteFlow({
|
await deleteFlow({
|
||||||
variables: { input: { id: flowId } },
|
variables: { input: { id: flowId } },
|
||||||
@@ -42,7 +61,9 @@ export default function ContextMenu(
|
|||||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
}, [flowId, deleteFlow]);
|
|
||||||
|
onClose();
|
||||||
|
}, [flowId, onClose, deleteFlow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -55,6 +76,8 @@ export default function ContextMenu(
|
|||||||
{formatMessage('flow.view')}
|
{formatMessage('flow.view')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
@@ -19,7 +19,7 @@ function FlowStepContextMenu(
|
|||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const { stepId, onClose, anchorEl, deletable } = props;
|
const { stepId, onClose, anchorEl, deletable } = props;
|
||||||
const [deleteStep] = useMutation(DELETE_STEP, {
|
const [deleteStep] = useMutation(DELETE_STEP, {
|
||||||
refetchQueries: ['GetFlow'],
|
refetchQueries: ['GetFlow', 'GetStepWithTestExecutions'],
|
||||||
});
|
});
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
@@ -83,7 +83,7 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
name={name}
|
name={name}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
shouldUnregister={shouldUnregister ?? true}
|
shouldUnregister={shouldUnregister ?? false}
|
||||||
render={({
|
render={({
|
||||||
field: {
|
field: {
|
||||||
value,
|
value,
|
||||||
|
@@ -77,7 +77,7 @@ export default function ResetPasswordForm() {
|
|||||||
error={touchedFields.password && !!errors?.password}
|
error={touchedFields.password && !!errors?.password}
|
||||||
helperText={
|
helperText={
|
||||||
touchedFields.password && errors?.password?.message
|
touchedFields.password && errors?.password?.message
|
||||||
? formatMessage(errors?.password?.message, {
|
? formatMessage(errors?.password?.message as string, {
|
||||||
inputName: formatMessage('resetPasswordForm.passwordFieldLabel'),
|
inputName: formatMessage('resetPasswordForm.passwordFieldLabel'),
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
@@ -94,7 +94,7 @@ export default function ResetPasswordForm() {
|
|||||||
helperText={
|
helperText={
|
||||||
touchedFields.confirmPassword &&
|
touchedFields.confirmPassword &&
|
||||||
errors?.confirmPassword?.message
|
errors?.confirmPassword?.message
|
||||||
? formatMessage(errors?.confirmPassword?.message, {
|
? formatMessage(errors?.confirmPassword?.message as string, {
|
||||||
inputName: formatMessage(
|
inputName: formatMessage(
|
||||||
'resetPasswordForm.confirmPasswordFieldLabel'
|
'resetPasswordForm.confirmPasswordFieldLabel'
|
||||||
),
|
),
|
||||||
|
@@ -101,7 +101,7 @@ function SignUpForm() {
|
|||||||
error={touchedFields.fullName && !!errors?.fullName}
|
error={touchedFields.fullName && !!errors?.fullName}
|
||||||
helperText={
|
helperText={
|
||||||
touchedFields.fullName && errors?.fullName?.message
|
touchedFields.fullName && errors?.fullName?.message
|
||||||
? formatMessage(errors?.fullName?.message, {
|
? formatMessage(errors?.fullName?.message as string, {
|
||||||
inputName: formatMessage('signupForm.fullNameFieldLabel'),
|
inputName: formatMessage('signupForm.fullNameFieldLabel'),
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
@@ -118,7 +118,7 @@ function SignUpForm() {
|
|||||||
error={touchedFields.email && !!errors?.email}
|
error={touchedFields.email && !!errors?.email}
|
||||||
helperText={
|
helperText={
|
||||||
touchedFields.email && errors?.email?.message
|
touchedFields.email && errors?.email?.message
|
||||||
? formatMessage(errors?.email?.message, {
|
? formatMessage(errors?.email?.message as string, {
|
||||||
inputName: formatMessage('signupForm.emailFieldLabel'),
|
inputName: formatMessage('signupForm.emailFieldLabel'),
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
@@ -134,7 +134,7 @@ function SignUpForm() {
|
|||||||
error={touchedFields.password && !!errors?.password}
|
error={touchedFields.password && !!errors?.password}
|
||||||
helperText={
|
helperText={
|
||||||
touchedFields.password && errors?.password?.message
|
touchedFields.password && errors?.password?.message
|
||||||
? formatMessage(errors?.password?.message, {
|
? formatMessage(errors?.password?.message as string, {
|
||||||
inputName: formatMessage('signupForm.passwordFieldLabel'),
|
inputName: formatMessage('signupForm.passwordFieldLabel'),
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
@@ -151,7 +151,7 @@ function SignUpForm() {
|
|||||||
helperText={
|
helperText={
|
||||||
touchedFields.confirmPassword &&
|
touchedFields.confirmPassword &&
|
||||||
errors?.confirmPassword?.message
|
errors?.confirmPassword?.message
|
||||||
? formatMessage(errors?.confirmPassword?.message, {
|
? formatMessage(errors?.confirmPassword?.message as string, {
|
||||||
inputName: formatMessage(
|
inputName: formatMessage(
|
||||||
'signupForm.confirmPasswordFieldLabel'
|
'signupForm.confirmPasswordFieldLabel'
|
||||||
),
|
),
|
||||||
|
@@ -58,7 +58,10 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
|
|||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const [executeFlow, { data, error, loading, called, reset }] = useMutation(
|
const [executeFlow, { data, error, loading, called, reset }] = useMutation(
|
||||||
EXECUTE_FLOW,
|
EXECUTE_FLOW,
|
||||||
{ context: { autoSnackbar: false } }
|
{
|
||||||
|
refetchQueries: ['GetStepWithTestExecutions'],
|
||||||
|
context: { autoSnackbar: false }
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const response = data?.executeFlow?.data;
|
const response = data?.executeFlow?.data;
|
||||||
|
|
||||||
|
@@ -38,7 +38,7 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
required,
|
required,
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
shouldUnregister = true,
|
shouldUnregister = false,
|
||||||
clickToCopy = false,
|
clickToCopy = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
28
packages/web/src/graphql/mutations/duplicate-flow.ts
Normal file
28
packages/web/src/graphql/mutations/duplicate-flow.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const DUPLICATE_FLOW = gql`
|
||||||
|
mutation DuplicateFlow($input: DuplicateFlowInput) {
|
||||||
|
duplicateFlow(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
active
|
||||||
|
status
|
||||||
|
steps {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
key
|
||||||
|
appKey
|
||||||
|
iconUrl
|
||||||
|
webhookUrl
|
||||||
|
status
|
||||||
|
position
|
||||||
|
connection {
|
||||||
|
id
|
||||||
|
verified
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
parameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@@ -4,6 +4,7 @@ import { useFormContext } from 'react-hook-form';
|
|||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import type { UseFormReturn } from 'react-hook-form';
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
import type {
|
import type {
|
||||||
IField,
|
IField,
|
||||||
IFieldDropdownSource,
|
IFieldDropdownSource,
|
||||||
|
@@ -46,6 +46,7 @@
|
|||||||
"flow.paused": "Paused",
|
"flow.paused": "Paused",
|
||||||
"flow.draft": "Draft",
|
"flow.draft": "Draft",
|
||||||
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
|
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
|
||||||
|
"flow.successfullyDuplicated": "The flow has been successfully duplicated.",
|
||||||
"flowEditor.publish": "PUBLISH",
|
"flowEditor.publish": "PUBLISH",
|
||||||
"flowEditor.unpublish": "UNPUBLISH",
|
"flowEditor.unpublish": "UNPUBLISH",
|
||||||
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
|
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
"flow.createdAt": "created {datetime}",
|
"flow.createdAt": "created {datetime}",
|
||||||
"flow.updatedAt": "updated {datetime}",
|
"flow.updatedAt": "updated {datetime}",
|
||||||
"flow.view": "View",
|
"flow.view": "View",
|
||||||
|
"flow.duplicate": "Duplicate",
|
||||||
"flow.delete": "Delete",
|
"flow.delete": "Delete",
|
||||||
"flowStep.triggerType": "Trigger",
|
"flowStep.triggerType": "Trigger",
|
||||||
"flowStep.actionType": "Action",
|
"flowStep.actionType": "Action",
|
||||||
@@ -77,12 +79,13 @@
|
|||||||
"flowEditor.goBack": "Go back to flows",
|
"flowEditor.goBack": "Go back to flows",
|
||||||
"executions.title": "Executions",
|
"executions.title": "Executions",
|
||||||
"executions.noExecutions": "There is no execution data point to show.",
|
"executions.noExecutions": "There is no execution data point to show.",
|
||||||
"execution.executedAt": "executed {datetime}",
|
"execution.updatedAt": "updated {datetime}",
|
||||||
"execution.test": "Test run",
|
"execution.test": "Test run",
|
||||||
"execution.statusSuccess": "Success",
|
"execution.statusSuccess": "Success",
|
||||||
"execution.statusFailure": "Failure",
|
"execution.statusFailure": "Failure",
|
||||||
"execution.noDataTitle": "No data",
|
"execution.noDataTitle": "No data",
|
||||||
"execution.noDataMessage": "We successfully ran the execution, but there was no new data to process.",
|
"execution.noDataMessage": "We successfully ran the execution, but there was no new data to process.",
|
||||||
|
"executionStep.executedAt": "executed {datetime}",
|
||||||
"profileSettings.title": "My Profile",
|
"profileSettings.title": "My Profile",
|
||||||
"profileSettings.fullName": "Full name",
|
"profileSettings.fullName": "Full name",
|
||||||
"profileSettings.email": "Email",
|
"profileSettings.email": "Email",
|
||||||
|
@@ -14634,10 +14634,10 @@ react-error-overlay@^6.0.10:
|
|||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
|
||||||
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
|
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
|
||||||
|
|
||||||
react-hook-form@^7.17.2:
|
react-hook-form@^7.43.9:
|
||||||
version "7.24.1"
|
version "7.43.9"
|
||||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.24.1.tgz#00e80ca20fe3bb3b86d236d74ed1a4e5f6525228"
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d"
|
||||||
integrity sha512-UndVzKetChAsO+qkRo/6vOgaeTP60x324mHQ4iXVgHDvFjd+X/caWW0/QuAqipt8Bs7pyKH8147UQCrPTYFc2g==
|
integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==
|
||||||
|
|
||||||
react-intl@^5.20.12:
|
react-intl@^5.20.12:
|
||||||
version "5.24.3"
|
version "5.24.3"
|
||||||
|
Reference in New Issue
Block a user