Compare commits

...

102 Commits

Author SHA1 Message Date
Rıdvan Akca
745263cb41 feat: add sftp integration 2024-04-17 14:07:22 +02:00
Ömer Faruk Aydın
e4292815cd Merge pull request #1812 from automatisch/AUT-917
fix: expose missing createdAt and updatedAt fields from flow
2024-04-16 14:10:26 +02:00
Rıdvan Akca
ab37250d5d fix: expose missing createdAt and updatedAt fields from flow 2024-04-15 13:57:38 +02:00
Ömer Faruk Aydın
e5be8d3ba7 Merge pull request #1802 from automatisch/AUT-688
refactor: rewrite get connected apps with RQ
2024-04-15 11:43:35 +02:00
Ali BARIN
96a421fa22 Merge pull request #1811 from automatisch/AUT-920
fix: make inputs look and behave disabled when flow is in published state
2024-04-12 16:19:37 +02:00
kasia.oczkowska
12f72401b1 fix: make inputs look and behave disabled when flow is in published state 2024-04-12 14:58:24 +01:00
Ali BARIN
7391a9eddc Merge pull request #1810 from automatisch/AUT-921
fix: disable add connection button for unauthorized users
2024-04-12 15:15:38 +02:00
Ali BARIN
30dee27f72 Merge pull request #1809 from automatisch/AUT-914
fix: invalidate app connections upon reconnecting a connection
2024-04-12 15:15:11 +02:00
Ali BARIN
51a9939034 Merge pull request #1808 from automatisch/AUT-922
fix: disable create flow button when user doesn't have permissions
2024-04-12 15:14:44 +02:00
Ali BARIN
e03c6e0ca4 Merge pull request #1807 from automatisch/update-query-key
fix: update old query key
2024-04-12 15:14:26 +02:00
Rıdvan Akca
bece5c6488 fix: invalidate app connections upon reconnecting a connection 2024-04-12 14:55:45 +02:00
kasia.oczkowska
d49bb4c52d fix: disable add connection button for unauthorized users 2024-04-12 13:43:26 +01:00
kattoczko
73d0eec30c Merge branch 'main' into update-query-key 2024-04-12 14:10:40 +02:00
kasia.oczkowska
5c756b16ca fix: disable create flow button when user doesn't have permissions 2024-04-12 12:41:50 +01:00
Ali BARIN
f482c2422c Merge pull request #1806 from automatisch/AUT-913
fix: invalidate app connections upon creating a connection
2024-04-12 13:31:03 +02:00
kasia.oczkowska
2e564c863f fix: update old query key 2024-04-12 12:25:02 +01:00
Rıdvan Akca
d9917a81bb fix: invalidate app connections upon creating a connection 2024-04-12 13:10:51 +02:00
Ali BARIN
61dc431f92 Merge pull request #1805 from automatisch/AUT-919
fix: pass current user id to usePlanAndUsage hook
2024-04-12 13:06:57 +02:00
Ali BARIN
7d2fb8d9d7 Merge pull request #1803 from automatisch/AUT-912
fix: invalidate useCurrentUser when updating profile settings
2024-04-12 13:06:24 +02:00
Ali BARIN
608b79b66f Merge pull request #1804 from automatisch/unify-query-keys 2024-04-12 11:57:13 +02:00
kasia.oczkowska
009754c18b fix: pass current user id to usePlanAndUsage hook 2024-04-12 10:43:40 +01:00
Rıdvan Akca
5df07c289e fix: invalidate useCurrentUser when updating profile settings 2024-04-12 11:28:13 +02:00
kasia.oczkowska
a36d10870b feat: unify react-query query keys 2024-04-12 10:07:51 +01:00
kasia.oczkowska
b549ba3e39 refactor: rewrite get connected apps with RQ 2024-04-11 14:00:53 +01:00
Ali BARIN
897c96361f Merge pull request #1801 from automatisch/remove-unused-get-app-auth-client
refactor: remove not used files related to gql get-app-auth-client
2024-04-11 12:18:03 +02:00
kasia.oczkowska
e7693d8aa6 refactor: remove not used files related to gql get-app-auth-client 2024-04-11 11:10:58 +01:00
Ali BARIN
1fe755f836 Merge pull request #1800 from automatisch/AUT-689
refactor: rewrite useDynamicData with RQ
2024-04-10 17:47:22 +02:00
Rıdvan Akca
ea1a63f7dd refactor: rewrite useDynamicData with RQ 2024-04-10 17:25:01 +02:00
Ali BARIN
85134722a5 Merge pull request #1799 from automatisch/AUT-709
refactor: rewrite test connection with RQ
2024-04-10 17:21:37 +02:00
Rıdvan Akca
5c9d3ed134 refactor: rewrite test connection with RQ 2024-04-10 16:39:55 +02:00
Ali BARIN
17fb935ea0 Merge pull request #1798 from automatisch/fix-flow-counts
fix: show flow counts using useConnectionFlows
2024-04-10 16:37:06 +02:00
Ali BARIN
196642a1cf feat(AppConnectionRow): embed skeleton in place of flow count 2024-04-10 14:08:30 +00:00
Rıdvan Akca
009cf63d8c fix: show flow counts using useConnectionFlows 2024-04-10 15:29:21 +02:00
Ali BARIN
da399aacd6 Merge pull request #1766 from automatisch/AUT-705
refactor: rewrite useStepWithTestExecutions with RQ
2024-04-10 13:30:04 +02:00
Rıdvan Akca
3632ee77e5 refactor: rewrite useStepWithTestExecutions with RQ 2024-04-09 16:32:52 +02:00
Ali BARIN
2901f337cc Merge pull request #1797 from automatisch/disable-retry-on-mount
fix: disable retry on mount by default
2024-04-09 14:31:57 +02:00
Ali BARIN
f0bd2f335b fix: disable retry on mount by default 2024-04-08 15:20:10 +00:00
Ali BARIN
acdd026448 Merge pull request #1780 from automatisch/AUT-686
refactor: rewrite useBillingAndUsageData with useSubscription and useUserTrial
2024-04-08 15:22:55 +02:00
Ali BARIN
fb0a328ab0 Merge pull request #1791 from automatisch/AUT-905
refactor: rewrite get app connections with RQ
2024-04-08 15:21:02 +02:00
Rıdvan Akca
d2a7889fc9 refactor: remove useBillingAndUsageData hook 2024-04-08 14:49:37 +02:00
Rıdvan Akca
88c50e014d fix: update SubscriptionCancelledAlert and CheckoutCompletedAlert based on useSubscription and useUserTrial 2024-04-08 14:45:42 +02:00
Rıdvan Akca
f0ef12f904 refactor: rewrite useSubscription with RQ and use it in UsageDataInformation 2024-04-08 14:45:42 +02:00
Rıdvan Akca
1827f5413f refactor(useUserTrial): return hasTrial field from hook 2024-04-08 14:45:42 +02:00
Rıdvan Akca
0609f30e25 feat: introduce usePlanAndUsage with RQ 2024-04-08 14:45:42 +02:00
Rıdvan Akca
d4e4d95b6d refactor: rewrite get app connections with RQ 2024-04-08 14:44:36 +02:00
Ali BARIN
d74af4931e Merge pull request #1793 from automatisch/AUT-682
refactor: rewrite useAuthClients with RQ
2024-04-08 14:40:44 +02:00
Ali BARIN
bee043d10d Merge pull request #1792 from automatisch/fix-deleting-flows
fix: refetch app flows after delete and duplicate
2024-04-08 14:25:52 +02:00
Rıdvan Akca
a65e48b98a fix: refetch app flows after delete and duplicate 2024-04-08 13:52:32 +02:00
Ali BARIN
ee26b54d54 Merge pull request #1761 from automatisch/AUT-859
refactor: rewrite useFlow and useStepConnection with RQ
2024-04-08 13:33:48 +02:00
Ömer Faruk Aydın
855ec53dc2 Merge pull request #1795 from automatisch/rest-get-user-apps
feat: Implement users get apps API endpoint
2024-04-07 03:54:47 +02:00
Faruk AYDIN
3e3e48110d feat: Implement users get apps API endpoint 2024-04-07 03:45:33 +02:00
Rıdvan Akca
fc04a357c8 refactor: rewrite useFlow and useStepConnection with RQ 2024-04-05 17:51:28 +02:00
Ali BARIN
c8147370de Merge pull request #1794 from automatisch/fix-application-page
fix: destructure app config data correctly on Application page
2024-04-05 16:50:57 +02:00
kasia.oczkowska
999426be89 fix: destructure app config data correctly on Application page 2024-04-05 15:36:50 +01:00
kasia.oczkowska
91458f91ef refactor: rewrite useAuthClients with RQ 2024-04-05 15:35:05 +01:00
Ali BARIN
4b9ed29cc0 Merge pull request #1758 from automatisch/dependabot/npm_and_yarn/webpack-dev-middleware-5.3.4
chore(deps): bump webpack-dev-middleware from 5.3.0 to 5.3.4
2024-04-05 16:03:44 +02:00
Ali BARIN
e3bcb673fb Merge pull request #1787 from automatisch/dependabot/npm_and_yarn/vite-3.2.10
chore(deps): bump vite from 3.2.8 to 3.2.10
2024-04-05 16:03:20 +02:00
Ali BARIN
bf4776ca4f Merge pull request #1788 from automatisch/AUT-867
fix: introduce fix for token management
2024-04-05 14:19:05 +02:00
Ali BARIN
9f7f30a92a Merge pull request #1790 from automatisch/AUT-907
fix: set loading false if there is no flowName
2024-04-05 14:18:03 +02:00
Rıdvan Akca
5c29fff55e fix: set loading false if there is no flowName 2024-04-05 14:06:29 +02:00
Ali BARIN
a0160c2573 Merge pull request #1789 from automatisch/fix-use-apps
fix: introduce fix for useApps not using name as param
2024-04-05 12:00:50 +02:00
kasia.oczkowska
87d3ca287d fix: introduce fix for useApps not using name as param 2024-04-05 10:48:49 +01:00
kasia.oczkowska
526e093689 fix: introduce fix for token management 2024-04-04 14:16:25 +01:00
Ömer Faruk Aydın
0930c9d8d6 Merge pull request #1786 from automatisch/flow-error-message
fix: Use soft deleted filter to get soft deleted user
2024-04-04 00:50:50 +02:00
dependabot[bot]
ec680a713d chore(deps): bump vite from 3.2.8 to 3.2.10
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.8 to 3.2.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.10/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 18:08:37 +00:00
Faruk AYDIN
583f90d1e9 fix: Use soft deleted filter to get soft deleted user 2024-04-03 19:10:40 +02:00
Ömer Faruk Aydın
8ba95381bc Merge pull request #1784 from automatisch/create-dynamic-data-action
feat: Implement create dynamic data API endpoint
2024-04-03 01:23:14 +02:00
Faruk AYDIN
ec6d634b99 feat: Implement create dynamic data API endpoint 2024-04-03 01:17:06 +02:00
Ali BARIN
bc082acbe7 Merge pull request #1785 from automatisch/make-stages-dynamic-in-pipedrive
feat(pipedrive/create-deal): add dynamic stages
2024-04-03 00:09:46 +02:00
Ali BARIN
e474ba02cb Merge pull request #1645 from automatisch/flex-http-request
feat(http-request/custom-request): utilize accept header for parsing response
2024-04-02 19:54:44 +02:00
Ali BARIN
ea922aaf10 feat(pipedrive/create-deal): add dynamic stages 2024-04-02 15:43:53 +00:00
Ömer Faruk Aydın
766e6e20d8 Merge pull request #1783 from automatisch/rest-test-connection
feat: Implement test connection API endpoint
2024-03-30 00:22:38 +01:00
Faruk AYDIN
8e646c244e feat: Implement test connection API endpoint 2024-03-30 00:12:59 +01:00
Ömer Faruk Aydın
26f31a5899 Merge pull request #1782 from automatisch/rest-get-app-connections
feat: Implement get app connections API endpoint
2024-03-29 00:44:15 +01:00
Faruk AYDIN
5c79e374dd feat: Implement get app connections API endpoint 2024-03-29 00:21:58 +01:00
Ömer Faruk Aydın
7c1473ea95 Merge pull request #1781 from automatisch/fix-app-config-endpoint
fix: Fetch app auth clients for app config endpoint
2024-03-28 22:55:46 +01:00
Faruk AYDIN
1fe4cc3258 fix: Fetch app auth clients for app config endpoint 2024-03-28 22:48:17 +01:00
Ömer Faruk Aydın
042ad4cea1 Merge pull request #1774 from automatisch/rest-admin-get-app-auth-client
feat: Implement new admin get app auth client API endpoint
2024-03-28 20:47:50 +01:00
Ömer Faruk Aydın
e4c998dbce Merge pull request #1773 from automatisch/rest-admin-get-app-auth-clients
feat: Implement new admin get auth clients API endpoint
2024-03-28 20:47:40 +01:00
Ömer Faruk Aydın
83c8cacdac Merge pull request #1771 from automatisch/rest-get-app-auth-clients
feat: Implement new get app auth clients API endpoint
2024-03-28 20:47:12 +01:00
Ömer Faruk Aydın
f75d5d906e Merge pull request #1770 from automatisch/add-app-key-to-auth-clients
feat: Implement new get auth clients api endpoint
2024-03-28 20:44:51 +01:00
Faruk AYDIN
85b3856564 chore: Correct the folder of get auth client mock 2024-03-28 20:41:12 +01:00
Faruk AYDIN
75cb2569b5 chore: Remove old app auth client routers 2024-03-28 20:41:12 +01:00
Faruk AYDIN
0a4ac1cece feat: Implement new admin get app auth client API endpoint 2024-03-28 20:41:12 +01:00
Faruk AYDIN
a873fd14bd chore: Remove old admin app auth clients API endpoint 2024-03-28 20:40:45 +01:00
Faruk AYDIN
85b4cd4998 feat: Implement new admin get auth clients API endpoint 2024-03-28 20:40:45 +01:00
Faruk AYDIN
e9bc9b1aa8 fix: Typo for the get auth clients test file 2024-03-28 20:40:45 +01:00
Faruk AYDIN
e3bf599bf6 feat: Implement new get app auth clients API endpoint 2024-03-28 20:40:14 +01:00
Faruk AYDIN
01ae96840e refactor: Remove redundant appConfigId from get auth clients mock 2024-03-28 20:38:46 +01:00
Faruk AYDIN
186160ebf4 feat: Make appKey column of app auth clients not nullable 2024-03-28 20:38:46 +01:00
Faruk AYDIN
70f5e45c1f chore: Remove old app auth clients API endpoint 2024-03-28 20:38:46 +01:00
Faruk AYDIN
6dc54ecabc feat: Implement new get auth clients api endpoint 2024-03-28 20:38:46 +01:00
Faruk AYDIN
d21888c047 feat: Remove app config relation from app auth clients 2024-03-28 20:38:46 +01:00
Faruk AYDIN
33f7a90042 feat: Remove app auth clients relation from app configs 2024-03-28 20:38:46 +01:00
Faruk AYDIN
a00d3a2c5e feat: Remove app config id from app auth clients 2024-03-28 20:38:46 +01:00
Faruk AYDIN
abc64d769c feat: Migrate app config id to app key 2024-03-28 20:38:46 +01:00
Faruk AYDIN
88754ac569 feat: Add appKey to app auth clients 2024-03-28 20:38:46 +01:00
Ali BARIN
e3ee05d47d Merge pull request #1772 from automatisch/fix-signal
fix(useDynamicFields): pass signal in RQ
2024-03-28 14:22:39 +01:00
Rıdvan Akca
3b004e7483 fix(useDynamicFields): pass signal in RQ 2024-03-27 10:57:17 +03:00
dependabot[bot]
565db852e0 chore(deps): bump webpack-dev-middleware from 5.3.0 to 5.3.4
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.0 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.0...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-22 09:08:14 +00:00
Ali BARIN
bd1ad5fa56 feat(http-request/custom-request): utilize accept header for parsing response 2024-02-23 18:03:56 +00:00
Ali BARIN
f2e22e7445 feat: keep axios defaults for instances 2024-02-23 18:02:33 +00:00
180 changed files with 2863 additions and 1831 deletions

View File

@@ -67,6 +67,7 @@
"pluralize": "^8.0.0",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"ssh2-sftp-client": "^10.0.3",
"winston": "^3.7.1",
"xmlrpc": "^1.3.2"
},
@@ -95,6 +96,7 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"node-gyp": "^10.1.0",
"nodemon": "^2.0.13",
"supertest": "^6.3.3",
"vitest": "^1.1.3"

View File

@@ -90,7 +90,7 @@ export default defineAction({
async run($) {
const method = $.step.parameters.method;
const data = $.step.parameters.data;
const data = $.step.parameters.data || null;
const url = $.step.parameters.url;
const headers = $.step.parameters.headers;
@@ -108,14 +108,17 @@ export default defineAction({
return result;
}, {});
let contentType = headersObject['content-type'];
let expectedResponseContentType = headersObject.accept;
// in case HEAD request is not supported by the URL
try {
const metadataResponse = await $.http.head(url, {
headers: headersObject,
});
contentType = metadataResponse.headers['content-type'];
if (!expectedResponseContentType) {
expectedResponseContentType = metadataResponse.headers['content-type'];
}
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
// eslint-disable-next-line no-empty
@@ -128,7 +131,7 @@ export default defineAction({
headers: headersObject,
};
if (!isPossiblyTextBased(contentType)) {
if (!isPossiblyTextBased(expectedResponseContentType)) {
requestData.responseType = 'arraybuffer';
}
@@ -138,7 +141,7 @@ export default defineAction({
let responseData = response.data;
if (!isPossiblyTextBased(contentType)) {
if (!isPossiblyTextBased(expectedResponseContentType)) {
responseData = Buffer.from(responseData).toString('base64');
}

View File

@@ -64,32 +64,17 @@ export default defineAction({
value: '1',
description:
'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.',
options: [
{
label: 'Qualified (Pipeline)',
value: 1,
},
{
label: 'Contact Made (Pipeline)',
value: 2,
},
{
label: 'Prospect Qualified (Pipeline)',
value: 3,
},
{
label: 'Needs Defined (Pipeline)',
value: 4,
},
{
label: 'Proposal Made (Pipeline)',
value: 5,
},
{
label: 'Negotiations Started (Pipeline)',
value: 6,
},
],
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listStages',
},
],
},
},
{
label: 'Owner',

View File

@@ -1,23 +1,25 @@
import listActivityTypes from './list-activity-types/index.js';
import listCurrencies from './list-currencies/index.js';
import listDeals from './list-deals/index.js';
import listLeads from './list-leads/index.js';
import listLeadLabels from './list-lead-labels/index.js';
import listOrganizations from './list-organizations/index.js';
import listLeads from './list-leads/index.js';
import listOrganizationLabelField from './list-organization-label-field/index.js';
import listOrganizations from './list-organizations/index.js';
import listPersonLabelField from './list-person-label-field/index.js';
import listPersons from './list-persons/index.js';
import listStages from './list-stages/index.js';
import listUsers from './list-users/index.js';
export default [
listActivityTypes,
listCurrencies,
listDeals,
listLeads,
listLeadLabels,
listOrganizations,
listLeads,
listOrganizationLabelField,
listOrganizations,
listPersonLabelField,
listPersons,
listStages,
listUsers,
];

View File

@@ -0,0 +1,23 @@
export default {
name: 'List stages',
key: 'listStages',
async run($) {
const stages = {
data: [],
};
const { data } = await $.http.get('/api/v1/stages');
if (data.data?.length) {
for (const stage of data.data) {
stages.data.push({
value: stage.id,
name: stage.name,
});
}
}
return stages;
},
};

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 103.95"><title>sftp</title><path d="M21.56,47.27l6.21-.39a4.56,4.56,0,0,0,.82,2.29,3.45,3.45,0,0,0,2.88,1.28,3.16,3.16,0,0,0,2.15-.65,2,2,0,0,0,.76-1.52,2,2,0,0,0-.71-1.48,8.45,8.45,0,0,0-3.34-1.22A14.06,14.06,0,0,1,24.21,43a5.14,5.14,0,0,1-1.85-4.07,5.44,5.44,0,0,1,.95-3.07,6.19,6.19,0,0,1,2.84-2.28,13.26,13.26,0,0,1,5.19-.83,10.58,10.58,0,0,1,6.18,1.51,6.53,6.53,0,0,1,2.53,4.8l-6.14.37a3.38,3.38,0,0,0-1-2.1,3.34,3.34,0,0,0-2.17-.65,2.63,2.63,0,0,0-1.72.48,1.49,1.49,0,0,0-.58,1.18,1.19,1.19,0,0,0,.47.9,5.23,5.23,0,0,0,2.18.78,27.38,27.38,0,0,1,6.13,1.87,6.38,6.38,0,0,1,2.7,2.35,5.94,5.94,0,0,1,.84,3.13,6.73,6.73,0,0,1-1.12,3.75,7.1,7.1,0,0,1-3.15,2.6,12.64,12.64,0,0,1-5.08.89c-3.6,0-6.09-.7-7.47-2.08a8.18,8.18,0,0,1-2.35-5.27ZM9.07,15.63h3.09V8.2A1.87,1.87,0,0,1,14,6.33h3.32V1.87A1.88,1.88,0,0,1,19.23,0H49.57a1.87,1.87,0,0,1,1.87,1.87V6.33h59.5a1.87,1.87,0,0,1,1.87,1.87v7.43h3.41a1.87,1.87,0,0,1,1.87,1.87,1.75,1.75,0,0,1,0,.4L111.51,66a7.12,7.12,0,0,1-2,4.07,5.62,5.62,0,0,1-4,1.69H69.25v5.57h.17A2.67,2.67,0,0,1,72.08,80v4.81h13a2.67,2.67,0,0,1,2.54,1.87h27.84a7,7,0,0,1,6.85,4.48,8.05,8.05,0,0,1,.57,3,8.14,8.14,0,0,1-.57,3,7,7,0,0,1-6.85,4.47H87.73A2.67,2.67,0,0,1,85.08,104H39.92a2.67,2.67,0,0,1-2.65-2.37H7.42A7,7,0,0,1,.57,97.11a8.14,8.14,0,0,1-.57-3,8.05,8.05,0,0,1,.57-3,7,7,0,0,1,6.85-4.48h30a2.67,2.67,0,0,1,2.54-1.87H50.68V80a2.67,2.67,0,0,1,2.66-2.66h1V71.75H18A5.69,5.69,0,0,1,13.89,70,6.84,6.84,0,0,1,12,65.92L6.92,17.69a1.83,1.83,0,0,1,.37-1.33,1.86,1.86,0,0,1,1.29-.71,4,4,0,0,1,.49,0ZM57.86,77.32h7.86v-5.5H57.86v5.5ZM87.74,90.19v7.86h27.72A3.63,3.63,0,0,0,119,95.8a4.68,4.68,0,0,0,.31-1.68,4.63,4.63,0,0,0-.31-1.67,3.63,3.63,0,0,0-3.58-2.26ZM37.26,98.05V90.19H7.42a3.63,3.63,0,0,0-3.58,2.26,4.63,4.63,0,0,0-.31,1.67,4.73,4.73,0,0,0,.31,1.68,3.63,3.63,0,0,0,3.58,2.25ZM10.85,19.37l4.84,46.17a3.14,3.14,0,0,0,.83,1.86A2.07,2.07,0,0,0,18,68h87.54a2,2,0,0,0,1.41-.6,3.28,3.28,0,0,0,.88-1.92l6.28-46.12Zm5.05-9.3v5.55l93.17-.79V10.07H49.57A1.87,1.87,0,0,1,47.69,8.2V3.74H21.1V8.2a1.87,1.87,0,0,1-1.87,1.87Zm28.3,23H60.35v4.54H50.76v3.7H59v4.28H50.76v8.62H44.2V33.12Zm18.35,0H82.42v5.22H75.75V54.26H69.22V38.34H62.55V33.12Zm22.93,0H96.35a7.48,7.48,0,0,1,5.32,1.68,6.36,6.36,0,0,1,1.77,4.82,6.55,6.55,0,0,1-1.93,5q-1.94,1.82-5.89,1.81H92v7.82H85.48V33.12Zm6.56,9h1.61a4.14,4.14,0,0,0,2.67-.66,2.13,2.13,0,0,0,.77-1.68,2.37,2.37,0,0,0-.67-1.69,3.49,3.49,0,0,0-2.51-.69H92v4.72Z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,69 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
{
key: 'host',
label: 'Host',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'The host information Automatisch will connect to.',
docUrl: 'https://automatisch.io/docs/ftp#host',
clickToCopy: false,
},
{
key: 'username',
label: 'Username',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your SFTP login credentials.',
docUrl: 'https://automatisch.io/docs/sftp#username',
clickToCopy: false,
},
{
key: 'password',
label: 'Password',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/sftp#password',
clickToCopy: false,
},
{
key: 'port',
label: 'Port',
type: 'string',
required: false,
readOnly: false,
value: '21',
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/sftp#port',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,8 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -0,0 +1,23 @@
import Client from 'ssh2-sftp-client';
const verifyCredentials = async ($) => {
const sftpClient = new Client();
try {
await sftpClient.connect({
host: $.auth.data.host,
port: $.auth.data.port,
username: $.auth.data.username,
password: $.auth.data.password,
});
} catch (err) {
console.log(err);
}
await sftpClient.end();
await $.auth.set({
screenName: $.auth.data.screenName,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,14 @@
import defineApp from '../../helpers/define-app.js';
import auth from './auth/index.js';
export default defineApp({
name: 'SFTP',
key: 'sftp',
iconUrl: '{BASE_URL}/apps/sftp/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/sftp/connection',
supportsConnections: true,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '000000',
auth,
});

View File

@@ -1,52 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients/:appAuthClientId', () => {
let currentUser, currentUserRole, currentAppAuthClient, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/admin/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client UUID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});
});

View File

@@ -1,41 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-clients.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients', () => {
let currentUser, currentUserRole, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return app auth clients', async () => {
const appAuthClientOne = await createAppAuthClient();
const appAuthClientTwo = await createAppAuthClient();
const response = await request(app)
.get('/api/v1/admin/app-auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});
});

View File

@@ -4,6 +4,7 @@ import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.where({ app_key: request.params.appKey })
.throwIfNotFound();
renderObject(response, appAuthClient);

View File

@@ -0,0 +1,55 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createRole } from '../../../../../../test/factories/role.js';
import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
let currentUser, adminRole, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {
const response = await request(app)
.get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(
`/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`
)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -2,10 +2,9 @@ import { renderObject } from '../../../../../helpers/renderer.js';
import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query().orderBy(
'created_at',
'desc'
);
const appAuthClients = await AppAuthClient.query()
.where({ app_key: request.params.appKey })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -0,0 +1,44 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createRole } from '../../../../../../test/factories/role.js';
import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
let currentUser, adminRole, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -4,7 +4,7 @@ import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.where({ active: true })
.where({ app_key: request.params.appKey, active: true })
.throwIfNotFound();
renderObject(response, appAuthClient);

View File

@@ -4,25 +4,27 @@ import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-client.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-auth-clients/:id', () => {
describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
let currentUser, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
currentAppAuthClient = await createAppAuthClient();
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
it('should return specified app auth client', async () => {
const response = await request(app)
.get(`/api/v1/app-auth-clients/${currentAppAuthClient.id}`)
.get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
@@ -34,14 +36,14 @@ describe('GET /api/v1/app-auth-clients/:id', () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/app-auth-clients/${notExistingAppAuthClientUUID}`)
.get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/app-auth-clients/invalidAppAuthClientUUID')
.get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});

View File

@@ -3,7 +3,7 @@ import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ active: true })
.where({ app_key: request.params.appKey, active: true })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);

View File

@@ -3,11 +3,11 @@ import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getAppAuthClientsMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-clients.js';
import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-auth-clients', () => {
describe('GET /api/v1/apps/:appKey/auth-clients', () => {
let currentUser, token;
beforeEach(async () => {
@@ -19,15 +19,20 @@ describe('GET /api/v1/app-auth-clients', () => {
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient();
const appAuthClientTwo = await createAppAuthClient();
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/app-auth-clients')
.get('/api/v1/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientsMock([
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);

View File

@@ -3,6 +3,9 @@ import AppConfig from '../../../../models/app-config.js';
export default async (request, response) => {
const appConfig = await AppConfig.query()
.withGraphFetched({
appAuthClients: true,
})
.findOne({
key: request.params.appKey,
})

View File

@@ -0,0 +1,24 @@
import { renderObject } from '../../../../helpers/renderer.js';
import App from '../../../../models/app.js';
export default async (request, response) => {
const app = await App.findOneByKey(request.params.appKey);
const connections = await request.currentUser.authorizedConnections
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': app.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
renderObject(response, connections);
};

View File

@@ -0,0 +1,101 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
import getConnectionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-connections.js';
describe('GET /api/v1/apps/:appKey/connections', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the connections data of specified app for current user', async () => {
const currentUserConnectionOne = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
const currentUserConnectionTwo = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
currentUserConnectionTwo,
currentUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return the connections data of specified app for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnectionOne = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
const anotherUserConnectionTwo = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
anotherUserConnectionTwo,
anotherUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid connection UUID', async () => {
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.get('/api/v1/connections/invalid-connection-id/connections')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -0,0 +1,14 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let connection = await request.currentUser.authorizedConnections
.clone()
.findOne({
id: request.params.connectionId,
})
.throwIfNotFound();
connection = await connection.testAndUpdateConnection();
renderObject(response, connection);
};

View File

@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
describe('POST /api/v1/connections/:connectionId/test', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should update the connection as not verified for current user', async () => {
const currentUserConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/connections/${currentUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should update the connection as not verified for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/connections/${anotherUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should return not found response for not existing connection UUID', async () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post(`/api/v1/connections/${notExistingConnectionUUID}/test`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post('/api/v1/connections/invalidConnectionUUID/test')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,18 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.where('steps.id', request.params.stepId)
.whereNotNull('steps.app_key')
.whereNotNull('steps.connection_id')
.first()
.throwIfNotFound();
const dynamicData = await step.createDynamicData(
request.body.dynamicDataKey,
request.body.parameters
);
renderObject(response, dynamicData);
};

View File

@@ -0,0 +1,244 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createConnection } from '../../../../../test/factories/connection';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import listRepos from '../../../../apps/github/dynamic-data/list-repos/index.js';
import HttpError from '../../../../errors/http.js';
describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
describe('should return dynamically created data', () => {
let repositories;
beforeEach(async () => {
repositories = [
{
value: 'automatisch/automatisch',
name: 'automatisch/automatisch',
},
{
value: 'automatisch/sample',
name: 'automatisch/sample',
},
];
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
return {
data: repositories,
};
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
it('of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const connection = await createConnection({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
});
describe('should return error for dynamically created data', () => {
let errors;
beforeEach(async () => {
errors = {
message: 'Not Found',
documentation_url:
'https://docs.github.com/rest/users/users#get-a-user',
};
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
throw new HttpError({ message: errors });
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.errors).toEqual(errors);
});
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const step = await createStep({ appKey: null });
await request(app)
.get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,7 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const apps = await request.currentUser.getApps(request.query.name);
renderObject(response, apps, { serializer: 'App' });
};

View File

@@ -0,0 +1,210 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../test/factories/role';
import { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission.js';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import getAppsMock from '../../../../../test/mocks/rest/api/v1/users/get-apps.js';
describe('GET /api/v1/users/:userId/apps', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return all apps of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const flowOne = await createFlow({ userId: currentUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: currentUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return all apps of the another user', async () => {
const anotherUser = await createUser();
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const flowOne = await createFlow({ userId: anotherUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: anotherUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: anotherUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return specified app of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const flowOne = await createFlow({ userId: currentUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: currentUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps?name=deepl`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.length).toEqual(1);
expect(response.body.data[0].key).toEqual('deepl');
});
});

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_key');
});
}

View File

@@ -0,0 +1,17 @@
export async function up(knex) {
const appAuthClients = await knex('app_auth_clients').select('*');
for (const appAuthClient of appAuthClients) {
const appConfig = await knex('app_configs')
.where('id', appAuthClient.app_config_id)
.first();
await knex('app_auth_clients')
.where('id', appAuthClient.id)
.update({ app_key: appConfig.key });
}
}
export async function down() {
// void
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_config_id');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.uuid('app_config_id').references('id').inTable('app_configs');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').notNullable().alter();
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').nullable().alter();
});
}

View File

@@ -1,24 +0,0 @@
import AppAuthClient from '../../models/app-auth-client.js';
const getAppAuthClient = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appAuthClient = AppAuthClient.query()
.findById(params.id)
.throwIfNotFound();
if (!canSeeAllClients) {
appAuthClient.where({ active: true });
}
return await appAuthClient;
};
export default getAppAuthClient;

View File

@@ -1,33 +0,0 @@
import AppConfig from '../../models/app-config.js';
const getAppAuthClients = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appConfig = await AppConfig.query()
.findOne({
key: params.appKey,
})
.throwIfNotFound();
const appAuthClients = appConfig
.$relatedQuery('appAuthClients')
.where({ active: params.active })
.skipUndefined();
if (!canSeeAllClients) {
appAuthClients.where({
active: true,
});
}
return await appAuthClients;
};
export default getAppAuthClients;

View File

@@ -1,41 +0,0 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
const getApp = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await connectionBaseQuery
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
return {
...app,
connections,
};
}
return app;
};
export default getApp;

View File

@@ -1,101 +0,0 @@
import { DateTime } from 'luxon';
import Billing from '../../helpers/billing/index.ee.js';
import ExecutionStep from '../../models/execution-step.js';
const getBillingAndUsage = async (_parent, _params, context) => {
const persistedSubscription = await context.currentUser.$relatedQuery(
'currentSubscription'
);
const subscription = persistedSubscription
? paidSubscription(persistedSubscription)
: freeTrialSubscription();
return {
subscription,
usage: {
task: executionStepCount(context),
},
};
};
const paidSubscription = (subscription) => {
const currentPlan = Billing.paddlePlans.find(
(plan) => plan.productId === subscription.paddlePlanId
);
return {
status: subscription.status,
monthlyQuota: {
title: currentPlan.limit,
action: {
type: 'link',
text: 'Cancel plan',
src: subscription.cancelUrl,
},
},
nextBillAmount: {
title: subscription.nextBillAmount
? '€' + subscription.nextBillAmount
: '---',
action: {
type: 'link',
text: 'Update payment method',
src: subscription.updateUrl,
},
},
nextBillDate: {
title: subscription.nextBillDate ? subscription.nextBillDate : '---',
action: {
type: 'text',
text: '(monthly payment)',
},
},
};
};
const freeTrialSubscription = () => {
return {
status: null,
monthlyQuota: {
title: 'Free Trial',
action: {
type: 'link',
text: 'Upgrade plan',
src: '/settings/billing/upgrade',
},
},
nextBillAmount: {
title: '---',
action: null,
},
nextBillDate: {
title: '---',
action: null,
},
};
};
const executionIds = async (context) => {
return (
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution) => execution.id);
};
const executionStepCount = async (context) => {
const executionStepCount = await ExecutionStep.query()
.whereIn('execution_id', await executionIds(context))
.andWhere(
'created_at',
'>=',
DateTime.now().minus({ days: 30 }).toISODate()
)
.count()
.first();
return executionStepCount.count;
};
export default getBillingAndUsage;

View File

@@ -1,67 +0,0 @@
import App from '../../models/app.js';
import Flow from '../../models/flow.js';
import Connection from '../../models/connection.js';
const getConnectedApps = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
let apps = await App.findAll(params.name);
const connections = await connectionBaseQuery
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await flowBaseQuery
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');
const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);
const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));
return apps;
};
export default getConnectedApps;

View File

@@ -1,65 +0,0 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import ExecutionStep from '../../models/execution-step.js';
import globalVariable from '../../helpers/global-variable.js';
import computeParameters from '../../helpers/compute-parameters.js';
const getDynamicData = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!connection || !step.appKey) return null;
const flow = step.flow;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow, step });
const command = app.dynamicData.find((data) => data.key === params.key);
// apply run-time parameters that're not persisted yet
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
// compute variables in parameters
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const fetchedData = await command.run($);
if (fetchedData.error) {
throw new Error(JSON.stringify(fetchedData.error));
}
return fetchedData.data;
};
export default getDynamicData;

View File

@@ -1,19 +0,0 @@
import Flow from '../../models/flow.js';
const getFlow = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
const flow = await baseQuery
.clone()
.withGraphJoined('[steps.[connection]]')
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': params.id })
.throwIfNotFound();
return flow;
};
export default getFlow;

View File

@@ -1,240 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import appConfig from '../../config/app';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../test/factories/role';
import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
import { createFlow } from '../../../test/factories/flow';
import { createStep } from '../../../test/factories/step';
import { createConnection } from '../../../test/factories/connection';
describe('graphQL getFlow query', () => {
const query = (flowId) => {
return `
query {
getFlow(id: "${flowId}") {
id
name
active
status
steps {
id
type
key
appKey
iconUrl
webhookUrl
status
position
connection {
id
verified
createdAt
}
parameters
}
}
}
`;
};
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const flow = await createFlow();
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(flow.id) })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and with correct permission', () => {
let currentUser, currentUserRole, currentUserFlow;
beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
currentUserFlow = await createFlow({ userId: currentUser.id });
});
describe('and with isCreator condition', () => {
it('should return executions data of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const triggerStep = await createStep({
flowId: currentUserFlow.id,
type: 'trigger',
key: 'catchRawWebhook',
webhookPath: `/webhooks/flows/${currentUserFlow.id}`,
});
const actionConnection = await createConnection({
userId: currentUser.id,
formattedData: {
screenName: 'Test',
authenticationKey: 'test key',
},
});
const actionStep = await createStep({
flowId: currentUserFlow.id,
type: 'action',
connectionId: actionConnection.id,
key: 'translateText',
});
const token = createAuthTokenByUserId(currentUser.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(currentUserFlow.id) })
.expect(200);
const expectedResponsePayload = {
data: {
getFlow: {
active: currentUserFlow.active,
id: currentUserFlow.id,
name: currentUserFlow.name,
status: 'draft',
steps: [
{
appKey: triggerStep.appKey,
connection: null,
iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`,
id: triggerStep.id,
key: 'catchRawWebhook',
parameters: {},
position: 1,
status: triggerStep.status,
type: 'trigger',
webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${currentUserFlow.id}`,
},
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`,
id: actionStep.id,
key: 'translateText',
parameters: {},
position: 2,
status: actionStep.status,
type: 'action',
webhookUrl: 'http://localhost:3000/null',
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and without isCreator condition', () => {
it('should return executions data of all users', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
key: 'catchRawWebhook',
webhookPath: `/webhooks/flows/${anotherUserFlow.id}`,
});
const actionConnection = await createConnection({
userId: anotherUser.id,
formattedData: {
screenName: 'Test',
authenticationKey: 'test key',
},
});
const actionStep = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
connectionId: actionConnection.id,
key: 'translateText',
});
const token = createAuthTokenByUserId(currentUser.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(anotherUserFlow.id) })
.expect(200);
const expectedResponsePayload = {
data: {
getFlow: {
active: anotherUserFlow.active,
id: anotherUserFlow.id,
name: anotherUserFlow.name,
status: 'draft',
steps: [
{
appKey: triggerStep.appKey,
connection: null,
iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`,
id: triggerStep.id,
key: 'catchRawWebhook',
parameters: {},
position: 1,
status: triggerStep.status,
type: 'trigger',
webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${anotherUserFlow.id}`,
},
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`,
id: actionStep.id,
key: 'translateText',
parameters: {},
position: 2,
status: actionStep.status,
type: 'action',
webhookUrl: 'http://localhost:3000/null',
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -1,34 +0,0 @@
import { ref } from 'objection';
import ExecutionStep from '../../models/execution-step.js';
import Step from '../../models/step.js';
const getStepWithTestExecutions = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.findOne({ 'steps.id': params.stepId })
.throwIfNotFound();
const previousStepsWithCurrentStep = await stepBaseQuery
.clone()
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)
.andWhere(
'executionSteps.created_at',
'=',
ExecutionStep.query()
.max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');
return previousStepsWithCurrentStep;
};
export default getStepWithTestExecutions;

View File

@@ -1,38 +0,0 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
import globalVariable from '../../helpers/global-variable.js';
const testConnection = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
let connection = await connectionBaseQuery
.clone()
.findOne({
id: params.id,
})
.throwIfNotFound();
const app = await App.findOneByKey(connection.key, false);
const $ = await globalVariable({ connection, app });
let isStillVerified;
try {
isStillVerified = !!(await app.auth.isStillVerified($));
} catch {
isStillVerified = false;
}
connection = await connection.$query().patchAndFetch({
formattedData: connection.formattedData,
verified: isStillVerified,
});
return connection;
};
export default testConnection;

View File

@@ -1,23 +0,0 @@
import getApp from './queries/get-app.js';
import getAppAuthClient from './queries/get-app-auth-client.ee.js';
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConnectedApps from './queries/get-connected-apps.js';
import getDynamicData from './queries/get-dynamic-data.js';
import getFlow from './queries/get-flow.js';
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
import testConnection from './queries/test-connection.js';
const queryResolvers = {
getApp,
getAppAuthClient,
getAppAuthClients,
getBillingAndUsage,
getConnectedApps,
getDynamicData,
getFlow,
getStepWithTestExecutions,
testConnection,
};
export default queryResolvers;

View File

@@ -1,8 +1,6 @@
import mutationResolvers from './mutation-resolvers.js';
import queryResolvers from './query-resolvers.js';
const resolvers = {
Query: queryResolvers,
Mutation: mutationResolvers,
};

View File

@@ -1,19 +1,6 @@
type Query {
getApp(key: String!): App
getAppAuthClient(id: String!): AppAuthClient
getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient]
getConnectedApps(name: String): [App]
testConnection(id: String!): Connection
getFlow(id: String!): Flow
getStepWithTestExecutions(stepId: String!): [Step]
getDynamicData(
stepId: String!
key: String!
parameters: JSONObject
): JSONObject
getBillingAndUsage: GetBillingAndUsage
placeholderQuery(name: String): Boolean
}
type Mutation {
createAppConfig(input: CreateAppConfigInput): AppConfig
createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient
@@ -563,43 +550,6 @@ type License {
verified: Boolean
}
type GetBillingAndUsage {
subscription: Subscription
usage: Usage
}
type MonthlyQuota {
title: String
action: BillingCardAction
}
type NextBillAmount {
title: String
action: BillingCardAction
}
type NextBillDate {
title: String
action: BillingCardAction
}
type BillingCardAction {
type: String
text: String
src: String
}
type Subscription {
status: String
monthlyQuota: MonthlyQuota
nextBillAmount: NextBillAmount
nextBillDate: NextBillDate
}
type Usage {
task: Int
}
type Permission {
id: String
action: String

View File

@@ -40,9 +40,6 @@ export const authenticateUser = async (request, response, next) => {
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {
Query: {
'*': isAuthenticatedRule,
},
Mutation: {
'*': isAuthenticatedRule,
forgotPassword: allow,

View File

@@ -42,19 +42,21 @@ describe('authentication rules', () => {
const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
describe('for queries', () => {
queries.forEach((query) => {
it(`should apply correct rule for query: ${query}`, () => {
const ruleApplied = authenticationRules.Query[query];
if (queries.length) {
describe('for queries', () => {
queries.forEach((query) => {
it(`should apply correct rule for query: ${query}`, () => {
const ruleApplied = authenticationRules.Query[query];
if (query === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toEqual(allow);
}
if (query === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toEqual(allow);
}
});
});
});
});
}
describe('for mutations', () => {
mutations.forEach((mutation) => {

View File

@@ -7,6 +7,10 @@ const authorizationList = {
action: 'read',
subject: 'User',
},
'GET /api/v1/users/:userId/apps': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/flows/:flowId': {
action: 'read',
subject: 'Flow',
@@ -27,14 +31,26 @@ const authorizationList = {
action: 'update',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/dynamic-data': {
action: 'update',
subject: 'Flow',
},
'GET /api/v1/connections/:connectionId/flows': {
action: 'read',
subject: 'Flow',
},
'POST /api/v1/connections/:connectionId/test': {
action: 'update',
subject: 'Connection',
},
'GET /api/v1/apps/:appKey/flows': {
action: 'read',
subject: 'Flow',
},
'GET /api/v1/apps/:appKey/connections': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/executions/:executionId': {
action: 'read',
subject: 'Execution',

View File

@@ -2,7 +2,7 @@ import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
const config = {};
const config = axios.defaults;
const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;

View File

@@ -2,6 +2,7 @@ import logger from './logger.js';
import objection from 'objection';
import * as Sentry from './sentry.ee.js';
const { NotFoundError, DataError } = objection;
import HttpError from '../errors/http.js';
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
// eslint-disable-next-line no-unused-vars
@@ -18,6 +19,17 @@ const errorHandler = (error, request, response, next) => {
response.status(400).end();
}
if (error instanceof HttpError) {
const httpErrorPayload = {
errors: JSON.parse(error.message),
meta: {
type: 'HttpError',
},
};
response.status(200).json(httpErrorPayload);
}
const statusCode = error.statusCode || 500;
logger.error(request.method + ' ' + request.url + ' ' + statusCode);
@@ -37,7 +49,7 @@ const errorHandler = (error, request, response, next) => {
const notFoundAppError = (error) => {
return (
error.message.includes('An application with the') ||
error.message.includes('An application with the') &&
error.message.includes("key couldn't be found.")
);
};

View File

@@ -1,7 +1,6 @@
import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js';
import AppConfig from './app-config.js';
import Base from './base.js';
class AppAuthClient extends Base {
@@ -9,11 +8,11 @@ class AppAuthClient extends Base {
static jsonSchema = {
type: 'object',
required: ['name', 'appConfigId', 'formattedAuthDefaults'],
required: ['name', 'appKey', 'formattedAuthDefaults'],
properties: {
id: { type: 'string', format: 'uuid' },
appConfigId: { type: 'string', format: 'uuid' },
appKey: { type: 'string' },
active: { type: 'boolean' },
authDefaults: { type: ['string', 'null'] },
formattedAuthDefaults: { type: 'object' },
@@ -22,17 +21,6 @@ class AppAuthClient extends Base {
},
};
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_config_id',
to: 'app_configs.id',
},
},
});
encryptData() {
if (!this.eligibleForEncryption()) return;

View File

@@ -1,6 +1,6 @@
import App from './app.js';
import Base from './base.js';
import AppAuthClient from './app-auth-client.js';
import Base from './base.js';
class AppConfig extends Base {
static tableName = 'app_configs';
@@ -18,21 +18,21 @@ class AppConfig extends Base {
},
};
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
static relationMappings = () => ({
appAuthClients: {
relation: Base.HasManyRelation,
modelClass: AppAuthClient,
join: {
from: 'app_configs.id',
to: 'app_auth_clients.app_config_id',
from: 'app_configs.key',
to: 'app_auth_clients.app_key',
},
},
});
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
get canCustomConnect() {
return !this.disabled && this.allowCustomConnection;
}

View File

@@ -153,6 +153,24 @@ class Connection extends Base {
return await App.findOneByKey(this.key);
}
async testAndUpdateConnection() {
const app = await this.getApp();
const $ = await globalVariable({ connection: this, app });
let isStillVerified;
try {
isStillVerified = !!(await app.auth.isStillVerified($));
} catch {
isStillVerified = false;
}
return await this.$query().patchAndFetch({
formattedData: this.formattedData,
verified: isStillVerified,
});
}
async verifyWebhook(request) {
if (!this.key) return true;

View File

@@ -160,7 +160,7 @@ class Flow extends Base {
}
async isPaused() {
const user = await this.$relatedQuery('user');
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}

View File

@@ -8,6 +8,7 @@ import ExecutionStep from './execution-step.js';
import Telemetry from '../helpers/telemetry/index.js';
import appConfig from '../config/app.js';
import globalVariable from '../helpers/global-variable.js';
import computeParameters from '../helpers/compute-parameters.js';
class Step extends Base {
static tableName = 'steps';
@@ -217,6 +218,39 @@ class Step extends Base {
return dynamicFields;
}
async createDynamicData(dynamicDataKey, parameters) {
const connection = await this.$relatedQuery('connection');
const flow = await this.$relatedQuery('flow');
const app = await this.getApp();
const $ = await globalVariable({ connection, app, flow, step: this });
const command = app.dynamicData.find((data) => data.key === dynamicDataKey);
for (const parameterKey in parameters) {
const parameterValue = parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const dynamicData = (await command.run($)).data;
return dynamicData;
}
async updateWebhookUrl() {
if (this.isAction) return this;

View File

@@ -7,6 +7,7 @@ import { hasValidLicense } from '../helpers/license.ee.js';
import userAbility from '../helpers/user-ability.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
import Base from './base.js';
import App from './app.js';
import Connection from './connection.js';
import Execution from './execution.js';
import Flow from './flow.js';
@@ -155,6 +156,13 @@ class User extends Base {
return conditions.isCreator ? this.$relatedQuery('steps') : Step.query();
}
get authorizedConnections() {
const conditions = this.can('read', 'Connection');
return conditions.isCreator
? this.$relatedQuery('connections')
: Connection.query();
}
get authorizedExecutions() {
const conditions = this.can('read', 'Execution');
return conditions.isCreator
@@ -306,6 +314,56 @@ class User extends Base {
return invoices;
}
async getApps(name) {
const connections = await this.authorizedConnections
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await this.authorizedFlows
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');
const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);
const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
let apps = await App.findAll(name);
apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));
return apps;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);

View File

@@ -3,25 +3,25 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../../helpers/authentication.js';
import { authorizeAdmin } from '../../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-clients.ee.js';
import getAdminAppAuthClientAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.ee.js';
import getAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-auth-clients.ee.js';
import getAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-auth-client.ee.js';
const router = Router();
router.get(
'/',
'/:appKey/auth-clients',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
asyncHandler(getAdminAppAuthClientsAction)
asyncHandler(getAuthClientsAction)
);
router.get(
'/:appAuthClientId',
'/:appKey/auth-clients/:appAuthClientId',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
asyncHandler(getAdminAppAuthClientAction)
asyncHandler(getAuthClientAction)
);
export default router;

View File

@@ -1,24 +0,0 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAuthClientAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-client.js';
import getAppAuthClientsAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-clients.js';
const router = Router();
router.get(
'/',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppAuthClientsAction)
);
router.get(
'/:appAuthClientId',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppAuthClientAction)
);
export default router;

View File

@@ -6,7 +6,10 @@ import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAction from '../../../controllers/api/v1/apps/get-app.js';
import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js';
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js';
import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js';
import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js';
import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js';
import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js';
import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js';
import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js';
import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js';
@@ -19,6 +22,13 @@ router.get('/', authenticateUser, asyncHandler(getAppsAction));
router.get('/:appKey', authenticateUser, asyncHandler(getAppAction));
router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction));
router.get(
'/:appKey/connections',
authenticateUser,
authorizeUser,
asyncHandler(getConnectionsAction)
);
router.get(
'/:appKey/config',
authenticateUser,
@@ -26,6 +36,20 @@ router.get(
asyncHandler(getConfigAction)
);
router.get(
'/:appKey/auth-clients',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAuthClientsAction)
);
router.get(
'/:appKey/auth-clients/:appAuthClientId',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAuthClientAction)
);
router.get(
'/:appKey/triggers',
authenticateUser,

View File

@@ -3,6 +3,7 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js';
import createTestAction from '../../../controllers/api/v1/connections/create-test.js';
const router = Router();
@@ -13,4 +14,11 @@ router.get(
asyncHandler(getFlowsAction)
);
router.post(
'/:connectionId/test',
authenticateUser,
authorizeUser,
asyncHandler(createTestAction)
);
export default router;

View File

@@ -5,6 +5,7 @@ import { authorizeUser } from '../../../helpers/authorization.js';
import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js';
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';
import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js';
import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js';
const router = Router();
@@ -29,4 +30,11 @@ router.post(
asyncHandler(createDynamicFieldsAction)
);
router.post(
'/:stepId/dynamic-data',
authenticateUser,
authorizeUser,
asyncHandler(createDynamicDataAction)
);
export default router;

View File

@@ -1,9 +1,11 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import checkIsCloud from '../../../helpers/check-is-cloud.js';
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
import getAppsAction from '../../../controllers/api/v1/users/get-apps.js';
import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js';
import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js';
import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js';
@@ -11,6 +13,14 @@ import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-an
const router = Router();
router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));
router.get(
'/:userId/apps',
authenticateUser,
authorizeUser,
asyncHandler(getAppsAction)
);
router.get(
'/invoices',
authenticateUser,

View File

@@ -7,18 +7,17 @@ import automatischRouter from './api/v1/automatisch.js';
import accessTokensRouter from './api/v1/access-tokens.js';
import usersRouter from './api/v1/users.js';
import paymentRouter from './api/v1/payment.ee.js';
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
import flowsRouter from './api/v1/flows.js';
import stepsRouter from './api/v1/steps.js';
import appsRouter from './api/v1/apps.js';
import connectionsRouter from './api/v1/connections.js';
import executionsRouter from './api/v1/executions.js';
import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js';
import adminAppsRouter from './api/v1/admin/apps.ee.js';
import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
import rolesRouter from './api/v1/admin/roles.ee.js';
import permissionsRouter from './api/v1/admin/permissions.ee.js';
import adminUsersRouter from './api/v1/admin/users.ee.js';
import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.ee.js';
const router = Router();
@@ -30,17 +29,16 @@ router.use('/api/v1/automatisch', automatischRouter);
router.use('/api/v1/access-tokens', accessTokensRouter);
router.use('/api/v1/users', usersRouter);
router.use('/api/v1/payment', paymentRouter);
router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
router.use('/api/v1/flows', flowsRouter);
router.use('/api/v1/steps', stepsRouter);
router.use('/api/v1/apps', appsRouter);
router.use('/api/v1/connections', connectionsRouter);
router.use('/api/v1/flows', flowsRouter);
router.use('/api/v1/steps', stepsRouter);
router.use('/api/v1/executions', executionsRouter);
router.use('/api/v1/saml-auth-providers', samlAuthProvidersRouter);
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
router.use('/api/v1/admin/apps', adminAppsRouter);
router.use('/api/v1/admin/users', adminUsersRouter);
router.use('/api/v1/admin/roles', rolesRouter);
router.use('/api/v1/admin/permissions', permissionsRouter);
router.use('/api/v1/admin/users', adminUsersRouter);
router.use('/api/v1/admin/app-auth-clients', adminAppAuthClientsRouter);
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
export default router;

View File

@@ -1,12 +1,22 @@
const appSerializer = (app) => {
return {
name: app.name,
let appData = {
key: app.key,
name: app.name,
iconUrl: app.iconUrl,
primaryColor: app.primaryColor,
authDocUrl: app.authDocUrl,
supportsConnections: app.supportsConnections,
primaryColor: app.primaryColor,
};
if (app.connectionCount) {
appData.connectionCount = app.connectionCount;
}
if (app.flowCount) {
appData.flowCount = app.flowCount;
}
return appData;
};
export default appSerializer;

View File

@@ -6,6 +6,8 @@ const flowSerializer = (flow) => {
name: flow.name,
active: flow.active,
status: flow.status,
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
};
if (flow.steps?.length > 0) {

View File

@@ -27,6 +27,8 @@ describe('flowSerializer', () => {
name: flow.name,
active: flow.active,
status: flow.status,
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
};
expect(flowSerializer(flow)).toEqual(expectedPayload);

View File

@@ -1,5 +1,4 @@
import { faker } from '@faker-js/faker';
import { createAppConfig } from './app-config.js';
import AppAuthClient from '../../src/models/app-auth-client';
const formattedAuthDefaults = {
@@ -12,7 +11,7 @@ const formattedAuthDefaults = {
export const createAppAuthClient = async (params = {}) => {
params.name = params?.name || faker.person.fullName();
params.id = params?.id || faker.string.uuid();
params.appConfigId = params?.appConfigId || (await createAppConfig()).id;
params.appKey = params?.appKey || 'deepl';
params.active = params?.active ?? true;
params.formattedAuthDefaults =
params?.formattedAuthDefaults || formattedAuthDefaults;

View File

@@ -19,6 +19,8 @@ export const createStep = async (params = {}) => {
params.appKey =
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
params.parameters = params?.parameters || {};
const step = await Step.query().insertAndFetch(params);
return step;

View File

@@ -1,7 +1,6 @@
const getAdminAppAuthClientsMock = (appAuthClients) => {
return {
data: appAuthClients.map((appAuthClient) => ({
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
active: appAuthClient.active,

View File

@@ -1,9 +1,9 @@
const getAdminAppAuthClientMock = (appAuthClient) => {
const getAppAuthClientMock = (appAuthClient) => {
return {
data: {
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
appConfigId: appAuthClient.appConfigId,
active: appAuthClient.active,
},
meta: {
@@ -16,4 +16,4 @@ const getAdminAppAuthClientMock = (appAuthClient) => {
};
};
export default getAdminAppAuthClientMock;
export default getAppAuthClientMock;

View File

@@ -1,7 +1,6 @@
const getAppAuthClientsMock = (appAuthClients) => {
return {
data: appAuthClients.map((appAuthClient) => ({
appConfigId: appAuthClient.appConfigId,
name: appAuthClient.name,
id: appAuthClient.id,
active: appAuthClient.active,

View File

@@ -0,0 +1,25 @@
const getConnectionsMock = (connections) => {
return {
data: connections.map((connection) => ({
id: connection.id,
key: connection.key,
reconnectable: connection.reconnectable,
verified: connection.verified,
appAuthClientId: connection.appAuthClientId,
formattedData: {
screenName: connection.formattedData.screenName,
},
createdAt: connection.createdAt.getTime(),
updatedAt: connection.updatedAt.getTime(),
})),
meta: {
count: connections.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Connection',
},
};
};
export default getConnectionsMock;

View File

@@ -9,6 +9,8 @@ const getExecutionMock = async (execution, flow, steps) => {
name: flow.name,
active: flow.active,
status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: steps.map((step) => ({
id: step.id,
type: step.type,

View File

@@ -10,6 +10,8 @@ const getExecutionsMock = async (executions, flow, steps) => {
name: flow.name,
active: flow.active,
status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: steps.map((step) => ({
id: step.id,
type: step.type,

View File

@@ -4,6 +4,8 @@ const getFlowMock = async (flow, steps) => {
id: flow.id,
name: flow.name,
status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: steps.map((step) => ({
appKey: step.appKey,
iconUrl: step.iconUrl,

View File

@@ -7,6 +7,8 @@ const getFlowsMock = async (flows, steps) => {
id: flow.id,
name: flow.name,
status: flow.active ? 'published' : 'draft',
createdAt: flow.createdAt.getTime(),
updatedAt: flow.updatedAt.getTime(),
steps: flowSteps.map((step) => ({
appKey: step.appKey,
iconUrl: step.iconUrl,

View File

@@ -0,0 +1,55 @@
const getAppsMock = () => {
const appsData = [
{
authDocUrl: 'https://automatisch.io/docs/apps/deepl/connection',
connectionCount: 1,
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/deepl/assets/favicon.svg',
key: 'deepl',
name: 'DeepL',
primaryColor: '0d2d45',
supportsConnections: true,
},
{
authDocUrl: 'https://automatisch.io/docs/apps/github/connection',
connectionCount: 1,
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/github/assets/favicon.svg',
key: 'github',
name: 'GitHub',
primaryColor: '000000',
supportsConnections: true,
},
{
authDocUrl: 'https://automatisch.io/docs/apps/slack/connection',
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/slack/assets/favicon.svg',
key: 'slack',
name: 'Slack',
primaryColor: '4a154b',
supportsConnections: true,
},
{
authDocUrl: 'https://automatisch.io/docs/apps/webhook/connection',
flowCount: 1,
iconUrl: 'http://localhost:3000/apps/webhook/assets/favicon.svg',
key: 'webhook',
name: 'Webhook',
primaryColor: '0059F7',
supportsConnections: false,
},
];
return {
data: appsData,
meta: {
count: appsData.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Object',
},
};
};
export default getAppsMock;

View File

@@ -1,6 +1,7 @@
import { Model } from 'objection';
import { client as knex } from '../../src/config/database.js';
import logger from '../../src/helpers/logger.js';
import { vi } from 'vitest';
global.beforeAll(async () => {
global.knex = null;
@@ -22,8 +23,8 @@ global.afterEach(async () => {
await global.knex.rollback();
Model.knex(knex);
// jest.restoreAllMocks();
// jest.clearAllMocks();
vi.restoreAllMocks();
vi.clearAllMocks();
});
global.afterAll(async () => {

View File

@@ -345,6 +345,12 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/scheduler/connection' },
],
},
{
text: 'SFTP',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/sftp/connection' }],
},
{
text: 'SignalWire',
collapsible: true,

View File

@@ -0,0 +1,15 @@
# SFTP
:::info
This page explains the steps you need to follow to set up the SFTP connection in Automatisch. If any of the steps are outdated, please let us know!
:::
SSH File Transfer Protocol (also known as Secure File Transfer Protocol or SFTP) is a network protocol that provides file access, file transfer, and file management over any reliable data stream. You need to provide the following information from Automatisch by using SFTP connection.
1. Write any screen name to be displayed in Automatisch.
2. Fill host address field with the SFTP host address.
3. Fill username field with the SFTP username.
4. Fill password field with the SFTP password.
5. Fill port field with the SFTP port. Default is 21.
6. Click **Submit** button on Automatisch.
7. Now, you can start using the SFTP connection with Automatisch.

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 103.95"><title>sftp</title><path d="M21.56,47.27l6.21-.39a4.56,4.56,0,0,0,.82,2.29,3.45,3.45,0,0,0,2.88,1.28,3.16,3.16,0,0,0,2.15-.65,2,2,0,0,0,.76-1.52,2,2,0,0,0-.71-1.48,8.45,8.45,0,0,0-3.34-1.22A14.06,14.06,0,0,1,24.21,43a5.14,5.14,0,0,1-1.85-4.07,5.44,5.44,0,0,1,.95-3.07,6.19,6.19,0,0,1,2.84-2.28,13.26,13.26,0,0,1,5.19-.83,10.58,10.58,0,0,1,6.18,1.51,6.53,6.53,0,0,1,2.53,4.8l-6.14.37a3.38,3.38,0,0,0-1-2.1,3.34,3.34,0,0,0-2.17-.65,2.63,2.63,0,0,0-1.72.48,1.49,1.49,0,0,0-.58,1.18,1.19,1.19,0,0,0,.47.9,5.23,5.23,0,0,0,2.18.78,27.38,27.38,0,0,1,6.13,1.87,6.38,6.38,0,0,1,2.7,2.35,5.94,5.94,0,0,1,.84,3.13,6.73,6.73,0,0,1-1.12,3.75,7.1,7.1,0,0,1-3.15,2.6,12.64,12.64,0,0,1-5.08.89c-3.6,0-6.09-.7-7.47-2.08a8.18,8.18,0,0,1-2.35-5.27ZM9.07,15.63h3.09V8.2A1.87,1.87,0,0,1,14,6.33h3.32V1.87A1.88,1.88,0,0,1,19.23,0H49.57a1.87,1.87,0,0,1,1.87,1.87V6.33h59.5a1.87,1.87,0,0,1,1.87,1.87v7.43h3.41a1.87,1.87,0,0,1,1.87,1.87,1.75,1.75,0,0,1,0,.4L111.51,66a7.12,7.12,0,0,1-2,4.07,5.62,5.62,0,0,1-4,1.69H69.25v5.57h.17A2.67,2.67,0,0,1,72.08,80v4.81h13a2.67,2.67,0,0,1,2.54,1.87h27.84a7,7,0,0,1,6.85,4.48,8.05,8.05,0,0,1,.57,3,8.14,8.14,0,0,1-.57,3,7,7,0,0,1-6.85,4.47H87.73A2.67,2.67,0,0,1,85.08,104H39.92a2.67,2.67,0,0,1-2.65-2.37H7.42A7,7,0,0,1,.57,97.11a8.14,8.14,0,0,1-.57-3,8.05,8.05,0,0,1,.57-3,7,7,0,0,1,6.85-4.48h30a2.67,2.67,0,0,1,2.54-1.87H50.68V80a2.67,2.67,0,0,1,2.66-2.66h1V71.75H18A5.69,5.69,0,0,1,13.89,70,6.84,6.84,0,0,1,12,65.92L6.92,17.69a1.83,1.83,0,0,1,.37-1.33,1.86,1.86,0,0,1,1.29-.71,4,4,0,0,1,.49,0ZM57.86,77.32h7.86v-5.5H57.86v5.5ZM87.74,90.19v7.86h27.72A3.63,3.63,0,0,0,119,95.8a4.68,4.68,0,0,0,.31-1.68,4.63,4.63,0,0,0-.31-1.67,3.63,3.63,0,0,0-3.58-2.26ZM37.26,98.05V90.19H7.42a3.63,3.63,0,0,0-3.58,2.26,4.63,4.63,0,0,0-.31,1.67,4.73,4.73,0,0,0,.31,1.68,3.63,3.63,0,0,0,3.58,2.25ZM10.85,19.37l4.84,46.17a3.14,3.14,0,0,0,.83,1.86A2.07,2.07,0,0,0,18,68h87.54a2,2,0,0,0,1.41-.6,3.28,3.28,0,0,0,.88-1.92l6.28-46.12Zm5.05-9.3v5.55l93.17-.79V10.07H49.57A1.87,1.87,0,0,1,47.69,8.2V3.74H21.1V8.2a1.87,1.87,0,0,1-1.87,1.87Zm28.3,23H60.35v4.54H50.76v3.7H59v4.28H50.76v8.62H44.2V33.12Zm18.35,0H82.42v5.22H75.75V54.26H69.22V38.34H62.55V33.12Zm22.93,0H96.35a7.48,7.48,0,0,1,5.32,1.68,6.36,6.36,0,0,1,1.77,4.82,6.55,6.55,0,0,1-1.93,5q-1.94,1.82-5.89,1.81H92v7.82H85.48V33.12Zm6.56,9h1.61a4.14,4.14,0,0,0,2.67-.66,2.13,2.13,0,0,0,.77-1.68,2.37,2.37,0,0,0-.67-1.69,3.49,3.49,0,0,0-2.51-.69H92v4.72Z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -15,7 +15,7 @@ function AccountDropdownMenu(props) {
const navigate = useNavigate();
const { open, onClose, anchorEl, id } = props;
const logout = async () => {
authentication.updateToken('');
authentication.removeToken();
await apolloClient.clearStore();
onClose();
navigate(URLS.LOGIN);

View File

@@ -17,6 +17,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { generateExternalLink } from 'helpers/translationValues';
import { Form } from './style';
import useAppAuth from 'hooks/useAppAuth';
import { useQueryClient } from '@tanstack/react-query';
function AddAppConnection(props) {
const { application, connectionId, onClose } = props;
@@ -36,6 +37,7 @@ function AddAppConnection(props) {
appAuthClientId,
useShared: !!appAuthClientId,
});
const queryClient = useQueryClient();
React.useEffect(function relayProviderData() {
if (window.opener) {
@@ -78,6 +80,10 @@ function AddAppConnection(props) {
const response = await authenticate({
fields: data,
});
await queryClient.invalidateQueries({
queryKey: ['apps', key, 'connections'],
});
onClose(response);
} catch (err) {
const error = err;

View File

@@ -10,16 +10,18 @@ import Chip from '@mui/material/Chip';
import Button from '@mui/material/Button';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
import useAdminAppAuthClients from 'hooks/useAdminAppAuthClients';
import NoResultFound from 'components/NoResultFound';
function AdminApplicationAuthClients(props) {
const { appKey } = props;
const formatMessage = useFormatMessage();
const { appAuthClients, loading } = useAppAuthClients({ appKey });
if (loading)
const { data: appAuthClients, isLoading } = useAdminAppAuthClients(appKey);
if (isLoading)
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
if (!appAuthClients?.length) {
if (!appAuthClients?.data.length) {
return (
<NoResultFound
to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)}
@@ -27,7 +29,8 @@ function AdminApplicationAuthClients(props) {
/>
);
}
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
const sortedAuthClients = appAuthClients.data.slice().sort((a, b) => {
if (a.id < b.id) {
return -1;
}
@@ -36,6 +39,7 @@ function AdminApplicationAuthClients(props) {
}
return 0;
});
return (
<div>
{sortedAuthClients.map((client) => (

View File

@@ -6,29 +6,33 @@ import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import * as React from 'react';
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
import useAppAuthClients from 'hooks/useAppAuthClients';
import useFormatMessage from 'hooks/useFormatMessage';
function AppAuthClientsDialog(props) {
const { appKey, onClientClick, onClose } = props;
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
const { data: appAuthClients } = useAppAuthClients(appKey);
const formatMessage = useFormatMessage();
React.useEffect(
function autoAuthenticateSingleClient() {
if (appAuthClients?.length === 1) {
onClientClick(appAuthClients[0].id);
if (appAuthClients?.data.length === 1) {
onClientClick(appAuthClients.data[0].id);
}
},
[appAuthClients],
[appAuthClients?.data],
);
if (!appAuthClients?.length || appAuthClients?.length === 1)
if (!appAuthClients?.data.length || appAuthClients?.data.length === 1)
return <React.Fragment />;
return (
<Dialog onClose={onClose} open={true}>
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
<List sx={{ pt: 0 }}>
{appAuthClients.map((appAuthClient) => (
{appAuthClients.data.map((appAuthClient) => (
<ListItem disableGutters key={appAuthClient.id}>
<ListItemButton onClick={() => onClientClick(appAuthClient.id)}>
<ListItemText primary={appAuthClient.name} />

View File

@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import { ConnectionPropType } from 'propTypes/propTypes';
import { useQueryClient } from '@tanstack/react-query';
function ContextMenu(props) {
const {
@@ -18,15 +19,24 @@ function ContextMenu(props) {
disableReconnection,
} = props;
const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
const createActionHandler = React.useCallback(
(action) => {
return function clickHandler(event) {
return async function clickHandler(event) {
onMenuItemClick(event, action);
if (['test', 'reconnect', 'delete'].includes(action.type)) {
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'connections'],
});
}
onClose();
};
},
[onMenuItemClick, onClose],
[onMenuItemClick, onClose, queryClient],
);
return (
<Menu
open={true}

View File

@@ -1,21 +1,24 @@
import { useLazyQuery, useMutation } from '@apollo/client';
import * as React from 'react';
import { useMutation } from '@apollo/client';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import Skeleton from '@mui/material/Skeleton';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import { DateTime } from 'luxon';
import * as React from 'react';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
import useFormatMessage from 'hooks/useFormatMessage';
import { ConnectionPropType } from 'propTypes/propTypes';
import { CardContent, Typography } from './style';
import useConnectionFlows from 'hooks/useConnectionFlows';
import useTestConnection from 'hooks/useTestConnection';
const countTranslation = (value) => (
<>
@@ -23,36 +26,39 @@ const countTranslation = (value) => (
<br />
</>
);
function AppConnectionRow(props) {
const enqueueSnackbar = useEnqueueSnackbar();
const [verificationVisible, setVerificationVisible] = React.useState(false);
const [testConnection, { called: testCalled, loading: testLoading }] =
useLazyQuery(TEST_CONNECTION, {
fetchPolicy: 'network-only',
onCompleted: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
onError: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
});
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage();
const {
id,
key,
formattedData,
verified,
createdAt,
flowCount,
reconnectable,
} = props.connection;
const enqueueSnackbar = useEnqueueSnackbar();
const { id, key, formattedData, verified, createdAt, reconnectable } =
props.connection;
const [verificationVisible, setVerificationVisible] = React.useState(false);
const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const { mutate: testConnection, isPending: isTestConnectionPending } =
useTestConnection(
{ connectionId: id },
{
onSettled: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
},
);
const handleClose = () => {
setAnchorEl(null);
};
const { data, isLoading: isConnectionFlowsLoading } = useConnectionFlows({
connectionId: id,
});
const flowCount = data?.meta?.count;
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
const onContextMenuAction = React.useCallback(
async (event, action) => {
if (action.type === 'delete') {
@@ -68,6 +74,7 @@ function AppConnectionRow(props) {
});
},
});
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
variant: 'success',
SnackbarProps: {
@@ -81,9 +88,11 @@ function AppConnectionRow(props) {
},
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar],
);
const relativeCreatedAt = DateTime.fromMillis(
parseInt(createdAt, 10),
).toRelative();
return (
<>
<Card sx={{ my: 2 }} data-test="app-connection-row">
@@ -103,7 +112,7 @@ function AppConnectionRow(props) {
<Box>
<Stack direction="row" alignItems="center" spacing={1}>
{verificationVisible && testCalled && testLoading && (
{verificationVisible && isTestConnectionPending && (
<>
<CircularProgress size={16} />
<Typography variant="caption">
@@ -112,8 +121,7 @@ function AppConnectionRow(props) {
</>
)}
{verificationVisible &&
testCalled &&
!testLoading &&
!isTestConnectionPending &&
verified && (
<>
<CheckCircleIcon fontSize="small" color="success" />
@@ -123,8 +131,7 @@ function AppConnectionRow(props) {
</>
)}
{verificationVisible &&
testCalled &&
!testLoading &&
!isTestConnectionPending &&
!verified && (
<>
<ErrorIcon fontSize="small" color="error" />
@@ -143,7 +150,13 @@ function AppConnectionRow(props) {
sx={{ display: ['none', 'inline-block'] }}
>
{formatMessage('connection.flowCount', {
count: countTranslation(flowCount),
count: countTranslation(
isConnectionFlowsLoading ? (
<Skeleton variant="text" width={15} />
) : (
flowCount
),
),
})}
</Typography>
</Box>

View File

@@ -1,20 +1,19 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { useQuery } from '@apollo/client';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls';
import useAppConnections from 'hooks/useAppConnections';
function AppConnections(props) {
const { appKey } = props;
const formatMessage = useFormatMessage();
const { data } = useQuery(GET_APP_CONNECTIONS, {
variables: { key: appKey },
});
const appConnections = data?.getApp?.connections || [];
const { data } = useAppConnections(appKey);
const appConnections = data?.data || [];
const hasConnections = appConnections?.length;
if (!hasConnections) {
return (
<NoResultFound

View File

@@ -47,7 +47,7 @@ function AppFlows(props) {
return (
<>
{flows?.map((appFlow) => (
<AppFlowRow key={appFlow.id} flow={appFlow} />
<AppFlowRow key={appFlow.id} flow={appFlow} appKey={appKey} />
))}
{pageInfo && pageInfo.totalPages > 1 && (

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { useLocation } from 'react-router-dom';
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import useFormatMessage from 'hooks/useFormatMessage';
export default function CheckoutCompletedAlert() {
@@ -9,7 +10,9 @@ export default function CheckoutCompletedAlert() {
const location = useLocation();
const state = location.state;
const checkoutCompleted = state?.checkoutCompleted;
if (!checkoutCompleted) return <React.Fragment />;
return (
<Alert
severity="success"

View File

@@ -1,5 +1,4 @@
import PropTypes from 'prop-types';
import { useLazyQuery, useQuery } from '@apollo/client';
import Autocomplete from '@mui/material/Autocomplete';
import Button from '@mui/material/Button';
import Collapse from '@mui/material/Collapse';
@@ -12,8 +11,6 @@ import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import useAppConfig from 'hooks/useAppConfig.ee';
import { EditorContext } from 'contexts/Editor';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import {
@@ -21,6 +18,10 @@ import {
StepPropType,
SubstepPropType,
} from 'propTypes/propTypes';
import useStepConnection from 'hooks/useStepConnection';
import { useQueryClient } from '@tanstack/react-query';
import useAppConnections from 'hooks/useAppConnections';
import useTestConnection from 'hooks/useTestConnection';
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION';
@@ -44,40 +45,42 @@ function ChooseConnectionSubstep(props) {
onChange,
application,
} = props;
const { connection, appKey } = step;
const { appKey } = step;
const formatMessage = useFormatMessage();
const editorContext = React.useContext(EditorContext);
const [showAddConnectionDialog, setShowAddConnectionDialog] =
React.useState(false);
const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] =
React.useState(false);
const queryClient = useQueryClient();
const { authenticate } = useAuthenticateApp({
appKey: application.key,
useShared: true,
});
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, {
variables: { key: appKey },
});
const {
data,
isLoading: isAppConnectionsLoading,
refetch,
} = useAppConnections(appKey);
const { data: appConfig } = useAppConfig(application.key);
const { data: stepConnectionData } = useStepConnection(step.id);
const stepConnection = stepConnectionData?.data;
// TODO: show detailed error when connection test/verification fails
const [
testConnection,
{ loading: testResultLoading, refetch: retestConnection },
] = useLazyQuery(TEST_CONNECTION, {
variables: {
id: connection?.id,
},
});
const { mutate: testConnection, isPending: isTestConnectionPending } =
useTestConnection({
connectionId: stepConnection?.id,
});
React.useEffect(() => {
if (connection?.id) {
if (stepConnection?.id) {
testConnection({
variables: {
id: connection.id,
id: stepConnection.id,
},
});
}
@@ -85,11 +88,10 @@ function ChooseConnectionSubstep(props) {
}, []);
const connectionOptions = React.useMemo(() => {
const appWithConnections = data?.getApp;
const appWithConnections = data?.data;
const options =
appWithConnections?.connections?.map((connection) =>
optionGenerator(connection),
) || [];
appWithConnections?.map((connection) => optionGenerator(connection)) ||
[];
if (!appConfig?.data || appConfig?.data?.canCustomConnect) {
options.push({
@@ -154,8 +156,9 @@ function ChooseConnectionSubstep(props) {
},
[onChange, refetch, step],
);
const handleChange = React.useCallback(
(event, selectedOption) => {
async (event, selectedOption) => {
if (typeof selectedOption === 'object') {
// TODO: try to simplify type casting below.
const typedSelectedOption = selectedOption;
@@ -172,7 +175,7 @@ function ChooseConnectionSubstep(props) {
return;
}
if (connectionId !== step.connection?.id) {
if (connectionId !== stepConnection?.id) {
onChange({
step: {
...step,
@@ -181,19 +184,23 @@ function ChooseConnectionSubstep(props) {
},
},
});
await queryClient.invalidateQueries({
queryKey: ['steps', step.id, 'connection'],
});
}
}
},
[step, onChange],
[step, onChange, queryClient],
);
React.useEffect(() => {
if (step.connection?.id) {
retestConnection({
id: step.connection.id,
if (stepConnection?.id) {
testConnection({
id: stepConnection?.id,
});
}
}, [step.connection?.id, retestConnection]);
}, [stepConnection?.id, testConnection]);
const onToggle = expanded ? onCollapse : onExpand;
@@ -203,7 +210,7 @@ function ChooseConnectionSubstep(props) {
expanded={expanded}
onClick={onToggle}
title={name}
valid={testResultLoading ? null : connection?.verified}
valid={isTestConnectionPending ? null : stepConnection?.verified}
/>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<ListItem
@@ -229,9 +236,9 @@ function ChooseConnectionSubstep(props) {
required
/>
)}
value={getOption(connectionOptions, connection?.id)}
value={getOption(connectionOptions, stepConnection?.id)}
onChange={handleChange}
loading={loading}
loading={isAppConnectionsLoading}
data-test="choose-connection-autocomplete"
/>
@@ -241,8 +248,8 @@ function ChooseConnectionSubstep(props) {
onClick={onSubmit}
sx={{ mt: 2 }}
disabled={
testResultLoading ||
!connection?.verified ||
isTestConnectionPending ||
!stepConnection?.verified ||
editorContext.readOnly
}
data-test="flow-substep-continue-button"

View File

@@ -61,31 +61,38 @@ function ControlledCustomAutocomplete(props) {
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef(null);
const renderElement = React.useCallback(
(props) => <Element {...props} disabled={disabled} />,
[disabled],
);
const [editor] = React.useState(() => customizeEditor(createEditor()));
const [showVariableSuggestions, setShowVariableSuggestions] =
React.useState(false);
let dependsOnValues = [];
if (dependsOn?.length) {
dependsOnValues = watch(dependsOn);
}
React.useEffect(() => {
const ref = ReactEditor.toDOMNode(editor, editor);
resizeObserver.observe(ref);
return () => resizeObserver.unobserve(ref);
}, []);
const promoteValue = () => {
const serializedValue = serialize(editor.children);
controllerOnChange(serializedValue);
};
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
return new ResizeObserver(() => {
forceUpdate();
});
}, []);
React.useEffect(() => {
const hasDependencies = dependsOnValues.length;
if (hasDependencies) {
@@ -93,6 +100,7 @@ function ControlledCustomAutocomplete(props) {
resetEditor(editor);
}
}, dependsOnValues);
React.useEffect(
function updateInitialValue() {
const hasOptions = options.length;
@@ -110,16 +118,19 @@ function ControlledCustomAutocomplete(props) {
},
[isInitialValueSet, options, loading],
);
React.useEffect(() => {
if (!showVariableSuggestions && value !== serialize(editor.children)) {
promoteValue();
}
}, [showVariableSuggestions]);
const hideSuggestionsOnShift = (event) => {
if (event.code === 'Tab') {
setShowVariableSuggestions(false);
}
};
const handleKeyDown = (event) => {
hideSuggestionsOnShift(event);
if (event.code === 'Tab') {
@@ -129,15 +140,18 @@ function ControlledCustomAutocomplete(props) {
event.preventDefault();
}
};
const stepsWithVariables = React.useMemo(() => {
return processStepWithExecutions(priorStepsWithExecutions);
}, [priorStepsWithExecutions]);
const handleVariableSuggestionClick = React.useCallback(
(variable) => {
insertVariable(editor, variable, stepsWithVariables);
},
[stepsWithVariables],
);
const handleOptionClick = React.useCallback(
(event, option) => {
event.stopPropagation();
@@ -147,17 +161,20 @@ function ControlledCustomAutocomplete(props) {
},
[stepsWithVariables],
);
const handleClearButtonClick = (event) => {
event.stopPropagation();
resetEditor(editor);
promoteValue();
setSingleChoice(undefined);
};
const reset = (tabIndex) => {
const isOptions = tabIndex === 0;
setSingleChoice(isOptions);
resetEditor(editor, { focus: true });
};
return (
<Slate
editor={editor}

View File

@@ -22,7 +22,7 @@ function DeleteAccountDialog(props) {
const handleConfirm = React.useCallback(async () => {
await deleteCurrentUser();
authentication.updateToken('');
authentication.removeToken();
await apolloClient.clearStore();
navigate(URLS.LOGIN);
}, [deleteCurrentUser, currentUser]);

View File

@@ -25,7 +25,7 @@ function DeleteRoleButton(props) {
const handleConfirm = React.useCallback(async () => {
try {
await deleteRole();
queryClient.invalidateQueries({ queryKey: ['roles'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'roles'] });
setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
variant: 'success',

View File

@@ -25,7 +25,6 @@ function DeleteUserButton(props) {
try {
await deleteUser();
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] });
setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
variant: 'success',

View File

@@ -3,47 +3,24 @@ import { useMutation } from '@apollo/client';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import { GET_FLOW } from 'graphql/queries/get-flow';
import { CREATE_STEP } from 'graphql/mutations/create-step';
import { UPDATE_STEP } from 'graphql/mutations/update-step';
import FlowStep from 'components/FlowStep';
import { FlowPropType } from 'propTypes/propTypes';
function updateHandlerFactory(flowId, previousStepId) {
return function createStepUpdateHandler(cache, mutationResult) {
const { data } = mutationResult;
const { createStep: createdStep } = data;
const { getFlow: flow } = cache.readQuery({
query: GET_FLOW,
variables: { id: flowId },
});
const steps = flow.steps.reduce((steps, currentStep) => {
if (currentStep.id === previousStepId) {
return [...steps, currentStep, createdStep];
}
return [...steps, currentStep];
}, []);
cache.writeQuery({
query: GET_FLOW,
variables: { id: flowId },
data: { getFlow: { ...flow, steps } },
});
};
}
import { useQueryClient } from '@tanstack/react-query';
function Editor(props) {
const [updateStep] = useMutation(UPDATE_STEP);
const [createStep, { loading: creationInProgress }] = useMutation(
CREATE_STEP,
{
refetchQueries: ['GetFlow'],
},
);
const [createStep, { loading: creationInProgress }] =
useMutation(CREATE_STEP);
const { flow } = props;
const [triggerStep] = flow.steps;
const [currentStepId, setCurrentStepId] = React.useState(triggerStep.id);
const queryClient = useQueryClient();
const onStepChange = React.useCallback(
(step) => {
async (step) => {
const mutationInput = {
id: step.id,
key: step.key,
@@ -55,13 +32,20 @@ function Editor(props) {
id: flow.id,
},
};
if (step.appKey) {
mutationInput.appKey = step.appKey;
}
updateStep({ variables: { input: mutationInput } });
await updateStep({ variables: { input: mutationInput } });
await queryClient.invalidateQueries({
queryKey: ['steps', step.id, 'connection'],
});
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
},
[updateStep, flow.id],
[updateStep, flow.id, queryClient],
);
const addStep = React.useCallback(
async (previousStepId) => {
const mutationInput = {
@@ -72,20 +56,24 @@ function Editor(props) {
id: flow.id,
},
};
const createdStep = await createStep({
variables: { input: mutationInput },
update: updateHandlerFactory(flow.id, previousStepId),
});
const createdStepId = createdStep.data.createStep.id;
setCurrentStepId(createdStepId);
await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] });
},
[createStep, flow.id],
[createStep, flow.id, queryClient],
);
const openNextStep = React.useCallback((nextStep) => {
return () => {
setCurrentStepId(nextStep?.id);
};
}, []);
return (
<Box
display="flex"
@@ -106,6 +94,7 @@ function Editor(props) {
onOpen={() => setCurrentStepId(step.id)}
onClose={() => setCurrentStepId(null)}
onChange={onStepChange}
flowId={flow.id}
onContinue={openNextStep(steps[index + 1])}
/>

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { Link, useParams } from 'react-router-dom';
import { useMutation, useQuery } from '@apollo/client';
import { useMutation } from '@apollo/client';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import Snackbar from '@mui/material/Snackbar';
import { EditorProvider } from 'contexts/Editor';
import EditableTypography from 'components/EditableTypography';
import Container from 'components/Container';
@@ -15,17 +16,20 @@ import Editor from 'components/Editor';
import useFormatMessage from 'hooks/useFormatMessage';
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
import { GET_FLOW } from 'graphql/queries/get-flow';
import * as URLS from 'config/urls';
import { TopBar } from './style';
import useFlow from 'hooks/useFlow';
import { useQueryClient } from '@tanstack/react-query';
export default function EditorLayout() {
const { flowId } = useParams();
const formatMessage = useFormatMessage();
const [updateFlow] = useMutation(UPDATE_FLOW);
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS);
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId } });
const flow = data?.getFlow;
const { data, isLoading: isFlowLoading } = useFlow(flowId);
const flow = data?.data;
const queryClient = useQueryClient();
const onFlowNameUpdate = React.useCallback(
async (name) => {
await updateFlow({
@@ -38,14 +42,17 @@ export default function EditorLayout() {
optimisticResponse: {
updateFlow: {
__typename: 'Flow',
id: flow?.id,
id: flowId,
name,
},
},
});
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
},
[flow?.id],
[flowId, queryClient],
);
const onFlowStatusUpdate = React.useCallback(
async (active) => {
await updateFlowStatus({
@@ -58,14 +65,17 @@ export default function EditorLayout() {
optimisticResponse: {
updateFlowStatus: {
__typename: 'Flow',
id: flow?.id,
id: flowId,
active,
},
},
});
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
},
[flow?.id],
[flowId, queryClient],
);
return (
<>
<TopBar
@@ -94,7 +104,7 @@ export default function EditorLayout() {
</IconButton>
</Tooltip>
{!loading && (
{!isFlowLoading && (
<EditableTypography
variant="body1"
onConfirm={onFlowNameUpdate}
@@ -124,7 +134,7 @@ export default function EditorLayout() {
<Stack direction="column" height="100%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !loading && 'not found'}
{!flow && !isFlowLoading && 'not found'}
{flow && <Editor flow={flow} />}
</EditorProvider>

View File

@@ -2,9 +2,12 @@ import PropTypes from 'prop-types';
import { useMutation } from '@apollo/client';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { useQueryClient } from '@tanstack/react-query';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react';
import { Link } from 'react-router-dom';
import Can from 'components/Can';
import * as URLS from 'config/urls';
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
@@ -12,25 +15,33 @@ import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
import useFormatMessage from 'hooks/useFormatMessage';
function ContextMenu(props) {
const { flowId, onClose, anchorEl } = props;
const { flowId, onClose, anchorEl, onDuplicateFlow, onDeleteFlow, appKey } =
props;
const enqueueSnackbar = useEnqueueSnackbar();
const [deleteFlow] = useMutation(DELETE_FLOW);
const [duplicateFlow] = useMutation(DUPLICATE_FLOW, {
refetchQueries: ['GetFlows'],
});
const formatMessage = useFormatMessage();
const queryClient = useQueryClient();
const [duplicateFlow] = useMutation(DUPLICATE_FLOW);
const [deleteFlow] = useMutation(DELETE_FLOW);
const onFlowDuplicate = React.useCallback(async () => {
await duplicateFlow({
variables: { input: { id: flowId } },
});
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-duplicate-flow-success',
},
});
onDuplicateFlow?.();
onClose();
}, [flowId, onClose, duplicateFlow]);
}, [flowId, onClose, duplicateFlow, queryClient, onDuplicateFlow]);
const onFlowDelete = React.useCallback(async () => {
await deleteFlow({
variables: { input: { id: flowId } },
@@ -44,11 +55,18 @@ function ContextMenu(props) {
});
},
});
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
variant: 'success',
});
onDeleteFlow?.();
onClose();
}, [flowId, onClose, deleteFlow]);
}, [flowId, onClose, deleteFlow, queryClient, onDeleteFlow]);
return (
<Menu
open={true}
@@ -90,6 +108,9 @@ ContextMenu.propTypes = {
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string.isRequired,
};
export default ContextMenu;

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