Compare commits
59 Commits
v0.6.1
...
execution-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
95c1d7c02c | ||
![]() |
93a2e2151e | ||
![]() |
663a1ed9d4 | ||
![]() |
4f46c55c85 | ||
![]() |
9701c98af9 | ||
![]() |
aabf2a1c79 | ||
![]() |
29539b090e | ||
![]() |
1c80677ac3 | ||
![]() |
ad419855e9 | ||
![]() |
30b75943f3 | ||
![]() |
41a67b402d | ||
![]() |
caa104b1cc | ||
![]() |
94085f2bc8 | ||
![]() |
d39c962314 | ||
![]() |
706fb0f063 | ||
![]() |
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 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
**/node_modules/
|
||||
**/dist/
|
||||
**/logs/
|
||||
**/.devcontainer
|
||||
**/.github
|
||||
**/.vscode
|
||||
packages/docs
|
||||
packages/e2e-test
|
@@ -4,7 +4,7 @@ WORKDIR /automatisch
|
||||
|
||||
RUN \
|
||||
apk --no-cache add --virtual build-dependencies python3 build-base && \
|
||||
yarn global add @automatisch/cli@0.6.1 --network-timeout 1000000 && \
|
||||
yarn global add @automatisch/cli@0.7.1 --network-timeout 1000000 && \
|
||||
rm -rf /usr/local/share/.cache/ && \
|
||||
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
|
||||
FROM automatischio/automatisch:0.6.1
|
||||
FROM automatischio/automatisch:0.7.1
|
||||
WORKDIR /automatisch
|
||||
|
||||
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/*"
|
||||
],
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/backend",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"prebuild": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/web": "^0.6.1",
|
||||
"@automatisch/web": "^0.7.1",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
"@graphql-tools/load": "^7.5.2",
|
||||
@@ -100,7 +100,7 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automatisch/types": "^0.6.1",
|
||||
"@automatisch/types": "^0.7.1",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bull": "^3.15.8",
|
||||
"@types/cors": "^2.8.12",
|
||||
|
@@ -10,7 +10,7 @@ type TGroupItem = {
|
||||
type TGroup = Record<'and', TGroupItem[]>;
|
||||
|
||||
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 isLessThan = (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 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 TOperators = {
|
||||
@@ -66,7 +96,7 @@ export default defineAction({
|
||||
return groups;
|
||||
}, []);
|
||||
|
||||
if (matchingGroups.length === 0) {
|
||||
if (!shouldContinue(orGroups)) {
|
||||
$.execution.exit();
|
||||
}
|
||||
|
||||
|
@@ -11,13 +11,20 @@ export default {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const params = {
|
||||
const params: Record<string, unknown> = {
|
||||
q: `mimeType='application/vnd.google-apps.folder'`,
|
||||
orderBy: 'createdTime desc',
|
||||
pageToken: undefined as unknown as string,
|
||||
pageSize: 1000,
|
||||
driveId: $.step.parameters.driveId,
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if ($.step.parameters.driveId) {
|
||||
params.includeItemsFromAllDrives = true;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(
|
||||
`https://www.googleapis.com/drive/v3/files`,
|
||||
|
@@ -32,6 +32,7 @@ export default defineTrigger({
|
||||
key: 'folderId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
dependsOn: ['parameters.driveId'],
|
||||
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.',
|
||||
variables: false,
|
||||
@@ -43,6 +44,10 @@ export default defineTrigger({
|
||||
name: 'key',
|
||||
value: 'listFolders',
|
||||
},
|
||||
{
|
||||
name: 'parameters.driveId',
|
||||
value: '{parameters.driveId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@@ -7,15 +7,21 @@ const newFilesInFolder = async ($: IGlobalVariable) => {
|
||||
} else {
|
||||
q += ` and parents in 'root'`;
|
||||
}
|
||||
const params = {
|
||||
const params: Record<string, unknown> = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q,
|
||||
driveId: $.step.parameters.driveId,
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if ($.step.parameters.driveId) {
|
||||
params.includeItemsFromAllDrives = true;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/files`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const newFiles = async ($: IGlobalVariable) => {
|
||||
const params = {
|
||||
const params: Record<string, unknown> = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q: `mimeType!='application/vnd.google-apps.folder'`,
|
||||
driveId: $.step.parameters.driveId,
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if ($.step.parameters.driveId) {
|
||||
params.includeItemsFromAllDrives = true;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get('/v3/files', { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
@@ -32,6 +32,7 @@ export default defineTrigger({
|
||||
key: 'folderId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
dependsOn: ['parameters.driveId'],
|
||||
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.',
|
||||
variables: false,
|
||||
@@ -43,6 +44,10 @@ export default defineTrigger({
|
||||
name: 'key',
|
||||
value: 'listFolders',
|
||||
},
|
||||
{
|
||||
name: 'parameters.driveId',
|
||||
value: '{parameters.driveId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@@ -8,15 +8,21 @@ const newFolders = async ($: IGlobalVariable) => {
|
||||
q += ` and parents in 'root'`;
|
||||
}
|
||||
|
||||
const params = {
|
||||
const params: Record<string, unknown> = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q,
|
||||
driveId: $.step.parameters.driveId,
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if ($.step.parameters.driveId) {
|
||||
params.includeItemsFromAllDrives = true;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/files`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
@@ -32,6 +32,7 @@ export default defineTrigger({
|
||||
key: 'folderId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
dependsOn: ['parameters.driveId'],
|
||||
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.',
|
||||
source: {
|
||||
@@ -42,6 +43,10 @@ export default defineTrigger({
|
||||
name: 'key',
|
||||
value: 'listFolders',
|
||||
},
|
||||
{
|
||||
name: 'parameters.driveId',
|
||||
value: '{parameters.driveId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@@ -12,15 +12,21 @@ const updatedFiles = async ($: IGlobalVariable) => {
|
||||
q += ` and parents in 'root'`;
|
||||
}
|
||||
|
||||
const params = {
|
||||
const params: Record<string, unknown> = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'modifiedTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q,
|
||||
driveId: $.step.parameters.driveId,
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if ($.step.parameters.driveId) {
|
||||
params.includeItemsFromAllDrives = true;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/files`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import listDrives from './list-drives';
|
||||
import listSpreadsheets from './list-spreadsheets';
|
||||
|
||||
export default [listDrives];
|
||||
export default [listDrives, listSpreadsheets];
|
||||
|
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
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 newWorksheets from './new-worksheets';
|
||||
|
||||
export default [newSpreadsheets];
|
||||
export default [newSpreadsheets, newWorksheets];
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const newSpreadsheets = async ($: IGlobalVariable) => {
|
||||
const params = {
|
||||
const params: Record<string, unknown> = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
q: `mimeType='application/vnd.google-apps.spreadsheet'`,
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
driveId: $.step.parameters.driveId,
|
||||
supportsAllDrives: true,
|
||||
};
|
||||
|
||||
if ($.step.parameters.driveId) {
|
||||
params.includeItemsFromAllDrives = true;
|
||||
params.corpora = 'drive';
|
||||
}
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(
|
||||
'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';
|
||||
|
||||
type TMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
@@ -9,6 +10,23 @@ type 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({
|
||||
name: 'Custom Request',
|
||||
key: 'customRequest',
|
||||
@@ -81,29 +99,51 @@ export default defineAction({
|
||||
const data = $.step.parameters.data as string;
|
||||
const url = $.step.parameters.url as string;
|
||||
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) {
|
||||
throw new Error(
|
||||
`Response is too large. Maximum size is 25MB. Actual size is ${metadataResponse.headers['content-length']}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
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,
|
||||
method,
|
||||
data,
|
||||
headers: headersObject,
|
||||
});
|
||||
};
|
||||
|
||||
if (!isPossiblyTextBased(contentType)) {
|
||||
requestData.responseType = 'arraybuffer';
|
||||
}
|
||||
|
||||
const response = await $.http.request(requestData);
|
||||
|
||||
throwIfFileSizeExceedsLimit(response.headers['content-length']);
|
||||
|
||||
let responseData = response.data;
|
||||
|
||||
if (typeof response.data === 'string') {
|
||||
responseData = response.data.replaceAll('\u0000', '');
|
||||
if (!isPossiblyTextBased(contentType)) {
|
||||
responseData = Buffer.from(responseData as string).toString('base64');
|
||||
}
|
||||
|
||||
$.setActionItem({ raw: { data: responseData } });
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import checkModeration from './check-moderation';
|
||||
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 findUserByEmail from './find-user-by-email';
|
||||
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 listUsers from './list-users';
|
||||
|
||||
export default [listChannels];
|
||||
export default [listChannels, listUsers];
|
||||
|
@@ -36,7 +36,7 @@ export default {
|
||||
do {
|
||||
const response: TResponse = await $.http.get('/conversations.list', {
|
||||
params: {
|
||||
types: 'public_channel,private_channel,im',
|
||||
types: 'public_channel,private_channel',
|
||||
cursor: nextCursor,
|
||||
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';
|
||||
|
||||
export default defineAction({
|
||||
@@ -8,11 +9,21 @@ export default defineAction({
|
||||
{
|
||||
label: 'From Number',
|
||||
key: 'fromNumber',
|
||||
type: 'string' as const,
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description:
|
||||
'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',
|
||||
@@ -35,14 +46,20 @@ export default defineAction({
|
||||
|
||||
async run($) {
|
||||
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 toNumber = '+' + ($.step.parameters.toNumber as string).trim();
|
||||
const fromNumber = ($.step.parameters.fromNumber 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(
|
||||
requestPath,
|
||||
`Body=${messageBody}&From=${fromNumber}&To=${toNumber}`
|
||||
payload,
|
||||
);
|
||||
|
||||
$.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 triggers from './triggers';
|
||||
import actions from './actions';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Twilio',
|
||||
@@ -17,4 +18,5 @@ export default defineApp({
|
||||
auth,
|
||||
triggers,
|
||||
actions,
|
||||
dynamicData,
|
||||
});
|
||||
|
@@ -1,23 +1,73 @@
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import fetchMessages from './fetch-messages';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'Receive SMS',
|
||||
key: 'receiveSms',
|
||||
pollInterval: 15,
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a new SMS is received.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'To Number',
|
||||
key: 'toNumber',
|
||||
type: 'string',
|
||||
key: 'phoneNumberSid',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description:
|
||||
'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($);
|
||||
|
||||
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
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@@ -9,7 +9,9 @@ type AppConfig = {
|
||||
webAppUrl: string;
|
||||
webhookUrl: string;
|
||||
appEnv: string;
|
||||
logLevel: string;
|
||||
isDev: boolean;
|
||||
isProd: boolean;
|
||||
postgresDatabase: string;
|
||||
postgresSchema: string;
|
||||
postgresPort: number;
|
||||
@@ -79,7 +81,9 @@ const appConfig: AppConfig = {
|
||||
protocol,
|
||||
port,
|
||||
appEnv: appEnv,
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
isDev: appEnv === 'development',
|
||||
isProd: appEnv === 'production',
|
||||
version: process.env.npm_package_version,
|
||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import Crypto from 'node:crypto';
|
||||
import { Response } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { IRequest, ITriggerItem } from '@automatisch/types';
|
||||
|
||||
import logger from '../../helpers/logger';
|
||||
import Flow from '../../models/flow';
|
||||
import { processTrigger } from '../../services/trigger';
|
||||
import actionQueue from '../../queues/action';
|
||||
@@ -13,8 +14,19 @@ import {
|
||||
} from '../../helpers/remove-job-configuration';
|
||||
|
||||
export default async (request: IRequest, response: Response) => {
|
||||
const flowId = request.params.flowId;
|
||||
|
||||
// in case it's our built-in generic webhook trigger
|
||||
let computedRequestPayload = {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
query: request.query,
|
||||
};
|
||||
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
|
||||
logger.debug(computedRequestPayload);
|
||||
|
||||
const flow = await Flow.query()
|
||||
.findById(request.params.flowId)
|
||||
.findById(flowId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const user = await flow.$relatedQuery('user');
|
||||
@@ -56,36 +68,26 @@ export default async (request: IRequest, response: Response) => {
|
||||
}
|
||||
|
||||
// in case trigger type is 'webhook'
|
||||
let payload = request.body;
|
||||
let rawInternalId: string | Buffer = request.rawBody;
|
||||
|
||||
// in case it's our built-in generic webhook trigger
|
||||
if (isWebhookApp) {
|
||||
payload = {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
query: request.query,
|
||||
};
|
||||
|
||||
rawInternalId = JSON.stringify(payload);
|
||||
if (!isWebhookApp) {
|
||||
computedRequestPayload = request.body;
|
||||
}
|
||||
|
||||
const triggerItem: ITriggerItem = {
|
||||
raw: payload,
|
||||
raw: computedRequestPayload,
|
||||
meta: {
|
||||
internalId: await bcrypt.hash(rawInternalId, 1),
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
const { flowId, executionId } = await processTrigger({
|
||||
flowId: flow.id,
|
||||
const { executionId } = await processTrigger({
|
||||
flowId,
|
||||
stepId: triggerStep.id,
|
||||
triggerItem,
|
||||
testRun,
|
||||
});
|
||||
|
||||
if (testRun) {
|
||||
return response.sendStatus(200);
|
||||
return response.sendStatus(204);
|
||||
}
|
||||
|
||||
const nextStep = await triggerStep.getNextStep();
|
||||
@@ -104,5 +106,5 @@ export default async (request: IRequest, response: Response) => {
|
||||
|
||||
await actionQueue.add(jobName, jobPayload, jobOptions);
|
||||
|
||||
return response.sendStatus(200);
|
||||
return response.sendStatus(204);
|
||||
};
|
||||
|
@@ -9,7 +9,7 @@ export default class BaseError extends Error {
|
||||
try {
|
||||
computedError = JSON.parse(error as string);
|
||||
} catch {
|
||||
computedError = typeof error === 'string' ? { error } : error;
|
||||
computedError = (typeof error === 'string' || Array.isArray(error)) ? { error } : error;
|
||||
}
|
||||
|
||||
let computedMessage: string;
|
||||
|
@@ -9,6 +9,7 @@ import updateFlow from './mutations/update-flow';
|
||||
import updateFlowStatus from './mutations/update-flow-status';
|
||||
import executeFlow from './mutations/execute-flow';
|
||||
import deleteFlow from './mutations/delete-flow';
|
||||
import duplicateFlow from './mutations/duplicate-flow';
|
||||
import createStep from './mutations/create-step';
|
||||
import updateStep from './mutations/update-step';
|
||||
import deleteStep from './mutations/delete-step';
|
||||
@@ -31,6 +32,7 @@ const mutationResolvers = {
|
||||
updateFlowStatus,
|
||||
executeFlow,
|
||||
deleteFlow,
|
||||
duplicateFlow,
|
||||
createStep,
|
||||
updateStep,
|
||||
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,
|
||||
},
|
||||
})
|
||||
.withSoftDeleted()
|
||||
.findById(params.executionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
|
@@ -32,7 +32,7 @@ const getExecutions = async (
|
||||
},
|
||||
})
|
||||
.groupBy('executions.id')
|
||||
.orderBy('created_at', 'desc');
|
||||
.orderBy('updated_at', 'desc');
|
||||
|
||||
return paginate(executions, params.limit, params.offset);
|
||||
};
|
||||
|
@@ -56,6 +56,7 @@ type Mutation {
|
||||
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
||||
executeFlow(input: ExecuteFlowInput): executeFlowType
|
||||
deleteFlow(input: DeleteFlowInput): Boolean
|
||||
duplicateFlow(input: DuplicateFlowInput): Flow
|
||||
createStep(input: CreateStepInput): Step
|
||||
updateStep(input: UpdateStepInput): Step
|
||||
deleteStep(input: DeleteStepInput): Step
|
||||
@@ -324,6 +325,10 @@ input DeleteFlowInput {
|
||||
id: String!
|
||||
}
|
||||
|
||||
input DuplicateFlowInput {
|
||||
id: String!
|
||||
}
|
||||
|
||||
input CreateStepInput {
|
||||
id: String
|
||||
previousStepId: String
|
||||
|
@@ -1,13 +1,27 @@
|
||||
const plans = [
|
||||
import appConfig from '../../config/app';
|
||||
|
||||
const testPlans = [
|
||||
{
|
||||
name: '10k - monthly',
|
||||
limit: '10,000',
|
||||
quota: 10000,
|
||||
price: '€20',
|
||||
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) {
|
||||
return plans.find((plan) => plan.productId === id);
|
||||
}
|
||||
|
@@ -2,7 +2,8 @@ import Step from '../models/step';
|
||||
import ExecutionStep from '../models/execution-step';
|
||||
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(
|
||||
parameters: Step['parameters'],
|
||||
|
@@ -11,24 +11,32 @@ import appConfig from '../config/app';
|
||||
|
||||
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) => {
|
||||
if (
|
||||
!appConfig.enableBullMQDashboard ||
|
||||
!appConfig.bullMQDashboardUsername ||
|
||||
!appConfig.bullMQDashboardPassword
|
||||
)
|
||||
return;
|
||||
if (!shouldEnableBullDashboard) return;
|
||||
|
||||
createBullBoard({
|
||||
queues: [
|
||||
new BullMQAdapter(flowQueue),
|
||||
new BullMQAdapter(triggerQueue),
|
||||
new BullMQAdapter(actionQueue),
|
||||
new BullMQAdapter(emailQueue),
|
||||
new BullMQAdapter(deleteUserQueue),
|
||||
new BullMQAdapter(removeCancelledSubscriptionsQueue),
|
||||
],
|
||||
serverAdapter: serverAdapter,
|
||||
queues,
|
||||
serverAdapter,
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { join } from 'path';
|
||||
import { join } from 'node:path';
|
||||
import { graphqlHTTP } from 'express-graphql';
|
||||
import { loadSchemaSync } from '@graphql-tools/load';
|
||||
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
|
||||
import { addResolversToSchema } from '@graphql-tools/schema';
|
||||
import { applyMiddleware } from 'graphql-middleware';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import logger from '../helpers/logger';
|
||||
import authentication from '../helpers/authentication';
|
||||
import * as Sentry from '../helpers/sentry.ee';
|
||||
@@ -22,7 +23,7 @@ const schemaWithResolvers = addResolversToSchema({
|
||||
|
||||
const graphQLInstance = graphqlHTTP({
|
||||
schema: applyMiddleware(schemaWithResolvers, authentication),
|
||||
graphiql: true,
|
||||
graphiql: appConfig.isDev,
|
||||
customFormatErrorFn: (error) => {
|
||||
logger.error(error.path + ' : ' + error.message + '\n' + error.stack);
|
||||
|
||||
|
@@ -9,10 +9,6 @@ const levels = {
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
const level = () => {
|
||||
return appConfig.appEnv === 'development' ? 'debug' : 'info';
|
||||
};
|
||||
|
||||
const colors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
@@ -41,7 +37,7 @@ const transports = [
|
||||
];
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: level(),
|
||||
level: appConfig.logLevel,
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
|
@@ -5,8 +5,10 @@ import * as Tracing from '@sentry/tracing';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
|
||||
const isSentryEnabled = !!appConfig.sentryDsn;
|
||||
|
||||
export function init(app?: Express) {
|
||||
if (!appConfig.isCloud) return;
|
||||
if (!isSentryEnabled) return;
|
||||
|
||||
return Sentry.init({
|
||||
enabled: !!appConfig.sentryDsn,
|
||||
@@ -22,19 +24,19 @@ export function init(app?: Express) {
|
||||
|
||||
|
||||
export function attachRequestHandler(app: Express) {
|
||||
if (!appConfig.isCloud) return;
|
||||
if (!isSentryEnabled) return;
|
||||
|
||||
app.use(Sentry.Handlers.requestHandler());
|
||||
}
|
||||
|
||||
export function attachTracingHandler(app: Express) {
|
||||
if (!appConfig.isCloud) return;
|
||||
if (!isSentryEnabled) return;
|
||||
|
||||
app.use(Sentry.Handlers.tracingHandler());
|
||||
}
|
||||
|
||||
export function attachErrorHandler(app: Express) {
|
||||
if (!appConfig.isCloud) return;
|
||||
if (!isSentryEnabled) return;
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler({
|
||||
shouldHandleError() {
|
||||
@@ -45,7 +47,7 @@ export function attachErrorHandler(app: Express) {
|
||||
}
|
||||
|
||||
export function captureException(exception: any, captureContext?: CaptureContext) {
|
||||
if (!appConfig.isCloud) return;
|
||||
if (!isSentryEnabled) return;
|
||||
|
||||
return Sentry.captureException(exception, captureContext);
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import Execution from '../models/execution';
|
||||
import ExecutionStep from '../models/execution-step';
|
||||
import computeParameters from '../helpers/compute-parameters';
|
||||
import globalVariable from '../helpers/global-variable';
|
||||
import { logger } from '../helpers/logger';
|
||||
import HttpError from '../errors/http';
|
||||
import EarlyExitError from '../errors/early-exit';
|
||||
import AlreadyProcessedError from '../errors/already-processed';
|
||||
@@ -53,6 +54,8 @@ export const processAction = async (options: ProcessActionOptions) => {
|
||||
const shouldNotConsiderAsError = shouldEarlyExit || shouldNotProcess;
|
||||
|
||||
if (!shouldNotConsiderAsError) {
|
||||
logger.error(error);
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
$.actionOutput.error = error.details;
|
||||
} else {
|
||||
|
@@ -3,6 +3,7 @@ import globalVariable from '../helpers/global-variable';
|
||||
import EarlyExitError from '../errors/early-exit';
|
||||
import AlreadyProcessedError from '../errors/already-processed';
|
||||
import HttpError from '../errors/http';
|
||||
import { logger } from '../helpers/logger';
|
||||
|
||||
type ProcessFlowOptions = {
|
||||
flowId: string;
|
||||
@@ -35,6 +36,8 @@ export const processFlow = async (options: ProcessFlowOptions) => {
|
||||
const shouldNotConsiderAsError = shouldEarlyExit || shouldNotProcess;
|
||||
|
||||
if (!shouldNotConsiderAsError) {
|
||||
logger.error(error);
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
$.triggerOutput.error = error.details;
|
||||
} else {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as Sentry from './helpers/sentry.ee';
|
||||
import appConfig from './config/app';
|
||||
|
||||
Sentry.init();
|
||||
|
||||
@@ -9,8 +10,12 @@ import './workers/trigger';
|
||||
import './workers/action';
|
||||
import './workers/email';
|
||||
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';
|
||||
|
||||
telemetry.setServiceType('worker');
|
||||
|
@@ -26,6 +26,8 @@ export const worker = new Worker(
|
||||
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
||||
await processAction(job.data as JobData);
|
||||
|
||||
if (executionStep.isFailed) return;
|
||||
|
||||
const step = await Step.query().findById(stepId).throwIfNotFound();
|
||||
const nextStep = await step.getNextStep();
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/cli",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"contributors": [
|
||||
@@ -33,7 +33,7 @@
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/backend": "^0.6.1",
|
||||
"@automatisch/backend": "^0.7.1",
|
||||
"@oclif/core": "^1",
|
||||
"@oclif/plugin-help": "^5",
|
||||
"@oclif/plugin-plugins": "^2.0.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/docs",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"private": true,
|
||||
|
@@ -68,6 +68,15 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/dropbox/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Filter',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Actions', link: '/apps/filter/actions' },
|
||||
{ text: 'Connection', link: '/apps/filter/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Flickr',
|
||||
collapsible: true,
|
||||
|
@@ -14,29 +14,31 @@ The default values for some environment variables might be different in our deve
|
||||
Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work.
|
||||
:::
|
||||
|
||||
| Variable Name | Type | Default Value | Description |
|
||||
| --------------------------- | ------- | ------------------ | ---------------------------------------------------- |
|
||||
| `HOST` | string | `localhost` | HTTP Host |
|
||||
| `PROTOCOL` | string | `http` | HTTP Protocol |
|
||||
| `PORT` | string | `3000` | HTTP Port |
|
||||
| `APP_ENV` | string | `production` | Automatisch Environment |
|
||||
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
||||
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
||||
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
||||
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
||||
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
||||
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
||||
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
||||
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
||||
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
|
||||
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
|
||||
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
|
||||
| `REDIS_HOST` | string | `redis` | Redis Host |
|
||||
| `REDIS_PORT` | number | `6379` | Redis Port |
|
||||
| `REDIS_USERNAME` | string | | Redis Username |
|
||||
| `REDIS_PASSWORD` | string | | Redis Password |
|
||||
| `REDIS_TLS` | boolean | `false` | Redis TLS |
|
||||
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
|
||||
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
|
||||
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
|
||||
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
|
||||
| Variable Name | Type | Default Value | Description |
|
||||
| --------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `HOST` | string | `localhost` | HTTP Host |
|
||||
| `PROTOCOL` | string | `http` | HTTP Protocol |
|
||||
| `PORT` | string | `3000` | HTTP Port |
|
||||
| `APP_ENV` | string | `production` | Automatisch Environment |
|
||||
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
||||
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
||||
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
|
||||
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
||||
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
||||
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
||||
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
|
||||
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
||||
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
||||
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
||||
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
|
||||
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
|
||||
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
|
||||
| `REDIS_HOST` | string | `redis` | Redis Host |
|
||||
| `REDIS_PORT` | number | `6379` | Redis Port |
|
||||
| `REDIS_USERNAME` | string | | Redis Username |
|
||||
| `REDIS_PASSWORD` | string | | Redis Password |
|
||||
| `REDIS_TLS` | boolean | `false` | Redis TLS |
|
||||
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
|
||||
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
|
||||
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
|
||||
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
|
||||
|
12
packages/docs/pages/apps/filter/actions.md
Normal file
12
packages/docs/pages/apps/filter/actions.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
favicon: /favicons/filter.svg
|
||||
items:
|
||||
- name: Continue if conditions match
|
||||
desc: Let the execution continue if the conditions match.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
12
packages/docs/pages/apps/filter/connection.md
Normal file
12
packages/docs/pages/apps/filter/connection.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Filter
|
||||
|
||||
Filter is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Filter app. It can be used as an action and it filters the flow based on the given conditions. Available conditions are:
|
||||
|
||||
- is equal
|
||||
- is not equal
|
||||
- is greater than
|
||||
- is less than
|
||||
- is greater than or equal
|
||||
- is less than or equal
|
||||
- contains
|
||||
- does not contain
|
@@ -3,6 +3,8 @@ favicon: /favicons/google-sheets.svg
|
||||
items:
|
||||
- name: New Spreadsheets
|
||||
desc: Triggers when you create a new spreadsheet
|
||||
- name: New Worksheets
|
||||
desc: Triggers when you create a new worksheet in a spreadsheet
|
||||
---
|
||||
|
||||
<script setup>
|
||||
|
@@ -7,6 +7,8 @@ items:
|
||||
desc: Finds a user by email.
|
||||
- name: Send a message to channel
|
||||
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>
|
||||
|
@@ -10,10 +10,12 @@ Following integrations are currently supported by Automatisch.
|
||||
- [Delay](/apps/delay/actions)
|
||||
- [Discord](/apps/discord/actions)
|
||||
- [Dropbox](/apps/dropbox/actions)
|
||||
- [Filter](/apps/filter/actions)
|
||||
- [Flickr](/apps/flickr/triggers)
|
||||
- [Github](/apps/github/triggers)
|
||||
- [Google Drive](/apps/google-drive/triggers)
|
||||
- [Google Forms](/apps/google-forms/triggers)
|
||||
- [Google Sheets](/apps/google-sheets/triggers)
|
||||
- [HTTP Request](/apps/http-request/actions)
|
||||
- [Ntfy](/apps/ntfy/actions)
|
||||
- [OpenAI](/apps/openai/actions)
|
||||
|
8
packages/docs/pages/public/favicons/filter.svg
Normal file
8
packages/docs/pages/public/favicons/filter.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Shape" fill="#000000" transform="translate(42.666667, 85.333333)">
|
||||
<path d="M3.55271368e-14,1.42108547e-14 L191.565013,234.666667 L192,234.666667 L192,384 L234.666667,384 L234.666667,234.666667 L426.666667,1.42108547e-14 L3.55271368e-14,1.42108547e-14 Z M214.448,192 L211.81248,192 L89.9076267,42.6666667 L336.630187,42.6666667 L214.448,192 Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 629 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/e2e-tests",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"private": true,
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/types",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "Type definitions for automatisch",
|
||||
"homepage": "https://github.com/automatisch/automatisch",
|
||||
|
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@automatisch/web",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.1",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@automatisch/types": "^0.6.1",
|
||||
"@automatisch/types": "^0.7.1",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@hookform/resolvers": "^2.8.8",
|
||||
"@mui/icons-material": "^5.11.9",
|
||||
"@mui/lab": "^5.0.0-alpha.120",
|
||||
"@mui/material": "^5.11.10",
|
||||
"@mui/x-date-pickers": "^6.5.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
@@ -30,7 +31,7 @@
|
||||
"notistack": "^2.0.2",
|
||||
"react": "^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-json-tree": "^0.16.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
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 type { IFieldDropdownOption } from '@automatisch/types';
|
||||
|
||||
@@ -18,6 +18,14 @@ interface ControlledAutocompleteProps
|
||||
const getOption = (options: readonly IFieldDropdownOption[], value: string) =>
|
||||
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(
|
||||
props: ControlledAutocompleteProps
|
||||
): React.ReactElement {
|
||||
@@ -27,7 +35,7 @@ function ControlledAutocomplete(
|
||||
required = false,
|
||||
name,
|
||||
defaultValue,
|
||||
shouldUnregister = true,
|
||||
shouldUnregister = false,
|
||||
onBlur,
|
||||
onChange,
|
||||
description,
|
||||
@@ -75,6 +83,7 @@ function ControlledAutocomplete(
|
||||
{...autocompleteProps}
|
||||
{...field}
|
||||
options={options}
|
||||
filterOptions={filterOptions}
|
||||
value={getOption(options, field.value)}
|
||||
onChange={(event, selectedOption, reason, details) => {
|
||||
const typedSelectedOption =
|
||||
|
@@ -36,7 +36,7 @@ function ExecutionDate(props: Pick<IExecution, 'createdAt'>) {
|
||||
const relativeCreatedAt = createdAt.toRelative();
|
||||
|
||||
return (
|
||||
<Tooltip title={createdAt.toLocaleString(DateTime.DATE_MED)}>
|
||||
<Tooltip title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{relativeCreatedAt}
|
||||
</Typography>
|
||||
|
@@ -23,8 +23,8 @@ export default function ExecutionRow(
|
||||
const { execution } = props;
|
||||
const { flow } = execution;
|
||||
|
||||
const createdAt = DateTime.fromMillis(parseInt(execution.createdAt, 10));
|
||||
const relativeCreatedAt = createdAt.toRelative();
|
||||
const updatedAt = DateTime.fromMillis(parseInt(execution.updatedAt, 10));
|
||||
const relativeUpdatedAt = updatedAt.toRelative();
|
||||
|
||||
return (
|
||||
<Link to={URLS.EXECUTION(execution.id)} data-test="execution-row">
|
||||
@@ -41,8 +41,8 @@ export default function ExecutionRow(
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" noWrap>
|
||||
{formatMessage('execution.executedAt', {
|
||||
datetime: relativeCreatedAt,
|
||||
{formatMessage('execution.updatedAt', {
|
||||
datetime: relativeUpdatedAt,
|
||||
})}
|
||||
</Typography>
|
||||
</Title>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import Stack from '@mui/material/Stack';
|
||||
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 Tab from '@mui/material/Tab';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Box from '@mui/material/Box';
|
||||
import type { IApp, IExecutionStep, IStep } from '@automatisch/types';
|
||||
|
||||
@@ -29,6 +31,22 @@ type ExecutionStepProps = {
|
||||
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 errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
@@ -56,7 +74,7 @@ export default function ExecutionStep(
|
||||
return (
|
||||
<Wrapper elevation={1} data-test="execution-step">
|
||||
<Header>
|
||||
<Stack direction="row" alignItems="center" gap={2}>
|
||||
<Stack direction="row" gap={2}>
|
||||
<AppIconWrapper>
|
||||
<AppIcon url={app?.iconUrl} name={app?.name} />
|
||||
|
||||
@@ -65,7 +83,7 @@ export default function ExecutionStep(
|
||||
</AppIconStatusIconWrapper>
|
||||
</AppIconWrapper>
|
||||
|
||||
<div>
|
||||
<Box flex="1">
|
||||
<Typography variant="caption">
|
||||
{isTrigger
|
||||
? formatMessage('flowStep.triggerType')
|
||||
@@ -75,7 +93,11 @@ export default function ExecutionStep(
|
||||
<Typography variant="body2">
|
||||
{step.position}. {app?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box alignSelf="flex-end">
|
||||
<ExecutionStepDate createdAt={executionStep.createdAt} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Header>
|
||||
|
||||
|
52
packages/web/src/components/ExecutionsFilter/index.tsx
Normal file
52
packages/web/src/components/ExecutionsFilter/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
export default function ExecutionFilters() {
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DemoContainer components={['DatePicker']}>
|
||||
<DatePicker label="From" />
|
||||
<DatePicker label="Until" />
|
||||
<TextField
|
||||
label="By data in/out"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
xs="auto"
|
||||
sm="auto"
|
||||
alignItems="center"
|
||||
order={{ xs: 1, sm: 2 }}
|
||||
>
|
||||
<ConditionalIconButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
icon={<SearchIcon />}
|
||||
data-test="create-flow-button"
|
||||
>
|
||||
Search
|
||||
</ConditionalIconButton>
|
||||
</Grid>
|
||||
</DemoContainer>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
}
|
@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
||||
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
@@ -22,8 +23,26 @@ export default function ContextMenu(
|
||||
const { flowId, onClose, anchorEl } = props;
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [deleteFlow] = useMutation(DELETE_FLOW);
|
||||
const [duplicateFlow] = useMutation(
|
||||
DUPLICATE_FLOW,
|
||||
{
|
||||
refetchQueries: ['GetFlows'],
|
||||
}
|
||||
);
|
||||
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 () => {
|
||||
await deleteFlow({
|
||||
variables: { input: { id: flowId } },
|
||||
@@ -42,7 +61,9 @@ export default function ContextMenu(
|
||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
});
|
||||
}, [flowId, deleteFlow]);
|
||||
|
||||
onClose();
|
||||
}, [flowId, onClose, deleteFlow]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -55,6 +76,8 @@ export default function ContextMenu(
|
||||
{formatMessage('flow.view')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
|
||||
|
||||
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
|
@@ -19,7 +19,7 @@ function FlowStepContextMenu(
|
||||
): React.ReactElement {
|
||||
const { stepId, onClose, anchorEl, deletable } = props;
|
||||
const [deleteStep] = useMutation(DELETE_STEP, {
|
||||
refetchQueries: ['GetFlow'],
|
||||
refetchQueries: ['GetFlow', 'GetStepWithTestExecutions'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
|
@@ -83,7 +83,7 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
shouldUnregister={shouldUnregister ?? true}
|
||||
shouldUnregister={shouldUnregister ?? false}
|
||||
render={({
|
||||
field: {
|
||||
value,
|
||||
|
@@ -77,7 +77,7 @@ export default function ResetPasswordForm() {
|
||||
error={touchedFields.password && !!errors?.password}
|
||||
helperText={
|
||||
touchedFields.password && errors?.password?.message
|
||||
? formatMessage(errors?.password?.message, {
|
||||
? formatMessage(errors?.password?.message as string, {
|
||||
inputName: formatMessage('resetPasswordForm.passwordFieldLabel'),
|
||||
})
|
||||
: ''
|
||||
@@ -94,7 +94,7 @@ export default function ResetPasswordForm() {
|
||||
helperText={
|
||||
touchedFields.confirmPassword &&
|
||||
errors?.confirmPassword?.message
|
||||
? formatMessage(errors?.confirmPassword?.message, {
|
||||
? formatMessage(errors?.confirmPassword?.message as string, {
|
||||
inputName: formatMessage(
|
||||
'resetPasswordForm.confirmPasswordFieldLabel'
|
||||
),
|
||||
|
@@ -101,7 +101,7 @@ function SignUpForm() {
|
||||
error={touchedFields.fullName && !!errors?.fullName}
|
||||
helperText={
|
||||
touchedFields.fullName && errors?.fullName?.message
|
||||
? formatMessage(errors?.fullName?.message, {
|
||||
? formatMessage(errors?.fullName?.message as string, {
|
||||
inputName: formatMessage('signupForm.fullNameFieldLabel'),
|
||||
})
|
||||
: ''
|
||||
@@ -118,7 +118,7 @@ function SignUpForm() {
|
||||
error={touchedFields.email && !!errors?.email}
|
||||
helperText={
|
||||
touchedFields.email && errors?.email?.message
|
||||
? formatMessage(errors?.email?.message, {
|
||||
? formatMessage(errors?.email?.message as string, {
|
||||
inputName: formatMessage('signupForm.emailFieldLabel'),
|
||||
})
|
||||
: ''
|
||||
@@ -134,7 +134,7 @@ function SignUpForm() {
|
||||
error={touchedFields.password && !!errors?.password}
|
||||
helperText={
|
||||
touchedFields.password && errors?.password?.message
|
||||
? formatMessage(errors?.password?.message, {
|
||||
? formatMessage(errors?.password?.message as string, {
|
||||
inputName: formatMessage('signupForm.passwordFieldLabel'),
|
||||
})
|
||||
: ''
|
||||
@@ -151,7 +151,7 @@ function SignUpForm() {
|
||||
helperText={
|
||||
touchedFields.confirmPassword &&
|
||||
errors?.confirmPassword?.message
|
||||
? formatMessage(errors?.confirmPassword?.message, {
|
||||
? formatMessage(errors?.confirmPassword?.message as string, {
|
||||
inputName: formatMessage(
|
||||
'signupForm.confirmPasswordFieldLabel'
|
||||
),
|
||||
|
@@ -58,7 +58,10 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const [executeFlow, { data, error, loading, called, reset }] = useMutation(
|
||||
EXECUTE_FLOW,
|
||||
{ context: { autoSnackbar: false } }
|
||||
{
|
||||
refetchQueries: ['GetStepWithTestExecutions'],
|
||||
context: { autoSnackbar: false }
|
||||
}
|
||||
);
|
||||
const response = data?.executeFlow?.data;
|
||||
|
||||
|
@@ -38,7 +38,7 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
||||
required,
|
||||
name,
|
||||
defaultValue,
|
||||
shouldUnregister = true,
|
||||
shouldUnregister = false,
|
||||
clickToCopy = false,
|
||||
readOnly = 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 type { UseFormReturn } from 'react-hook-form';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import omit from 'lodash/omit';
|
||||
import type {
|
||||
IField,
|
||||
IFieldDropdownSource,
|
||||
|
@@ -46,6 +46,7 @@
|
||||
"flow.paused": "Paused",
|
||||
"flow.draft": "Draft",
|
||||
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
|
||||
"flow.successfullyDuplicated": "The flow has been successfully duplicated.",
|
||||
"flowEditor.publish": "PUBLISH",
|
||||
"flowEditor.unpublish": "UNPUBLISH",
|
||||
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
|
||||
@@ -68,6 +69,7 @@
|
||||
"flow.createdAt": "created {datetime}",
|
||||
"flow.updatedAt": "updated {datetime}",
|
||||
"flow.view": "View",
|
||||
"flow.duplicate": "Duplicate",
|
||||
"flow.delete": "Delete",
|
||||
"flowStep.triggerType": "Trigger",
|
||||
"flowStep.actionType": "Action",
|
||||
@@ -77,12 +79,13 @@
|
||||
"flowEditor.goBack": "Go back to flows",
|
||||
"executions.title": "Executions",
|
||||
"executions.noExecutions": "There is no execution data point to show.",
|
||||
"execution.executedAt": "executed {datetime}",
|
||||
"execution.updatedAt": "updated {datetime}",
|
||||
"execution.test": "Test run",
|
||||
"execution.statusSuccess": "Success",
|
||||
"execution.statusFailure": "Failure",
|
||||
"execution.noDataTitle": "No data",
|
||||
"execution.noDataMessage": "We successfully ran the execution, but there was no new data to process.",
|
||||
"executionStep.executedAt": "executed {datetime}",
|
||||
"profileSettings.title": "My Profile",
|
||||
"profileSettings.fullName": "Full name",
|
||||
"profileSettings.email": "Email",
|
||||
|
@@ -10,6 +10,7 @@ import PaginationItem from '@mui/material/PaginationItem';
|
||||
import type { IExecution } from '@automatisch/types';
|
||||
|
||||
import NoResultFound from 'components/NoResultFound';
|
||||
import ExecutionsFilter from 'components/ExecutionsFilter';
|
||||
import ExecutionRow from 'components/ExecutionRow';
|
||||
import Container from 'components/Container';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
@@ -60,25 +61,22 @@ export default function Executions(): React.ReactElement {
|
||||
<PageTitle>{formatMessage('executions.title')}</PageTitle>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||
|
||||
<ExecutionsFilter />
|
||||
<Divider sx={{ mt: 2, mb: 2 }} />
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
data-test="executions-loader"
|
||||
sx={{ display: 'block', margin: '20px auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && !hasExecutions && (
|
||||
<NoResultFound text={formatMessage('executions.noExecutions')} />
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
executions?.map((execution) => (
|
||||
<ExecutionRow key={execution.id} execution={execution} />
|
||||
))}
|
||||
|
||||
{pageInfo && pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
|
52
yarn.lock
52
yarn.lock
@@ -1343,6 +1343,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.21.0":
|
||||
version "7.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
|
||||
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/template@^7.16.7", "@babel/template@^7.3.3":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
||||
@@ -2944,6 +2951,29 @@
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/utils@^5.12.3":
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.13.1.tgz#86199e46014215f95da046a5ec803f4a39c96eee"
|
||||
integrity sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@types/prop-types" "^15.7.5"
|
||||
"@types/react-is" "^18.2.0"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/x-date-pickers@^6.5.0":
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.5.0.tgz#b71dbf9d8961fb34d9d829a4c6f9159ebb4e9206"
|
||||
integrity sha512-dRCO1mzHjfOqsa4LdKxiXQnV0cuGiAkliyxSDCdRn6clK2WdF9Oj+1+4Mkx7fcJA61SV1eP4Yg29s0/VDsZKZw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@mui/utils" "^5.12.3"
|
||||
"@types/react-transition-group" "^4.4.6"
|
||||
clsx "^1.2.1"
|
||||
prop-types "^15.8.1"
|
||||
react-transition-group "^4.4.5"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -4171,6 +4201,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-is@^18.2.0":
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-18.2.0.tgz#2f5137853a46017b3d56447940fb3eb92bbf24a5"
|
||||
integrity sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.5":
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
|
||||
@@ -4178,6 +4215,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.6":
|
||||
version "4.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e"
|
||||
integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@16 || 17", "@types/react@^17.0.0":
|
||||
version "17.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
|
||||
@@ -14634,10 +14678,10 @@ react-error-overlay@^6.0.10:
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
|
||||
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
|
||||
|
||||
react-hook-form@^7.17.2:
|
||||
version "7.24.1"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.24.1.tgz#00e80ca20fe3bb3b86d236d74ed1a4e5f6525228"
|
||||
integrity sha512-UndVzKetChAsO+qkRo/6vOgaeTP60x324mHQ4iXVgHDvFjd+X/caWW0/QuAqipt8Bs7pyKH8147UQCrPTYFc2g==
|
||||
react-hook-form@^7.43.9:
|
||||
version "7.43.9"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d"
|
||||
integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==
|
||||
|
||||
react-intl@^5.20.12:
|
||||
version "5.24.3"
|
||||
|
Reference in New Issue
Block a user