Compare commits
43 Commits
deployment
...
AUT-660
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c762f0562f | ||
![]() |
98274c3d71 | ||
![]() |
8c936a91be | ||
![]() |
24451892ff | ||
![]() |
6bba2c82fe | ||
![]() |
3320dc6bc4 | ||
![]() |
9d42fd9293 | ||
![]() |
e6b806616f | ||
![]() |
6ec5872391 | ||
![]() |
a26cf932a1 | ||
![]() |
38a3e3ab9f | ||
![]() |
32b17c1418 | ||
![]() |
44aa6a1579 | ||
![]() |
2369aacd2a | ||
![]() |
7dafc6364b | ||
![]() |
3d25fa0aeb | ||
![]() |
0297b0f296 | ||
![]() |
4c7d09c3d8 | ||
![]() |
48a74826e8 | ||
![]() |
ef34068ac4 | ||
![]() |
3987a8db77 | ||
![]() |
953c5a5b5b | ||
![]() |
4313265c00 | ||
![]() |
9405f267ba | ||
![]() |
1d29238199 | ||
![]() |
c5bf66f462 | ||
![]() |
e6180bdfaa | ||
![]() |
55c391afc8 | ||
![]() |
782fa67320 | ||
![]() |
1e3ab75bb7 | ||
![]() |
5f6dd12a73 | ||
![]() |
d18c06d2c4 | ||
![]() |
baf99a9cfe | ||
![]() |
159931a6ea | ||
![]() |
7831f2925b | ||
![]() |
8fcb7840de | ||
![]() |
9ece9461dc | ||
![]() |
b304acaaba | ||
![]() |
5a1960609a | ||
![]() |
476aa6e3aa | ||
![]() |
aa76007fd0 | ||
![]() |
17a8813c4b | ||
![]() |
fe79fc9003 |
@@ -8,7 +8,7 @@
|
|||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": 16
|
"version": 20
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/common-utils:1": {
|
"ghcr.io/devcontainers/features/common-utils:1": {
|
||||||
"username": "vscode",
|
"username": "vscode",
|
||||||
|
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -83,20 +83,3 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: false
|
CI: false
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
build-cli:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
|
|
||||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
|
|
||||||
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
cache: 'yarn'
|
|
||||||
cache-dependency-path: yarn.lock
|
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
|
||||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
|
||||||
- run: cd packages/cli && yarn build
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
|
@@ -6,8 +6,7 @@
|
|||||||
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||||
"start:web": "lerna run --stream --scope=@*/web dev",
|
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||||
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||||
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} lint",
|
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
|
||||||
"build:watch": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} build:watch",
|
|
||||||
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
@@ -18,7 +17,6 @@
|
|||||||
"**/babel-loader",
|
"**/babel-loader",
|
||||||
"**/webpack",
|
"**/webpack",
|
||||||
"**/@automatisch/web",
|
"**/@automatisch/web",
|
||||||
"**/@automatisch/types",
|
|
||||||
"**/ajv"
|
"**/ajv"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
1
packages/backend/src/apps/airbrake/assets/favicon.svg
Normal file
1
packages/backend/src/apps/airbrake/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
44
packages/backend/src/apps/airbrake/auth/index.js
Normal file
44
packages/backend/src/apps/airbrake/auth/index.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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: 'instanceUrl',
|
||||||
|
label: 'Instance URL',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: 'Your subdomain as https://{yoursubdomain}.airbrake.io',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authToken',
|
||||||
|
label: 'Auth Token',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: 'Airbrake Auth Token of your account.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
};
|
@@ -0,0 +1,8 @@
|
|||||||
|
import verifyCredentials from './verify-credentials.js';
|
||||||
|
|
||||||
|
const isStillVerified = async ($) => {
|
||||||
|
await verifyCredentials($);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
@@ -0,0 +1,14 @@
|
|||||||
|
const verifyCredentials = async ($) => {
|
||||||
|
await $.http.get(`/api/v4/projects?key=${$.auth.data.authToken}`, {
|
||||||
|
additionalProperties: {
|
||||||
|
skipAddingAuthToken: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
screenName: $.auth.data.screenName,
|
||||||
|
authToken: $.auth.data.authToken,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
10
packages/backend/src/apps/airbrake/common/add-auth-token.js
Normal file
10
packages/backend/src/apps/airbrake/common/add-auth-token.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const addAuthToken = ($, requestConfig) => {
|
||||||
|
if (requestConfig.additionalProperties?.skipAddingAuthToken)
|
||||||
|
return requestConfig;
|
||||||
|
|
||||||
|
requestConfig.url = requestConfig.url + `?key=${$.auth.data.authToken}`;
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthToken;
|
11
packages/backend/src/apps/airbrake/common/set-base-url.js
Normal file
11
packages/backend/src/apps/airbrake/common/set-base-url.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const setBaseUrl = ($, requestConfig) => {
|
||||||
|
const subdomain = $.auth.data.instanceUrl;
|
||||||
|
|
||||||
|
if (subdomain) {
|
||||||
|
requestConfig.baseURL = `https://${subdomain}.airbrake.io`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setBaseUrl;
|
3
packages/backend/src/apps/airbrake/dynamic-data/index.js
Normal file
3
packages/backend/src/apps/airbrake/dynamic-data/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import listProjects from './list-projects/index.js';
|
||||||
|
|
||||||
|
export default [listProjects];
|
@@ -0,0 +1,23 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List projects',
|
||||||
|
key: 'listProjects',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const projects = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await $.http.get('/api/v4/projects');
|
||||||
|
|
||||||
|
if (data.projects.length) {
|
||||||
|
for (const project of data.projects) {
|
||||||
|
projects.data.push({
|
||||||
|
value: project.id,
|
||||||
|
name: project.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
},
|
||||||
|
};
|
21
packages/backend/src/apps/airbrake/index.js
Normal file
21
packages/backend/src/apps/airbrake/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
import setBaseUrl from './common/set-base-url.js';
|
||||||
|
import auth from './auth/index.js';
|
||||||
|
import addAuthToken from './common/add-auth-token.js';
|
||||||
|
import triggers from './triggers/index.js';
|
||||||
|
import dynamicData from './dynamic-data/index.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Airbrake',
|
||||||
|
key: 'airbrake',
|
||||||
|
iconUrl: '{BASE_URL}/apps/airbrake/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/airbrake/connection',
|
||||||
|
supportsConnections: true,
|
||||||
|
baseUrl: 'https://www.airbrake.io',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
primaryColor: 'f58c54',
|
||||||
|
beforeRequest: [setBaseUrl, addAuthToken],
|
||||||
|
auth,
|
||||||
|
triggers,
|
||||||
|
dynamicData,
|
||||||
|
});
|
3
packages/backend/src/apps/airbrake/triggers/index.js
Normal file
3
packages/backend/src/apps/airbrake/triggers/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import newErrors from './new-errors/index.js';
|
||||||
|
|
||||||
|
export default [newErrors];
|
@@ -0,0 +1,66 @@
|
|||||||
|
//import { URLSearchParams } from 'node:url';
|
||||||
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New errors',
|
||||||
|
key: 'newErrors',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when a new error occurs.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Project',
|
||||||
|
key: 'projectId',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listProjects',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const projectId = $.step.parameters.projectId;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let next = false;
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(
|
||||||
|
`/api/v4/projects/${projectId}/groups`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.count > params.limit) {
|
||||||
|
params.page = params.page + 1;
|
||||||
|
next = true;
|
||||||
|
} else {
|
||||||
|
next = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.groups?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of data.groups) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: group,
|
||||||
|
meta: {
|
||||||
|
internalId: group.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (next);
|
||||||
|
},
|
||||||
|
});
|
@@ -1,5 +1,6 @@
|
|||||||
import defineAction from '../../../../helpers/define-action.js';
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
import base64ToString from './transformers/base64-to-string.js';
|
||||||
import capitalize from './transformers/capitalize.js';
|
import capitalize from './transformers/capitalize.js';
|
||||||
import extractEmailAddress from './transformers/extract-email-address.js';
|
import extractEmailAddress from './transformers/extract-email-address.js';
|
||||||
import extractNumber from './transformers/extract-number.js';
|
import extractNumber from './transformers/extract-number.js';
|
||||||
@@ -8,10 +9,12 @@ import lowercase from './transformers/lowercase.js';
|
|||||||
import markdownToHtml from './transformers/markdown-to-html.js';
|
import markdownToHtml from './transformers/markdown-to-html.js';
|
||||||
import pluralize from './transformers/pluralize.js';
|
import pluralize from './transformers/pluralize.js';
|
||||||
import replace from './transformers/replace.js';
|
import replace from './transformers/replace.js';
|
||||||
|
import stringToBase64 from './transformers/string-to-base64.js';
|
||||||
import trimWhitespace from './transformers/trim-whitespace.js';
|
import trimWhitespace from './transformers/trim-whitespace.js';
|
||||||
import useDefaultValue from './transformers/use-default-value.js';
|
import useDefaultValue from './transformers/use-default-value.js';
|
||||||
|
|
||||||
const transformers = {
|
const transformers = {
|
||||||
|
base64ToString,
|
||||||
capitalize,
|
capitalize,
|
||||||
extractEmailAddress,
|
extractEmailAddress,
|
||||||
extractNumber,
|
extractNumber,
|
||||||
@@ -20,6 +23,7 @@ const transformers = {
|
|||||||
markdownToHtml,
|
markdownToHtml,
|
||||||
pluralize,
|
pluralize,
|
||||||
replace,
|
replace,
|
||||||
|
stringToBase64,
|
||||||
trimWhitespace,
|
trimWhitespace,
|
||||||
useDefaultValue,
|
useDefaultValue,
|
||||||
};
|
};
|
||||||
@@ -37,6 +41,7 @@ export default defineAction({
|
|||||||
required: true,
|
required: true,
|
||||||
variables: true,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'Base64 to String', value: 'base64ToString' },
|
||||||
{ label: 'Capitalize', value: 'capitalize' },
|
{ label: 'Capitalize', value: 'capitalize' },
|
||||||
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
|
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
|
||||||
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
|
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
|
||||||
@@ -45,6 +50,7 @@ export default defineAction({
|
|||||||
{ label: 'Lowercase', value: 'lowercase' },
|
{ label: 'Lowercase', value: 'lowercase' },
|
||||||
{ label: 'Pluralize', value: 'pluralize' },
|
{ label: 'Pluralize', value: 'pluralize' },
|
||||||
{ label: 'Replace', value: 'replace' },
|
{ label: 'Replace', value: 'replace' },
|
||||||
|
{ label: 'String to Base64', value: 'stringToBase64' },
|
||||||
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
|
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
|
||||||
{ label: 'Use Default Value', value: 'useDefaultValue' },
|
{ label: 'Use Default Value', value: 'useDefaultValue' },
|
||||||
],
|
],
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
const base64ToString = ($) => {
|
||||||
|
const input = $.step.parameters.input;
|
||||||
|
const decodedString = Buffer.from(input, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
return decodedString;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default base64ToString;
|
@@ -0,0 +1,8 @@
|
|||||||
|
const stringtoBase64 = ($) => {
|
||||||
|
const input = $.step.parameters.input;
|
||||||
|
const base64String = Buffer.from(input).toString('base64');
|
||||||
|
|
||||||
|
return base64String;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default stringtoBase64;
|
@@ -1,3 +1,4 @@
|
|||||||
|
import base64ToString from './text/base64-to-string.js';
|
||||||
import capitalize from './text/capitalize.js';
|
import capitalize from './text/capitalize.js';
|
||||||
import extractEmailAddress from './text/extract-email-address.js';
|
import extractEmailAddress from './text/extract-email-address.js';
|
||||||
import extractNumber from './text/extract-number.js';
|
import extractNumber from './text/extract-number.js';
|
||||||
@@ -6,6 +7,7 @@ import lowercase from './text/lowercase.js';
|
|||||||
import markdownToHtml from './text/markdown-to-html.js';
|
import markdownToHtml from './text/markdown-to-html.js';
|
||||||
import pluralize from './text/pluralize.js';
|
import pluralize from './text/pluralize.js';
|
||||||
import replace from './text/replace.js';
|
import replace from './text/replace.js';
|
||||||
|
import stringToBase64 from './text/string-to-base64.js';
|
||||||
import trimWhitespace from './text/trim-whitespace.js';
|
import trimWhitespace from './text/trim-whitespace.js';
|
||||||
import useDefaultValue from './text/use-default-value.js';
|
import useDefaultValue from './text/use-default-value.js';
|
||||||
import performMathOperation from './numbers/perform-math-operation.js';
|
import performMathOperation from './numbers/perform-math-operation.js';
|
||||||
@@ -15,6 +17,7 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
|
|||||||
import formatDateTime from './date-time/format-date-time.js';
|
import formatDateTime from './date-time/format-date-time.js';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
base64ToString,
|
||||||
capitalize,
|
capitalize,
|
||||||
extractEmailAddress,
|
extractEmailAddress,
|
||||||
extractNumber,
|
extractNumber,
|
||||||
@@ -23,6 +26,7 @@ const options = {
|
|||||||
markdownToHtml,
|
markdownToHtml,
|
||||||
pluralize,
|
pluralize,
|
||||||
replace,
|
replace,
|
||||||
|
stringToBase64,
|
||||||
trimWhitespace,
|
trimWhitespace,
|
||||||
useDefaultValue,
|
useDefaultValue,
|
||||||
performMathOperation,
|
performMathOperation,
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
const base64ToString = [
|
||||||
|
{
|
||||||
|
label: 'Input',
|
||||||
|
key: 'input',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'Text that will be converted from Base64 to string.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default base64ToString;
|
@@ -0,0 +1,12 @@
|
|||||||
|
const stringToBase64 = [
|
||||||
|
{
|
||||||
|
label: 'Input',
|
||||||
|
key: 'input',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'Text that will be converted to Base64.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default stringToBase64;
|
@@ -18,7 +18,9 @@ const port = process.env.PORT || '3000';
|
|||||||
const serveWebAppSeparately =
|
const serveWebAppSeparately =
|
||||||
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
||||||
|
|
||||||
let apiUrl = new URL(`${protocol}://${host}:${port}`).toString();
|
let apiUrl = new URL(
|
||||||
|
process.env.API_URL || `${protocol}://${host}:${port}`
|
||||||
|
).toString();
|
||||||
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
|
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
|
||||||
|
|
||||||
// use apiUrl by default, which has less priority over the following cases
|
// use apiUrl by default, which has less priority over the following cases
|
||||||
@@ -88,6 +90,10 @@ const appConfig = {
|
|||||||
licenseKey: process.env.LICENSE_KEY,
|
licenseKey: process.env.LICENSE_KEY,
|
||||||
sentryDsn: process.env.SENTRY_DSN,
|
sentryDsn: process.env.SENTRY_DSN,
|
||||||
CI: process.env.CI === 'true',
|
CI: process.env.CI === 'true',
|
||||||
|
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
|
||||||
|
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
||||||
|
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
||||||
|
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!appConfig.encryptionKey) {
|
if (!appConfig.encryptionKey) {
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
|
import appConfig from '../../config/app.js';
|
||||||
import User from '../../models/user.js';
|
import User from '../../models/user.js';
|
||||||
import Role from '../../models/role.js';
|
import Role from '../../models/role.js';
|
||||||
|
|
||||||
const registerUser = async (_parent, params) => {
|
const registerUser = async (_parent, params) => {
|
||||||
|
if (!appConfig.isCloud) return;
|
||||||
|
|
||||||
const { fullName, email, password } = params.input;
|
const { fullName, email, password } = params.input;
|
||||||
|
|
||||||
const existingUser = await User.query().findOne({
|
const existingUser = await User.query().findOne({
|
||||||
|
@@ -1,9 +1,17 @@
|
|||||||
|
import appConfig from '../../config/app.js';
|
||||||
import { hasValidLicense } from '../../helpers/license.ee.js';
|
import { hasValidLicense } from '../../helpers/license.ee.js';
|
||||||
import Config from '../../models/config.js';
|
import Config from '../../models/config.js';
|
||||||
|
|
||||||
const getConfig = async (_parent, params) => {
|
const getConfig = async (_parent, params) => {
|
||||||
if (!(await hasValidLicense())) return {};
|
if (!(await hasValidLicense())) return {};
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
disableNotificationsPage: appConfig.disableNotificationsPage,
|
||||||
|
disableFavicon: appConfig.disableFavicon,
|
||||||
|
additionalDrawerLink: appConfig.additionalDrawerLink,
|
||||||
|
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
|
||||||
|
};
|
||||||
|
|
||||||
const configQuery = Config.query();
|
const configQuery = Config.query();
|
||||||
|
|
||||||
if (Array.isArray(params.keys)) {
|
if (Array.isArray(params.keys)) {
|
||||||
@@ -18,7 +26,7 @@ const getConfig = async (_parent, params) => {
|
|||||||
computedConfig[key] = value?.data;
|
computedConfig[key] = value?.data;
|
||||||
|
|
||||||
return computedConfig;
|
return computedConfig;
|
||||||
}, {});
|
}, defaultConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getConfig;
|
export default getConfig;
|
||||||
|
@@ -2,6 +2,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import app from '../../app';
|
import app from '../../app';
|
||||||
import { createConfig } from '../../../test/factories/config';
|
import { createConfig } from '../../../test/factories/config';
|
||||||
|
import appConfig from '../../config/app';
|
||||||
import * as license from '../../helpers/license.ee';
|
import * as license from '../../helpers/license.ee';
|
||||||
|
|
||||||
describe('graphQL getConfig query', () => {
|
describe('graphQL getConfig query', () => {
|
||||||
@@ -56,6 +57,10 @@ describe('graphQL getConfig query', () => {
|
|||||||
[configOne.key]: configOne.value.data,
|
[configOne.key]: configOne.value.data,
|
||||||
[configTwo.key]: configTwo.value.data,
|
[configTwo.key]: configTwo.value.data,
|
||||||
[configThree.key]: configThree.value.data,
|
[configThree.key]: configThree.value.data,
|
||||||
|
disableNotificationsPage: false,
|
||||||
|
disableFavicon: false,
|
||||||
|
additionalDrawerLink: undefined,
|
||||||
|
additionalDrawerLinkText: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -82,6 +87,48 @@ describe('graphQL getConfig query', () => {
|
|||||||
getConfig: {
|
getConfig: {
|
||||||
[configOne.key]: configOne.value.data,
|
[configOne.key]: configOne.value.data,
|
||||||
[configTwo.key]: configTwo.value.data,
|
[configTwo.key]: configTwo.value.data,
|
||||||
|
disableNotificationsPage: false,
|
||||||
|
disableFavicon: false,
|
||||||
|
additionalDrawerLink: undefined,
|
||||||
|
additionalDrawerLinkText: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and with different defaults', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
|
||||||
|
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
|
||||||
|
'https://automatisch.io'
|
||||||
|
);
|
||||||
|
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
|
||||||
|
'Automatisch'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return custom config', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/graphql')
|
||||||
|
.send({ query })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedResponsePayload = {
|
||||||
|
data: {
|
||||||
|
getConfig: {
|
||||||
|
[configOne.key]: configOne.value.data,
|
||||||
|
[configTwo.key]: configTwo.value.data,
|
||||||
|
[configThree.key]: configThree.value.data,
|
||||||
|
disableNotificationsPage: true,
|
||||||
|
disableFavicon: true,
|
||||||
|
additionalDrawerLink: 'https://automatisch.io',
|
||||||
|
additionalDrawerLinkText: 'Automatisch',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -6,100 +6,74 @@ import { createRole } from '../../../test/factories/role';
|
|||||||
import { createUser } from '../../../test/factories/user';
|
import { createUser } from '../../../test/factories/user';
|
||||||
|
|
||||||
describe('graphQL getCurrentUser query', () => {
|
describe('graphQL getCurrentUser query', () => {
|
||||||
describe('with unauthenticated user', () => {
|
let role, currentUser, token, requestObject;
|
||||||
it('should throw not authorized error', async () => {
|
|
||||||
const invalidUserToken = 'invalid-token';
|
|
||||||
|
|
||||||
const query = `
|
beforeEach(async () => {
|
||||||
query {
|
role = await createRole({
|
||||||
getCurrentUser {
|
key: 'sample',
|
||||||
id
|
name: 'sample',
|
||||||
email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', invalidUserToken)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
requestObject = request(app).post('/graphql').set('Authorization', token);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
it('should return user data', async () => {
|
||||||
let role, currentUser, token, requestObject;
|
const query = `
|
||||||
|
query {
|
||||||
beforeEach(async () => {
|
getCurrentUser {
|
||||||
role = await createRole({
|
id
|
||||||
key: 'sample',
|
email
|
||||||
name: 'sample',
|
fullName
|
||||||
});
|
email
|
||||||
|
createdAt
|
||||||
currentUser = await createUser({
|
updatedAt
|
||||||
roleId: role.id,
|
role {
|
||||||
});
|
|
||||||
|
|
||||||
token = createAuthTokenByUserId(currentUser.id);
|
|
||||||
requestObject = request(app).post('/graphql').set('Authorization', token);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return user data', async () => {
|
|
||||||
const query = `
|
|
||||||
query {
|
|
||||||
getCurrentUser {
|
|
||||||
id
|
id
|
||||||
email
|
name
|
||||||
fullName
|
|
||||||
email
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
role {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const response = await requestObject.send({ query }).expect(200);
|
const response = await requestObject.send({ query }).expect(200);
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
const expectedResponsePayload = {
|
||||||
data: {
|
data: {
|
||||||
getCurrentUser: {
|
getCurrentUser: {
|
||||||
createdAt: currentUser.createdAt.getTime().toString(),
|
createdAt: currentUser.createdAt.getTime().toString(),
|
||||||
email: currentUser.email,
|
email: currentUser.email,
|
||||||
fullName: currentUser.fullName,
|
fullName: currentUser.fullName,
|
||||||
id: currentUser.id,
|
id: currentUser.id,
|
||||||
role: { id: role.id, name: role.name },
|
role: { id: role.id, name: role.name },
|
||||||
updatedAt: currentUser.updatedAt.getTime().toString(),
|
updatedAt: currentUser.updatedAt.getTime().toString(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return user password', async () => {
|
it('should not return user password', async () => {
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
query {
|
||||||
getCurrentUser {
|
getCurrentUser {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
password
|
password
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const response = await requestObject.send({ query }).expect(400);
|
const response = await requestObject.send({ query }).expect(400);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual(
|
expect(response.body.errors[0].message).toEqual(
|
||||||
'Cannot query field "password" on type "User".'
|
'Cannot query field "password" on type "User".'
|
||||||
);
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -40,307 +40,291 @@ describe('graphQL getExecutions query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const invalidToken = 'invalid-token';
|
describe('and without correct permissions', () => {
|
||||||
|
|
||||||
describe('with unauthenticated user', () => {
|
|
||||||
it('should throw not authorized error', async () => {
|
it('should throw not authorized error', async () => {
|
||||||
|
const userWithoutPermissions = await createUser();
|
||||||
|
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', invalidToken)
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
describe('and with correct permission', () => {
|
||||||
describe('and without permissions', () => {
|
let role,
|
||||||
it('should throw not authorized error', async () => {
|
currentUser,
|
||||||
const userWithoutPermissions = await createUser();
|
anotherUser,
|
||||||
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
token,
|
||||||
|
flowOne,
|
||||||
|
stepOneForFlowOne,
|
||||||
|
stepTwoForFlowOne,
|
||||||
|
executionOne,
|
||||||
|
flowTwo,
|
||||||
|
stepOneForFlowTwo,
|
||||||
|
stepTwoForFlowTwo,
|
||||||
|
executionTwo,
|
||||||
|
flowThree,
|
||||||
|
stepOneForFlowThree,
|
||||||
|
stepTwoForFlowThree,
|
||||||
|
executionThree,
|
||||||
|
expectedResponseForExecutionOne,
|
||||||
|
expectedResponseForExecutionTwo,
|
||||||
|
expectedResponseForExecutionThree;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
role = await createRole({
|
||||||
|
key: 'sample',
|
||||||
|
name: 'sample',
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
fullName: 'Current User',
|
||||||
|
});
|
||||||
|
|
||||||
|
anotherUser = await createUser();
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
|
||||||
|
flowOne = await createFlow({
|
||||||
|
userId: currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
stepOneForFlowOne = await createStep({
|
||||||
|
flowId: flowOne.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
stepTwoForFlowOne = await createStep({
|
||||||
|
flowId: flowOne.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
executionOne = await createExecution({
|
||||||
|
flowId: flowOne.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createExecutionStep({
|
||||||
|
executionId: executionOne.id,
|
||||||
|
stepId: stepOneForFlowOne.id,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createExecutionStep({
|
||||||
|
executionId: executionOne.id,
|
||||||
|
stepId: stepTwoForFlowOne.id,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
flowTwo = await createFlow({
|
||||||
|
userId: currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
stepOneForFlowTwo = await createStep({
|
||||||
|
flowId: flowTwo.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
stepTwoForFlowTwo = await createStep({
|
||||||
|
flowId: flowTwo.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
executionTwo = await createExecution({
|
||||||
|
flowId: flowTwo.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createExecutionStep({
|
||||||
|
executionId: executionTwo.id,
|
||||||
|
stepId: stepOneForFlowTwo.id,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createExecutionStep({
|
||||||
|
executionId: executionTwo.id,
|
||||||
|
stepId: stepTwoForFlowTwo.id,
|
||||||
|
status: 'failure',
|
||||||
|
});
|
||||||
|
|
||||||
|
flowThree = await createFlow({
|
||||||
|
userId: anotherUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
stepOneForFlowThree = await createStep({
|
||||||
|
flowId: flowThree.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
stepTwoForFlowThree = await createStep({
|
||||||
|
flowId: flowThree.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
executionThree = await createExecution({
|
||||||
|
flowId: flowThree.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createExecutionStep({
|
||||||
|
executionId: executionThree.id,
|
||||||
|
stepId: stepOneForFlowThree.id,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createExecutionStep({
|
||||||
|
executionId: executionThree.id,
|
||||||
|
stepId: stepTwoForFlowThree.id,
|
||||||
|
status: 'failure',
|
||||||
|
});
|
||||||
|
|
||||||
|
expectedResponseForExecutionOne = {
|
||||||
|
node: {
|
||||||
|
createdAt: executionOne.createdAt.getTime().toString(),
|
||||||
|
flow: {
|
||||||
|
active: flowOne.active,
|
||||||
|
id: flowOne.id,
|
||||||
|
name: flowOne.name,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowOne.appKey}/assets/favicon.svg`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowOne.appKey}/assets/favicon.svg`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: executionOne.id,
|
||||||
|
status: 'success',
|
||||||
|
testRun: executionOne.testRun,
|
||||||
|
updatedAt: executionOne.updatedAt.getTime().toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expectedResponseForExecutionTwo = {
|
||||||
|
node: {
|
||||||
|
createdAt: executionTwo.createdAt.getTime().toString(),
|
||||||
|
flow: {
|
||||||
|
active: flowTwo.active,
|
||||||
|
id: flowTwo.id,
|
||||||
|
name: flowTwo.name,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: executionTwo.id,
|
||||||
|
status: 'failure',
|
||||||
|
testRun: executionTwo.testRun,
|
||||||
|
updatedAt: executionTwo.updatedAt.getTime().toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expectedResponseForExecutionThree = {
|
||||||
|
node: {
|
||||||
|
createdAt: executionThree.createdAt.getTime().toString(),
|
||||||
|
flow: {
|
||||||
|
active: flowThree.active,
|
||||||
|
id: flowThree.id,
|
||||||
|
name: flowThree.name,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowThree.appKey}/assets/favicon.svg`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowThree.appKey}/assets/favicon.svg`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: executionThree.id,
|
||||||
|
status: 'failure',
|
||||||
|
testRun: executionThree.testRun,
|
||||||
|
updatedAt: executionThree.updatedAt.getTime().toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and with isCreator condition', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: role.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return executions data of the current user', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
const expectedResponsePayload = {
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
data: {
|
||||||
|
getExecutions: {
|
||||||
|
edges: [
|
||||||
|
expectedResponseForExecutionTwo,
|
||||||
|
expectedResponseForExecutionOne,
|
||||||
|
],
|
||||||
|
pageInfo: { currentPage: 1, totalPages: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and with correct permission', () => {
|
describe('and without isCreator condition', () => {
|
||||||
let role,
|
|
||||||
currentUser,
|
|
||||||
anotherUser,
|
|
||||||
token,
|
|
||||||
flowOne,
|
|
||||||
stepOneForFlowOne,
|
|
||||||
stepTwoForFlowOne,
|
|
||||||
executionOne,
|
|
||||||
flowTwo,
|
|
||||||
stepOneForFlowTwo,
|
|
||||||
stepTwoForFlowTwo,
|
|
||||||
executionTwo,
|
|
||||||
flowThree,
|
|
||||||
stepOneForFlowThree,
|
|
||||||
stepTwoForFlowThree,
|
|
||||||
executionThree,
|
|
||||||
expectedResponseForExecutionOne,
|
|
||||||
expectedResponseForExecutionTwo,
|
|
||||||
expectedResponseForExecutionThree;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
role = await createRole({
|
await createPermission({
|
||||||
key: 'sample',
|
action: 'read',
|
||||||
name: 'sample',
|
subject: 'Execution',
|
||||||
});
|
|
||||||
|
|
||||||
currentUser = await createUser({
|
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
fullName: 'Current User',
|
conditions: [],
|
||||||
});
|
|
||||||
|
|
||||||
anotherUser = await createUser();
|
|
||||||
|
|
||||||
token = createAuthTokenByUserId(currentUser.id);
|
|
||||||
|
|
||||||
flowOne = await createFlow({
|
|
||||||
userId: currentUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
stepOneForFlowOne = await createStep({
|
|
||||||
flowId: flowOne.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
stepTwoForFlowOne = await createStep({
|
|
||||||
flowId: flowOne.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
executionOne = await createExecution({
|
|
||||||
flowId: flowOne.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await createExecutionStep({
|
|
||||||
executionId: executionOne.id,
|
|
||||||
stepId: stepOneForFlowOne.id,
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createExecutionStep({
|
|
||||||
executionId: executionOne.id,
|
|
||||||
stepId: stepTwoForFlowOne.id,
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
flowTwo = await createFlow({
|
|
||||||
userId: currentUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
stepOneForFlowTwo = await createStep({
|
|
||||||
flowId: flowTwo.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
stepTwoForFlowTwo = await createStep({
|
|
||||||
flowId: flowTwo.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
executionTwo = await createExecution({
|
|
||||||
flowId: flowTwo.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await createExecutionStep({
|
|
||||||
executionId: executionTwo.id,
|
|
||||||
stepId: stepOneForFlowTwo.id,
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createExecutionStep({
|
|
||||||
executionId: executionTwo.id,
|
|
||||||
stepId: stepTwoForFlowTwo.id,
|
|
||||||
status: 'failure',
|
|
||||||
});
|
|
||||||
|
|
||||||
flowThree = await createFlow({
|
|
||||||
userId: anotherUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
stepOneForFlowThree = await createStep({
|
|
||||||
flowId: flowThree.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
stepTwoForFlowThree = await createStep({
|
|
||||||
flowId: flowThree.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
executionThree = await createExecution({
|
|
||||||
flowId: flowThree.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await createExecutionStep({
|
|
||||||
executionId: executionThree.id,
|
|
||||||
stepId: stepOneForFlowThree.id,
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createExecutionStep({
|
|
||||||
executionId: executionThree.id,
|
|
||||||
stepId: stepTwoForFlowThree.id,
|
|
||||||
status: 'failure',
|
|
||||||
});
|
|
||||||
|
|
||||||
expectedResponseForExecutionOne = {
|
|
||||||
node: {
|
|
||||||
createdAt: executionOne.createdAt.getTime().toString(),
|
|
||||||
flow: {
|
|
||||||
active: flowOne.active,
|
|
||||||
id: flowOne.id,
|
|
||||||
name: flowOne.name,
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowOne.appKey}/assets/favicon.svg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowOne.appKey}/assets/favicon.svg`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
id: executionOne.id,
|
|
||||||
status: 'success',
|
|
||||||
testRun: executionOne.testRun,
|
|
||||||
updatedAt: executionOne.updatedAt.getTime().toString(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expectedResponseForExecutionTwo = {
|
|
||||||
node: {
|
|
||||||
createdAt: executionTwo.createdAt.getTime().toString(),
|
|
||||||
flow: {
|
|
||||||
active: flowTwo.active,
|
|
||||||
id: flowTwo.id,
|
|
||||||
name: flowTwo.name,
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
id: executionTwo.id,
|
|
||||||
status: 'failure',
|
|
||||||
testRun: executionTwo.testRun,
|
|
||||||
updatedAt: executionTwo.updatedAt.getTime().toString(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expectedResponseForExecutionThree = {
|
|
||||||
node: {
|
|
||||||
createdAt: executionThree.createdAt.getTime().toString(),
|
|
||||||
flow: {
|
|
||||||
active: flowThree.active,
|
|
||||||
id: flowThree.id,
|
|
||||||
name: flowThree.name,
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowThree.appKey}/assets/favicon.svg`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowThree.appKey}/assets/favicon.svg`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
id: executionThree.id,
|
|
||||||
status: 'failure',
|
|
||||||
testRun: executionThree.testRun,
|
|
||||||
updatedAt: executionThree.updatedAt.getTime().toString(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and with isCreator condition', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await createPermission({
|
|
||||||
action: 'read',
|
|
||||||
subject: 'Execution',
|
|
||||||
roleId: role.id,
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return executions data of the current user', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', token)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getExecutions: {
|
|
||||||
edges: [
|
|
||||||
expectedResponseForExecutionTwo,
|
|
||||||
expectedResponseForExecutionOne,
|
|
||||||
],
|
|
||||||
pageInfo: { currentPage: 1, totalPages: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and without isCreator condition', () => {
|
it('should return executions data of all users', async () => {
|
||||||
beforeEach(async () => {
|
const response = await request(app)
|
||||||
await createPermission({
|
.post('/graphql')
|
||||||
action: 'read',
|
.set('Authorization', token)
|
||||||
subject: 'Execution',
|
.send({ query })
|
||||||
roleId: role.id,
|
.expect(200);
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return executions data of all users', async () => {
|
const expectedResponsePayload = {
|
||||||
const response = await request(app)
|
data: {
|
||||||
.post('/graphql')
|
getExecutions: {
|
||||||
.set('Authorization', token)
|
edges: [
|
||||||
.send({ query })
|
expectedResponseForExecutionThree,
|
||||||
.expect(200);
|
expectedResponseForExecutionTwo,
|
||||||
|
expectedResponseForExecutionOne,
|
||||||
const expectedResponsePayload = {
|
],
|
||||||
data: {
|
pageInfo: { currentPage: 1, totalPages: 1 },
|
||||||
getExecutions: {
|
|
||||||
edges: [
|
|
||||||
expectedResponseForExecutionThree,
|
|
||||||
expectedResponseForExecutionTwo,
|
|
||||||
expectedResponseForExecutionOne,
|
|
||||||
],
|
|
||||||
pageInfo: { currentPage: 1, totalPages: 1 },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and with filters', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: role.id,
|
||||||
|
conditions: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and with filters', () => {
|
it('should return executions data for the specified flow', async () => {
|
||||||
beforeEach(async () => {
|
const query = `
|
||||||
await createPermission({
|
|
||||||
action: 'read',
|
|
||||||
subject: 'Execution',
|
|
||||||
roleId: role.id,
|
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return executions data for the specified flow', async () => {
|
|
||||||
const query = `
|
|
||||||
query {
|
query {
|
||||||
getExecutions(limit: 10, offset: 0, filters: { flowId: "${flowOne.id}" }) {
|
getExecutions(limit: 10, offset: 0, filters: { flowId: "${flowOne.id}" }) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -368,26 +352,26 @@ describe('graphQL getExecutions query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
const expectedResponsePayload = {
|
||||||
data: {
|
data: {
|
||||||
getExecutions: {
|
getExecutions: {
|
||||||
edges: [expectedResponseForExecutionOne],
|
edges: [expectedResponseForExecutionOne],
|
||||||
pageInfo: { currentPage: 1, totalPages: 1 },
|
pageInfo: { currentPage: 1, totalPages: 1 },
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return only executions data with success status', async () => {
|
it('should return only executions data with success status', async () => {
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
query {
|
||||||
getExecutions(limit: 10, offset: 0, filters: { status: "success" }) {
|
getExecutions(limit: 10, offset: 0, filters: { status: "success" }) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -415,30 +399,30 @@ describe('graphQL getExecutions query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
const expectedResponsePayload = {
|
||||||
data: {
|
data: {
|
||||||
getExecutions: {
|
getExecutions: {
|
||||||
edges: [expectedResponseForExecutionOne],
|
edges: [expectedResponseForExecutionOne],
|
||||||
pageInfo: { currentPage: 1, totalPages: 1 },
|
pageInfo: { currentPage: 1, totalPages: 1 },
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return only executions data within date range', async () => {
|
it('should return only executions data within date range', async () => {
|
||||||
const createdAtFrom = executionOne.createdAt.getTime().toString();
|
const createdAtFrom = executionOne.createdAt.getTime().toString();
|
||||||
|
|
||||||
const createdAtTo = executionOne.createdAt.getTime().toString();
|
const createdAtTo = executionOne.createdAt.getTime().toString();
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
query {
|
||||||
getExecutions(limit: 10, offset: 0, filters: { createdAt: { from: "${createdAtFrom}", to: "${createdAtTo}" }}) {
|
getExecutions(limit: 10, offset: 0, filters: { createdAt: { from: "${createdAtFrom}", to: "${createdAtTo}" }}) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -466,23 +450,22 @@ describe('graphQL getExecutions query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
const expectedResponsePayload = {
|
||||||
data: {
|
data: {
|
||||||
getExecutions: {
|
getExecutions: {
|
||||||
edges: [expectedResponseForExecutionOne],
|
edges: [expectedResponseForExecutionOne],
|
||||||
pageInfo: { currentPage: 1, totalPages: 1 },
|
pageInfo: { currentPage: 1, totalPages: 1 },
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -40,222 +40,200 @@ describe('graphQL getFlow query', () => {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('with unauthenticated user', () => {
|
describe('and without permissions', () => {
|
||||||
it('should throw not authorized error', async () => {
|
it('should throw not authorized error', async () => {
|
||||||
const invalidToken = 'invalid-token';
|
const userWithoutPermissions = await createUser();
|
||||||
|
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
||||||
const flow = await createFlow();
|
const flow = await createFlow();
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', invalidToken)
|
.set('Authorization', token)
|
||||||
.send({ query: query(flow.id) })
|
.send({ query: query(flow.id) })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
describe('and with correct permission', () => {
|
||||||
describe('and without permissions', () => {
|
let currentUser, currentUserRole, currentUserFlow;
|
||||||
it('should throw not authorized error', async () => {
|
|
||||||
const userWithoutPermissions = await createUser();
|
beforeEach(async () => {
|
||||||
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
currentUserRole = await createRole();
|
||||||
const flow = await createFlow();
|
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)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send({ query: query(flow.id) })
|
.send({ query: query(currentUserFlow.id) })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
const expectedResponsePayload = {
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
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: 1,
|
||||||
|
status: actionStep.status,
|
||||||
|
type: 'action',
|
||||||
|
webhookUrl: 'http://localhost:3000/null',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and with correct permission', () => {
|
describe('and without isCreator condition', () => {
|
||||||
let currentUser, currentUserRole, currentUserFlow;
|
it('should return executions data of all users', async () => {
|
||||||
|
await createPermission({
|
||||||
beforeEach(async () => {
|
action: 'read',
|
||||||
currentUserRole = await createRole();
|
subject: 'Flow',
|
||||||
currentUser = await createUser({ roleId: currentUserRole.id });
|
roleId: currentUserRole.id,
|
||||||
currentUserFlow = await createFlow({ userId: currentUser.id });
|
conditions: [],
|
||||||
});
|
|
||||||
|
|
||||||
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: 1,
|
|
||||||
status: actionStep.status,
|
|
||||||
type: 'action',
|
|
||||||
webhookUrl: 'http://localhost:3000/null',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('and without isCreator condition', () => {
|
const anotherUser = await createUser();
|
||||||
it('should return executions data of all users', async () => {
|
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
||||||
await createPermission({
|
|
||||||
action: 'read',
|
|
||||||
subject: 'Flow',
|
|
||||||
roleId: currentUserRole.id,
|
|
||||||
conditions: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const anotherUser = await createUser();
|
const triggerStep = await createStep({
|
||||||
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
const triggerStep = await createStep({
|
key: 'catchRawWebhook',
|
||||||
flowId: anotherUserFlow.id,
|
webhookPath: `/webhooks/flows/${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: 1,
|
|
||||||
status: actionStep.status,
|
|
||||||
type: 'action',
|
|
||||||
webhookUrl: 'http://localhost:3000/null',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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: 1,
|
||||||
|
status: actionStep.status,
|
||||||
|
type: 'action',
|
||||||
|
webhookUrl: 'http://localhost:3000/null',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -17,7 +17,6 @@ describe('graphQL getRole query', () => {
|
|||||||
userWithoutPermissions,
|
userWithoutPermissions,
|
||||||
tokenWithPermissions,
|
tokenWithPermissions,
|
||||||
tokenWithoutPermissions,
|
tokenWithoutPermissions,
|
||||||
invalidToken,
|
|
||||||
permissionOne,
|
permissionOne,
|
||||||
permissionTwo;
|
permissionTwo;
|
||||||
|
|
||||||
@@ -74,108 +73,91 @@ describe('graphQL getRole query', () => {
|
|||||||
tokenWithoutPermissions = createAuthTokenByUserId(
|
tokenWithoutPermissions = createAuthTokenByUserId(
|
||||||
userWithoutPermissions.id
|
userWithoutPermissions.id
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidToken = 'invalid-token';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with unauthenticated user', () => {
|
describe('and with valid license', () => {
|
||||||
it('should throw not authorized error', async () => {
|
beforeEach(async () => {
|
||||||
const response = await request(app)
|
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', invalidToken)
|
|
||||||
.send({ query: queryWithValidRole })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
describe('and without permissions', () => {
|
||||||
describe('and with valid license', () => {
|
it('should throw not authorized error', async () => {
|
||||||
beforeEach(async () => {
|
const response = await request(app)
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
.post('/graphql')
|
||||||
|
.set('Authorization', tokenWithoutPermissions)
|
||||||
|
.send({ query: queryWithValidRole })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.errors).toBeDefined();
|
||||||
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('and without permissions', () => {
|
describe('and correct permissions', () => {
|
||||||
it('should throw not authorized error', async () => {
|
it('should return role data for a valid role id', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', tokenWithoutPermissions)
|
.set('Authorization', tokenWithPermissions)
|
||||||
.send({ query: queryWithValidRole })
|
.send({ query: queryWithValidRole })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
const expectedResponsePayload = {
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
data: {
|
||||||
});
|
getRole: {
|
||||||
});
|
description: validRole.description,
|
||||||
|
id: validRole.id,
|
||||||
describe('and correct permissions', () => {
|
isAdmin: validRole.key === 'admin',
|
||||||
it('should return role data for a valid role id', async () => {
|
key: validRole.key,
|
||||||
const response = await request(app)
|
name: validRole.name,
|
||||||
.post('/graphql')
|
permissions: [
|
||||||
.set('Authorization', tokenWithPermissions)
|
{
|
||||||
.send({ query: queryWithValidRole })
|
action: permissionOne.action,
|
||||||
.expect(200);
|
conditions: permissionOne.conditions,
|
||||||
|
id: permissionOne.id,
|
||||||
const expectedResponsePayload = {
|
subject: permissionOne.subject,
|
||||||
data: {
|
},
|
||||||
getRole: {
|
{
|
||||||
description: validRole.description,
|
action: permissionTwo.action,
|
||||||
id: validRole.id,
|
conditions: permissionTwo.conditions,
|
||||||
isAdmin: validRole.key === 'admin',
|
id: permissionTwo.id,
|
||||||
key: validRole.key,
|
subject: permissionTwo.subject,
|
||||||
name: validRole.name,
|
},
|
||||||
permissions: [
|
],
|
||||||
{
|
|
||||||
action: permissionOne.action,
|
|
||||||
conditions: permissionOne.conditions,
|
|
||||||
id: permissionOne.id,
|
|
||||||
subject: permissionOne.subject,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: permissionTwo.action,
|
|
||||||
conditions: permissionTwo.conditions,
|
|
||||||
id: permissionTwo.id,
|
|
||||||
subject: permissionTwo.subject,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found for invalid role id', async () => {
|
it('should return not found for invalid role id', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', tokenWithPermissions)
|
.set('Authorization', tokenWithPermissions)
|
||||||
.send({ query: queryWithInvalidRole })
|
.send({ query: queryWithInvalidRole })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('NotFoundError');
|
expect(response.body.errors[0].message).toEqual('NotFoundError');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('and without valid license', () => {
|
describe('and without valid license', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
|
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and correct permissions', () => {
|
describe('and correct permissions', () => {
|
||||||
it('should throw not authorized error', async () => {
|
it('should throw not authorized error', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', tokenWithPermissions)
|
.set('Authorization', tokenWithPermissions)
|
||||||
.send({ query: queryWithInvalidRole })
|
.send({ query: queryWithInvalidRole })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -15,8 +15,7 @@ describe('graphQL getRoles query', () => {
|
|||||||
userWithPermissions,
|
userWithPermissions,
|
||||||
userWithoutPermissions,
|
userWithoutPermissions,
|
||||||
tokenWithPermissions,
|
tokenWithPermissions,
|
||||||
tokenWithoutPermissions,
|
tokenWithoutPermissions;
|
||||||
invalidToken;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
currentUserRole = await createRole({ name: 'Current user role' });
|
currentUserRole = await createRole({ name: 'Current user role' });
|
||||||
@@ -53,99 +52,82 @@ describe('graphQL getRoles query', () => {
|
|||||||
tokenWithoutPermissions = createAuthTokenByUserId(
|
tokenWithoutPermissions = createAuthTokenByUserId(
|
||||||
userWithoutPermissions.id
|
userWithoutPermissions.id
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidToken = 'invalid-token';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with unauthenticated user', () => {
|
describe('and with valid license', () => {
|
||||||
it('should throw not authorized error', async () => {
|
beforeEach(async () => {
|
||||||
const response = await request(app)
|
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', invalidToken)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
describe('and without permissions', () => {
|
||||||
describe('and with valid license', () => {
|
it('should throw not authorized error', async () => {
|
||||||
beforeEach(async () => {
|
const response = await request(app)
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
.post('/graphql')
|
||||||
});
|
.set('Authorization', tokenWithoutPermissions)
|
||||||
|
.send({ query })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
describe('and without permissions', () => {
|
expect(response.body.errors).toBeDefined();
|
||||||
it('should throw not authorized error', async () => {
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', tokenWithoutPermissions)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and correct permissions', () => {
|
|
||||||
it('should return roles data', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', tokenWithPermissions)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getRoles: [
|
|
||||||
{
|
|
||||||
description: currentUserRole.description,
|
|
||||||
id: currentUserRole.id,
|
|
||||||
isAdmin: currentUserRole.key === 'admin',
|
|
||||||
key: currentUserRole.key,
|
|
||||||
name: currentUserRole.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: roleOne.description,
|
|
||||||
id: roleOne.id,
|
|
||||||
isAdmin: roleOne.key === 'admin',
|
|
||||||
key: roleOne.key,
|
|
||||||
name: roleOne.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: roleSecond.description,
|
|
||||||
id: roleSecond.id,
|
|
||||||
isAdmin: roleSecond.key === 'admin',
|
|
||||||
key: roleSecond.key,
|
|
||||||
name: roleSecond.name,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and without valid license', () => {
|
describe('and correct permissions', () => {
|
||||||
beforeEach(async () => {
|
it('should return roles data', async () => {
|
||||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
|
const response = await request(app)
|
||||||
|
.post('/graphql')
|
||||||
|
.set('Authorization', tokenWithPermissions)
|
||||||
|
.send({ query })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedResponsePayload = {
|
||||||
|
data: {
|
||||||
|
getRoles: [
|
||||||
|
{
|
||||||
|
description: currentUserRole.description,
|
||||||
|
id: currentUserRole.id,
|
||||||
|
isAdmin: currentUserRole.key === 'admin',
|
||||||
|
key: currentUserRole.key,
|
||||||
|
name: currentUserRole.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: roleOne.description,
|
||||||
|
id: roleOne.id,
|
||||||
|
isAdmin: roleOne.key === 'admin',
|
||||||
|
key: roleOne.key,
|
||||||
|
name: roleOne.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: roleSecond.description,
|
||||||
|
id: roleSecond.id,
|
||||||
|
isAdmin: roleSecond.key === 'admin',
|
||||||
|
key: roleSecond.key,
|
||||||
|
name: roleSecond.name,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('and correct permissions', () => {
|
describe('and without valid license', () => {
|
||||||
it('should throw not authorized error', async () => {
|
beforeEach(async () => {
|
||||||
const response = await request(app)
|
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
|
||||||
.post('/graphql')
|
});
|
||||||
.set('Authorization', tokenWithPermissions)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
describe('and correct permissions', () => {
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
it('should throw not authorized error', async () => {
|
||||||
});
|
const response = await request(app)
|
||||||
|
.post('/graphql')
|
||||||
|
.set('Authorization', tokenWithPermissions)
|
||||||
|
.send({ query })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.errors).toBeDefined();
|
||||||
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -16,34 +16,46 @@ describe('graphQL getTrialStatus query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const invalidToken = 'invalid-token';
|
let user, userToken;
|
||||||
|
|
||||||
describe('with unauthenticated user', () => {
|
beforeEach(async () => {
|
||||||
it('should throw not authorized error', async () => {
|
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
|
|
||||||
|
user = await createUser({ trialExpiryDate });
|
||||||
|
userToken = createAuthTokenByUserId(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and with cloud flag disabled', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', invalidToken)
|
.set('Authorization', userToken)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
const expectedResponsePayload = {
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
data: { getTrialStatus: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
describe('and with cloud flag enabled', () => {
|
||||||
let user, userToken;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||||
|
|
||||||
user = await createUser({ trialExpiryDate });
|
|
||||||
userToken = createAuthTokenByUserId(user.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and with cloud flag disabled', () => {
|
describe('and not in trial and has active subscription', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
|
||||||
|
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null', async () => {
|
it('should return null', async () => {
|
||||||
@@ -61,56 +73,27 @@ describe('graphQL getTrialStatus query', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and with cloud flag enabled', () => {
|
describe('and in trial period', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and not in trial and has active subscription', () => {
|
it('should return null', async () => {
|
||||||
beforeEach(async () => {
|
const response = await request(app)
|
||||||
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
|
.post('/graphql')
|
||||||
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(
|
.set('Authorization', userToken)
|
||||||
true
|
.send({ query })
|
||||||
);
|
.expect(200);
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', async () => {
|
const expectedResponsePayload = {
|
||||||
const response = await request(app)
|
data: {
|
||||||
.post('/graphql')
|
getTrialStatus: {
|
||||||
.set('Authorization', userToken)
|
expireAt: new Date(user.trialExpiryDate).getTime().toString(),
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: { getTrialStatus: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and in trial period', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', userToken)
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getTrialStatus: {
|
|
||||||
expireAt: new Date(user.trialExpiryDate).getTime().toString(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -8,37 +8,12 @@ import { createPermission } from '../../../test/factories/permission';
|
|||||||
import { createUser } from '../../../test/factories/user';
|
import { createUser } from '../../../test/factories/user';
|
||||||
|
|
||||||
describe('graphQL getUser query', () => {
|
describe('graphQL getUser query', () => {
|
||||||
describe('with unauthenticated user', () => {
|
describe('and without permissions', () => {
|
||||||
it('should throw not authorized error', async () => {
|
it('should throw not authorized error', async () => {
|
||||||
const invalidUserId = '123123123';
|
const userWithoutPermissions = await createUser();
|
||||||
|
const anotherUser = await createUser();
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
|
||||||
getUser(id: "${invalidUserId}") {
|
|
||||||
id
|
|
||||||
email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', 'invalid-token')
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
|
||||||
describe('and without permissions', () => {
|
|
||||||
it('should throw not authorized error', async () => {
|
|
||||||
const userWithoutPermissions = await createUser();
|
|
||||||
const anotherUser = await createUser();
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
query {
|
query {
|
||||||
getUser(id: "${anotherUser.id}") {
|
getUser(id: "${anotherUser.id}") {
|
||||||
id
|
id
|
||||||
@@ -47,50 +22,48 @@ describe('graphQL getUser query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and correct permissions', () => {
|
||||||
|
let role, currentUser, anotherUser, token, requestObject;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
role = await createRole({
|
||||||
|
key: 'sample',
|
||||||
|
name: 'sample',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'User',
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
anotherUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
requestObject = request(app).post('/graphql').set('Authorization', token);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and correct permissions', () => {
|
it('should return user data for a valid user id', async () => {
|
||||||
let role, currentUser, anotherUser, token, requestObject;
|
const query = `
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
role = await createRole({
|
|
||||||
key: 'sample',
|
|
||||||
name: 'sample',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
action: 'read',
|
|
||||||
subject: 'User',
|
|
||||||
roleId: role.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
currentUser = await createUser({
|
|
||||||
roleId: role.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
anotherUser = await createUser({
|
|
||||||
roleId: role.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
token = createAuthTokenByUserId(currentUser.id);
|
|
||||||
requestObject = request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', token);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return user data for a valid user id', async () => {
|
|
||||||
const query = `
|
|
||||||
query {
|
query {
|
||||||
getUser(id: "${anotherUser.id}") {
|
getUser(id: "${anotherUser.id}") {
|
||||||
id
|
id
|
||||||
@@ -107,26 +80,26 @@ describe('graphQL getUser query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await requestObject.send({ query }).expect(200);
|
const response = await requestObject.send({ query }).expect(200);
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
const expectedResponsePayload = {
|
||||||
data: {
|
data: {
|
||||||
getUser: {
|
getUser: {
|
||||||
createdAt: anotherUser.createdAt.getTime().toString(),
|
createdAt: anotherUser.createdAt.getTime().toString(),
|
||||||
email: anotherUser.email,
|
email: anotherUser.email,
|
||||||
fullName: anotherUser.fullName,
|
fullName: anotherUser.fullName,
|
||||||
id: anotherUser.id,
|
id: anotherUser.id,
|
||||||
role: { id: role.id, name: role.name },
|
role: { id: role.id, name: role.name },
|
||||||
updatedAt: anotherUser.updatedAt.getTime().toString(),
|
updatedAt: anotherUser.updatedAt.getTime().toString(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return user password for a valid user id', async () => {
|
it('should not return user password for a valid user id', async () => {
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
query {
|
||||||
getUser(id: "${anotherUser.id}") {
|
getUser(id: "${anotherUser.id}") {
|
||||||
id
|
id
|
||||||
@@ -136,18 +109,18 @@ describe('graphQL getUser query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await requestObject.send({ query }).expect(400);
|
const response = await requestObject.send({ query }).expect(400);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual(
|
expect(response.body.errors[0].message).toEqual(
|
||||||
'Cannot query field "password" on type "User".'
|
'Cannot query field "password" on type "User".'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found for invalid user id', async () => {
|
it('should return not found for invalid user id', async () => {
|
||||||
const invalidUserId = Crypto.randomUUID();
|
const invalidUserId = Crypto.randomUUID();
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
query {
|
||||||
getUser(id: "${invalidUserId}") {
|
getUser(id: "${invalidUserId}") {
|
||||||
id
|
id
|
||||||
@@ -164,11 +137,10 @@ describe('graphQL getUser query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await requestObject.send({ query }).expect(200);
|
const response = await requestObject.send({ query }).expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('NotFoundError');
|
expect(response.body.errors[0].message).toEqual('NotFoundError');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -30,111 +30,95 @@ describe('graphQL getUsers query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
describe('with unauthenticated user', () => {
|
describe('and without permissions', () => {
|
||||||
it('should throw not authorized error', async () => {
|
it('should throw not authorized error', async () => {
|
||||||
|
const userWithoutPermissions = await createUser();
|
||||||
|
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/graphql')
|
.post('/graphql')
|
||||||
.set('Authorization', 'invalid-token')
|
.set('Authorization', token)
|
||||||
.send({ query })
|
.send({ query })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual('Not Authorised!');
|
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with authenticated user', () => {
|
describe('and with correct permissions', () => {
|
||||||
describe('and without permissions', () => {
|
let role, currentUser, anotherUser, token, requestObject;
|
||||||
it('should throw not authorized error', async () => {
|
|
||||||
const userWithoutPermissions = await createUser();
|
|
||||||
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
|
||||||
|
|
||||||
const response = await request(app)
|
beforeEach(async () => {
|
||||||
.post('/graphql')
|
role = await createRole({
|
||||||
.set('Authorization', token)
|
key: 'sample',
|
||||||
.send({ query })
|
name: 'sample',
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
|
||||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'User',
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
fullName: 'Current User',
|
||||||
|
});
|
||||||
|
|
||||||
|
anotherUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
fullName: 'Another User',
|
||||||
|
});
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
requestObject = request(app).post('/graphql').set('Authorization', token);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and with correct permissions', () => {
|
it('should return users data', async () => {
|
||||||
let role, currentUser, anotherUser, token, requestObject;
|
const response = await requestObject.send({ query }).expect(200);
|
||||||
|
|
||||||
beforeEach(async () => {
|
const expectedResponsePayload = {
|
||||||
role = await createRole({
|
data: {
|
||||||
key: 'sample',
|
getUsers: {
|
||||||
name: 'sample',
|
edges: [
|
||||||
});
|
{
|
||||||
|
node: {
|
||||||
await createPermission({
|
email: anotherUser.email,
|
||||||
action: 'read',
|
fullName: anotherUser.fullName,
|
||||||
subject: 'User',
|
id: anotherUser.id,
|
||||||
roleId: role.id,
|
role: {
|
||||||
});
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
currentUser = await createUser({
|
|
||||||
roleId: role.id,
|
|
||||||
fullName: 'Current User',
|
|
||||||
});
|
|
||||||
|
|
||||||
anotherUser = await createUser({
|
|
||||||
roleId: role.id,
|
|
||||||
fullName: 'Another User',
|
|
||||||
});
|
|
||||||
|
|
||||||
token = createAuthTokenByUserId(currentUser.id);
|
|
||||||
requestObject = request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', token);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return users data', async () => {
|
|
||||||
const response = await requestObject.send({ query }).expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getUsers: {
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
email: anotherUser.email,
|
|
||||||
fullName: anotherUser.fullName,
|
|
||||||
id: anotherUser.id,
|
|
||||||
role: {
|
|
||||||
id: role.id,
|
|
||||||
name: role.name,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
node: {
|
|
||||||
email: currentUser.email,
|
|
||||||
fullName: currentUser.fullName,
|
|
||||||
id: currentUser.id,
|
|
||||||
role: {
|
|
||||||
id: role.id,
|
|
||||||
name: role.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pageInfo: {
|
|
||||||
currentPage: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
},
|
},
|
||||||
totalCount: 2,
|
{
|
||||||
|
node: {
|
||||||
|
email: currentUser.email,
|
||||||
|
fullName: currentUser.fullName,
|
||||||
|
id: currentUser.id,
|
||||||
|
role: {
|
||||||
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pageInfo: {
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 1,
|
||||||
},
|
},
|
||||||
|
totalCount: 2,
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return users data with password', async () => {
|
it('should not return users data with password', async () => {
|
||||||
const query = `
|
const query = `
|
||||||
query {
|
query {
|
||||||
getUsers(limit: 10, offset: 0) {
|
getUsers(limit: 10, offset: 0) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
@@ -153,13 +137,12 @@ describe('graphQL getUsers query', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await requestObject.send({ query }).expect(400);
|
const response = await requestObject.send({ query }).expect(400);
|
||||||
|
|
||||||
expect(response.body.errors).toBeDefined();
|
expect(response.body.errors).toBeDefined();
|
||||||
expect(response.body.errors[0].message).toEqual(
|
expect(response.body.errors[0].message).toEqual(
|
||||||
'Cannot query field "password" on type "User".'
|
'Cannot query field "password" on type "User".'
|
||||||
);
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
|
|||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import User from '../models/user.js';
|
import User from '../models/user.js';
|
||||||
|
|
||||||
const isAuthenticated = rule()(async (_parent, _args, req) => {
|
export const isAuthenticated = async (_parent, _args, req) => {
|
||||||
const token = req.headers['authorization'];
|
const token = req.headers['authorization'];
|
||||||
|
|
||||||
if (token == null) return false;
|
if (token == null) return false;
|
||||||
@@ -26,29 +26,32 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
const authentication = shield(
|
const isAuthenticatedRule = rule()(isAuthenticated);
|
||||||
{
|
|
||||||
Query: {
|
export const authenticationRules = {
|
||||||
'*': isAuthenticated,
|
Query: {
|
||||||
getAutomatischInfo: allow,
|
'*': isAuthenticatedRule,
|
||||||
getConfig: allow,
|
getAutomatischInfo: allow,
|
||||||
getNotifications: allow,
|
getConfig: allow,
|
||||||
healthcheck: allow,
|
getNotifications: allow,
|
||||||
listSamlAuthProviders: allow,
|
healthcheck: allow,
|
||||||
},
|
listSamlAuthProviders: allow,
|
||||||
Mutation: {
|
|
||||||
'*': isAuthenticated,
|
|
||||||
forgotPassword: allow,
|
|
||||||
login: allow,
|
|
||||||
registerUser: allow,
|
|
||||||
resetPassword: allow,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
Mutation: {
|
||||||
allowExternalErrors: true,
|
'*': isAuthenticatedRule,
|
||||||
}
|
forgotPassword: allow,
|
||||||
);
|
login: allow,
|
||||||
|
registerUser: allow,
|
||||||
|
resetPassword: allow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticationOptions = {
|
||||||
|
allowExternalErrors: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authentication = shield(authenticationRules, authenticationOptions);
|
||||||
|
|
||||||
export default authentication;
|
export default authentication;
|
||||||
|
78
packages/backend/src/helpers/authentication.test.js
Normal file
78
packages/backend/src/helpers/authentication.test.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { allow } from 'graphql-shield';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import User from '../models/user.js';
|
||||||
|
import { isAuthenticated, authenticationRules } from './authentication.js';
|
||||||
|
|
||||||
|
vi.mock('jsonwebtoken');
|
||||||
|
vi.mock('../models/user.js');
|
||||||
|
|
||||||
|
describe('isAuthenticated', () => {
|
||||||
|
it('should return false if no token is provided', async () => {
|
||||||
|
const req = { headers: {} };
|
||||||
|
expect(await isAuthenticated(null, null, req)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if token is invalid', async () => {
|
||||||
|
jwt.verify.mockImplementation(() => {
|
||||||
|
throw new Error('invalid token');
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { headers: { authorization: 'invalidToken' } };
|
||||||
|
expect(await isAuthenticated(null, null, req)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if token is valid', async () => {
|
||||||
|
jwt.verify.mockReturnValue({ userId: '123' });
|
||||||
|
|
||||||
|
User.query.mockReturnValue({
|
||||||
|
findById: vi.fn().mockReturnValue({
|
||||||
|
leftJoinRelated: vi.fn().mockReturnThis(),
|
||||||
|
withGraphFetched: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { headers: { authorization: 'validToken' } };
|
||||||
|
expect(await isAuthenticated(null, null, req)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authentication rules', () => {
|
||||||
|
const getQueryAndMutationNames = (rules) => {
|
||||||
|
const queries = Object.keys(rules.Query || {});
|
||||||
|
const mutations = Object.keys(rules.Mutation || {});
|
||||||
|
return { queries, mutations };
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (query === '*') {
|
||||||
|
expect(ruleApplied.func).toBe(isAuthenticated);
|
||||||
|
} else {
|
||||||
|
expect(ruleApplied).toEqual(allow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for mutations', () => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
it(`should apply correct rule for mutation: ${mutation}`, () => {
|
||||||
|
const ruleApplied = authenticationRules.Mutation[mutation];
|
||||||
|
|
||||||
|
if (mutation === '*') {
|
||||||
|
expect(ruleApplied.func).toBe(isAuthenticated);
|
||||||
|
} else {
|
||||||
|
expect(ruleApplied).toBe(allow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,6 +1,9 @@
|
|||||||
import * as path from 'path';
|
import path from 'path';
|
||||||
import * as fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as handlebars from 'handlebars';
|
import handlebars from 'handlebars';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const compileEmail = (emailPath, replacements = {}) => {
|
const compileEmail = (emailPath, replacements = {}) => {
|
||||||
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);
|
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);
|
||||||
|
@@ -15,7 +15,7 @@ const webUIHandler = async (app) => {
|
|||||||
app.use(express.static(webBuildPath));
|
app.use(express.static(webBuildPath));
|
||||||
|
|
||||||
app.get('*', (_req, res) => {
|
app.get('*', (_req, res) => {
|
||||||
res.set('Content-Security-Policy', 'frame-ancestors: none;');
|
res.set('Content-Security-Policy', 'frame-ancestors \'none\';');
|
||||||
res.set('X-Frame-Options', 'DENY');
|
res.set('X-Frame-Options', 'DENY');
|
||||||
|
|
||||||
res.sendFile(indexHtml);
|
res.sendFile(indexHtml);
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
@@ -1 +0,0 @@
|
|||||||
/dist
|
|
16
packages/cli/.github/dependabot.yml
vendored
16
packages/cli/.github/dependabot.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: 'npm'
|
|
||||||
versioning-strategy: increase
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: 'monthly'
|
|
||||||
labels:
|
|
||||||
- 'dependencies'
|
|
||||||
open-pull-requests-limit: 100
|
|
||||||
pull-request-branch-name:
|
|
||||||
separator: '-'
|
|
||||||
ignore:
|
|
||||||
- dependency-name: 'fs-extra'
|
|
||||||
- dependency-name: '*'
|
|
||||||
update-types: ['version-update:semver-major']
|
|
9
packages/cli/.gitignore
vendored
9
packages/cli/.gitignore
vendored
@@ -1,9 +0,0 @@
|
|||||||
*-debug.log
|
|
||||||
*-error.log
|
|
||||||
/.nyc_output
|
|
||||||
/dist
|
|
||||||
/lib
|
|
||||||
/package-lock.json
|
|
||||||
/tmp
|
|
||||||
node_modules
|
|
||||||
oclif.manifest.json
|
|
@@ -1,4 +0,0 @@
|
|||||||
# `@automatisch/cli`
|
|
||||||
|
|
||||||
The open source Zapier alternative. Build workflow automation without spending
|
|
||||||
time and money.
|
|
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const oclif = require('@oclif/core')
|
|
||||||
|
|
||||||
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
|
|
@@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
node "%~dp0\automatisch" %*
|
|
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@automatisch/cli",
|
|
||||||
"version": "0.10.0",
|
|
||||||
"license": "See LICENSE file",
|
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "automatisch contributors",
|
|
||||||
"url": "https://github.com/automatisch/automatisch/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/automatisch/automatisch#readme",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"bin": {
|
|
||||||
"automatisch": "./bin/automatisch"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"/bin",
|
|
||||||
"/dist",
|
|
||||||
"oclif.manifest.json"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/automatisch/automatisch.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "shx rm -rf dist && tsc -b",
|
|
||||||
"build:watch": "nodemon --watch 'src/**/*.ts' --exec 'shx rm -rf dist && tsc -b' --ext 'ts'",
|
|
||||||
"lint": "eslint . --ext .js --ignore-path ../../.eslintignore",
|
|
||||||
"postpack": "shx rm -f oclif.manifest.json",
|
|
||||||
"posttest": "yarn lint",
|
|
||||||
"prepack": "yarn build && oclif manifest && oclif readme",
|
|
||||||
"version": "oclif readme && git add README.md"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@automatisch/backend": "^0.10.0",
|
|
||||||
"@oclif/core": "^1",
|
|
||||||
"@oclif/plugin-help": "^5",
|
|
||||||
"@oclif/plugin-plugins": "^2.0.1",
|
|
||||||
"dotenv": "^10.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@oclif/test": "^2",
|
|
||||||
"@types/node": "^16.9.4",
|
|
||||||
"eslint-config-oclif": "^4",
|
|
||||||
"eslint-config-oclif-typescript": "^1.0.2",
|
|
||||||
"globby": "^11",
|
|
||||||
"oclif": "^2",
|
|
||||||
"shx": "^0.3.3",
|
|
||||||
"ts-node": "^10.2.1",
|
|
||||||
"tslib": "^2.3.1",
|
|
||||||
"typescript": "^4.6.3"
|
|
||||||
},
|
|
||||||
"oclif": {
|
|
||||||
"bin": "automatisch",
|
|
||||||
"dirname": "automatisch",
|
|
||||||
"commands": "./dist/commands",
|
|
||||||
"plugins": [
|
|
||||||
"@oclif/plugin-help",
|
|
||||||
"@oclif/plugin-plugins"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
|
||||||
},
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,50 +0,0 @@
|
|||||||
import { readFileSync } from 'fs';
|
|
||||||
import { Command, Flags } from '@oclif/core';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
import process from 'process';
|
|
||||||
|
|
||||||
export default class StartWorker extends Command {
|
|
||||||
static description = 'Run automatisch worker';
|
|
||||||
|
|
||||||
static flags = {
|
|
||||||
env: Flags.string({
|
|
||||||
multiple: true,
|
|
||||||
char: 'e',
|
|
||||||
}),
|
|
||||||
'env-file': Flags.string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
async prepareEnvVars() {
|
|
||||||
const { flags } = await this.parse(StartWorker);
|
|
||||||
|
|
||||||
if (flags['env-file']) {
|
|
||||||
const envFile = readFileSync(flags['env-file'], 'utf8');
|
|
||||||
const envConfig = dotenv.parse(envFile);
|
|
||||||
|
|
||||||
for (const key in envConfig) {
|
|
||||||
const value = envConfig[key];
|
|
||||||
process.env[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags.env) {
|
|
||||||
for (const env of flags.env) {
|
|
||||||
const [key, value] = env.split('=');
|
|
||||||
process.env[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// must serve until more customization is introduced
|
|
||||||
delete process.env.SERVE_WEB_APP_SEPARATELY;
|
|
||||||
}
|
|
||||||
|
|
||||||
async runWorker() {
|
|
||||||
await import('@automatisch/backend/worker');
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
await this.prepareEnvVars();
|
|
||||||
|
|
||||||
await this.runWorker();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,96 +0,0 @@
|
|||||||
import { readFileSync } from 'fs';
|
|
||||||
import { Command, Flags } from '@oclif/core';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
import process from 'process';
|
|
||||||
|
|
||||||
export default class Start extends Command {
|
|
||||||
static description = 'Run automatisch';
|
|
||||||
|
|
||||||
static flags = {
|
|
||||||
env: Flags.string({
|
|
||||||
multiple: true,
|
|
||||||
char: 'e',
|
|
||||||
}),
|
|
||||||
'env-file': Flags.string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
get isProduction() {
|
|
||||||
return process.env.APP_ENV === 'production';
|
|
||||||
}
|
|
||||||
|
|
||||||
async prepareEnvVars() {
|
|
||||||
const { flags } = await this.parse(Start);
|
|
||||||
|
|
||||||
if (flags['env-file']) {
|
|
||||||
const envFile = readFileSync(flags['env-file'], 'utf8');
|
|
||||||
const envConfig = dotenv.parse(envFile);
|
|
||||||
|
|
||||||
for (const key in envConfig) {
|
|
||||||
const value = envConfig[key];
|
|
||||||
process.env[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags.env) {
|
|
||||||
for (const env of flags.env) {
|
|
||||||
const [key, value] = env.split('=');
|
|
||||||
process.env[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// must serve until more customization is introduced
|
|
||||||
delete process.env.SERVE_WEB_APP_SEPARATELY;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createDatabaseAndUser() {
|
|
||||||
const utils = await import('@automatisch/backend/database-utils');
|
|
||||||
|
|
||||||
await utils.createDatabaseAndUser(
|
|
||||||
process.env.POSTGRES_DATABASE,
|
|
||||||
process.env.POSTGRES_USERNAME
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationsIfNeeded() {
|
|
||||||
const { logger } = await import('@automatisch/backend/logger');
|
|
||||||
const database = await import('@automatisch/backend/database');
|
|
||||||
const migrator = database.client.migrate;
|
|
||||||
|
|
||||||
const [, pendingMigrations] = await migrator.list();
|
|
||||||
const pendingMigrationsCount = pendingMigrations.length;
|
|
||||||
const needsToMigrate = pendingMigrationsCount > 0;
|
|
||||||
|
|
||||||
if (needsToMigrate) {
|
|
||||||
logger.info(`Processing ${pendingMigrationsCount} migrations.`);
|
|
||||||
|
|
||||||
await migrator.latest();
|
|
||||||
logger.info(`Completed ${pendingMigrationsCount} migrations.`);
|
|
||||||
} else {
|
|
||||||
logger.info('No migrations needed.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async seedUser() {
|
|
||||||
const utils = await import('@automatisch/backend/database-utils');
|
|
||||||
|
|
||||||
await utils.createUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
async runApp() {
|
|
||||||
await import('@automatisch/backend/server');
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
await this.prepareEnvVars();
|
|
||||||
|
|
||||||
if (!this.isProduction) {
|
|
||||||
await this.createDatabaseAndUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.runMigrationsIfNeeded();
|
|
||||||
|
|
||||||
await this.seedUser();
|
|
||||||
|
|
||||||
await this.runApp();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
export { run } from '@oclif/core';
|
|
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"importHelpers": true,
|
|
||||||
"lib": ["es2021"],
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": "src",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"target": "es2021",
|
|
||||||
"typeRoots": ["node_modules/@types", "./src/types"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
15993
packages/cli/yarn.lock
15993
packages/cli/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,15 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
sidebar: {
|
sidebar: {
|
||||||
'/apps/': [
|
'/apps/': [
|
||||||
|
{
|
||||||
|
text: 'Airbrake',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/airbrake/triggers' },
|
||||||
|
{ text: 'Connection', link: '/apps/airbrake/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Carbone',
|
text: 'Carbone',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
@@ -305,7 +314,7 @@ export default defineConfig({
|
|||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Actions', link: '/apps/removebg/actions' },
|
{ text: 'Actions', link: '/apps/removebg/actions' },
|
||||||
{ text: 'Connection', link: '/apps/removebg/connection' }
|
{ text: 'Connection', link: '/apps/removebg/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -14,31 +14,33 @@ The default values for some environment variables might be different in our deve
|
|||||||
Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work.
|
Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
| Variable Name | Type | Default Value | Description |
|
| Variable Name | Type | Default Value | Description |
|
||||||
| --------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
|
| ---------------------------- | ------- | ------------------ | ----------------------------------------------------------------------------------- |
|
||||||
| `HOST` | string | `localhost` | HTTP Host |
|
| `HOST` | string | `localhost` | HTTP Host |
|
||||||
| `PROTOCOL` | string | `http` | HTTP Protocol |
|
| `PROTOCOL` | string | `http` | HTTP Protocol |
|
||||||
| `PORT` | string | `3000` | HTTP Port |
|
| `PORT` | string | `3000` | HTTP Port |
|
||||||
| `APP_ENV` | string | `production` | Automatisch Environment |
|
| `APP_ENV` | string | `production` | Automatisch Environment |
|
||||||
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
|
||||||
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
|
||||||
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
|
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
|
||||||
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
|
||||||
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
|
||||||
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
| `POSTGRES_PORT` | number | `5432` | Database Port |
|
||||||
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
|
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
|
||||||
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
| `POSTGRES_HOST` | string | `postgres` | Database Host |
|
||||||
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
|
||||||
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
| `POSTGRES_PASSWORD` | string | | Password of Database User |
|
||||||
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
|
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
|
||||||
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
|
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
|
||||||
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
|
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
|
||||||
| `REDIS_HOST` | string | `redis` | Redis Host |
|
| `REDIS_HOST` | string | `redis` | Redis Host |
|
||||||
| `REDIS_PORT` | number | `6379` | Redis Port |
|
| `REDIS_PORT` | number | `6379` | Redis Port |
|
||||||
| `REDIS_USERNAME` | string | | Redis Username |
|
| `REDIS_USERNAME` | string | | Redis Username |
|
||||||
| `REDIS_PASSWORD` | string | | Redis Password |
|
| `REDIS_PASSWORD` | string | | Redis Password |
|
||||||
| `REDIS_TLS` | boolean | `false` | Redis TLS |
|
| `REDIS_TLS` | boolean | `false` | Redis TLS |
|
||||||
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
|
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
|
||||||
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
|
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
|
||||||
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
|
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
|
||||||
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
|
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
|
||||||
|
| `DISABLE_NOTIFICATIONS_PAGE` | boolean | `false` | Enable/Disable notifications page |
|
||||||
|
| `DISABLE_FAVICON` | boolean | `false` | Enable/Disable favicon |
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# Telemetry
|
# Telemetry
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
We want to be very transparent about the data we collect and how we use it. Therefore, we have abstracted all of the code we use with our telemetry system into a single, easily accessible place. You can check the code [here](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/helpers/telemetry/index.ts) and let us know if you have any suggestions for changes.
|
We want to be very transparent about the data we collect and how we use it. Therefore, we have abstracted all of the code we use with our telemetry system into a single, easily accessible place. You can check the code [here](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/helpers/telemetry/index.js) and let us know if you have any suggestions for changes.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Automatisch comes with a built-in telemetry system that collects anonymous usage data. This data is used to help us improve the product and to make sure we are focusing on the right features. While we're doing it, we don't collect any personal information. You can also disable the telemetry system by setting the `TELEMETRY_ENABLED` environment variable. See the [environment variables](/advanced/configuration#environment-variables) section for more information.
|
Automatisch comes with a built-in telemetry system that collects anonymous usage data. This data is used to help us improve the product and to make sure we are focusing on the right features. While we're doing it, we don't collect any personal information. You can also disable the telemetry system by setting the `TELEMETRY_ENABLED` environment variable. See the [environment variables](/advanced/configuration#environment-variables) section for more information.
|
||||||
|
13
packages/docs/pages/apps/airbrake/connection.md
Normal file
13
packages/docs/pages/apps/airbrake/connection.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Airbrake
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Airbrake
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Login to your Airbrake account: [https://www.airbrake.io/](https://www.airbrake.io/).
|
||||||
|
2. Go to your profile & notifications page.
|
||||||
|
3. Copy `Auth Token` from the page to the `Auth Token` field on Automatisch.
|
||||||
|
4. Fill the instance URL field with your subdomain. (https://{yoursubdomain}.airbrake.io)
|
||||||
|
5. Write any screen name to be displayed in Automatisch.
|
||||||
|
6. Now, you can start using the Airbrake connection with Automatisch.
|
12
packages/docs/pages/apps/airbrake/triggers.md
Normal file
12
packages/docs/pages/apps/airbrake/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/airbrake.svg
|
||||||
|
items:
|
||||||
|
- name: New errors
|
||||||
|
desc: Triggers when a new error occurs.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
@@ -16,13 +16,13 @@ The build integrations section is best understood when read from beginning to en
|
|||||||
|
|
||||||
## Add actions to the app.
|
## Add actions to the app.
|
||||||
|
|
||||||
Open the `thecatapi/index.ts` file and add the highlighted lines for actions.
|
Open the `thecatapi/index.js` file and add the highlighted lines for actions.
|
||||||
|
|
||||||
```typescript{4,17}
|
```javascript{4,17}
|
||||||
import defineApp from '../../helpers/define-app';
|
import defineApp from '../../helpers/define-app.js';
|
||||||
import auth from './auth';
|
import auth from './auth/index.js';
|
||||||
import triggers from './triggers';
|
import triggers from './triggers/index.js';
|
||||||
import actions from './actions';
|
import actions from './actions/index.js';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'The cat API',
|
name: 'The cat API',
|
||||||
@@ -41,24 +41,24 @@ export default defineApp({
|
|||||||
|
|
||||||
## Define actions
|
## Define actions
|
||||||
|
|
||||||
Create the `actions/index.ts` file inside of the `thecatapi` folder.
|
Create the `actions/index.js` file inside of the `thecatapi` folder.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import markCatImageAsFavorite from './mark-cat-image-as-favorite';
|
import markCatImageAsFavorite from './mark-cat-image-as-favorite/index.js';
|
||||||
|
|
||||||
export default [markCatImageAsFavorite];
|
export default [markCatImageAsFavorite];
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
If you add new actions, you need to add them to the actions/index.ts file and export all actions as an array.
|
If you add new actions, you need to add them to the actions/index.js file and export all actions as an array.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Add metadata
|
## Add metadata
|
||||||
|
|
||||||
Create the `actions/mark-cat-image-as-favorite/index.ts` file inside the `thecatapi` folder.
|
Create the `actions/mark-cat-image-as-favorite/index.js` file inside the `thecatapi` folder.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import defineAction from '../../../../helpers/define-action';
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
export default defineAction({
|
export default defineAction({
|
||||||
name: 'Mark the cat image as favorite',
|
name: 'Mark the cat image as favorite',
|
||||||
@@ -68,7 +68,7 @@ export default defineAction({
|
|||||||
{
|
{
|
||||||
label: 'Image ID',
|
label: 'Image ID',
|
||||||
key: 'imageId',
|
key: 'imageId',
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'The ID of the cat image you want to mark as favorite.',
|
description: 'The ID of the cat image you want to mark as favorite.',
|
||||||
variables: true,
|
variables: true,
|
||||||
@@ -91,10 +91,10 @@ Let's briefly explain what we defined here.
|
|||||||
|
|
||||||
## Implement the action
|
## Implement the action
|
||||||
|
|
||||||
Open the `actions/mark-cat-image-as-favorite.ts` file and add the highlighted lines.
|
Open the `actions/mark-cat-image-as-favorite.js` file and add the highlighted lines.
|
||||||
|
|
||||||
```typescript{7-20}
|
```javascript{7-20}
|
||||||
import defineAction from '../../../../helpers/define-action';
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
export default defineAction({
|
export default defineAction({
|
||||||
// ...
|
// ...
|
||||||
@@ -104,7 +104,7 @@ export default defineAction({
|
|||||||
const imageId = $.step.parameters.imageId;
|
const imageId = $.step.parameters.imageId;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'x-api-key': $.auth.data.apiKey as string,
|
'x-api-key': $.auth.data.apiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await $.http.post(
|
const response = await $.http.post(
|
||||||
|
@@ -27,17 +27,17 @@ cd packages/backend/src/apps
|
|||||||
mkdir thecatapi
|
mkdir thecatapi
|
||||||
```
|
```
|
||||||
|
|
||||||
We need to create an `index.ts` file inside of the `thecatapi` folder.
|
We need to create an `index.js` file inside of the `thecatapi` folder.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd thecatapi
|
cd thecatapi
|
||||||
touch index.ts
|
touch index.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Then let's define the app inside of the `index.ts` file as follows:
|
Then let's define the app inside of the `index.js` file as follows:
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import defineApp from '../../helpers/define-app';
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'The cat API',
|
name: 'The cat API',
|
||||||
|
@@ -24,11 +24,11 @@ You can find detailed documentation of the cat API [here](https://docs.thecatapi
|
|||||||
|
|
||||||
## Add auth to the app
|
## Add auth to the app
|
||||||
|
|
||||||
Open the `thecatapi/index.ts` file and add the highlighted lines for authentication.
|
Open the `thecatapi/index.js` file and add the highlighted lines for authentication.
|
||||||
|
|
||||||
```typescript{2,13}
|
```javascript{2,13}
|
||||||
import defineApp from '../../helpers/define-app';
|
import defineApp from '../../helpers/define-app.js';
|
||||||
import auth from './auth';
|
import auth from './auth/index.js';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'The cat API',
|
name: 'The cat API',
|
||||||
@@ -45,22 +45,22 @@ export default defineApp({
|
|||||||
|
|
||||||
## Define auth fields
|
## Define auth fields
|
||||||
|
|
||||||
Let's create the `auth/index.ts` file inside of the `thecatapi` folder.
|
Let's create the `auth/index.js` file inside of the `thecatapi` folder.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir auth
|
mkdir auth
|
||||||
touch auth/index.ts
|
touch auth/index.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Then let's start with defining fields the auth inside of the `auth/index.ts` file as follows:
|
Then let's start with defining fields the auth inside of the `auth/index.js` file as follows:
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
export default {
|
export default {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'screenName',
|
key: 'screenName',
|
||||||
label: 'Screen Name',
|
label: 'Screen Name',
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
@@ -72,7 +72,7 @@ export default {
|
|||||||
{
|
{
|
||||||
key: 'apiKey',
|
key: 'apiKey',
|
||||||
label: 'API Key',
|
label: 'API Key',
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
@@ -101,10 +101,10 @@ If the third-party service you use provides both an API key and OAuth for the au
|
|||||||
|
|
||||||
So until now, we integrated auth folder with the app definition and defined the auth fields. Now we need to verify the credentials that the user entered. We will do that by defining the `verifyCredentials` method.
|
So until now, we integrated auth folder with the app definition and defined the auth fields. Now we need to verify the credentials that the user entered. We will do that by defining the `verifyCredentials` method.
|
||||||
|
|
||||||
Start with adding the `verifyCredentials` method to the `auth/index.ts` file.
|
Start with adding the `verifyCredentials` method to the `auth/index.js` file.
|
||||||
|
|
||||||
```typescript{1,8}
|
```javascript{1,8}
|
||||||
import verifyCredentials from './verify-credentials';
|
import verifyCredentials from './verify-credentials.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fields: [
|
fields: [
|
||||||
@@ -115,12 +115,10 @@ export default {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's create the `verify-credentials.ts` file inside the `auth` folder.
|
Let's create the `verify-credentials.js` file inside the `auth` folder.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import { IGlobalVariable } from '@automatisch/types';
|
const verifyCredentials = async ($) => {
|
||||||
|
|
||||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
|
||||||
// TODO: Implement verification of the credentials
|
// TODO: Implement verification of the credentials
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,12 +127,10 @@ export default verifyCredentials;
|
|||||||
|
|
||||||
We generally use the `users/me` endpoint or any other endpoint that we can validate the API key or any other credentials that the user provides. For our example, we don't have a specific API endpoint to check whether the credentials are correct or not. So we will randomly pick one of the API endpoints, which will be the `GET /v1/images/search` endpoint. We will send a request to this endpoint with the API key. If the API key is correct, we will get a response from the API. If the API key is incorrect, we will get an error response from the API.
|
We generally use the `users/me` endpoint or any other endpoint that we can validate the API key or any other credentials that the user provides. For our example, we don't have a specific API endpoint to check whether the credentials are correct or not. So we will randomly pick one of the API endpoints, which will be the `GET /v1/images/search` endpoint. We will send a request to this endpoint with the API key. If the API key is correct, we will get a response from the API. If the API key is incorrect, we will get an error response from the API.
|
||||||
|
|
||||||
Let's implement the authentication logic that we mentioned above in the `verify-credentials.ts` file.
|
Let's implement the authentication logic that we mentioned above in the `verify-credentials.js` file.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import { IGlobalVariable } from '@automatisch/types';
|
const verifyCredentials = async ($) => {
|
||||||
|
|
||||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
|
||||||
await $.http.get('/v1/images/search');
|
await $.http.get('/v1/images/search');
|
||||||
|
|
||||||
await $.auth.set({
|
await $.auth.set({
|
||||||
@@ -155,11 +151,11 @@ You must always provide a `screenName` field to auth data in the `verifyCredenti
|
|||||||
|
|
||||||
We have implemented the `verifyCredentials` method. Now we need to check whether the credentials are still valid or not for the test connection functionality in Automatisch. We will do that by defining the `isStillVerified` method.
|
We have implemented the `verifyCredentials` method. Now we need to check whether the credentials are still valid or not for the test connection functionality in Automatisch. We will do that by defining the `isStillVerified` method.
|
||||||
|
|
||||||
Start with adding the `isStillVerified` method to the `auth/index.ts` file.
|
Start with adding the `isStillVerified` method to the `auth/index.js` file.
|
||||||
|
|
||||||
```typescript{2,10}
|
```javascript{2,10}
|
||||||
import verifyCredentials from './verify-credentials';
|
import verifyCredentials from './verify-credentials.js';
|
||||||
import isStillVerified from './is-still-verified';
|
import isStillVerified from './is-still-verified.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fields: [
|
fields: [
|
||||||
@@ -171,13 +167,12 @@ export default {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's create the `is-still-verified.ts` file inside the `auth` folder.
|
Let's create the `is-still-verified.js` file inside the `auth` folder.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import { IGlobalVariable } from '@automatisch/types';
|
import verifyCredentials from './verify-credentials.js';
|
||||||
import verifyCredentials from './verify-credentials';
|
|
||||||
|
|
||||||
const isStillVerified = async ($: IGlobalVariable) => {
|
const isStillVerified = async ($) => {
|
||||||
await verifyCredentials($);
|
await verifyCredentials($);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
@@ -18,35 +18,35 @@ The build integrations section is best understood when read from beginning to en
|
|||||||
|
|
||||||
### 3-legged OAuth
|
### 3-legged OAuth
|
||||||
|
|
||||||
- [Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/auth/index.ts)
|
- [Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/auth/index.js)
|
||||||
- [Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/auth/index.ts)
|
- [Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/auth/index.js)
|
||||||
- [Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/auth/index.ts)
|
- [Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/auth/index.js)
|
||||||
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.ts)
|
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.js)
|
||||||
- [Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/auth/index.ts)
|
- [Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/auth/index.js)
|
||||||
- [Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/auth/index.ts)
|
- [Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/auth/index.js)
|
||||||
|
|
||||||
### OAuth with the refresh token
|
### OAuth with the refresh token
|
||||||
|
|
||||||
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.ts)
|
- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.js)
|
||||||
|
|
||||||
### API key
|
### API key
|
||||||
|
|
||||||
- [DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/auth/index.ts)
|
- [DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/auth/index.js)
|
||||||
- [Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/auth/index.ts)
|
- [Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/auth/index.js)
|
||||||
- [SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/auth/index.ts)
|
- [SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/auth/index.js)
|
||||||
- [SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/auth/index.ts)
|
- [SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/auth/index.js)
|
||||||
|
|
||||||
### Without authentication
|
### Without authentication
|
||||||
|
|
||||||
- [RSS](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/rss/index.ts)
|
- [RSS](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/rss/index.js)
|
||||||
- [Scheduler](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/scheduler/index.ts)
|
- [Scheduler](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/scheduler/index.js)
|
||||||
|
|
||||||
## Triggers
|
## Triggers
|
||||||
|
|
||||||
### Polling-based triggers
|
### Polling-based triggers
|
||||||
|
|
||||||
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts)
|
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.js)
|
||||||
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.ts)
|
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.js)
|
||||||
|
|
||||||
### Webhook-based triggers
|
### Webhook-based triggers
|
||||||
|
|
||||||
@@ -54,27 +54,27 @@ The build integrations section is best understood when read from beginning to en
|
|||||||
If you are developing a webhook-based trigger, you need to ensure that the webhook is publicly accessible. You can use [ngrok](https://ngrok.com) for this purpose and override the webhook URL by setting the **WEBHOOK_URL** environment variable.
|
If you are developing a webhook-based trigger, you need to ensure that the webhook is publicly accessible. You can use [ngrok](https://ngrok.com) for this purpose and override the webhook URL by setting the **WEBHOOK_URL** environment variable.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
- [New entry - Typeform](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/typeform/triggers/new-entry/index.ts)
|
- [New entry - Typeform](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/typeform/triggers/new-entry/index.js)
|
||||||
|
|
||||||
### Pagination with descending order
|
### Pagination with descending order
|
||||||
|
|
||||||
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts)
|
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.js)
|
||||||
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.ts)
|
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.js)
|
||||||
- [Receive SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/triggers/receive-sms/index.ts)
|
- [Receive SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/triggers/receive-sms/index.js)
|
||||||
- [Receive SMS - SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/triggers/receive-sms/index.ts)
|
- [Receive SMS - SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/triggers/receive-sms/index.js)
|
||||||
- [New photos - Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/triggers/new-photos/index.ts)
|
- [New photos - Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/triggers/new-photos/index.js)
|
||||||
|
|
||||||
### Pagination with ascending order
|
### Pagination with ascending order
|
||||||
|
|
||||||
- [New stargazers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-stargazers/index.ts)
|
- [New stargazers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-stargazers/index.js)
|
||||||
- [New watchers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-watchers/index.ts)
|
- [New watchers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-watchers/index.js)
|
||||||
|
|
||||||
## Actions
|
## Actions
|
||||||
|
|
||||||
- [Send a message to channel - Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.ts)
|
- [Send a message to channel - Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.js)
|
||||||
- [Send SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/actions/send-sms/index.ts)
|
- [Send SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/actions/send-sms/index.js)
|
||||||
- [Send a message to channel - Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/actions/send-message-to-channel/index.ts)
|
- [Send a message to channel - Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/actions/send-message-to-channel/index.js)
|
||||||
- [Create issue - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/actions/create-issue/index.ts)
|
- [Create issue - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/actions/create-issue/index.js)
|
||||||
- [Send an email - SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/actions/send-email/index.ts)
|
- [Send an email - SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/actions/send-email/index.js)
|
||||||
- [Create tweet - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/actions/create-tweet/index.ts)
|
- [Create tweet - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/actions/create-tweet/index.js)
|
||||||
- [Translate text - DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/actions/translate-text/index.ts)
|
- [Translate text - DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/actions/translate-text/index.js)
|
||||||
|
@@ -35,13 +35,13 @@ Here, you can see the folder structure of an example app. We will briefly walk t
|
|||||||
├── auth
|
├── auth
|
||||||
├── common
|
├── common
|
||||||
├── dynamic-data
|
├── dynamic-data
|
||||||
├── index.ts
|
├── index.js
|
||||||
└── triggers
|
└── triggers
|
||||||
```
|
```
|
||||||
|
|
||||||
## App
|
## App
|
||||||
|
|
||||||
The `index.ts` file is the entry point of the app. It contains the definition of the app and the app's metadata. It also includes the list of triggers, actions, and data sources that the app provides. So, whatever you build inside the app, you need to associate it within the `index.ts` file.
|
The `index.js` file is the entry point of the app. It contains the definition of the app and the app's metadata. It also includes the list of triggers, actions, and data sources that the app provides. So, whatever you build inside the app, you need to associate it within the `index.js` file.
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
|
@@ -16,11 +16,11 @@ The build integrations section is best understood when read from beginning to en
|
|||||||
|
|
||||||
Before handling authentication and building a trigger and an action, it's better to explain the `global variable` concept in Automatisch. Automatisch provides you the global variable that you need to use with authentication, triggers, actions, and basically all the stuff you will build for the integration.
|
Before handling authentication and building a trigger and an action, it's better to explain the `global variable` concept in Automatisch. Automatisch provides you the global variable that you need to use with authentication, triggers, actions, and basically all the stuff you will build for the integration.
|
||||||
|
|
||||||
The global variable is represented as `$` variable in the codebase, and it's a JSON object that contains the following properties:
|
The global variable is represented as `$` variable in the codebase, and it's a JS object that contains the following properties:
|
||||||
|
|
||||||
## $.auth.set
|
## $.auth.set
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
await $.auth.set({
|
await $.auth.set({
|
||||||
key: 'value',
|
key: 'value',
|
||||||
});
|
});
|
||||||
@@ -30,7 +30,7 @@ It's used to set the authentication data, and you can use this method with multi
|
|||||||
|
|
||||||
## $.auth.data
|
## $.auth.data
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.auth.data; // { key: 'value' }
|
$.auth.data; // { key: 'value' }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ It's used to retrieve the authentication data that we set with `$.auth.set()`. T
|
|||||||
|
|
||||||
## $.app.baseUrl
|
## $.app.baseUrl
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.app.baseUrl; // https://thecatapi.com
|
$.app.baseUrl; // https://thecatapi.com
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ It's used to retrieve the base URL of the app that we defined previously. In our
|
|||||||
|
|
||||||
## $.app.apiBaseUrl
|
## $.app.apiBaseUrl
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.app.apiBaseUrl; // https://api.thecatapi.com
|
$.app.apiBaseUrl; // https://api.thecatapi.com
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ It's used to retrieve the API base URL of the app that we defined previously. In
|
|||||||
|
|
||||||
## $.app.auth.fields
|
## $.app.auth.fields
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.app.auth.fields;
|
$.app.auth.fields;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ It's used to retrieve the fields that we defined in the `auth` section of the ap
|
|||||||
|
|
||||||
It's an HTTP client to be used for making HTTP requests. It's a wrapper around the [axios](https://axios-http.com) library. We use this property when we need to make HTTP requests to the third-party service. The `apiBaseUrl` field we set up in the app will be used as the base URL for the HTTP requests. For example, to search the cat images, we can use the following code:
|
It's an HTTP client to be used for making HTTP requests. It's a wrapper around the [axios](https://axios-http.com) library. We use this property when we need to make HTTP requests to the third-party service. The `apiBaseUrl` field we set up in the app will be used as the base URL for the HTTP requests. For example, to search the cat images, we can use the following code:
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
await $.http.get('/v1/images/search?order=DESC', {
|
await $.http.get('/v1/images/search?order=DESC', {
|
||||||
headers: {
|
headers: {
|
||||||
'x-api-key': $.auth.data.apiKey,
|
'x-api-key': $.auth.data.apiKey,
|
||||||
@@ -76,15 +76,15 @@ Keep in mind that the HTTP client handles the error with the status code that fa
|
|||||||
|
|
||||||
## $.step.parameters
|
## $.step.parameters
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.step.parameters; // { key: 'value' }
|
$.step.parameters; // { key: 'value' }
|
||||||
```
|
```
|
||||||
|
|
||||||
It refers to the parameters that are set by users in the UI. We use this property when we need to get the parameters for corresponding triggers and actions. For example [Send a message to channel](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.ts) action from Slack integration, we have a step parameter called `message` that we need to use in the action. We can use `$.step.parameters.message` to get the value of the message to send a message to the Slack channel.
|
It refers to the parameters that are set by users in the UI. We use this property when we need to get the parameters for corresponding triggers and actions. For example [Send a message to channel](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.js) action from Slack integration, we have a step parameter called `message` that we need to use in the action. We can use `$.step.parameters.message` to get the value of the message to send a message to the Slack channel.
|
||||||
|
|
||||||
## $.pushTriggerItem
|
## $.pushTriggerItem
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.pushTriggerItem({
|
$.pushTriggerItem({
|
||||||
raw: resourceData,
|
raw: resourceData,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -97,7 +97,7 @@ It's used to push trigger data to be processed by Automatisch. It must reflect t
|
|||||||
|
|
||||||
## $.setActionItem
|
## $.setActionItem
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
$.setActionItem({
|
$.setActionItem({
|
||||||
raw: resourceData,
|
raw: resourceData,
|
||||||
});
|
});
|
||||||
|
@@ -20,12 +20,12 @@ We used a polling-based HTTP trigger in our example but if you need to use a web
|
|||||||
|
|
||||||
## Add triggers to the app
|
## Add triggers to the app
|
||||||
|
|
||||||
Open the `thecatapi/index.ts` file and add the highlighted lines for triggers.
|
Open the `thecatapi/index.js` file and add the highlighted lines for triggers.
|
||||||
|
|
||||||
```typescript{3,15}
|
```javascript{3,15}
|
||||||
import defineApp from '../../helpers/define-app';
|
import defineApp from '../../helpers/define-app.js';
|
||||||
import auth from './auth';
|
import auth from './auth/index.js';
|
||||||
import triggers from './triggers';
|
import triggers from './triggers/index.js';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'The cat API',
|
name: 'The cat API',
|
||||||
@@ -43,24 +43,24 @@ export default defineApp({
|
|||||||
|
|
||||||
## Define triggers
|
## Define triggers
|
||||||
|
|
||||||
Create the `triggers/index.ts` file inside of the `thecatapi` folder.
|
Create the `triggers/index.js` file inside of the `thecatapi` folder.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import searchCatImages from './search-cat-images';
|
import searchCatImages from './search-cat-images/index.js';
|
||||||
|
|
||||||
export default [searchCatImages];
|
export default [searchCatImages];
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
If you add new triggers, you need to add them to the `triggers/index.ts` file and export all triggers as an array. The order of triggers in this array will be reflected in the Automatisch user interface.
|
If you add new triggers, you need to add them to the `triggers/index.js` file and export all triggers as an array. The order of triggers in this array will be reflected in the Automatisch user interface.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Add metadata
|
## Add metadata
|
||||||
|
|
||||||
Create the `triggers/search-cat-images/index.ts` file inside of the `thecatapi` folder.
|
Create the `triggers/search-cat-images/index.js` file inside of the `thecatapi` folder.
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
import defineTrigger from '../../../../helpers/define-trigger';
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
|
|
||||||
export default defineTrigger({
|
export default defineTrigger({
|
||||||
name: 'Search cat images',
|
name: 'Search cat images',
|
||||||
@@ -93,9 +93,8 @@ Let's briefly explain what we defined here.
|
|||||||
|
|
||||||
Implement the `run` function by adding highlighted lines.
|
Implement the `run` function by adding highlighted lines.
|
||||||
|
|
||||||
```typescript{1,7-30}
|
```javascript{1,7-30}
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
import defineTrigger from '../../../../helpers/define-trigger';
|
|
||||||
|
|
||||||
export default defineTrigger({
|
export default defineTrigger({
|
||||||
// ...
|
// ...
|
||||||
@@ -104,18 +103,18 @@ export default defineTrigger({
|
|||||||
let response;
|
let response;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'x-api-key': $.auth.data.apiKey as string,
|
'x-api-key': $.auth.data.apiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let requestPath = `/v1/images/search?page=${page}&limit=10&order=DESC`;
|
let requestPath = `/v1/images/search?page=${page}&limit=10&order=DESC`;
|
||||||
response = await $.http.get(requestPath, { headers });
|
response = await $.http.get(requestPath, { headers });
|
||||||
|
|
||||||
response.data.forEach((image: IJSONObject) => {
|
response.data.forEach((image) => {
|
||||||
const dataItem = {
|
const dataItem = {
|
||||||
raw: image,
|
raw: image,
|
||||||
meta: {
|
meta: {
|
||||||
internalId: image.id as string
|
internalId: image.id
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ yarn db:create
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
`yarn db:create` commands expect that you have the `postgres` superuser. If not, you can create a superuser called `postgres` manually or you can create the database manually by checking PostgreSQL-related default values from the [app config](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/config/app.ts).
|
`yarn db:create` commands expect that you have the `postgres` superuser. If not, you can create a superuser called `postgres` manually or you can create the database manually by checking PostgreSQL-related default values from the [app config](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/config/app.js).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Run the database migrations in the backend folder.
|
Run the database migrations in the backend folder.
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
The following integrations are currently supported by Automatisch.
|
The following integrations are currently supported by Automatisch.
|
||||||
|
|
||||||
- [Carbone](/apps/carbone/actions)
|
- [Carbone](/apps/carbone/actions)
|
||||||
|
- [Airbrake](/apps/airbrake/triggers)
|
||||||
- [DeepL](/apps/deepl/actions)
|
- [DeepL](/apps/deepl/actions)
|
||||||
- [Delay](/apps/delay/actions)
|
- [Delay](/apps/delay/actions)
|
||||||
- [Discord](/apps/discord/actions)
|
- [Discord](/apps/discord/actions)
|
||||||
|
1
packages/docs/pages/public/favicons/airbrake.svg
Normal file
1
packages/docs/pages/public/favicons/airbrake.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +0,0 @@
|
|||||||
# `@automatisch/types`
|
|
||||||
|
|
||||||
The open source Zapier alternative. Build workflow automation without spending
|
|
||||||
time and money.
|
|
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@automatisch/types",
|
|
||||||
"version": "0.10.0",
|
|
||||||
"license": "See LICENSE file",
|
|
||||||
"description": "Type definitions for automatisch",
|
|
||||||
"homepage": "https://github.com/automatisch/automatisch",
|
|
||||||
"types": "./index.d.ts",
|
|
||||||
"scripts": {},
|
|
||||||
"typeScriptVersion": "4.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/automatisch/automatisch.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,7 +5,6 @@
|
|||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.6.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@automatisch/types": "^0.10.0",
|
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@casl/react": "^3.1.0",
|
"@casl/react": "^3.1.0",
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -2,7 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#0059F7" />
|
<meta name="theme-color" content="#0059F7" />
|
||||||
<meta
|
<meta
|
||||||
|
@@ -2,13 +2,6 @@
|
|||||||
"short_name": "automatisch",
|
"short_name": "automatisch",
|
||||||
"name": "automatisch",
|
"name": "automatisch",
|
||||||
"description": "Build workflow automation without spending time and money. No code is required.",
|
"description": "Build workflow automation without spending time and money. No code is required.",
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { IApp, IField, IJSONObject } from '@automatisch/types';
|
import type { IApp, IField, IJSONObject } from 'types';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
@@ -19,7 +19,7 @@ import InputLabel from '@mui/material/InputLabel';
|
|||||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { IApp } from '@automatisch/types';
|
import type { IApp } from 'types';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { IField } from '@automatisch/types';
|
import type { IField } from 'types';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import type { IApp } from '@automatisch/types';
|
import type { IApp } from 'types';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { IApp } from '@automatisch/types';
|
import type { IApp } from 'types';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
||||||
|
@@ -15,6 +15,7 @@ import { SvgIconComponent } from '@mui/icons-material';
|
|||||||
import AppBar from 'components/AppBar';
|
import AppBar from 'components/AppBar';
|
||||||
import Drawer from 'components/Drawer';
|
import Drawer from 'components/Drawer';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
|
||||||
type SettingsLayoutProps = {
|
type SettingsLayoutProps = {
|
||||||
@@ -86,19 +87,11 @@ function createDrawerLinks({
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawerBottomLinks = [
|
|
||||||
{
|
|
||||||
Icon: ArrowBackIosNewIcon,
|
|
||||||
primary: 'adminSettingsDrawer.goBack',
|
|
||||||
to: '/',
|
|
||||||
dataTest: 'go-back-drawer-link',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function SettingsLayout({
|
export default function SettingsLayout({
|
||||||
children,
|
children,
|
||||||
}: SettingsLayoutProps): React.ReactElement {
|
}: SettingsLayoutProps): React.ReactElement {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
@@ -116,6 +109,15 @@ export default function SettingsLayout({
|
|||||||
canUpdateApp: currentUserAbility.can('update', 'App'),
|
canUpdateApp: currentUserAbility.can('update', 'App'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const drawerBottomLinks = [
|
||||||
|
{
|
||||||
|
Icon: ArrowBackIosNewIcon,
|
||||||
|
primary: formatMessage('adminSettingsDrawer.goBack'),
|
||||||
|
to: '/',
|
||||||
|
dataTest: 'go-back-drawer-link',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBar
|
<AppBar
|
||||||
|
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import type { PopoverProps } from '@mui/material/Popover';
|
import type { PopoverProps } from '@mui/material/Popover';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import type { IConnection } from '@automatisch/types';
|
import type { IConnection } from 'types';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
@@ -11,7 +11,7 @@ import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IConnection } from '@automatisch/types';
|
import type { IConnection } from 'types';
|
||||||
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
||||||
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
||||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||||
@@ -83,8 +83,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-delete-connection-success'
|
'data-test': 'snackbar-delete-connection-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else if (action.type === 'test') {
|
} else if (action.type === 'test') {
|
||||||
setVerificationVisible(true);
|
setVerificationVisible(true);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
|
|
||||||
import type { IConnection } from '@automatisch/types';
|
import type { IConnection } from 'types';
|
||||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||||
import AppConnectionRow from 'components/AppConnectionRow';
|
import AppConnectionRow from 'components/AppConnectionRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
@@ -8,7 +8,7 @@ import * as URLS from 'config/urls';
|
|||||||
import AppFlowRow from 'components/FlowRow';
|
import AppFlowRow from 'components/FlowRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import type { IFlow } from '@automatisch/types';
|
import type { IFlow } from 'types';
|
||||||
|
|
||||||
type AppFlowsProps = {
|
type AppFlowsProps = {
|
||||||
appKey: string;
|
appKey: string;
|
||||||
|
@@ -7,7 +7,7 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
|||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import type { IApp } from '@automatisch/types';
|
import type { IApp } from 'types';
|
||||||
|
|
||||||
import { CardContent, Typography } from './style';
|
import { CardContent, Typography } from './style';
|
||||||
|
|
||||||
|
@@ -7,13 +7,7 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import type {
|
import type { IApp, IStep, ISubstep, ITrigger, IAction } from 'types';
|
||||||
IApp,
|
|
||||||
IStep,
|
|
||||||
ISubstep,
|
|
||||||
ITrigger,
|
|
||||||
IAction,
|
|
||||||
} from '@automatisch/types';
|
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useApps from 'hooks/useApps';
|
import useApps from 'hooks/useApps';
|
||||||
|
@@ -6,7 +6,7 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types';
|
import type { IApp, IConnection, IStep, ISubstep } from 'types';
|
||||||
import AddAppConnection from 'components/AddAppConnection';
|
import AddAppConnection from 'components/AddAppConnection';
|
||||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import Autocomplete, { AutocompleteProps, createFilterOptions } from '@mui/material/Autocomplete';
|
import Autocomplete, {
|
||||||
|
AutocompleteProps,
|
||||||
|
createFilterOptions,
|
||||||
|
} from '@mui/material/Autocomplete';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
import type { IFieldDropdownOption } from 'types';
|
||||||
|
|
||||||
interface ControlledAutocompleteProps
|
interface ControlledAutocompleteProps
|
||||||
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
||||||
@@ -23,8 +26,8 @@ const filterOptions = createFilterOptions<IFieldDropdownOption>({
|
|||||||
stringify: ({ label, value }) => `
|
stringify: ({ label, value }) => `
|
||||||
${label}
|
${label}
|
||||||
${value}
|
${value}
|
||||||
`
|
`,
|
||||||
})
|
});
|
||||||
|
|
||||||
function ControlledAutocomplete(
|
function ControlledAutocomplete(
|
||||||
props: ControlledAutocompleteProps
|
props: ControlledAutocompleteProps
|
||||||
|
@@ -3,7 +3,7 @@ import Popper from '@mui/material/Popper';
|
|||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
import type { IFieldDropdownOption } from 'types';
|
||||||
import Suggestions from 'components/PowerInput/Suggestions';
|
import Suggestions from 'components/PowerInput/Suggestions';
|
||||||
import TabPanel from 'components/TabPanel';
|
import TabPanel from 'components/TabPanel';
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
import type { IFieldDropdownOption } from 'types';
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
@@ -13,7 +13,7 @@ import { SearchInputWrapper } from './style';
|
|||||||
interface OptionsProps {
|
interface OptionsProps {
|
||||||
data: readonly IFieldDropdownOption[];
|
data: readonly IFieldDropdownOption[];
|
||||||
onOptionClick: (event: React.MouseEvent, option: any) => void;
|
onOptionClick: (event: React.MouseEvent, option: any) => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
const SHORT_LIST_LENGTH = 4;
|
const SHORT_LIST_LENGTH = 4;
|
||||||
const LIST_ITEM_HEIGHT = 64;
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
@@ -21,80 +21,89 @@ const LIST_ITEM_HEIGHT = 64;
|
|||||||
const computeListHeight = (currentLength: number) => {
|
const computeListHeight = (currentLength: number) => {
|
||||||
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
|
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
|
||||||
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
|
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
|
||||||
}
|
|
||||||
|
|
||||||
const renderItemFactory = ({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'>) => (props: ListChildComponentProps) => {
|
|
||||||
const { index, style, data } = props;
|
|
||||||
|
|
||||||
const suboption = data[index];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItemButton
|
|
||||||
sx={{ pl: 4 }}
|
|
||||||
divider
|
|
||||||
onClick={(event) => onOptionClick(event, suboption)}
|
|
||||||
data-test="power-input-suggestion-item"
|
|
||||||
key={index}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={suboption.label}
|
|
||||||
primaryTypographyProps={{
|
|
||||||
variant: 'subtitle1',
|
|
||||||
title: 'Property name',
|
|
||||||
sx: { fontWeight: 700 },
|
|
||||||
}}
|
|
||||||
secondary={suboption.value}
|
|
||||||
secondaryTypographyProps={{
|
|
||||||
variant: 'subtitle2',
|
|
||||||
title: 'Sample value',
|
|
||||||
noWrap: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderItemFactory =
|
||||||
|
({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'>) =>
|
||||||
|
(props: ListChildComponentProps) => {
|
||||||
|
const { index, style, data } = props;
|
||||||
|
|
||||||
|
const suboption = data[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ pl: 4 }}
|
||||||
|
divider
|
||||||
|
onClick={(event) => onOptionClick(event, suboption)}
|
||||||
|
data-test="power-input-suggestion-item"
|
||||||
|
key={index}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={suboption.label}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
variant: 'subtitle1',
|
||||||
|
title: 'Property name',
|
||||||
|
sx: { fontWeight: 700 },
|
||||||
|
}}
|
||||||
|
secondary={suboption.value}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
variant: 'subtitle2',
|
||||||
|
title: 'Sample value',
|
||||||
|
noWrap: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Options = (props: OptionsProps) => {
|
const Options = (props: OptionsProps) => {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const { data, onOptionClick } = props;
|
||||||
data,
|
const [filteredData, setFilteredData] =
|
||||||
onOptionClick
|
React.useState<readonly IFieldDropdownOption[]>(data);
|
||||||
} = props;
|
|
||||||
const [filteredData, setFilteredData] = React.useState<readonly IFieldDropdownOption[]>(
|
React.useEffect(
|
||||||
data
|
function syncOptions() {
|
||||||
|
setFilteredData((filteredData) => {
|
||||||
|
if (filteredData.length === 0 && filteredData.length !== data.length) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredData;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(function syncOptions() {
|
const renderItem = React.useMemo(
|
||||||
setFilteredData((filteredData) => {
|
() =>
|
||||||
if (filteredData.length === 0 && filteredData.length !== data.length) {
|
renderItemFactory({
|
||||||
return data;
|
onOptionClick,
|
||||||
}
|
}),
|
||||||
|
[onOptionClick]
|
||||||
return filteredData;
|
);
|
||||||
})
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const renderItem = React.useMemo(() => renderItemFactory({
|
|
||||||
onOptionClick
|
|
||||||
}), [onOptionClick]);
|
|
||||||
|
|
||||||
const onSearchChange = React.useMemo(
|
const onSearchChange = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle((event: React.ChangeEvent) => {
|
throttle((event: React.ChangeEvent) => {
|
||||||
const search = (event.target as HTMLInputElement).value.toLowerCase();
|
const search = (event.target as HTMLInputElement).value.toLowerCase();
|
||||||
|
|
||||||
if (!search) {
|
if (!search) {
|
||||||
setFilteredData(data);
|
setFilteredData(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFilteredData = data.filter(option => `${option.label}\n${option.value}`.toLowerCase().includes(search.toLowerCase()));
|
const newFilteredData = data.filter((option) =>
|
||||||
|
`${option.label}\n${option.value}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
setFilteredData(newFilteredData);
|
setFilteredData(newFilteredData);
|
||||||
}, 400),
|
}, 400),
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -122,5 +131,4 @@ const Options = (props: OptionsProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default Options;
|
export default Options;
|
||||||
|
@@ -5,7 +5,7 @@ import FormHelperText from '@mui/material/FormHelperText';
|
|||||||
import { AutocompleteProps } from '@mui/material/Autocomplete';
|
import { AutocompleteProps } from '@mui/material/Autocomplete';
|
||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
import type { IFieldDropdownOption } from 'types';
|
||||||
import { ActionButtonsWrapper } from './style';
|
import { ActionButtonsWrapper } from './style';
|
||||||
|
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
|
@@ -19,6 +19,7 @@ type DrawerLink = {
|
|||||||
Icon: React.ElementType;
|
Icon: React.ElementType;
|
||||||
primary: string;
|
primary: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
target?: '_blank';
|
||||||
badgeContent?: React.ReactNode;
|
badgeContent?: React.ReactNode;
|
||||||
dataTest?: string;
|
dataTest?: string;
|
||||||
};
|
};
|
||||||
@@ -69,7 +70,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
|||||||
|
|
||||||
<List sx={{ py: 0, mt: 3 }}>
|
<List sx={{ py: 0, mt: 3 }}>
|
||||||
{bottomLinks.map(
|
{bottomLinks.map(
|
||||||
({ Icon, badgeContent, primary, to, dataTest }, index) => (
|
({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
|
||||||
<ListItemLink
|
<ListItemLink
|
||||||
key={`${to}-${index}`}
|
key={`${to}-${index}`}
|
||||||
icon={
|
icon={
|
||||||
@@ -77,9 +78,10 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
|||||||
<Icon htmlColor={theme.palette.primary.main} />
|
<Icon htmlColor={theme.palette.primary.main} />
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
primary={formatMessage(primary)}
|
primary={primary}
|
||||||
to={to}
|
to={to}
|
||||||
onClick={closeOnClick}
|
onClick={closeOnClick}
|
||||||
|
target={target}
|
||||||
data-test={dataTest}
|
data-test={dataTest}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -8,7 +8,7 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import RemoveIcon from '@mui/icons-material/Remove';
|
import RemoveIcon from '@mui/icons-material/Remove';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
|
||||||
import { IFieldDynamic } from '@automatisch/types';
|
import { IFieldDynamic } from 'types';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ import { useMutation } from '@apollo/client';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import type { IFlow, IStep } from '@automatisch/types';
|
import type { IFlow, IStep } from 'types';
|
||||||
|
|
||||||
import { GET_FLOW } from 'graphql/queries/get-flow';
|
import { GET_FLOW } from 'graphql/queries/get-flow';
|
||||||
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
import { CREATE_STEP } from 'graphql/mutations/create-step';
|
||||||
|
@@ -17,7 +17,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
|
import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status';
|
||||||
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
|
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
|
||||||
import { GET_FLOW } from 'graphql/queries/get-flow';
|
import { GET_FLOW } from 'graphql/queries/get-flow';
|
||||||
import type { IFlow } from '@automatisch/types';
|
import type { IFlow } from 'types';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
|
||||||
export default function EditorLayout(): React.ReactElement {
|
export default function EditorLayout(): React.ReactElement {
|
||||||
|
@@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import type { IExecution } from '@automatisch/types';
|
import type { IExecution } from 'types';
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import CardActionArea from '@mui/material/CardActionArea';
|
|||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type { IExecution } from '@automatisch/types';
|
import type { IExecution } from 'types';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
@@ -8,7 +8,7 @@ import Tab from '@mui/material/Tab';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { IApp, IExecutionStep, IStep } from '@automatisch/types';
|
import type { IApp, IExecutionStep, IStep } from 'types';
|
||||||
|
|
||||||
import TabPanel from 'components/TabPanel';
|
import TabPanel from 'components/TabPanel';
|
||||||
import SearchableJSONViewer from 'components/SearchableJSONViewer';
|
import SearchableJSONViewer from 'components/SearchableJSONViewer';
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { IStep } from '@automatisch/types';
|
import type { IStep } from 'types';
|
||||||
|
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import IntermediateStepCount from 'components/IntermediateStepCount';
|
import IntermediateStepCount from 'components/IntermediateStepCount';
|
||||||
|
@@ -7,7 +7,7 @@ import Chip from '@mui/material/Chip';
|
|||||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import type { IFlow } from '@automatisch/types';
|
import type { IFlow } from 'types';
|
||||||
import FlowAppIcons from 'components/FlowAppIcons';
|
import FlowAppIcons from 'components/FlowAppIcons';
|
||||||
import FlowContextMenu from 'components/FlowContextMenu';
|
import FlowContextMenu from 'components/FlowContextMenu';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
@@ -14,13 +14,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import type { BaseSchema } from 'yup';
|
import type { BaseSchema } from 'yup';
|
||||||
import type {
|
import type { IApp, ITrigger, IAction, IStep, ISubstep } from 'types';
|
||||||
IApp,
|
|
||||||
ITrigger,
|
|
||||||
IAction,
|
|
||||||
IStep,
|
|
||||||
ISubstep,
|
|
||||||
} from '@automatisch/types';
|
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||||
|
@@ -8,7 +8,7 @@ import Divider from '@mui/material/Divider';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import RemoveIcon from '@mui/icons-material/Remove';
|
import RemoveIcon from '@mui/icons-material/Remove';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import type { IField, IFieldText, IFieldDropdown } from '@automatisch/types';
|
import type { IField, IFieldText, IFieldDropdown } from 'types';
|
||||||
|
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
@@ -19,7 +19,7 @@ type TGroupItem = {
|
|||||||
operator: string;
|
operator: string;
|
||||||
value: string;
|
value: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type TGroup = Record<'and', TGroupItem[]>;
|
type TGroup = Record<'and', TGroupItem[]>;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ const createGroupItem = (): TGroupItem => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createGroup = (): TGroup => ({
|
const createGroup = (): TGroup => ({
|
||||||
and: [createGroupItem()]
|
and: [createGroupItem()],
|
||||||
});
|
});
|
||||||
|
|
||||||
const operators = [
|
const operators = [
|
||||||
@@ -69,7 +69,9 @@ const operators = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const createStringArgument = (argumentOptions: Omit<IFieldText, 'type' | 'required' | 'variables'>): IField => {
|
const createStringArgument = (
|
||||||
|
argumentOptions: Omit<IFieldText, 'type' | 'required' | 'variables'>
|
||||||
|
): IField => {
|
||||||
return {
|
return {
|
||||||
...argumentOptions,
|
...argumentOptions,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -78,7 +80,9 @@ const createStringArgument = (argumentOptions: Omit<IFieldText, 'type' | 'requir
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDropdownArgument = (argumentOptions: Omit<IFieldDropdown, 'type' | 'required'>): IField => {
|
const createDropdownArgument = (
|
||||||
|
argumentOptions: Omit<IFieldDropdown, 'type' | 'required'>
|
||||||
|
): IField => {
|
||||||
return {
|
return {
|
||||||
...argumentOptions,
|
...argumentOptions,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -91,9 +95,7 @@ type FilterConditionsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
||||||
const {
|
const { stepId } = props;
|
||||||
stepId
|
|
||||||
} = props;
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { control, setValue, getValues } = useFormContext();
|
const { control, setValue, getValues } = useFormContext();
|
||||||
const groups = useWatch({ control, name: 'parameters.or' });
|
const groups = useWatch({ control, name: 'parameters.or' });
|
||||||
@@ -110,7 +112,7 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
|||||||
const appendGroup = React.useCallback(() => {
|
const appendGroup = React.useCallback(() => {
|
||||||
const values = getValues('parameters.or');
|
const values = getValues('parameters.or');
|
||||||
|
|
||||||
setValue('parameters.or', values.concat(createGroup()))
|
setValue('parameters.or', values.concat(createGroup()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const appendGroupItem = React.useCallback((index) => {
|
const appendGroupItem = React.useCallback((index) => {
|
||||||
@@ -124,65 +126,107 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
|||||||
if (group.length === 1) {
|
if (group.length === 1) {
|
||||||
const groups: TGroup[] = getValues('parameters.or');
|
const groups: TGroup[] = getValues('parameters.or');
|
||||||
|
|
||||||
setValue('parameters.or', groups.filter((group, index) => index !== groupIndex));
|
setValue(
|
||||||
|
'parameters.or',
|
||||||
|
groups.filter((group, index) => index !== groupIndex)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setValue(`parameters.or.${groupIndex}.and`, group.filter((groupItem, index) => index !== groupItemIndex));
|
setValue(
|
||||||
|
`parameters.or.${groupIndex}.and`,
|
||||||
|
group.filter((groupItem, index) => index !== groupItemIndex)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Stack sx={{ width: "100%" }} direction="column" spacing={2} mt={2}>
|
<Stack sx={{ width: '100%' }} direction="column" spacing={2} mt={2}>
|
||||||
{groups?.map((group: TGroup, groupIndex: number) => (
|
{groups?.map((group: TGroup, groupIndex: number) => (
|
||||||
<>
|
<>
|
||||||
{groupIndex !== 0 && <Divider />}
|
{groupIndex !== 0 && <Divider />}
|
||||||
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
{groupIndex === 0 && formatMessage('filterConditions.onlyContinueIf')}
|
{groupIndex === 0 &&
|
||||||
{groupIndex !== 0 && formatMessage('filterConditions.orContinueIf')}
|
formatMessage('filterConditions.onlyContinueIf')}
|
||||||
|
{groupIndex !== 0 &&
|
||||||
|
formatMessage('filterConditions.orContinueIf')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{group?.and?.map((groupItem: TGroupItem, groupItemIndex: number) => (
|
{group?.and?.map(
|
||||||
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
|
(groupItem: TGroupItem, groupItemIndex: number) => (
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 2 }} sx={{ display: 'flex', flex: 1 }}>
|
<Stack direction="row" spacing={2} key={`item-${groupItem.id}`}>
|
||||||
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
|
<Stack
|
||||||
<InputCreator
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
schema={createStringArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.key`, label: 'Choose field' })}
|
spacing={{ xs: 2 }}
|
||||||
namePrefix="parameters"
|
sx={{ display: 'flex', flex: 1 }}
|
||||||
stepId={stepId}
|
>
|
||||||
disabled={editorContext.readOnly}
|
<Box
|
||||||
/>
|
sx={{
|
||||||
</Box>
|
display: 'flex',
|
||||||
|
flex: '1 0 0px',
|
||||||
|
maxWidth: ['100%', '33%'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputCreator
|
||||||
|
schema={createStringArgument({
|
||||||
|
key: `or.${groupIndex}.and.${groupItemIndex}.key`,
|
||||||
|
label: 'Choose field',
|
||||||
|
})}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
|
<Box
|
||||||
<InputCreator
|
sx={{
|
||||||
schema={createDropdownArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.operator`, options: operators, label: 'Choose condition' })}
|
display: 'flex',
|
||||||
namePrefix="parameters"
|
flex: '1 0 0px',
|
||||||
stepId={stepId}
|
maxWidth: ['100%', '33%'],
|
||||||
disabled={editorContext.readOnly}
|
}}
|
||||||
/>
|
>
|
||||||
</Box>
|
<InputCreator
|
||||||
|
schema={createDropdownArgument({
|
||||||
|
key: `or.${groupIndex}.and.${groupItemIndex}.operator`,
|
||||||
|
options: operators,
|
||||||
|
label: 'Choose condition',
|
||||||
|
})}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flex: '1 0 0px', maxWidth: ['100%', '33%'] }}>
|
<Box
|
||||||
<InputCreator
|
sx={{
|
||||||
schema={createStringArgument({ key: `or.${groupIndex}.and.${groupItemIndex}.value`, label: 'Enter text' })}
|
display: 'flex',
|
||||||
namePrefix="parameters"
|
flex: '1 0 0px',
|
||||||
stepId={stepId}
|
maxWidth: ['100%', '33%'],
|
||||||
disabled={editorContext.readOnly}
|
}}
|
||||||
/>
|
>
|
||||||
</Box>
|
<InputCreator
|
||||||
|
schema={createStringArgument({
|
||||||
|
key: `or.${groupIndex}.and.${groupItemIndex}.value`,
|
||||||
|
label: 'Enter text',
|
||||||
|
})}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={editorContext.readOnly}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
edge="start"
|
||||||
|
onClick={() => removeGroupItem(groupIndex, groupItemIndex)}
|
||||||
|
sx={{ width: 61, height: 61 }}
|
||||||
|
>
|
||||||
|
<RemoveIcon />
|
||||||
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)
|
||||||
<IconButton
|
)}
|
||||||
size="small"
|
|
||||||
edge="start"
|
|
||||||
onClick={() => removeGroupItem(groupIndex, groupItemIndex)}
|
|
||||||
sx={{ width: 61, height: 61 }}
|
|
||||||
>
|
|
||||||
<RemoveIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Stack spacing={1} direction="row">
|
<Stack spacing={1} direction="row">
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -194,14 +238,16 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement {
|
|||||||
<AddIcon /> And
|
<AddIcon /> And
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{(groups.length - 1) === groupIndex && <IconButton
|
{groups.length - 1 === groupIndex && (
|
||||||
size="small"
|
<IconButton
|
||||||
edge="start"
|
size="small"
|
||||||
onClick={appendGroup}
|
edge="start"
|
||||||
sx={{ width: 61, height: 61 }}
|
onClick={appendGroup}
|
||||||
>
|
sx={{ width: 61, height: 61 }}
|
||||||
<AddIcon /> Or
|
>
|
||||||
</IconButton>}
|
<AddIcon /> Or
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
@@ -4,7 +4,7 @@ import Collapse from '@mui/material/Collapse';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import type { IStep, ISubstep } from '@automatisch/types';
|
import type { IStep, ISubstep } from 'types';
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
@@ -54,7 +54,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
pb: 3,
|
pb: 3,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
position: 'relative'
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!!args?.length && (
|
{!!args?.length && (
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import type { IField, IFieldDropdownOption } from '@automatisch/types';
|
import type { IField, IFieldDropdownOption } from 'types';
|
||||||
|
|
||||||
import useDynamicFields from 'hooks/useDynamicFields';
|
import useDynamicFields from 'hooks/useDynamicFields';
|
||||||
import useDynamicData from 'hooks/useDynamicData';
|
import useDynamicData from 'hooks/useDynamicData';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user