Compare commits
73 Commits
update-sam
...
v0.9.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8156b8b356 | ||
![]() |
3a2cbae0a0 | ||
![]() |
0ad8da097b | ||
![]() |
e2dcdd2811 | ||
![]() |
8074f9146b | ||
![]() |
df24bac913 | ||
![]() |
4d4091adcc | ||
![]() |
cac54c41a1 | ||
![]() |
130931d7af | ||
![]() |
d35b08b35e | ||
![]() |
82031da6a6 | ||
![]() |
9df5ee7b11 | ||
![]() |
2ed1a57cd9 | ||
![]() |
101450cba6 | ||
![]() |
6bab5b3f7c | ||
![]() |
ca3c0e00a7 | ||
![]() |
6d64daf324 | ||
![]() |
9ae4578e19 | ||
![]() |
e06b7ab87a | ||
![]() |
1e2adedcbf | ||
![]() |
adf763c1b0 | ||
![]() |
36ee0df256 | ||
![]() |
823d85b24a | ||
![]() |
a3b3038709 | ||
![]() |
ddeb18f626 | ||
![]() |
90cd11bd38 | ||
![]() |
e9ba37b8de | ||
![]() |
d5e4a1b1ad | ||
![]() |
129e6d60e5 | ||
![]() |
4b77f2f590 | ||
![]() |
a909966562 | ||
![]() |
fd184239d6 | ||
![]() |
52bc49dc6a | ||
![]() |
b9352ccc06 | ||
![]() |
525b2baf06 | ||
![]() |
a8edeb2459 | ||
![]() |
e3830d64e0 | ||
![]() |
91f3e2c2b4 | ||
![]() |
77b4408416 | ||
![]() |
cede96f018 | ||
![]() |
8e0a28d238 | ||
![]() |
da5d594428 | ||
![]() |
9f9ee0bb58 | ||
![]() |
163aca6179 | ||
![]() |
cb06d3b0ae | ||
![]() |
dbe18dd100 | ||
![]() |
217970667a | ||
![]() |
dace794167 | ||
![]() |
590780a539 | ||
![]() |
cbd1f47e87 | ||
![]() |
f89cff4e4a | ||
![]() |
cb08e0bf9f | ||
![]() |
3b54b29a99 | ||
![]() |
25983e046c | ||
![]() |
a6a124d2e6 | ||
![]() |
c7e1d30553 | ||
![]() |
6cc8c45634 | ||
![]() |
ee9a9114b7 | ||
![]() |
11f00f866c | ||
![]() |
03ea61ba81 | ||
![]() |
f6c500c998 | ||
![]() |
b590f0f98f | ||
![]() |
ef9359b208 | ||
![]() |
efd243a340 | ||
![]() |
bafb8b86db | ||
![]() |
84b701747f | ||
![]() |
ec42446daa | ||
![]() |
5046c4c911 | ||
![]() |
ce8c9906cb | ||
![]() |
6fb5482bba | ||
![]() |
58189963f5 | ||
![]() |
f488a71304 | ||
![]() |
4b706e004d |
@@ -29,7 +29,6 @@ rm -rf .env
|
||||
echo "
|
||||
PORT=$WEB_PORT
|
||||
REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
|
||||
REACT_APP_NOTIFICATIONS_URL=https://notifications.automatisch.io
|
||||
" >> .env
|
||||
cd $CURRENT_DIR
|
||||
|
||||
|
87
.github/workflows/playwright.yml
vendored
Normal file
87
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Automatisch UI Tests
|
||||
on:
|
||||
push:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ENCRYPTION_KEY: sample_encryption_key
|
||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||
APP_SECRET_KEY: sample_app_secret_key
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_DATABASE: automatisch
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_USERNAME: automatisch_user
|
||||
POSTGRES_PASSWORD: automatisch_password
|
||||
REDIS_HOST: localhost
|
||||
APP_ENV: production
|
||||
LICENSE_KEY: ${{ secrets.E2E_LICENSE_KEY }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14.5-alpine
|
||||
env:
|
||||
POSTGRES_DB: automatisch
|
||||
POSTGRES_USER: automatisch_user
|
||||
POSTGRES_PASSWORD: automatisch_password
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U automatisch_user -d automatisch"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis:7.0.4-alpine
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn && yarn lerna bootstrap
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Build Automatisch
|
||||
run: yarn lerna run --scope=@*/{web,backend,cli} build
|
||||
env:
|
||||
# Keep this until we clean up warnings in build processes
|
||||
CI: false
|
||||
- name: Migrate database
|
||||
working-directory: ./packages/backend
|
||||
run: yarn db:migrate --migrations-directory ./dist/src/db/migrations
|
||||
- name: Seed user
|
||||
working-directory: ./packages/backend
|
||||
run: yarn db:seed:user &
|
||||
- name: Run Automatisch
|
||||
run: yarn start &
|
||||
working-directory: ./packages/backend
|
||||
- name: Run Automatisch worker
|
||||
run: node dist/src/worker.js &
|
||||
working-directory: ./packages/backend
|
||||
- name: Run Playwright tests
|
||||
working-directory: ./packages/e2e-tests
|
||||
env:
|
||||
LOGIN_EMAIL: ${{ secrets.LOGIN_EMAIL }}
|
||||
LOGIN_PASSWORD: ${{ secrets.LOGIN_PASSWORD }}
|
||||
BASE_URL: ${{ vars.E2E_BASE_URL }}
|
||||
run: yarn test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: ./packages/e2e-tests/test-results/**/*
|
||||
retention-days: 30
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ WORKDIR /automatisch
|
||||
|
||||
RUN \
|
||||
apk --no-cache add --virtual build-dependencies python3 build-base && \
|
||||
yarn global add @automatisch/cli@0.8.0 --network-timeout 1000000 && \
|
||||
yarn global add @automatisch/cli@0.9.3 --network-timeout 1000000 && \
|
||||
rm -rf /usr/local/share/.cache/ && \
|
||||
apk del build-dependencies
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM automatischio/automatisch:0.8.0
|
||||
FROM automatischio/automatisch:0.9.3
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN apk add --no-cache openssl dos2unix
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.3",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/backend",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.3",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"prebuild": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/web": "^0.8.0",
|
||||
"@automatisch/web": "^0.9.3",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
@@ -62,12 +62,14 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"nodemailer": "6.7.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"objection": "^3.0.0",
|
||||
"passport": "^0.6.0",
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
"showdown": "^2.1.0",
|
||||
"stripe": "^11.13.0",
|
||||
"winston": "^3.7.1",
|
||||
"xmlrpc": "^1.3.2"
|
||||
@@ -108,7 +110,7 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automatisch/types": "^0.8.0",
|
||||
"@automatisch/types": "^0.9.3",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bull": "^3.15.8",
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -124,6 +126,7 @@
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/pg": "^8.6.1",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/showdown": "^2.0.1",
|
||||
"ava": "^3.15.0",
|
||||
"nodemon": "^2.0.13",
|
||||
"sinon": "^11.1.2",
|
||||
|
3
packages/backend/src/apps/formatter/actions/index.ts
Normal file
3
packages/backend/src/apps/formatter/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import text from './text';
|
||||
|
||||
export default [text];
|
67
packages/backend/src/apps/formatter/actions/text/index.ts
Normal file
67
packages/backend/src/apps/formatter/actions/text/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
import capitalize from './transformers/capitalize';
|
||||
import htmlToMarkdown from './transformers/html-to-markdown';
|
||||
import markdownToHtml from './transformers/markdown-to-html';
|
||||
import useDefaultValue from './transformers/use-default-value';
|
||||
import extractEmailAddress from './transformers/extract-email-address';
|
||||
import extractNumber from './transformers/extract-number';
|
||||
|
||||
const transformers = {
|
||||
capitalize,
|
||||
htmlToMarkdown,
|
||||
markdownToHtml,
|
||||
useDefaultValue,
|
||||
extractEmailAddress,
|
||||
extractNumber,
|
||||
};
|
||||
|
||||
export default defineAction({
|
||||
name: 'Text',
|
||||
key: 'text',
|
||||
description:
|
||||
'Transform text data to capitalize, extract emails, apply default value, and much more.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Transform',
|
||||
key: 'transform',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description: 'Pick a channel to send the message to.',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'Capitalize', value: 'capitalize' },
|
||||
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
|
||||
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
|
||||
{ label: 'Use Default Value', value: 'useDefaultValue' },
|
||||
{ label: 'Extract Email Address', value: 'extractEmailAddress' },
|
||||
{ label: 'Extract Number', value: 'extractNumber' },
|
||||
],
|
||||
additionalFields: {
|
||||
type: 'query',
|
||||
name: 'getDynamicFields',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTransformOptions',
|
||||
},
|
||||
{
|
||||
name: 'parameters.transform',
|
||||
value: '{parameters.transform}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const transformerName = $.step.parameters
|
||||
.transform as keyof typeof transformers;
|
||||
const output = transformers[transformerName]($);
|
||||
|
||||
$.setActionItem({
|
||||
raw: {
|
||||
output,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@@ -0,0 +1,11 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import { capitalize as lodashCapitalize } from 'lodash';
|
||||
|
||||
const capitalize = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
const capitalizedInput = input.replace(/\w+/g, lodashCapitalize);
|
||||
|
||||
return capitalizedInput;
|
||||
};
|
||||
|
||||
export default capitalize;
|
@@ -0,0 +1,12 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const extractEmailAddress = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
const emailRegexp =
|
||||
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||
|
||||
const email = input.match(emailRegexp);
|
||||
return email ? email[0] : '';
|
||||
};
|
||||
|
||||
export default extractEmailAddress;
|
@@ -0,0 +1,26 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const extractNumber = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
// Example numbers that's supported:
|
||||
// 123
|
||||
// -123
|
||||
// 123456
|
||||
// -123456
|
||||
// 121,234
|
||||
// -121,234
|
||||
// 121.234
|
||||
// -121.234
|
||||
// 1,234,567.89
|
||||
// -1,234,567.89
|
||||
// 1.234.567,89
|
||||
// -1.234.567,89
|
||||
|
||||
const numberRegexp = /-?((\d{1,3})+\.?,?)+/g;
|
||||
|
||||
const numbers = input.match(numberRegexp);
|
||||
return numbers ? numbers[0] : '';
|
||||
};
|
||||
|
||||
export default extractNumber;
|
@@ -0,0 +1,11 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import { NodeHtmlMarkdown } from 'node-html-markdown';
|
||||
|
||||
const htmlToMarkdown = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
const markdown = NodeHtmlMarkdown.translate(input);
|
||||
return markdown;
|
||||
};
|
||||
|
||||
export default htmlToMarkdown;
|
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import showdown from 'showdown';
|
||||
|
||||
const converter = new showdown.Converter();
|
||||
|
||||
const markdownToHtml = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
const html = converter.makeHtml(input);
|
||||
return html;
|
||||
};
|
||||
|
||||
export default markdownToHtml;
|
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const useDefaultValue = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
if (input && input.trim().length > 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return $.step.parameters.defaultValue as string;
|
||||
};
|
||||
|
||||
export default useDefaultValue;
|
3
packages/backend/src/apps/formatter/assets/favicon.svg
Normal file
3
packages/backend/src/apps/formatter/assets/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4H20M4 12H20M4 20H20M4 8H14M4 16H14" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,3 @@
|
||||
import listTransformOptions from './list-transform-options';
|
||||
|
||||
export default [listTransformOptions];
|
@@ -0,0 +1,25 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
import capitalize from './options/capitalize';
|
||||
import htmlToMarkdown from './options/html-to-markdown';
|
||||
import markdownToHtml from './options/markdown-to-html';
|
||||
import useDefaultValue from './options/use-default-value';
|
||||
import extractEmailAddress from './options/extract-email-address';
|
||||
import extractNumber from './options/extract-number';
|
||||
|
||||
const options: IJSONObject = {
|
||||
capitalize,
|
||||
htmlToMarkdown,
|
||||
markdownToHtml,
|
||||
useDefaultValue,
|
||||
extractEmailAddress,
|
||||
extractNumber,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'List fields after transform',
|
||||
key: 'listTransformOptions',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
return options[$.step.parameters.transform as string];
|
||||
},
|
||||
};
|
@@ -0,0 +1,12 @@
|
||||
const capitalize = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text that will be capitalized.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default capitalize;
|
@@ -0,0 +1,12 @@
|
||||
const extractEmailAddress = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text that will be searched for an email address.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default extractEmailAddress;
|
@@ -0,0 +1,12 @@
|
||||
const extractNumber = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text that will be searched for a number.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default extractNumber;
|
@@ -0,0 +1,12 @@
|
||||
const htmlToMarkdown = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'HTML that will be converted to Markdown.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default htmlToMarkdown;
|
@@ -0,0 +1,12 @@
|
||||
const markdownToHtml = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Markdown text that will be converted to HTML.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default markdownToHtml;
|
@@ -0,0 +1,21 @@
|
||||
const useDefaultValue = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text you want to check whether it is empty or not.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Default Value',
|
||||
key: 'defaultValue',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'Text that will be used as a default value if the input is empty.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default useDefaultValue;
|
0
packages/backend/src/apps/formatter/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/formatter/index.d.ts
vendored
Normal file
16
packages/backend/src/apps/formatter/index.ts
Normal file
16
packages/backend/src/apps/formatter/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import actions from './actions';
|
||||
import dynamicFields from './dynamic-fields';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Formatter',
|
||||
key: 'formatter',
|
||||
iconUrl: '{BASE_URL}/apps/formatter/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/formatter/connection',
|
||||
supportsConnections: false,
|
||||
baseUrl: '',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '001F52',
|
||||
actions,
|
||||
dynamicFields,
|
||||
});
|
@@ -25,6 +25,12 @@ const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
$.auth.data.accessToken = data.access_token;
|
||||
|
||||
const currentUser = await getCurrentUser($);
|
||||
const screenName = [
|
||||
currentUser.username,
|
||||
$.auth.data.instanceUrl,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' @ ');
|
||||
|
||||
await $.auth.set({
|
||||
clientId: $.auth.data.clientId,
|
||||
@@ -34,7 +40,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
scope: data.scope,
|
||||
tokenType: data.token_type,
|
||||
userId: currentUser.id,
|
||||
screenName: `${currentUser.username} @ ${$.auth.data.instanceUrl}`,
|
||||
screenName,
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -4,9 +4,13 @@ import bcrypt from 'bcrypt';
|
||||
|
||||
const getInternalId = async (item: IJSONObject): Promise<string> => {
|
||||
if (item.guid) {
|
||||
return item.guid.toString();
|
||||
return typeof item.guid === 'object'
|
||||
? (item.guid as IJSONObject)['#text'].toString()
|
||||
: item.guid.toString();
|
||||
} else if (item.id) {
|
||||
return item.id.toString();
|
||||
return typeof item.id === 'object'
|
||||
? (item.id as IJSONObject)['#text'].toString()
|
||||
: item.id.toString();
|
||||
}
|
||||
|
||||
return await hashItem(JSON.stringify(item));
|
||||
|
6
packages/backend/src/apps/wordpress/assets/favicon.svg
Normal file
6
packages/backend/src/apps/wordpress/assets/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<svg width="256px" height="255px" viewBox="0 0 256 255" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g fill="#464342">
|
||||
<path d="M18.1239675,127.500488 C18.1239675,170.795707 43.284813,208.211252 79.7700163,225.941854 L27.5938862,82.985626 C21.524813,96.5890081 18.1239675,111.643057 18.1239675,127.500488 L18.1239675,127.500488 Z M201.345041,121.980878 C201.345041,108.462829 196.489366,99.1011382 192.324683,91.8145041 C186.780098,82.8045528 181.583089,75.1745041 181.583089,66.1645528 C181.583089,56.1097886 189.208976,46.7501789 199.950569,46.7501789 C200.435512,46.7501789 200.89548,46.8105366 201.367935,46.8375935 C181.907772,29.0091707 155.981008,18.1239675 127.50465,18.1239675 C89.2919675,18.1239675 55.6727154,37.7298211 36.1147317,67.4258211 C38.6809756,67.5028293 41.0994472,67.5569431 43.1536911,67.5569431 C54.5946016,67.5569431 72.3043902,66.1687154 72.3043902,66.1687154 C78.2007154,65.8211382 78.8958699,74.4814309 73.0057886,75.1786667 C73.0057886,75.1786667 67.0803252,75.8759024 60.4867642,76.2213984 L100.318699,194.699447 L124.25574,122.909138 L107.214049,76.2172358 C101.323967,75.8717398 95.744,75.1745041 95.744,75.1745041 C89.8497561,74.8290081 90.540748,65.8169756 96.4349919,66.1645528 C96.4349919,66.1645528 114.498602,67.5527805 125.246439,67.5527805 C136.685268,67.5527805 154.397138,66.1645528 154.397138,66.1645528 C160.297626,65.8169756 160.990699,74.4772683 155.098537,75.1745041 C155.098537,75.1745041 149.160585,75.8717398 142.579512,76.2172358 L182.107577,193.798244 L193.017756,157.340098 C197.746472,142.211122 201.345041,131.34465 201.345041,121.980878 L201.345041,121.980878 Z M129.42361,137.068228 L96.6056585,232.43135 C106.404423,235.31187 116.76722,236.887415 127.50465,236.887415 C140.242211,236.887415 152.457366,234.685398 163.827512,230.68722 C163.534049,230.218927 163.267642,229.721496 163.049106,229.180358 L129.42361,137.068228 L129.42361,137.068228 Z M223.481756,75.0225691 C223.95213,78.5066667 224.218537,82.2467642 224.218537,86.2699187 C224.218537,97.3694959 222.145561,109.846894 215.901659,125.448325 L182.490537,222.04774 C215.00878,203.085008 236.881171,167.854829 236.881171,127.502569 C236.883252,108.485724 232.025496,90.603187 223.481756,75.0225691 L223.481756,75.0225691 Z M127.50465,0 C57.2003902,0 0,57.1962276 0,127.500488 C0,197.813073 57.2003902,255.00722 127.50465,255.00722 C197.806829,255.00722 255.015545,197.813073 255.015545,127.500488 C255.013463,57.1962276 197.806829,0 127.50465,0 L127.50465,0 Z M127.50465,249.162927 C60.4243252,249.162927 5.84637398,194.584976 5.84637398,127.500488 C5.84637398,60.4201626 60.4222439,5.84637398 127.50465,5.84637398 C194.582894,5.84637398 249.156683,60.4201626 249.156683,127.500488 C249.156683,194.584976 194.582894,249.162927 127.50465,249.162927 L127.50465,249.162927 Z"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,24 @@
|
||||
import { URL, URLSearchParams } from 'node:url';
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
import appConfig from '../../../config/app';
|
||||
import getInstanceUrl from '../common/get-instance-url';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const successUrl = new URL(
|
||||
'/app/wordpress/connections/add',
|
||||
appConfig.webAppUrl
|
||||
).toString();
|
||||
const baseUrl = getInstanceUrl($);
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
app_name: 'automatisch',
|
||||
success_url: successUrl,
|
||||
});
|
||||
|
||||
const url = new URL(`/wp-admin/authorize-application.php?${searchParams}`, baseUrl).toString();
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
24
packages/backend/src/apps/wordpress/auth/index.ts
Normal file
24
packages/backend/src/apps/wordpress/auth/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import isStillVerified from './is-still-verified';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'instanceUrl',
|
||||
label: 'WordPress instance URL',
|
||||
type: 'string' as const,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Your WordPress instance URL.',
|
||||
docUrl: 'https://automatisch.io/docs/wordpress#instance-url',
|
||||
clickToCopy: true,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
isStillVerified,
|
||||
verifyCredentials,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
await $.http.get('?rest_route=/wp/v2/settings');
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,24 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const instanceUrl = $.auth.data.instanceUrl as string;
|
||||
const password = $.auth.data.password as string;
|
||||
const siteUrl = $.auth.data.site_url as string;
|
||||
const url = $.auth.data.url as string;
|
||||
const userLogin = $.auth.data.user_login as string;
|
||||
|
||||
if (!password) {
|
||||
throw new Error('Failed while authorizing!');
|
||||
}
|
||||
|
||||
await $.auth.set({
|
||||
screenName: `${userLogin} @ ${siteUrl}`,
|
||||
instanceUrl,
|
||||
password,
|
||||
siteUrl,
|
||||
url,
|
||||
userLogin,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,17 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
const userLogin = $.auth.data.userLogin as string;
|
||||
const password = $.auth.data.password as string;
|
||||
|
||||
if (userLogin && password) {
|
||||
requestConfig.auth = {
|
||||
username: userLogin,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,7 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const getInstanceUrl = ($: IGlobalVariable): string => {
|
||||
return $.auth.data.instanceUrl as string;
|
||||
};
|
||||
|
||||
export default getInstanceUrl;
|
12
packages/backend/src/apps/wordpress/common/set-base-url.ts
Normal file
12
packages/backend/src/apps/wordpress/common/set-base-url.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const setBaseUrl: TBeforeRequest = ($, requestConfig) => {
|
||||
const instanceUrl = $.auth.data.instanceUrl as string;
|
||||
if (instanceUrl) {
|
||||
requestConfig.baseURL = instanceUrl;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default setBaseUrl;
|
@@ -0,0 +1,3 @@
|
||||
import listStatuses from './list-statuses';
|
||||
|
||||
export default [listStatuses];
|
@@ -0,0 +1,37 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
type Status = {
|
||||
slug: string;
|
||||
name: string;
|
||||
}
|
||||
type Statuses = Record<string, Status>;
|
||||
|
||||
export default {
|
||||
name: 'List statuses',
|
||||
key: 'listStatuses',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const statuses: {
|
||||
data: IJSONObject[];
|
||||
} = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const { data } = await $.http.get<Statuses>('?rest_route=/wp/v2/statuses');
|
||||
|
||||
if (!data) return statuses;
|
||||
|
||||
const values = Object.values(data);
|
||||
|
||||
if (!values?.length) return statuses;
|
||||
|
||||
for (const status of values) {
|
||||
statuses.data.push({
|
||||
value: status.slug,
|
||||
name: status.name,
|
||||
})
|
||||
}
|
||||
|
||||
return statuses;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/wordpress/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/wordpress/index.d.ts
vendored
Normal file
21
packages/backend/src/apps/wordpress/index.ts
Normal file
21
packages/backend/src/apps/wordpress/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import setBaseUrl from './common/set-base-url';
|
||||
import auth from './auth';
|
||||
import triggers from './triggers';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'WordPress',
|
||||
key: 'wordpress',
|
||||
iconUrl: '{BASE_URL}/apps/wordpress/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/wordpress/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://wordpress.com',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '464342',
|
||||
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
});
|
3
packages/backend/src/apps/wordpress/triggers/index.ts
Normal file
3
packages/backend/src/apps/wordpress/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import newPost from './new-post';
|
||||
|
||||
export default [newPost];
|
@@ -0,0 +1,60 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New post',
|
||||
key: 'newPost',
|
||||
description: 'Triggers when a new post is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Status',
|
||||
key: 'status',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listStatuses',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const params = {
|
||||
per_page: 100,
|
||||
page: 1,
|
||||
order: 'desc',
|
||||
orderby: 'date',
|
||||
status: $.step.parameters.status || '',
|
||||
};
|
||||
|
||||
let totalPages = 1;
|
||||
do {
|
||||
const {
|
||||
data,
|
||||
headers
|
||||
} = await $.http.get('?rest_route=/wp/v2/posts', { params });
|
||||
|
||||
params.page = params.page + 1;
|
||||
totalPages = Number(headers['x-wp-totalpages']);
|
||||
|
||||
if (data.length) {
|
||||
for (const post of data) {
|
||||
const dataItem = {
|
||||
raw: post,
|
||||
meta: {
|
||||
internalId: post.id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
}
|
||||
}
|
||||
} while (params.page <= totalPages);
|
||||
},
|
||||
});
|
@@ -0,0 +1,15 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('config', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('key').unique().notNullable();
|
||||
table.jsonb('value').notNullable().defaultTo({});
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('config');
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermissionForRole = (
|
||||
roleId: string,
|
||||
subject: string,
|
||||
actions: string[]
|
||||
) =>
|
||||
actions.map((action) => ({
|
||||
role_id: roleId,
|
||||
subject,
|
||||
action,
|
||||
conditions: [],
|
||||
}));
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const role = (await knex('roles')
|
||||
.first(['id', 'key'])
|
||||
.where({ key: 'admin' })
|
||||
.limit(1)) as { id: string; key: string };
|
||||
|
||||
await knex('permissions').insert(
|
||||
getPermissionForRole(role.id, 'Config', [
|
||||
'update',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex('permissions').where({ subject: 'Config' }).delete();
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable(
|
||||
'saml_auth_providers_role_mappings',
|
||||
(table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table
|
||||
.uuid('saml_auth_provider_id')
|
||||
.references('id')
|
||||
.inTable('saml_auth_providers');
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('remote_role_name').notNullable();
|
||||
|
||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||
|
||||
table.timestamps(true, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('saml_auth_providers_role_mappings');
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('app_configs', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('key').unique().notNullable();
|
||||
table.boolean('allow_custom_connection').notNullable().defaultTo(false);
|
||||
table.boolean('shared').notNullable().defaultTo(false);
|
||||
table.boolean('disabled').notNullable().defaultTo(false);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('app_configs');
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('app_auth_clients', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('name').unique().notNullable();
|
||||
table.uuid('app_config_id').notNullable().references('id').inTable('app_configs');
|
||||
table.text('auth_defaults').notNullable();
|
||||
table.boolean('active').notNullable().defaultTo(false);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('app_auth_clients');
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.table('connections', async (table) => {
|
||||
table.uuid('app_auth_client_id').references('id').inTable('app_auth_clients');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.table('connections', (table) => {
|
||||
table.dropColumn('app_auth_client_id');
|
||||
});
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermissionForRole = (
|
||||
roleId: string,
|
||||
subject: string,
|
||||
actions: string[]
|
||||
) =>
|
||||
actions.map((action) => ({
|
||||
role_id: roleId,
|
||||
subject,
|
||||
action,
|
||||
conditions: [],
|
||||
}));
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const role = (await knex('roles')
|
||||
.first(['id', 'key'])
|
||||
.where({ key: 'admin' })
|
||||
.limit(1)) as { id: string; key: string };
|
||||
|
||||
await knex('permissions').insert(
|
||||
getPermissionForRole(role.id, 'App', [
|
||||
'create',
|
||||
'read',
|
||||
'delete',
|
||||
'update',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex('permissions').where({ subject: 'App' }).delete();
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const role = await knex('roles')
|
||||
.select('id')
|
||||
.whereIn('key', ['user', 'admin'])
|
||||
.orderBy('key', 'desc')
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
if (role) {
|
||||
// backfill nulls
|
||||
await knex('users').whereNull('role_id').update({ role_id: role.id });
|
||||
}
|
||||
|
||||
return await knex.schema.alterTable('users', (table) => {
|
||||
table.uuid('role_id').notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.alterTable('users', (table) => {
|
||||
table.uuid('role_id').nullable().alter();
|
||||
});
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const users = await knex('users').whereNotNull('deleted_at');
|
||||
const userIds = users.map((user) => user.id);
|
||||
|
||||
const flows = await knex('flows').whereIn('user_id', userIds);
|
||||
const flowIds = flows.map((flow) => flow.id);
|
||||
const executions = await knex('executions').whereIn('flow_id', flowIds);
|
||||
const executionIds = executions.map((execution) => execution.id);
|
||||
|
||||
await knex('execution_steps').whereIn('execution_id', executionIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('executions').whereIn('id', executionIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('steps').whereIn('flow_id', flowIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('flows').whereIn('id', flowIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('connections').whereIn('user_id', userIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// void
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex('permissions')
|
||||
.where(knex.raw('conditions::text'), '=', knex.raw("'{}'::text"))
|
||||
.update('conditions', JSON.stringify([]));
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// void
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
import createAppAuthClient from './mutations/create-app-auth-client.ee';
|
||||
import createAppConfig from './mutations/create-app-config.ee';
|
||||
import createConnection from './mutations/create-connection';
|
||||
import createFlow from './mutations/create-flow';
|
||||
import createRole from './mutations/create-role.ee';
|
||||
@@ -17,6 +19,9 @@ import login from './mutations/login';
|
||||
import registerUser from './mutations/register-user.ee';
|
||||
import resetConnection from './mutations/reset-connection';
|
||||
import resetPassword from './mutations/reset-password.ee';
|
||||
import updateAppAuthClient from './mutations/update-app-auth-client.ee';
|
||||
import updateAppConfig from './mutations/update-app-config.ee';
|
||||
import updateConfig from './mutations/update-config.ee';
|
||||
import updateConnection from './mutations/update-connection';
|
||||
import updateCurrentUser from './mutations/update-current-user';
|
||||
import updateFlow from './mutations/update-flow';
|
||||
@@ -24,10 +29,13 @@ import updateFlowStatus from './mutations/update-flow-status';
|
||||
import updateRole from './mutations/update-role.ee';
|
||||
import updateStep from './mutations/update-step';
|
||||
import updateUser from './mutations/update-user.ee';
|
||||
import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee';
|
||||
import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee';
|
||||
import verifyConnection from './mutations/verify-connection';
|
||||
import createSamlAuthProvider from './mutations/create-saml-auth-provider.ee';
|
||||
|
||||
const mutationResolvers = {
|
||||
createAppAuthClient,
|
||||
createAppConfig,
|
||||
createConnection,
|
||||
createFlow,
|
||||
createRole,
|
||||
@@ -47,15 +55,19 @@ const mutationResolvers = {
|
||||
registerUser,
|
||||
resetConnection,
|
||||
resetPassword,
|
||||
updateAppAuthClient,
|
||||
updateAppConfig,
|
||||
updateConfig,
|
||||
updateConnection,
|
||||
updateCurrentUser,
|
||||
updateUser,
|
||||
updateFlow,
|
||||
updateFlowStatus,
|
||||
updateRole,
|
||||
updateStep,
|
||||
updateUser,
|
||||
upsertSamlAuthProvider,
|
||||
upsertSamlAuthProvidersRoleMappings,
|
||||
verifyConnection,
|
||||
createSamlAuthProvider,
|
||||
};
|
||||
|
||||
export default mutationResolvers;
|
||||
|
@@ -0,0 +1,35 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
appConfigId: string;
|
||||
name: string;
|
||||
formattedAuthDefaults?: IJSONObject;
|
||||
active?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const createAppAuthClient = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.findById(params.input.appConfigId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const appAuthClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.insert(
|
||||
params.input
|
||||
);
|
||||
|
||||
return appAuthClient;
|
||||
};
|
||||
|
||||
export default createAppAuthClient;
|
@@ -0,0 +1,36 @@
|
||||
import App from '../../models/app';
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
key: string;
|
||||
allowCustomConnection?: boolean;
|
||||
shared?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const createAppConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const key = params.input.key;
|
||||
|
||||
const app = await App.findOneByKey(key);
|
||||
|
||||
if (!app) throw new Error('The app cannot be found!');
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.insert(
|
||||
params.input
|
||||
);
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
||||
export default createAppConfig;
|
@@ -1,13 +1,16 @@
|
||||
import App from '../../models/app';
|
||||
import Context from '../../types/express/context';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
key: string;
|
||||
appAuthClientId: string;
|
||||
formattedData: IJSONObject;
|
||||
};
|
||||
};
|
||||
|
||||
const createConnection = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
@@ -15,13 +18,42 @@ const createConnection = async (
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
await App.findOneByKey(params.input.key);
|
||||
const { key, appAuthClientId } = params.input;
|
||||
|
||||
return await context.currentUser.$relatedQuery('connections').insert({
|
||||
key: params.input.key,
|
||||
formattedData: params.input.formattedData,
|
||||
verified: false,
|
||||
});
|
||||
const app = await App.findOneByKey(key);
|
||||
|
||||
const appConfig = await AppConfig.query().findOne({ key });
|
||||
|
||||
let formattedData = params.input.formattedData;
|
||||
if (appConfig) {
|
||||
if (appConfig.disabled) throw new Error('This application has been disabled for new connections!');
|
||||
|
||||
if (!appConfig.allowCustomConnection && formattedData) throw new Error(`Custom connections cannot be created for ${app.name}!`);
|
||||
|
||||
if (appConfig.shared && !formattedData) {
|
||||
const authClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.findById(appAuthClientId)
|
||||
.where({
|
||||
active: true
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
formattedData = authClient.formattedAuthDefaults;
|
||||
}
|
||||
}
|
||||
|
||||
const createdConnection = await context
|
||||
.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.insert({
|
||||
key,
|
||||
appAuthClientId,
|
||||
formattedData,
|
||||
verified: false,
|
||||
});
|
||||
|
||||
return createdConnection;
|
||||
};
|
||||
|
||||
export default createConnection;
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import Context from '../../types/express/context';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAppAuthClient = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'App');
|
||||
|
||||
await AppAuthClient
|
||||
.query()
|
||||
.delete()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default deleteAppAuthClient;
|
@@ -1,17 +1,59 @@
|
||||
import { Duration } from 'luxon';
|
||||
import Context from '../../types/express/context';
|
||||
import deleteUserQueue from '../../queues/delete-user.ee';
|
||||
import flowQueue from '../../queues/flow';
|
||||
import Flow from '../../models/flow';
|
||||
import Execution from '../../models/execution';
|
||||
import ExecutionStep from '../../models/execution-step';
|
||||
import appConfig from '../../config/app';
|
||||
|
||||
const deleteCurrentUser = async (_parent: unknown, params: never, context: Context) => {
|
||||
const deleteCurrentUser = async (
|
||||
_parent: unknown,
|
||||
params: never,
|
||||
context: Context
|
||||
) => {
|
||||
const id = context.currentUser.id;
|
||||
|
||||
const flows = await context.currentUser.$relatedQuery('flows').where({
|
||||
active: true,
|
||||
});
|
||||
|
||||
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
||||
|
||||
for (const flow of flows) {
|
||||
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||
|
||||
if (job) {
|
||||
await flowQueue.removeRepeatableByKey(job.key);
|
||||
}
|
||||
}
|
||||
|
||||
const executionIds = (
|
||||
await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.select('executions.id')
|
||||
).map((execution: Execution) => execution.id);
|
||||
const flowIds = flows.map((flow) => flow.id);
|
||||
|
||||
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
||||
await context.currentUser.$relatedQuery('executions').delete();
|
||||
await context.currentUser.$relatedQuery('steps').delete();
|
||||
await Flow.query().whereIn('id', flowIds).delete();
|
||||
await context.currentUser.$relatedQuery('connections').delete();
|
||||
await context.currentUser.$relatedQuery('identities').delete();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
await context.currentUser.$relatedQuery('subscriptions').delete();
|
||||
await context.currentUser.$relatedQuery('usageData').delete();
|
||||
}
|
||||
|
||||
await context.currentUser.$query().delete();
|
||||
|
||||
const jobName = `Delete user - ${id}`;
|
||||
const jobPayload = { id };
|
||||
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||
const jobOptions = {
|
||||
delay: millisecondsFor30Days
|
||||
delay: millisecondsFor30Days,
|
||||
};
|
||||
|
||||
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import Context from '../../types/express/context';
|
||||
import testRun from '../../services/test-run';
|
||||
import Step from '../../models/step';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -12,12 +13,16 @@ const executeFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const isCreator = conditions.isCreator;
|
||||
const allSteps = Step.query();
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const baseQuery = isCreator ? userSteps : allSteps;
|
||||
|
||||
const { stepId } = params.input;
|
||||
|
||||
const untilStep = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
const untilStep = await baseQuery
|
||||
.clone()
|
||||
.findById(stepId)
|
||||
.throwIfNotFound();
|
||||
|
||||
|
@@ -0,0 +1,38 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
name: string;
|
||||
formattedAuthDefaults?: IJSONObject;
|
||||
active?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const updateAppAuthClient = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const {
|
||||
id,
|
||||
...appAuthClientData
|
||||
} = params.input;
|
||||
|
||||
const appAuthClient = await AppAuthClient
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
await appAuthClient
|
||||
.$query()
|
||||
.patch(appAuthClientData);
|
||||
|
||||
return appAuthClient;
|
||||
};
|
||||
|
||||
export default updateAppAuthClient;
|
@@ -0,0 +1,39 @@
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
allowCustomConnection?: boolean;
|
||||
shared?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const updateAppConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const {
|
||||
id,
|
||||
...appConfigToUpdate
|
||||
} = params.input;
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
await appConfig
|
||||
.$query()
|
||||
.patch(
|
||||
appConfigToUpdate
|
||||
);
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
||||
export default updateAppConfig;
|
52
packages/backend/src/graphql/mutations/update-config.ee.ts
Normal file
52
packages/backend/src/graphql/mutations/update-config.ee.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { IJSONValue } from '@automatisch/types';
|
||||
import Config from '../../models/config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
[index: string]: IJSONValue;
|
||||
};
|
||||
};
|
||||
|
||||
const updateConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Config');
|
||||
|
||||
const config = params.input;
|
||||
const configKeys = Object.keys(config);
|
||||
const updates = [];
|
||||
|
||||
for (const key of configKeys) {
|
||||
const newValue = config[key];
|
||||
|
||||
if (newValue) {
|
||||
const entryUpdate = Config.query()
|
||||
.insert({
|
||||
key,
|
||||
value: {
|
||||
data: newValue,
|
||||
},
|
||||
})
|
||||
.onConflict('key')
|
||||
.merge({
|
||||
value: {
|
||||
data: newValue,
|
||||
},
|
||||
});
|
||||
|
||||
updates.push(entryUpdate);
|
||||
} else {
|
||||
const entryUpdate = Config.query().findOne({ key }).delete();
|
||||
updates.push(entryUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export default updateConfig;
|
@@ -1,10 +1,12 @@
|
||||
import Context from '../../types/express/context';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import Context from '../../types/express/context';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
formattedData: IJSONObject;
|
||||
formattedData?: IJSONObject;
|
||||
appAuthClientId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,10 +24,21 @@ const updateConnection = async (
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
let formattedData = params.input.formattedData;
|
||||
|
||||
if (params.input.appAuthClientId) {
|
||||
const appAuthClient = await AppAuthClient
|
||||
.query()
|
||||
.findById(params.input.appAuthClientId)
|
||||
.throwIfNotFound();
|
||||
|
||||
formattedData = appAuthClient.formattedAuthDefaults;
|
||||
}
|
||||
|
||||
connection = await connection.$query().patchAndFetch({
|
||||
formattedData: {
|
||||
...connection.formattedData,
|
||||
...params.input.formattedData,
|
||||
...formattedData,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Flow from '../../models/flow';
|
||||
import Context from '../../types/express/context';
|
||||
import flowQueue from '../../queues/flow';
|
||||
import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration';
|
||||
@@ -18,10 +19,14 @@ const updateFlowStatus = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('publish', 'Flow');
|
||||
const conditions = context.currentUser.can('publish', 'Flow');
|
||||
const isCreator = conditions.isCreator;
|
||||
const allFlows = Flow.query();
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const baseQuery = isCreator ? userFlows : allFlows;
|
||||
|
||||
let flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
let flow = await baseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import Step from '../../models/step';
|
||||
import Connection from '../../models/connection';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
@@ -23,12 +24,14 @@ const updateStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
const { isCreator } = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const baseQuery = isCreator ? userSteps : allSteps;
|
||||
|
||||
const { input } = params;
|
||||
|
||||
let step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
let step = await baseQuery
|
||||
.findOne({
|
||||
'steps.id': input.id,
|
||||
flow_id: input.flow.id,
|
||||
@@ -36,11 +39,24 @@ const updateStep = async (
|
||||
.throwIfNotFound();
|
||||
|
||||
if (input.connection.id) {
|
||||
const hasConnection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findById(input.connection?.id);
|
||||
let canSeeAllConnections = false;
|
||||
try {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
|
||||
if (!hasConnection) {
|
||||
canSeeAllConnections = !conditions.isCreator;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
|
||||
|
||||
const connection = await baseConnectionsQuery
|
||||
.clone()
|
||||
.findById(input.connection?.id)
|
||||
|
||||
if (!connection) {
|
||||
throw new Error('The connection does not exist!');
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
const createSamlAuthProvider = async (
|
||||
const upsertSamlAuthProvider = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
@@ -33,17 +33,15 @@ const createSamlAuthProvider = async (
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
let samlAuthProvider: SamlAuthProvider;
|
||||
|
||||
if (!existingSamlAuthProvider) {
|
||||
samlAuthProvider = await SamlAuthProvider.query().insert(
|
||||
const samlAuthProvider = await SamlAuthProvider.query().insert(
|
||||
samlAuthProviderPayload
|
||||
);
|
||||
|
||||
return samlAuthProvider;
|
||||
}
|
||||
|
||||
samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
|
||||
const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
|
||||
existingSamlAuthProvider.id,
|
||||
samlAuthProviderPayload
|
||||
);
|
||||
@@ -51,4 +49,4 @@ const createSamlAuthProvider = async (
|
||||
return samlAuthProvider;
|
||||
};
|
||||
|
||||
export default createSamlAuthProvider;
|
||||
export default upsertSamlAuthProvider;
|
@@ -0,0 +1,54 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../../models/saml-auth-providers-role-mapping.ee';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
samlAuthProviderId: string;
|
||||
samlAuthProvidersRoleMappings: [
|
||||
{
|
||||
roleId: string;
|
||||
remoteRoleName: string;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
const upsertSamlAuthProvidersRoleMappings = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'SamlAuthProvider');
|
||||
|
||||
const samlAuthProviderId = params.input.samlAuthProviderId;
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider.query()
|
||||
.findById(samlAuthProviderId)
|
||||
.throwIfNotFound();
|
||||
|
||||
await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.delete();
|
||||
|
||||
if (!params.input.samlAuthProvidersRoleMappings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const samlAuthProvidersRoleMappingsData =
|
||||
params.input.samlAuthProvidersRoleMappings.map(
|
||||
(samlAuthProvidersRoleMapping) => ({
|
||||
...samlAuthProvidersRoleMapping,
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
})
|
||||
);
|
||||
|
||||
const samlAuthProvidersRoleMappings =
|
||||
await SamlAuthProvidersRoleMapping.query().insert(
|
||||
samlAuthProvidersRoleMappingsData
|
||||
);
|
||||
|
||||
return samlAuthProvidersRoleMappings;
|
||||
};
|
||||
|
||||
export default upsertSamlAuthProvidersRoleMappings;
|
@@ -0,0 +1,30 @@
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const getAppAuthClient = async (_parent: unknown, params: Params, context: Context) => {
|
||||
let canSeeAllClients = false;
|
||||
try {
|
||||
context.currentUser.can('read', 'App');
|
||||
|
||||
canSeeAllClients = true;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const appAuthClient = AppAuthClient
|
||||
.query()
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
|
||||
if (!canSeeAllClients) {
|
||||
appAuthClient.where({ active: true });
|
||||
}
|
||||
|
||||
return await appAuthClient;
|
||||
};
|
||||
|
||||
export default getAppAuthClient;
|
@@ -0,0 +1,40 @@
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
appKey: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
const getAppAuthClients = async (_parent: unknown, params: Params, context: Context) => {
|
||||
let canSeeAllClients = false;
|
||||
try {
|
||||
context.currentUser.can('read', 'App');
|
||||
|
||||
canSeeAllClients = true;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.findOne({
|
||||
key: params.appKey,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
const appAuthClients = appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.where({ active: params.active })
|
||||
.skipUndefined();
|
||||
|
||||
if (!canSeeAllClients) {
|
||||
appAuthClients.where({
|
||||
active: true
|
||||
})
|
||||
}
|
||||
|
||||
return await appAuthClients;
|
||||
};
|
||||
|
||||
export default getAppAuthClients;
|
23
packages/backend/src/graphql/queries/get-app-config.ee.ts
Normal file
23
packages/backend/src/graphql/queries/get-app-config.ee.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
const getAppConfig = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.withGraphFetched({
|
||||
appAuthClients: true
|
||||
})
|
||||
.findOne({
|
||||
key: params.key
|
||||
});
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
||||
export default getAppConfig;
|
@@ -19,6 +19,10 @@ const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.*')
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
appAuthClient: true
|
||||
})
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
'connections.key': params.key,
|
||||
|
@@ -1,8 +1,19 @@
|
||||
import appConfig from '../../config/app';
|
||||
import { getLicense } from '../../helpers/license.ee';
|
||||
|
||||
const getAutomatischInfo = async () => {
|
||||
const license = await getLicense();
|
||||
|
||||
const computedLicense = {
|
||||
id: license ? license.id : null,
|
||||
name: license ? license.name : null,
|
||||
expireAt: license ? license.expireAt : null,
|
||||
verified: license ? true : false,
|
||||
};
|
||||
|
||||
return {
|
||||
isCloud: appConfig.isCloud,
|
||||
license: computedLicense,
|
||||
};
|
||||
};
|
||||
|
||||
|
34
packages/backend/src/graphql/queries/get-config.ee.ts
Normal file
34
packages/backend/src/graphql/queries/get-config.ee.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { hasValidLicense } from '../../helpers/license.ee';
|
||||
import Config from '../../models/config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
keys: string[];
|
||||
};
|
||||
|
||||
const getConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
if (!await hasValidLicense()) return {};
|
||||
|
||||
const configQuery = Config
|
||||
.query();
|
||||
|
||||
if (Array.isArray(params.keys)) {
|
||||
configQuery.whereIn('key', params.keys);
|
||||
}
|
||||
|
||||
const config = await configQuery;
|
||||
|
||||
return config.reduce((computedConfig, configEntry) => {
|
||||
const { key, value } = configEntry;
|
||||
|
||||
computedConfig[key] = value?.data;
|
||||
|
||||
return computedConfig;
|
||||
}, {} as Record<string, unknown>);
|
||||
};
|
||||
|
||||
export default getConfig;
|
@@ -32,7 +32,7 @@ const getDynamicFields = async (
|
||||
|
||||
const connection = step.connection;
|
||||
|
||||
if (!connection || !step.appKey) return null;
|
||||
if (!step.appKey) return null;
|
||||
|
||||
const app = await App.findOneByKey(step.appKey);
|
||||
const $ = await globalVariable({ connection, app, flow: step.flow, step });
|
||||
|
15
packages/backend/src/graphql/queries/get-notifications.ts
Normal file
15
packages/backend/src/graphql/queries/get-notifications.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import axios from '../../helpers/axios-with-proxy';
|
||||
|
||||
const NOTIFICATIONS_URL = 'https://notifications.automatisch.io/notifications.json';
|
||||
|
||||
const getNotifications = async () => {
|
||||
try {
|
||||
const { data: notifications = [] } = await axios.get(NOTIFICATIONS_URL);
|
||||
|
||||
return notifications;
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default getNotifications;
|
@@ -0,0 +1,23 @@
|
||||
import Context from '../../types/express/context';
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const getSamlAuthProviderRoleMappings = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'SamlAuthProvider');
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider
|
||||
.query()
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
|
||||
const roleMappings = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.orderBy('remote_role_name', 'asc')
|
||||
|
||||
return roleMappings;
|
||||
};
|
||||
|
||||
export default getSamlAuthProviderRoleMappings;
|
@@ -0,0 +1,19 @@
|
||||
import Context from '../../types/express/context';
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProvider = async (
|
||||
_parent: unknown,
|
||||
params: unknown,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('read', 'SamlAuthProvider');
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider.query()
|
||||
.limit(1)
|
||||
.first()
|
||||
.throwIfNotFound();
|
||||
|
||||
return samlAuthProvider;
|
||||
};
|
||||
|
||||
export default getSamlAuthProvider;
|
@@ -10,15 +10,14 @@ type Params = {
|
||||
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
const usersQuery = User
|
||||
.query()
|
||||
const usersQuery = User.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
role: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
role: true,
|
||||
})
|
||||
.orderBy('full_name', 'desc');
|
||||
.orderBy('full_name', 'asc');
|
||||
|
||||
return paginate(usersQuery, params.limit, params.offset);
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProviders = async () => {
|
||||
const listSamlAuthProviders = async () => {
|
||||
const providers = await SamlAuthProvider.query().where({ active: true });
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
||||
export default getSamlAuthProviders;
|
||||
export default listSamlAuthProviders;
|
@@ -1,7 +1,11 @@
|
||||
import getApp from './queries/get-app';
|
||||
import getAppAuthClient from './queries/get-app-auth-client.ee';
|
||||
import getAppAuthClients from './queries/get-app-auth-clients.ee';
|
||||
import getAppConfig from './queries/get-app-config.ee';
|
||||
import getApps from './queries/get-apps';
|
||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
||||
import getConfig from './queries/get-config.ee';
|
||||
import getConnectedApps from './queries/get-connected-apps';
|
||||
import getCurrentUser from './queries/get-current-user';
|
||||
import getDynamicData from './queries/get-dynamic-data';
|
||||
@@ -11,26 +15,33 @@ import getExecutionSteps from './queries/get-execution-steps';
|
||||
import getExecutions from './queries/get-executions';
|
||||
import getFlow from './queries/get-flow';
|
||||
import getFlows from './queries/get-flows';
|
||||
import getUser from './queries/get-user';
|
||||
import getUsers from './queries/get-users';
|
||||
import getInvoices from './queries/get-invoices.ee';
|
||||
import getNotifications from './queries/get-notifications';
|
||||
import getPaddleInfo from './queries/get-paddle-info.ee';
|
||||
import getPaymentPlans from './queries/get-payment-plans.ee';
|
||||
import getPermissionCatalog from './queries/get-permission-catalog.ee';
|
||||
import getRole from './queries/get-role.ee';
|
||||
import getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
|
||||
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
|
||||
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||
import getTrialStatus from './queries/get-trial-status.ee';
|
||||
import getUser from './queries/get-user';
|
||||
import getUsers from './queries/get-users';
|
||||
import healthcheck from './queries/healthcheck';
|
||||
import listSamlAuthProviders from './queries/list-saml-auth-providers.ee';
|
||||
import testConnection from './queries/test-connection';
|
||||
|
||||
const queryResolvers = {
|
||||
getApp,
|
||||
getAppAuthClient,
|
||||
getAppAuthClients,
|
||||
getAppConfig,
|
||||
getApps,
|
||||
getAutomatischInfo,
|
||||
getBillingAndUsage,
|
||||
getConfig,
|
||||
getConnectedApps,
|
||||
getCurrentUser,
|
||||
getDynamicData,
|
||||
@@ -41,18 +52,21 @@ const queryResolvers = {
|
||||
getFlow,
|
||||
getFlows,
|
||||
getInvoices,
|
||||
getNotifications,
|
||||
getPaddleInfo,
|
||||
getPaymentPlans,
|
||||
getPermissionCatalog,
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProviders,
|
||||
getSamlAuthProvider,
|
||||
getSamlAuthProviderRoleMappings,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
getUser,
|
||||
getUsers,
|
||||
healthcheck,
|
||||
listSamlAuthProviders,
|
||||
testConnection,
|
||||
};
|
||||
|
||||
|
@@ -5,6 +5,9 @@ type Query {
|
||||
onlyWithActions: Boolean
|
||||
): [App]
|
||||
getApp(key: String!): App
|
||||
getAppConfig(key: String!): AppConfig
|
||||
getAppAuthClient(id: String!): AppAuthClient
|
||||
getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient]
|
||||
getConnectedApps(name: String): [App]
|
||||
testConnection(id: String!): Connection
|
||||
getFlow(id: String!): Flow
|
||||
@@ -33,24 +36,30 @@ type Query {
|
||||
key: String!
|
||||
parameters: JSONObject
|
||||
): [SubstepArgument]
|
||||
getCurrentUser: User
|
||||
getPaymentPlans: [PaymentPlan]
|
||||
getPaddleInfo: GetPaddleInfo
|
||||
getBillingAndUsage: GetBillingAndUsage
|
||||
getInvoices: [Invoice]
|
||||
getAutomatischInfo: GetAutomatischInfo
|
||||
getTrialStatus: GetTrialStatus
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getSamlAuthProviders: [GetSamlAuthProviders]
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
getUser(id: String!): User
|
||||
getRoles: [Role]
|
||||
getRole(id: String!): Role
|
||||
getBillingAndUsage: GetBillingAndUsage
|
||||
getCurrentUser: User
|
||||
getConfig(keys: [String]): JSONObject
|
||||
getInvoices: [Invoice]
|
||||
getPaddleInfo: GetPaddleInfo
|
||||
getPaymentPlans: [PaymentPlan]
|
||||
getPermissionCatalog: PermissionCatalog
|
||||
getRole(id: String!): Role
|
||||
getRoles: [Role]
|
||||
getNotifications: [Notification]
|
||||
getSamlAuthProvider: SamlAuthProvider
|
||||
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getTrialStatus: GetTrialStatus
|
||||
getUser(id: String!): User
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
healthcheck: AppHealth
|
||||
listSamlAuthProviders: [ListSamlAuthProvider]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createAppConfig(input: CreateAppConfigInput): AppConfig
|
||||
createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient
|
||||
createConnection(input: CreateConnectionInput): Connection
|
||||
createFlow(input: CreateFlowInput): Flow
|
||||
createRole(input: CreateRoleInput): Role
|
||||
@@ -70,6 +79,9 @@ type Mutation {
|
||||
registerUser(input: RegisterUserInput): User
|
||||
resetConnection(input: ResetConnectionInput): Connection
|
||||
resetPassword(input: ResetPasswordInput): Boolean
|
||||
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
|
||||
updateAppConfig(input: UpdateAppConfigInput): AppConfig
|
||||
updateConfig(input: JSONObject): JSONObject
|
||||
updateConnection(input: UpdateConnectionInput): Connection
|
||||
updateCurrentUser(input: UpdateCurrentUserInput): User
|
||||
updateFlow(input: UpdateFlowInput): Flow
|
||||
@@ -77,8 +89,11 @@ type Mutation {
|
||||
updateRole(input: UpdateRoleInput): Role
|
||||
updateStep(input: UpdateStepInput): Step
|
||||
updateUser(input: UpdateUserInput): User
|
||||
upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider
|
||||
upsertSamlAuthProvidersRoleMappings(
|
||||
input: UpsertSamlAuthProvidersRoleMappingsInput
|
||||
): [SamlAuthProvidersRoleMapping]
|
||||
verifyConnection(input: VerifyConnectionInput): Connection
|
||||
createSamlAuthProvider(input: CreateSamlAuthProviderInput): SamlAuthProvider
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -156,6 +171,16 @@ type SubstepArgumentAdditionalFieldsArgument {
|
||||
value: String
|
||||
}
|
||||
|
||||
type AppConfig {
|
||||
id: String
|
||||
key: String
|
||||
allowCustomConnection: Boolean
|
||||
canConnect: Boolean
|
||||
canCustomConnect: Boolean
|
||||
shared: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
type App {
|
||||
name: String
|
||||
key: String
|
||||
@@ -175,7 +200,9 @@ type App {
|
||||
type AppAuth {
|
||||
fields: [Field]
|
||||
authenticationSteps: [AuthenticationStep]
|
||||
sharedAuthenticationSteps: [AuthenticationStep]
|
||||
reconnectionSteps: [ReconnectionStep]
|
||||
sharedReconnectionSteps: [ReconnectionStep]
|
||||
}
|
||||
|
||||
enum ArgumentEnumType {
|
||||
@@ -213,6 +240,8 @@ type AuthLink {
|
||||
type Connection {
|
||||
id: String
|
||||
key: String
|
||||
reconnectable: Boolean
|
||||
appAuthClientId: String
|
||||
formattedData: ConnectionData
|
||||
verified: Boolean
|
||||
app: App
|
||||
@@ -302,11 +331,20 @@ type SamlAuthProvider {
|
||||
emailAttributeName: String
|
||||
roleAttributeName: String
|
||||
active: Boolean
|
||||
defaultRoleId: String
|
||||
}
|
||||
|
||||
type SamlAuthProvidersRoleMapping {
|
||||
id: String
|
||||
samlAuthProviderId: String
|
||||
roleId: String
|
||||
remoteRoleName: String
|
||||
}
|
||||
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
totalCount: Int
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
@@ -315,7 +353,8 @@ type UserEdge {
|
||||
|
||||
input CreateConnectionInput {
|
||||
key: String!
|
||||
formattedData: JSONObject!
|
||||
appAuthClientId: String
|
||||
formattedData: JSONObject
|
||||
}
|
||||
|
||||
input GenerateAuthUrlInput {
|
||||
@@ -324,7 +363,8 @@ input GenerateAuthUrlInput {
|
||||
|
||||
input UpdateConnectionInput {
|
||||
id: String!
|
||||
formattedData: JSONObject!
|
||||
formattedData: JSONObject
|
||||
appAuthClientId: String
|
||||
}
|
||||
|
||||
input ResetConnectionInput {
|
||||
@@ -335,7 +375,7 @@ input VerifyConnectionInput {
|
||||
id: String!
|
||||
}
|
||||
|
||||
input CreateSamlAuthProviderInput {
|
||||
input UpsertSamlAuthProviderInput {
|
||||
name: String!
|
||||
certificate: String!
|
||||
signatureAlgorithm: String!
|
||||
@@ -349,6 +389,16 @@ input CreateSamlAuthProviderInput {
|
||||
active: Boolean!
|
||||
}
|
||||
|
||||
input UpsertSamlAuthProvidersRoleMappingsInput {
|
||||
samlAuthProviderId: String!
|
||||
samlAuthProvidersRoleMappings: [SamlAuthProviderRoleMappingInput]
|
||||
}
|
||||
|
||||
input SamlAuthProviderRoleMappingInput {
|
||||
roleId: String!
|
||||
remoteRoleName: String!
|
||||
}
|
||||
|
||||
input DeleteConnectionInput {
|
||||
id: String!
|
||||
}
|
||||
@@ -592,6 +642,14 @@ type AppHealth {
|
||||
|
||||
type GetAutomatischInfo {
|
||||
isCloud: Boolean
|
||||
license: License
|
||||
}
|
||||
|
||||
type License {
|
||||
id: String
|
||||
name: String
|
||||
expireAt: String
|
||||
verified: Boolean
|
||||
}
|
||||
|
||||
type GetTrialStatus {
|
||||
@@ -659,10 +717,11 @@ type PaymentPlan {
|
||||
productId: String
|
||||
}
|
||||
|
||||
type GetSamlAuthProviders {
|
||||
type ListSamlAuthProvider {
|
||||
id: String
|
||||
name: String
|
||||
issuer: String
|
||||
loginUrl: String
|
||||
}
|
||||
|
||||
type Permission {
|
||||
@@ -694,6 +753,48 @@ type Subject {
|
||||
key: String
|
||||
}
|
||||
|
||||
input CreateAppConfigInput {
|
||||
key: String
|
||||
allowCustomConnection: Boolean
|
||||
shared: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
input UpdateAppConfigInput {
|
||||
id: String
|
||||
allowCustomConnection: Boolean
|
||||
shared: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
type AppAuthClient {
|
||||
id: String
|
||||
appConfigId: String
|
||||
name: String
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
input CreateAppAuthClientInput {
|
||||
appConfigId: String
|
||||
name: String
|
||||
formattedAuthDefaults: JSONObject
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
input UpdateAppAuthClientInput {
|
||||
id: String
|
||||
name: String
|
||||
formattedAuthDefaults: JSONObject
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
type Notification {
|
||||
name: String
|
||||
createdAt: String
|
||||
documentationUrl: String
|
||||
description: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
|
@@ -3,6 +3,7 @@ import { IApp } from '@automatisch/types';
|
||||
function addAuthenticationSteps(app: IApp): IApp {
|
||||
if (app.auth.generateAuthUrl) {
|
||||
app.auth.authenticationSteps = authenticationStepsWithAuthUrl;
|
||||
app.auth.sharedAuthenticationSteps = sharedAuthenticationStepsWithAuthUrl;
|
||||
} else {
|
||||
app.auth.authenticationSteps = authenticationStepsWithoutAuthUrl;
|
||||
}
|
||||
@@ -98,4 +99,65 @@ const authenticationStepsWithAuthUrl = [
|
||||
},
|
||||
];
|
||||
|
||||
const sharedAuthenticationStepsWithAuthUrl = [
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'createConnection',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: '{key}',
|
||||
},
|
||||
{
|
||||
name: 'appAuthClientId',
|
||||
value: '{appAuthClientId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'generateAuthUrl',
|
||||
arguments: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '{createConnection.id}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'openWithPopup' as const,
|
||||
name: 'openAuthPopup',
|
||||
arguments: [
|
||||
{
|
||||
name: 'url',
|
||||
value: '{generateAuthUrl.url}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'updateConnection',
|
||||
arguments: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '{createConnection.id}',
|
||||
},
|
||||
{
|
||||
name: 'formattedData',
|
||||
value: '{openAuthPopup.all}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'verifyConnection',
|
||||
arguments: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '{createConnection.id}',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default addAuthenticationSteps;
|
||||
|
@@ -67,11 +67,21 @@ function addReconnectionSteps(app: IApp): IApp {
|
||||
|
||||
if (hasReconnectionSteps) return app;
|
||||
|
||||
const updatedSteps = replaceCreateConnectionsWithUpdate(
|
||||
app.auth.authenticationSteps
|
||||
);
|
||||
if (app.auth.authenticationSteps) {
|
||||
const updatedSteps = replaceCreateConnectionsWithUpdate(
|
||||
app.auth.authenticationSteps
|
||||
);
|
||||
|
||||
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
|
||||
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
|
||||
}
|
||||
|
||||
if (app.auth.sharedAuthenticationSteps) {
|
||||
const updatedStepsWithEmbeddedDefaults = replaceCreateConnectionsWithUpdate(
|
||||
app.auth.sharedAuthenticationSteps
|
||||
);
|
||||
|
||||
app.auth.sharedReconnectionSteps = [resetConnectionStep, ...updatedStepsWithEmbeddedDefaults];
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { rule, shield, allow } from 'graphql-shield';
|
||||
import { allow, rule, shield } from 'graphql-shield';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User from '../models/user';
|
||||
import appConfig from '../config/app';
|
||||
import User from '../models/user';
|
||||
|
||||
const isAuthenticated = rule()(async (_parent, _args, req) => {
|
||||
const token = req.headers['authorization'];
|
||||
@@ -12,8 +12,7 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
|
||||
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
||||
userId: string;
|
||||
};
|
||||
req.currentUser = await User
|
||||
.query()
|
||||
req.currentUser = await User.query()
|
||||
.findById(userId)
|
||||
.leftJoinRelated({
|
||||
role: true,
|
||||
@@ -35,14 +34,16 @@ const authentication = shield(
|
||||
Query: {
|
||||
'*': isAuthenticated,
|
||||
getAutomatischInfo: allow,
|
||||
getSamlAuthProviders: allow,
|
||||
getConfig: allow,
|
||||
getNotifications: allow,
|
||||
healthcheck: allow,
|
||||
listSamlAuthProviders: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': isAuthenticated,
|
||||
registerUser: allow,
|
||||
forgotPassword: allow,
|
||||
login: allow,
|
||||
registerUser: allow,
|
||||
resetPassword: allow,
|
||||
},
|
||||
},
|
||||
|
@@ -2,8 +2,7 @@ import Step from '../models/step';
|
||||
import ExecutionStep from '../models/execution-step';
|
||||
import get from 'lodash.get';
|
||||
|
||||
// INFO: don't remove space in allowed character group!
|
||||
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ :]+)+}})/g;
|
||||
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[^.}{]+)+}})/g;
|
||||
|
||||
export default function computeParameters(
|
||||
parameters: Step['parameters'],
|
||||
|
@@ -1,19 +1,27 @@
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import User from '../models/user';
|
||||
import Identity from '../models/identity.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
|
||||
const getUser = (user: Record<string, unknown>, providerConfig: SamlAuthProvider) => ({
|
||||
const getUser = (
|
||||
user: Record<string, unknown>,
|
||||
providerConfig: SamlAuthProvider
|
||||
) => ({
|
||||
name: user[providerConfig.firstnameAttributeName],
|
||||
surname: user[providerConfig.surnameAttributeName],
|
||||
id: user.nameID,
|
||||
email: user[providerConfig.emailAttributeName],
|
||||
role: user[providerConfig.roleAttributeName],
|
||||
})
|
||||
role: user[providerConfig.roleAttributeName] as string | string[],
|
||||
});
|
||||
|
||||
const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unknown>, samlAuthProvider: SamlAuthProvider) => {
|
||||
const findOrCreateUserBySamlIdentity = async (
|
||||
userIdentity: Record<string, unknown>,
|
||||
samlAuthProvider: SamlAuthProvider
|
||||
) => {
|
||||
const mappedUser = getUser(userIdentity, samlAuthProvider);
|
||||
const identity = await Identity.query().findOne({
|
||||
remote_id: mappedUser.id,
|
||||
provider_type: 'saml',
|
||||
});
|
||||
|
||||
if (identity) {
|
||||
@@ -22,25 +30,38 @@ const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unkno
|
||||
return user;
|
||||
}
|
||||
|
||||
const createdUser = await User.query().insertGraph({
|
||||
fullName: [
|
||||
mappedUser.name,
|
||||
mappedUser.surname
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId: samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
const mappedRoles = Array.isArray(mappedUser.role)
|
||||
? mappedUser.role
|
||||
: [mappedUser.role];
|
||||
|
||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.whereIn('remote_role_name', mappedRoles)
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
const createdUser = await User.query()
|
||||
.insertGraph(
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
providerId: samlAuthProvider.id,
|
||||
providerType: 'saml'
|
||||
fullName: [mappedUser.name, mappedUser.surname]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId:
|
||||
samlAuthProviderRoleMapping?.roleId || samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
providerId: samlAuthProvider.id,
|
||||
providerType: 'saml',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
relate: ['identities'],
|
||||
}
|
||||
]
|
||||
}, {
|
||||
relate: ['identities']
|
||||
}).returning('*');
|
||||
)
|
||||
.returning('*');
|
||||
|
||||
return createdUser;
|
||||
};
|
||||
|
@@ -1,11 +1,16 @@
|
||||
// TODO: replace with axios-with-proxy
|
||||
import axios from 'axios';
|
||||
import appConfig from '../config/app';
|
||||
import memoryCache from 'memory-cache';
|
||||
import appConfig from '../config/app';
|
||||
import axios from './axios-with-proxy';
|
||||
|
||||
const CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds
|
||||
|
||||
const checkLicense = async () => {
|
||||
const hasValidLicense = async () => {
|
||||
const license = await getLicense();
|
||||
|
||||
return license ? true : false;
|
||||
};
|
||||
|
||||
const getLicense = async () => {
|
||||
const licenseKey = appConfig.licenseKey;
|
||||
|
||||
if (!licenseKey) {
|
||||
@@ -20,13 +25,13 @@ const checkLicense = async () => {
|
||||
} else {
|
||||
try {
|
||||
const { data } = await axios.post(url, { licenseKey });
|
||||
memoryCache.put(url, data.verified, CACHE_DURATION);
|
||||
memoryCache.put(url, data, CACHE_DURATION);
|
||||
|
||||
return data.verified;
|
||||
return data;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default checkLicense;
|
||||
export { getLicense, hasValidLicense };
|
@@ -21,6 +21,7 @@ const paginate = async (
|
||||
currentPage: Math.ceil(offset / limit + 1),
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
totalCount: count,
|
||||
edges: records.map((record: Base) => ({
|
||||
node: record,
|
||||
})),
|
||||
|
91
packages/backend/src/models/app-auth-client.ts
Normal file
91
packages/backend/src/models/app-auth-client.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
import appConfig from '../config/app';
|
||||
import AppConfig from './app-config';
|
||||
import Base from './base';
|
||||
|
||||
class AppAuthClient extends Base {
|
||||
id!: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
appConfigId!: string;
|
||||
authDefaults: string;
|
||||
formattedAuthDefaults?: IJSONObject;
|
||||
appConfig?: AppConfig;
|
||||
|
||||
static tableName = 'app_auth_clients';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['name', 'appConfigId', 'formattedAuthDefaults'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
appConfigId: { type: 'string', format: 'uuid' },
|
||||
active: { type: 'boolean' },
|
||||
authDefaults: { type: ['string', 'null'] },
|
||||
formattedAuthDefaults: { type: 'object' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'app_auth_clients.app_config_id',
|
||||
to: 'app_configs.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
encryptData(): void {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
this.authDefaults = AES.encrypt(
|
||||
JSON.stringify(this.formattedAuthDefaults),
|
||||
appConfig.encryptionKey
|
||||
).toString();
|
||||
|
||||
delete this.formattedAuthDefaults;
|
||||
}
|
||||
decryptData(): void {
|
||||
if (!this.eligibleForDecryption()) return;
|
||||
|
||||
this.formattedAuthDefaults = JSON.parse(
|
||||
AES.decrypt(this.authDefaults, appConfig.encryptionKey).toString(enc.Utf8)
|
||||
);
|
||||
}
|
||||
|
||||
eligibleForEncryption(): boolean {
|
||||
return this.formattedAuthDefaults ? true : false;
|
||||
}
|
||||
|
||||
eligibleForDecryption(): boolean {
|
||||
return this.authDefaults ? true : false;
|
||||
}
|
||||
|
||||
// TODO: Make another abstraction like beforeSave instead of using
|
||||
// beforeInsert and beforeUpdate separately for the same operation.
|
||||
async $beforeInsert(queryContext: QueryContext): Promise<void> {
|
||||
await super.$beforeInsert(queryContext);
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $beforeUpdate(
|
||||
opt: ModelOptions,
|
||||
queryContext: QueryContext
|
||||
): Promise<void> {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $afterFind(): Promise<void> {
|
||||
this.decryptData();
|
||||
}
|
||||
}
|
||||
|
||||
export default AppAuthClient;
|
70
packages/backend/src/models/app-config.ts
Normal file
70
packages/backend/src/models/app-config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import App from './app';
|
||||
import Base from './base';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
|
||||
class AppConfig extends Base {
|
||||
id!: string;
|
||||
key!: string;
|
||||
allowCustomConnection: boolean;
|
||||
shared: boolean;
|
||||
disabled: boolean;
|
||||
app?: App;
|
||||
appAuthClients?: AppAuthClient[];
|
||||
|
||||
static tableName = 'app_configs';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['key'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string' },
|
||||
allowCustomConnection: { type: 'boolean', default: false },
|
||||
shared: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['canConnect', 'canCustomConnect'];
|
||||
}
|
||||
|
||||
static relationMappings = () => ({
|
||||
appAuthClients: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'app_configs.id',
|
||||
to: 'app_auth_clients.app_config_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get canCustomConnect() {
|
||||
return !this.disabled && this.allowCustomConnection;
|
||||
}
|
||||
|
||||
get canConnect() {
|
||||
const hasSomeActiveAppAuthClients = !!this.appAuthClients
|
||||
?.some(appAuthClient => appAuthClient.active);
|
||||
const shared = this.shared;
|
||||
const active = this.disabled === false;
|
||||
|
||||
const conditions = [
|
||||
hasSomeActiveAppAuthClients,
|
||||
shared,
|
||||
active
|
||||
];
|
||||
|
||||
return conditions.every(Boolean);
|
||||
}
|
||||
|
||||
async getApp() {
|
||||
if (!this.key) return null;
|
||||
|
||||
return await App.findOneByKey(this.key);
|
||||
}
|
||||
}
|
||||
|
||||
export default AppConfig;
|
23
packages/backend/src/models/config.ts
Normal file
23
packages/backend/src/models/config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IJSONValue } from '@automatisch/types';
|
||||
import Base from './base';
|
||||
|
||||
class Config extends Base {
|
||||
id!: string;
|
||||
key!: string;
|
||||
value!: { data: IJSONValue };
|
||||
|
||||
static tableName = 'config';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['key', 'value'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string', minLength: 1 },
|
||||
value: { type: 'object' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Config;
|
@@ -3,6 +3,8 @@ import type { RelationMappings } from 'objection';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import App from './app';
|
||||
import AppConfig from './app-config';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
import Base from './base';
|
||||
import User from './user';
|
||||
import Step from './step';
|
||||
@@ -25,6 +27,9 @@ class Connection extends Base {
|
||||
user?: User;
|
||||
steps?: Step[];
|
||||
triggerSteps?: Step[];
|
||||
appAuthClientId?: string;
|
||||
appAuthClient?: AppAuthClient;
|
||||
appConfig?: AppConfig;
|
||||
|
||||
static tableName = 'connections';
|
||||
|
||||
@@ -38,6 +43,7 @@ class Connection extends Base {
|
||||
data: { type: 'string' },
|
||||
formattedData: { type: 'object' },
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
appAuthClientId: { type: 'string', format: 'uuid' },
|
||||
verified: { type: 'boolean', default: false },
|
||||
draft: { type: 'boolean' },
|
||||
deletedAt: { type: 'string' },
|
||||
@@ -46,6 +52,10 @@ class Connection extends Base {
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['reconnectable'];
|
||||
}
|
||||
|
||||
static relationMappings = (): RelationMappings => ({
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
@@ -74,8 +84,36 @@ class Connection extends Base {
|
||||
builder.where('type', '=', 'trigger');
|
||||
},
|
||||
},
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'connections.key',
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
appAuthClient: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'connections.app_auth_client_id',
|
||||
to: 'app_auth_clients.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get reconnectable() {
|
||||
if (this.appAuthClientId) {
|
||||
return this.appAuthClient.active;
|
||||
}
|
||||
|
||||
if (this.appConfig) {
|
||||
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
encryptData(): void {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import type { SamlConfig } from '@node-saml/passport-saml';
|
||||
import appConfig from '../config/app';
|
||||
import Base from './base';
|
||||
import Identity from './identity.ee';
|
||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee';
|
||||
|
||||
class SamlAuthProvider extends Base {
|
||||
id!: string;
|
||||
@@ -17,6 +18,7 @@ class SamlAuthProvider extends Base {
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
active: boolean;
|
||||
samlAuthProvidersRoleMappings?: SamlAuthProvidersRoleMapping[];
|
||||
|
||||
static tableName = 'saml_auth_providers';
|
||||
|
||||
@@ -63,8 +65,24 @@ class SamlAuthProvider extends Base {
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
samlAuthProvidersRoleMappings: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SamlAuthProvidersRoleMapping,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['loginUrl'];
|
||||
}
|
||||
|
||||
get loginUrl() {
|
||||
return new URL(`/login/saml/${this.issuer}`, appConfig.baseUrl).toString();
|
||||
}
|
||||
|
||||
get config(): SamlConfig {
|
||||
const callbackUrl = new URL(
|
||||
`/login/saml/${this.issuer}/callback`,
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import Base from './base';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||
|
||||
class SamlAuthProvidersRoleMapping extends Base {
|
||||
id!: string;
|
||||
samlAuthProviderId: string;
|
||||
roleId: string;
|
||||
remoteRoleName: string;
|
||||
|
||||
static tableName = 'saml_auth_providers_role_mappings';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['samlAuthProviderId', 'roleId', 'remoteRoleName'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
samlAuthProviderId: { type: 'string', format: 'uuid' },
|
||||
roleId: { type: 'string', format: 'uuid' },
|
||||
remoteRoleName: { type: 'string', minLength: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
samlAuthProvider: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default SamlAuthProvidersRoleMapping;
|
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import checkLicense from '../helpers/check-license.ee';
|
||||
import { hasValidLicense } from '../helpers/license.ee';
|
||||
import userAbility from '../helpers/user-ability';
|
||||
import Base from './base';
|
||||
import Connection from './connection';
|
||||
@@ -289,17 +289,18 @@ class User extends Base {
|
||||
}
|
||||
|
||||
async $afterFind(): Promise<any> {
|
||||
const hasValidLicense = await checkLicense();
|
||||
|
||||
if (hasValidLicense) return this;
|
||||
if (await hasValidLicense()) return this;
|
||||
|
||||
if (Array.isArray(this.permissions)) {
|
||||
this.permissions = this.permissions.filter((permission) => {
|
||||
const isRolePermission = permission.subject === 'Role';
|
||||
const isSamlAuthProviderPermission =
|
||||
permission.subject === 'SamlAuthProvider';
|
||||
const restrictedSubjects = [
|
||||
'App',
|
||||
'Role',
|
||||
'SamlAuthProvider',
|
||||
'Config',
|
||||
];
|
||||
|
||||
return !isRolePermission && !isSamlAuthProviderPermission;
|
||||
return !restrictedSubjects.includes(permission.subject);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { Worker } from 'bullmq';
|
||||
import * as Sentry from '../helpers/sentry.ee';
|
||||
import redisConfig from '../config/redis';
|
||||
import logger from '../helpers/logger';
|
||||
import appConfig from '../config/app';
|
||||
import User from '../models/user';
|
||||
import Execution from '../models/execution';
|
||||
import ExecutionStep from '../models/execution-step';
|
||||
@@ -12,21 +13,34 @@ export const worker = new Worker(
|
||||
async (job) => {
|
||||
const { id } = job.data;
|
||||
|
||||
const user = await User.query().findById(id).throwIfNotFound();
|
||||
const user = await User.query()
|
||||
.withSoftDeleted()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
const executionIds = (
|
||||
await user.$relatedQuery('executions').select('executions.id')
|
||||
await user
|
||||
.$relatedQuery('executions')
|
||||
.withSoftDeleted()
|
||||
.select('executions.id')
|
||||
).map((execution: Execution) => execution.id);
|
||||
|
||||
await ExecutionStep.query()
|
||||
.hardDelete()
|
||||
.whereIn('execution_id', executionIds);
|
||||
await user.$relatedQuery('executions').hardDelete();
|
||||
await user.$relatedQuery('steps').hardDelete();
|
||||
await user.$relatedQuery('flows').hardDelete();
|
||||
await user.$relatedQuery('connections').hardDelete();
|
||||
.withSoftDeleted()
|
||||
.whereIn('execution_id', executionIds)
|
||||
.hardDelete();
|
||||
await user.$relatedQuery('executions').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('steps').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('flows').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('connections').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('identities').withSoftDeleted().hardDelete();
|
||||
|
||||
await user.$query().hardDelete();
|
||||
if (appConfig.isCloud) {
|
||||
await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
||||
}
|
||||
|
||||
await user.$query().withSoftDeleted().hardDelete();
|
||||
},
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
@@ -3,6 +3,7 @@ import { Worker } from 'bullmq';
|
||||
import * as Sentry from '../helpers/sentry.ee';
|
||||
import redisConfig from '../config/redis';
|
||||
import logger from '../helpers/logger';
|
||||
import flowQueue from '../queues/flow';
|
||||
import triggerQueue from '../queues/trigger';
|
||||
import { processFlow } from '../services/flow';
|
||||
import Flow from '../models/flow';
|
||||
@@ -66,7 +67,7 @@ worker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
worker.on('failed', async (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -74,6 +75,18 @@ worker.on('failed', (job, err) => {
|
||||
|
||||
logger.error(errorMessage);
|
||||
|
||||
const flow = await Flow.query().findById(job.data.flowId);
|
||||
|
||||
if (!flow) {
|
||||
await flowQueue.removeRepeatableByKey(job.repeatJobKey);
|
||||
|
||||
const flowNotFoundErrorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has been deleted from Redis because flow was not found!
|
||||
`;
|
||||
|
||||
logger.error(flowNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
Sentry.captureException(err, {
|
||||
extra: {
|
||||
jobId: job.id,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/cli",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.3",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"contributors": [
|
||||
@@ -33,7 +33,7 @@
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/backend": "^0.8.0",
|
||||
"@automatisch/backend": "^0.9.3",
|
||||
"@oclif/core": "^1",
|
||||
"@oclif/plugin-help": "^5",
|
||||
"@oclif/plugin-plugins": "^2.0.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/docs",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.3",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"private": true,
|
||||
|
@@ -86,6 +86,15 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/flickr/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Formatter',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Actions', link: '/apps/formatter/actions' },
|
||||
{ text: 'Connection', link: '/apps/formatter/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'GitHub',
|
||||
collapsible: true,
|
||||
@@ -337,6 +346,15 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/webhooks/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'WordPress',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Triggers', link: '/apps/wordpress/triggers' },
|
||||
{ text: 'Connection', link: '/apps/wordpress/connection' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/': [
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user