Compare commits

...

96 Commits

Author SHA1 Message Date
Faruk AYDIN
0b8e33f96d feat(formatter): Implement find array item by property transformer 2023-09-28 12:51:29 +02:00
Rıdvan Akca
c193f9334f feat(wordpress): add new comment trigger 2023-09-15 18:33:38 +02:00
Rıdvan Akca
6e682dc752 fix(create-role): check isCreator by default when permissioon is checked 2023-09-15 15:32:52 +02:00
Rıdvan Akca
da86fe56bd feat(wordpress): add new page trigger 2023-09-15 13:05:41 +02:00
Ali BARIN
45865d701a chore: embed env. vars. for playwright actions 2023-09-15 12:56:15 +02:00
Ömer Faruk Aydın
a66a31b474 Merge pull request #1284 from automatisch/tests/ava
feat: Introduce backend test suite with ava
2023-09-14 12:43:45 +02:00
Faruk AYDIN
2661e7102f feat: Add env file existince check for test suite 2023-09-14 12:25:09 +02:00
Faruk AYDIN
224965b91e feat: Introduce backend test suite with ava 2023-09-14 11:56:53 +02:00
Ömer Faruk Aydın
a9c7375534 Merge pull request #1279 from automatisch/feature/hubspot
feat(hubspot): Implement create contact action
2023-09-14 11:52:18 +02:00
Faruk AYDIN
e77f7ee0bf docs(hubspot): Order alphabetically & correct connection name typo 2023-09-13 23:39:34 +02:00
Faruk AYDIN
ae5dd0cad6 fix(hubspot): Remove redundant field descriptions 2023-09-13 23:16:23 +02:00
Faruk AYDIN
a128907a4e fix(hubspot): Correct website URL typo and primary color 2023-09-13 23:16:23 +02:00
Faruk AYDIN
d6453a8ed0 chore: Use camelCase convention for hubspot actions 2023-09-13 23:16:23 +02:00
Faruk AYDIN
dd1e8240b8 feat(hubspot): Implement verify credentials for OAuth 2023-09-13 23:16:23 +02:00
Faruk AYDIN
b12f39916f feat(hubspot): Implement generate auth url for OAuth 2023-09-13 23:16:23 +02:00
Faruk AYDIN
aae88fe1ad docs(hubspot): Adjust connection page for OAuth setup 2023-09-13 23:16:23 +02:00
Faruk AYDIN
83bb400df1 chore: Change hubspot auth doc url 2023-09-13 23:16:23 +02:00
Vitalii Mykytiuk
8ea8067788 feat(hubspot): Implement create contact action 2023-09-13 23:16:23 +02:00
Rıdvan Akca
9fbc9d59f5 feat: make authentication role mappings emptiable 2023-09-13 22:32:18 +02:00
Ali BARIN
b96ba69a72 chore: run GH actions on push to main branch 2023-09-13 22:25:20 +02:00
Faruk AYDIN
c4ccab6a5d chore: Run CI builds only for pull requests 2023-09-13 22:25:20 +02:00
Rıdvan Akca
f84f27bb56 feat(user-interface): introduce optimistic response 2023-09-13 22:10:47 +02:00
Ömer Faruk Aydın
416cc0ffa9 Merge pull request #1280 from automatisch/node-version
chore: Add .node-version and .nvmrc files to the root
2023-09-13 12:41:37 +02:00
Faruk AYDIN
1fd5ec4db6 chore: Add .node-version and .nvmrc files to the root 2023-09-13 12:36:20 +02:00
Rıdvan Akca
4795c35c68 feat(create-role): make isCreator condition checked by default (#1276) 2023-09-11 15:28:47 +02:00
Rıdvan Akca
25ce63b86d feat(user-interface): use default config as fallback (#1251)
* feat(user-interface): return default app values

* test: remove skip in UI tests

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-09-11 14:06:05 +02:00
Ömer Faruk Aydın
5271033d34 Merge pull request #1275 from automatisch/docs/formatter
docs(formatter): Add numbers and date-time actions
2023-09-11 10:05:44 +02:00
Faruk AYDIN
6ba8f33399 docs(formatter): Add numbers and date-time actions 2023-09-11 09:57:50 +02:00
Ali BARIN
7ab79bd815 Merge pull request #1273 from automatisch/role-mappings 2023-09-10 10:48:34 +02:00
Faruk AYDIN
04a0a847c7 fix: Check role mappings data with isEmpty method 2023-09-10 10:43:08 +02:00
Ali BARIN
436fa9af69 Merge pull request #1267 from automatisch/AUT-276
feat(user-interface): add title field
2023-09-08 21:04:58 +02:00
Ali BARIN
ca0bbb0f08 Merge pull request #1270 from automatisch/feat/random-number
feat(formatter): add decimal point to random number transformer
2023-09-08 21:02:10 +02:00
Ali BARIN
88996144a5 Merge pull request #1269 from automatisch/formatter/date-format
feat(formatter): Implement format date time transformer
2023-09-08 17:27:36 +02:00
Faruk AYDIN
44d5eee99e feat(formatter): Implement format date time transformer for date time action 2023-09-08 13:10:09 +00:00
Ali BARIN
0d1ff6074f Merge pull request #1266 from automatisch/numbers/format-number
feat(formatter): Add format number transformer to numbers action
2023-09-08 15:09:21 +02:00
kattoczko
d63757634a feat: introduce role mappings form on authentication page (#1256) 2023-09-08 14:09:53 +02:00
Faruk AYDIN
fd61cf3388 feat(formatter): Add decimal point to random number transformer 2023-09-06 17:06:13 +02:00
Faruk AYDIN
a6a6b63e5a feat(formatter): Add format number transformer to numbers action 2023-09-05 16:10:39 +02:00
Rıdvan Akca
c02c2def29 feat(user-interface): add title field 2023-09-05 17:10:01 +03:00
Ömer Faruk Aydın
ff66548462 feat(formatter): add random number transformer to numbers action (#1265) 2023-09-05 16:08:30 +02:00
Ömer Faruk Aydın
c9f292e252 feat(formatter): add number action with math operation transformer (#1264)
* feat(formatter): Add number action with math operation transformer

* chore: Use different folders for list transform options of formatter
2023-09-05 13:02:43 +02:00
Faruk AYDIN
18cef5f3bd chore: Sort formatter text transformers alphabetically 2023-09-01 18:02:14 +02:00
Faruk AYDIN
e19340f1e0 feat(formatter): Add replace transformer to text action 2023-09-01 18:02:14 +02:00
Ömer Faruk Aydın
feb613cb6d docs: add upgrade guide for docker compose installation (#1262) 2023-09-01 17:53:21 +02:00
Ömer Faruk Aydın
afa6bdfa44 feat(formatter): add trim whitespace transformer to text action (#1261) 2023-09-01 14:07:50 +02:00
Ömer Faruk Aydın
200e6d9905 feat(formatter): add pluralize transformer for text action (#1260) 2023-09-01 13:37:35 +02:00
Ömer Faruk Aydın
70772c49bd feat(formatter): add lowercase to text transformers (#1259) 2023-09-01 13:26:15 +02:00
Ömer Faruk Aydın
762ea97e8b Merge pull request #1258 from automatisch/release/0.9.3
Release v0.9.3
2023-09-01 12:55:35 +02:00
Faruk AYDIN
8156b8b356 Release v0.9.3 2023-09-01 12:35:19 +02:00
Faruk AYDIN
3a2cbae0a0 chore: Update version to 0.9.3 in Dockerfiles 2023-09-01 12:34:25 +02:00
Ömer Faruk Aydın
0ad8da097b fix(rss): get text for internal ID if the guid or id is object (#1257) 2023-09-01 12:10:42 +02:00
Ömer Faruk Aydın
e2dcdd2811 feat(formatter): add extract number transform to text action (#1255) 2023-08-31 16:35:28 +02:00
Ömer Faruk Aydın
8074f9146b Merge pull request #1253 from automatisch/refactor-notifications
refactor: fetch notifications over graphql query
2023-08-29 16:37:37 +02:00
Ali BARIN
df24bac913 refactor: fetch notifications over graphql query 2023-08-28 20:44:55 +00:00
Ali BARIN
4d4091adcc test: write login page tests 2023-08-28 20:11:21 +02:00
Ali BARIN
cac54c41a1 chore: run automatisch in playwright workflow 2023-08-28 20:11:21 +02:00
Ali BARIN
130931d7af fix: use axios with proxy in license check (#1252) 2023-08-28 17:19:19 +02:00
Ömer Faruk Aydın
d35b08b35e Merge pull request #1250 from automatisch/release/0.9.2
Release v0.9.2
2023-08-28 16:54:32 +02:00
Faruk AYDIN
82031da6a6 Release v0.9.2 2023-08-28 16:30:29 +02:00
Faruk AYDIN
9df5ee7b11 chore: Update version to 0.9.2 in Dockerfiles 2023-08-28 16:29:53 +02:00
Ömer Faruk Aydın
2ed1a57cd9 Merge pull request #1249 from automatisch/permission-contions
chore: Convert conditions of permissions to array
2023-08-28 16:27:29 +02:00
Faruk AYDIN
101450cba6 chore: Convert conditions of permissions to array 2023-08-28 16:24:39 +02:00
Ömer Faruk Aydın
6bab5b3f7c Merge pull request #1248 from automatisch/release/0.9.1
Release v0.9.1
2023-08-28 15:15:25 +02:00
Faruk AYDIN
ca3c0e00a7 Release v0.9.1 2023-08-28 14:47:05 +02:00
Faruk AYDIN
6d64daf324 chore: Update version to 0.9.1 in Dockerfiles 2023-08-28 14:46:26 +02:00
Ömer Faruk Aydın
9ae4578e19 Merge pull request #1247 from automatisch/remove-api-url
chore(web): Remove API url env variable
2023-08-28 14:44:41 +02:00
Faruk AYDIN
e06b7ab87a chore(web): Remove API url env variable 2023-08-28 14:37:10 +02:00
Ömer Faruk Aydın
1e2adedcbf Merge pull request #1246 from automatisch/release/0.9.0
Release v0.9.0
2023-08-28 13:34:24 +02:00
Faruk AYDIN
adf763c1b0 Release v0.9.0 2023-08-28 13:13:23 +02:00
Faruk AYDIN
36ee0df256 chore: Update version to 0.9.0 in Dockerfiles 2023-08-28 13:06:20 +02:00
Rıdvan Akca
823d85b24a feat(custom-logo): constraint svg logo dimensions (#1243) 2023-08-25 21:43:53 +02:00
Rıdvan Akca
a3b3038709 test(user-interface-configuration): write initial tests (#1242)
* test(user-interface): add tests with playwright

* test: refactor UI configuration tests

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-25 21:31:02 +02:00
kattoczko
ddeb18f626 feat: introduce authentication page (#1241)
* feat: introduce authentication page

* feat: update page width

* fix(saml): cover non-existing role mapping on onboarding

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-25 15:24:50 +02:00
Rıdvan Akca
90cd11bd38 feat: align admin pages vertically (#1240) 2023-08-24 16:34:18 +02:00
Ömer Faruk Aydın
e9ba37b8de fix: use withSoftDeleted scope to remove user associations permanently (#1239) 2023-08-24 16:34:07 +02:00
Faruk AYDIN
d5e4a1b1ad fix: Soft delete existing associations of soft deleted users 2023-08-24 15:05:54 +02:00
Faruk AYDIN
129e6d60e5 fix: Remove all related records when user is deleted 2023-08-24 15:05:54 +02:00
Faruk AYDIN
4b77f2f590 fix: Remove deleted flows from Redis 2023-08-24 15:05:54 +02:00
Rıdvan Akca
a909966562 feat(executions): display execution step id (#1232)
* feat(executions): display execution step id

* refactor: remove conditional components in execution steps

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-23 21:53:16 +02:00
Ömer Faruk Aydın
fd184239d6 Merge pull request #1233 from automatisch/enhance-variable-coverage
feat: enhance step variable coverage
2023-08-23 12:35:07 +02:00
Ali BARIN
52bc49dc6a feat: enhance step variable coverage 2023-08-22 16:17:20 +02:00
Ali BARIN
b9352ccc06 fix(mutations/update-flow-status): correct permission check 2023-08-22 16:17:20 +02:00
Ali BARIN
525b2baf06 fix(mutations/execute-flow): correct permission check 2023-08-22 16:17:20 +02:00
Ali BARIN
a8edeb2459 fix(mutations/update-step): correct permission check 2023-08-22 16:17:20 +02:00
Ali BARIN
e3830d64e0 feat: add getSamlAuthProviderRoleMappings query (#1229) 2023-08-22 14:50:01 +02:00
Ali BARIN
91f3e2c2b4 feat: make user.role_id not nullable (#1217) 2023-08-22 14:49:53 +02:00
Ali BARIN
77b4408416 chore: correct e2e test results path in GH actions (#1231) 2023-08-22 10:57:40 +03:00
QAComet
cede96f018 test: refactor create flow test cases with test.step (#1228) 2023-08-22 00:27:10 +02:00
dependabot[bot]
8e0a28d238 chore(deps): bump @node-saml/node-saml from 4.0.4 to 4.0.5 (#1227)
Bumps [@node-saml/node-saml](https://github.com/node-saml/node-saml) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/node-saml/node-saml/releases)
- [Changelog](https://github.com/node-saml/node-saml/blob/v4.0.5/CHANGELOG.md)
- [Commits](https://github.com/node-saml/node-saml/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: "@node-saml/node-saml"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 23:35:38 +02:00
Rıdvan Akca
da5d594428 feat(user-interface): introduce user interface page (#1226) 2023-08-21 23:11:25 +02:00
kattoczko
9f9ee0bb58 feat: create clear button for ControlledCustomAutocomplete (#1222)
Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-21 22:52:59 +02:00
Rıdvan Akca
163aca6179 feat(user-list): add pagination (#1219)
* feat(user-list): add pagination

* feat: add actual total count in getUsers

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-21 21:15:07 +02:00
Ali BARIN
cb06d3b0ae test: add in-between assertions and more fixtures (#1224) 2023-08-18 18:34:52 +02:00
Ali BARIN
dbe18dd100 chore: configure login env. vars. in e2e test workflow (#1221) 2023-08-18 09:48:41 +02:00
Ali BARIN
217970667a chore: make e2e tests manually triggerable (#1220) 2023-08-18 10:25:43 +03:00
Rıdvan Akca
dace794167 feat: introduce playwright (#1194)
* feat: introduce playwright

* test: migrate apps folder to playwright (#1201)

* test: rewrite connections tests with playwright (#1203)

* test: rewrite executions tests with playwright (#1207)

* test: rewrite flow editor tests with playwright (#1212)

* test(flow-editor): rewrite tests using serial mode (#1218)

* test: update custom connection creation paths

* test: move login logic to page fixture

* test: remove cypress tests and deps

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-17 23:31:38 +02:00
196 changed files with 5376 additions and 1457 deletions

View File

@@ -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

View File

@@ -1,5 +1,11 @@
name: Automatisch CI
on: [push]
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
linter:
runs-on: ubuntu-latest

88
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Automatisch UI Tests
on:
push:
branches:
- main
pull_request:
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: user@automatisch.io
LOGIN_PASSWORD: sample
BASE_URL: http://localhost:3000
run: yarn test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: ./packages/e2e-tests/test-results/**/*
retention-days: 30

1
.node-version Normal file
View File

@@ -0,0 +1 @@
16.15.0

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
16.15.0

View File

@@ -1,4 +1,7 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,7 @@
"packages": [
"packages/*"
],
"version": "0.8.0",
"version": "0.9.3",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {

View File

@@ -0,0 +1,16 @@
APP_ENV=test
HOST=localhost
PROTOCOL=http
PORT=3000
LOG_LEVEL=debug
WEBHOOK_SECRET_KEY=secret
POSTGRES_DATABASE=automatisch_test
POSTGRES_PORT=5432
POSTGRES_HOST=localhost
POSTGRES_USERNAME=automatisch_test_user
POSTGRES_PASSWORD=
POSTGRES_ENABLE_SSL=false
ENCRYPTION_KEY=secret
APP_SECRET_KEY=secret
REDIS_PORT=6379
REDIS_HOST=127.0.0.1

View File

@@ -0,0 +1,5 @@
export default {
require: ['ts-node/register', './src/config/app.ts'],
files: ['**/*.test.ts'],
extensions: ['ts'],
};

View File

@@ -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": {
@@ -9,7 +9,8 @@
"build": "tsc && yarn copy-statics",
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
"start": "node dist/src/server.js",
"test": "ava",
"pretest": "APP_ENV=test ts-node ./test/setup/prepare-test-env.ts",
"test": "APP_ENV=test ava",
"lint": "eslint . --ignore-path ../../.eslintignore",
"db:create": "ts-node ./bin/database/create.ts",
"db:seed:user": "ts-node ./bin/database/seed-user.ts",
@@ -22,7 +23,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",
@@ -31,9 +32,11 @@
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0",
"@sentry/tracing": "^7.42.0",
"@types/accounting": "^0.4.2",
"@types/luxon": "^2.3.1",
"@types/passport": "^1.0.12",
"@types/xmlrpc": "^1.3.7",
"accounting": "^0.4.1",
"ajv-formats": "^2.1.1",
"axios": "0.24.0",
"bcrypt": "^5.0.1",
@@ -69,6 +72,7 @@
"passport": "^0.6.0",
"pg": "^8.7.1",
"php-serialize": "^4.0.2",
"pluralize": "^8.0.0",
"showdown": "^2.1.0",
"stripe": "^11.13.0",
"winston": "^3.7.1",
@@ -110,7 +114,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",
@@ -126,24 +130,14 @@
"@types/nodemailer": "^6.4.4",
"@types/pg": "^8.6.1",
"@types/pino": "^7.0.5",
"@types/pluralize": "^0.0.30",
"@types/showdown": "^2.0.1",
"ava": "^3.15.0",
"ava": "^5.3.1",
"nodemon": "^2.0.13",
"sinon": "^11.1.2",
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8"
},
"ava": {
"files": [
"test/**/*"
],
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
},
"publishConfig": {
"access": "public"
}

View File

@@ -0,0 +1,49 @@
import defineAction from '../../../../helpers/define-action';
import formatDateTime from './transformers/format-date-time';
const transformers = {
formatDateTime,
};
export default defineAction({
name: 'Date / Time',
key: 'date-time',
description: 'Perform date and time related transformations on your data.',
arguments: [
{
label: 'Transform',
key: 'transform',
type: 'dropdown' as const,
required: true,
variables: true,
options: [{ label: 'Format Date / Time', value: 'formatDateTime' }],
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,
},
});
},
});

View File

@@ -0,0 +1,23 @@
import { IGlobalVariable } from '@automatisch/types';
import { DateTime } from 'luxon';
const formatDateTime = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
const fromFormat = $.step.parameters.fromFormat as string;
const fromTimezone = $.step.parameters.fromTimezone as string;
const inputDateTime = DateTime.fromFormat(input, fromFormat, {
zone: fromTimezone,
setZone: true,
});
const toFormat = $.step.parameters.toFormat as string;
const toTimezone = $.step.parameters.toTimezone as string;
const outputDateTime = inputDateTime.setZone(toTimezone).toFormat(toFormat);
return outputDateTime;
};
export default formatDateTime;

View File

@@ -1,3 +1,6 @@
import text from './text';
import numbers from './numbers';
import dateTime from './date-time';
import utilities from './utilities';
export default [text];
export default [text, numbers, dateTime, utilities];

View File

@@ -0,0 +1,58 @@
import defineAction from '../../../../helpers/define-action';
import performMathOperation from './transformers/perform-math-operation';
import randomNumber from './transformers/random-number';
import formatNumber from './transformers/format-number';
const transformers = {
performMathOperation,
randomNumber,
formatNumber,
};
export default defineAction({
name: 'Numbers',
key: 'numbers',
description:
'Transform numbers to perform math operations, generate random numbers, format numbers, and much more.',
arguments: [
{
label: 'Transform',
key: 'transform',
type: 'dropdown' as const,
required: true,
variables: true,
options: [
{ label: 'Perform Math Operation', value: 'performMathOperation' },
{ label: 'Random Number', value: 'randomNumber' },
{ label: 'Format Number', value: 'formatNumber' },
],
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,
},
});
},
});

View File

@@ -0,0 +1,28 @@
import { IGlobalVariable } from '@automatisch/types';
import accounting from 'accounting';
const formatNumber = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
const inputDecimalMark = $.step.parameters.inputDecimalMark as string;
const toFormat = $.step.parameters.toFormat as string;
const normalizedNumber = accounting.unformat(input, inputDecimalMark);
const decimalPart = normalizedNumber.toString().split('.')[1];
const precision = decimalPart ? decimalPart.length : 0;
if (toFormat === '0') {
// Comma for grouping & period for decimal
return accounting.formatNumber(normalizedNumber, precision, ',', '.');
} else if (toFormat === '1') {
// Period for grouping & comma for decimal
return accounting.formatNumber(normalizedNumber, precision, '.', ',');
} else if (toFormat === '2') {
// Space for grouping & period for decimal
return accounting.formatNumber(normalizedNumber, precision, ' ', '.');
} else if (toFormat === '3') {
// Space for grouping & comma for decimal
return accounting.formatNumber(normalizedNumber, precision, ' ', ',');
}
};
export default formatNumber;

View File

@@ -0,0 +1,23 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import { add, divide, multiply, subtract } from 'lodash';
const mathOperation = ($: IGlobalVariable) => {
const mathOperation = $.step.parameters.mathOperation as string;
const values = ($.step.parameters.values as IJSONObject[]).map((value) =>
Number(value.input)
) as number[];
if (mathOperation === 'add') {
return values.reduce((acc, curr) => add(acc, curr), 0);
} else if (mathOperation === 'divide') {
return values.reduce((acc, curr) => divide(acc, curr));
} else if (mathOperation === 'makeNegative') {
return values.map((value) => -value);
} else if (mathOperation === 'multiply') {
return values.reduce((acc, curr) => multiply(acc, curr), 1);
} else if (mathOperation === 'subtract') {
return values.reduce((acc, curr) => subtract(acc, curr));
}
};
export default mathOperation;

View File

@@ -0,0 +1,15 @@
import { IGlobalVariable } from '@automatisch/types';
const randomNumber = ($: IGlobalVariable) => {
const lowerRange = Number($.step.parameters.lowerRange);
const upperRange = Number($.step.parameters.upperRange);
const decimalPoints = Number($.step.parameters.decimalPoints) || 0;
return Number(
(Math.random() * (upperRange - lowerRange) + lowerRange).toFixed(
decimalPoints
)
);
};
export default randomNumber;

View File

@@ -1,16 +1,27 @@
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';
import htmlToMarkdown from './transformers/html-to-markdown';
import lowercase from './transformers/lowercase';
import markdownToHtml from './transformers/markdown-to-html';
import pluralize from './transformers/pluralize';
import replace from './transformers/replace';
import trimWhitespace from './transformers/trim-whitespace';
import useDefaultValue from './transformers/use-default-value';
const transformers = {
capitalize,
htmlToMarkdown,
markdownToHtml,
useDefaultValue,
extractEmailAddress,
extractNumber,
htmlToMarkdown,
lowercase,
markdownToHtml,
pluralize,
replace,
trimWhitespace,
useDefaultValue,
};
export default defineAction({
@@ -24,14 +35,18 @@ export default defineAction({
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' },
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],
additionalFields: {
type: 'query',

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
import { IGlobalVariable } from '@automatisch/types';
const lowercase = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
return input.toLowerCase();
};
export default lowercase;

View File

@@ -0,0 +1,9 @@
import { IGlobalVariable } from '@automatisch/types';
import pluralizeLibrary from 'pluralize';
const pluralize = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
return pluralizeLibrary(input);
};
export default pluralize;

View File

@@ -0,0 +1,12 @@
import { IGlobalVariable } from '@automatisch/types';
const replace = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
const find = $.step.parameters.find as string;
const replace = $.step.parameters.replace as string;
return input.replaceAll(find, replace);
};
export default replace;

View File

@@ -0,0 +1,8 @@
import { IGlobalVariable } from '@automatisch/types';
const trimWhitespace = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
return input.trim();
};
export default trimWhitespace;

View File

@@ -0,0 +1,54 @@
import defineAction from '../../../../helpers/define-action';
import findArrayItemByProperty from './transformers/find-array-item-by-property';
const transformers = {
findArrayItemByProperty,
};
export default defineAction({
name: 'Utilities',
key: 'utilities',
description: 'Specific utilities to help you transform your data.',
arguments: [
{
label: 'Transform',
key: 'transform',
type: 'dropdown' as const,
required: true,
variables: true,
options: [
{
label: 'Find Array Item By Property',
value: 'findArrayItemByProperty',
},
],
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,
},
});
},
});

View File

@@ -0,0 +1,13 @@
import { IGlobalVariable } from '@automatisch/types';
import { find } from 'lodash';
const findArrayItemByProperty = ($: IGlobalVariable) => {
const value = JSON.parse($.step.parameters.value as string);
const propertyName = $.step.parameters.propertyName as string;
const propertyValue = $.step.parameters.propertyValue as string;
const foundItem = find(value, { [propertyName]: propertyValue });
return foundItem;
};
export default findArrayItemByProperty;

View File

@@ -0,0 +1,51 @@
import formatOptions from './options/format';
import timezoneOptions from './options/timezone';
const formatDateTime = [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'The datetime you want to format.',
variables: true,
},
{
label: 'From Format',
key: 'fromFormat',
type: 'dropdown' as const,
required: true,
description: 'The format of the input.',
variables: true,
options: formatOptions,
},
{
label: 'From Timezone',
key: 'fromTimezone',
type: 'dropdown' as const,
required: true,
description: 'The timezone of the input.',
variables: true,
options: timezoneOptions,
},
{
label: 'To Format',
key: 'toFormat',
type: 'dropdown' as const,
required: true,
description: 'The format of the output.',
variables: true,
options: formatOptions,
},
{
label: 'To Timezone',
key: 'toTimezone',
type: 'dropdown' as const,
required: true,
description: 'The timezone of the output.',
variables: true,
options: timezoneOptions,
},
];
export default formatDateTime;

View File

@@ -0,0 +1,64 @@
const formatOptions = [
{
label: 'ccc MMM dd HH:mm:ssZZZ yyyy (Wed Aug 23 12:25:36-0000 2023)',
value: 'ccc MMM dd HH:mm:ssZZZ yyyy',
},
{
label: 'MMMM dd yyyy HH:mm:ss (August 23 2023 12:25:36)',
value: 'MMMM dd yyyy HH:mm:ss',
},
{
label: 'MMMM dd yyyy (August 23 2023)',
value: 'MMMM dd yyyy',
},
{
label: 'MMM dd yyyy (Aug 23 2023)',
value: 'MMM dd yyyy',
},
{
label: 'yyyy-MM-dd HH:mm:ss ZZZ (2023-08-23 12:25:36 -0000)',
value: 'yyyy-MM-dd HH:mm:ss ZZZ',
},
{
label: 'yyyy-MM-dd (2023-08-23)',
value: 'yyyy-MM-dd',
},
{
label: 'MM-dd-yyyy (08-23-2023)',
value: 'MM-dd-yyyy',
},
{
label: 'MM/dd/yyyy (08/23/2023)',
value: 'MM/dd/yyyy',
},
{
label: 'MM/dd/yy (08/23/23)',
value: 'MM/dd/yy',
},
{
label: 'dd-MM-yyyy (23-08-2023)',
value: 'dd-MM-yyyy',
},
{
label: 'dd/MM/yyyy (23/08/2023)',
value: 'dd/MM/yyyy',
},
{
label: 'dd/MM/yy (23/08/23)',
value: 'dd/MM/yy',
},
{
label: 'MM-yyyy (08-2023)',
value: 'MM-yyyy',
},
{
label: 'Unix timestamp in seconds (1694008283)',
value: 'X',
},
{
label: 'Unix timestamp in milliseconds (1694008306315)',
value: 'x',
},
];
export default formatOptions;

View File

@@ -0,0 +1,449 @@
// The list from Intl.supportedValuesOf('timeZone') which is used by Luxon.
const timezoneOptions = [
{ label: 'Africa/Abidjan', value: 'Africa/Abidjan' },
{ label: 'Africa/Accra', value: 'Africa/Accra' },
{ label: 'Africa/Addis_Ababa', value: 'Africa/Addis_Ababa' },
{ label: 'Africa/Algiers', value: 'Africa/Algiers' },
{ label: 'Africa/Asmera', value: 'Africa/Asmera' },
{ label: 'Africa/Bamako', value: 'Africa/Bamako' },
{ label: 'Africa/Bangui', value: 'Africa/Bangui' },
{ label: 'Africa/Banjul', value: 'Africa/Banjul' },
{ label: 'Africa/Bissau', value: 'Africa/Bissau' },
{ label: 'Africa/Blantyre', value: 'Africa/Blantyre' },
{ label: 'Africa/Brazzaville', value: 'Africa/Brazzaville' },
{ label: 'Africa/Bujumbura', value: 'Africa/Bujumbura' },
{ label: 'Africa/Cairo', value: 'Africa/Cairo' },
{ label: 'Africa/Casablanca', value: 'Africa/Casablanca' },
{ label: 'Africa/Ceuta', value: 'Africa/Ceuta' },
{ label: 'Africa/Conakry', value: 'Africa/Conakry' },
{ label: 'Africa/Dakar', value: 'Africa/Dakar' },
{ label: 'Africa/Dar_es_Salaam', value: 'Africa/Dar_es_Salaam' },
{ label: 'Africa/Djibouti', value: 'Africa/Djibouti' },
{ label: 'Africa/Douala', value: 'Africa/Douala' },
{ label: 'Africa/El_Aaiun', value: 'Africa/El_Aaiun' },
{ label: 'Africa/Freetown', value: 'Africa/Freetown' },
{ label: 'Africa/Gaborone', value: 'Africa/Gaborone' },
{ label: 'Africa/Harare', value: 'Africa/Harare' },
{ label: 'Africa/Johannesburg', value: 'Africa/Johannesburg' },
{ label: 'Africa/Juba', value: 'Africa/Juba' },
{ label: 'Africa/Kampala', value: 'Africa/Kampala' },
{ label: 'Africa/Khartoum', value: 'Africa/Khartoum' },
{ label: 'Africa/Kigali', value: 'Africa/Kigali' },
{ label: 'Africa/Kinshasa', value: 'Africa/Kinshasa' },
{ label: 'Africa/Lagos', value: 'Africa/Lagos' },
{ label: 'Africa/Libreville', value: 'Africa/Libreville' },
{ label: 'Africa/Lome', value: 'Africa/Lome' },
{ label: 'Africa/Luanda', value: 'Africa/Luanda' },
{ label: 'Africa/Lubumbashi', value: 'Africa/Lubumbashi' },
{ label: 'Africa/Lusaka', value: 'Africa/Lusaka' },
{ label: 'Africa/Malabo', value: 'Africa/Malabo' },
{ label: 'Africa/Maputo', value: 'Africa/Maputo' },
{ label: 'Africa/Maseru', value: 'Africa/Maseru' },
{ label: 'Africa/Mbabane', value: 'Africa/Mbabane' },
{ label: 'Africa/Mogadishu', value: 'Africa/Mogadishu' },
{ label: 'Africa/Monrovia', value: 'Africa/Monrovia' },
{ label: 'Africa/Nairobi', value: 'Africa/Nairobi' },
{ label: 'Africa/Ndjamena', value: 'Africa/Ndjamena' },
{ label: 'Africa/Niamey', value: 'Africa/Niamey' },
{ label: 'Africa/Nouakchott', value: 'Africa/Nouakchott' },
{ label: 'Africa/Ouagadougou', value: 'Africa/Ouagadougou' },
{ label: 'Africa/Porto-Novo', value: 'Africa/Porto-Novo' },
{ label: 'Africa/Sao_Tome', value: 'Africa/Sao_Tome' },
{ label: 'Africa/Tripoli', value: 'Africa/Tripoli' },
{ label: 'Africa/Tunis', value: 'Africa/Tunis' },
{ label: 'Africa/Windhoek', value: 'Africa/Windhoek' },
{ label: 'America/Adak', value: 'America/Adak' },
{ label: 'America/Anchorage', value: 'America/Anchorage' },
{ label: 'America/Anguilla', value: 'America/Anguilla' },
{ label: 'America/Antigua', value: 'America/Antigua' },
{ label: 'America/Araguaina', value: 'America/Araguaina' },
{ label: 'America/Argentina/La_Rioja', value: 'America/Argentina/La_Rioja' },
{
label: 'America/Argentina/Rio_Gallegos',
value: 'America/Argentina/Rio_Gallegos',
},
{ label: 'America/Argentina/Salta', value: 'America/Argentina/Salta' },
{ label: 'America/Argentina/San_Juan', value: 'America/Argentina/San_Juan' },
{ label: 'America/Argentina/San_Luis', value: 'America/Argentina/San_Luis' },
{ label: 'America/Argentina/Tucuman', value: 'America/Argentina/Tucuman' },
{ label: 'America/Argentina/Ushuaia', value: 'America/Argentina/Ushuaia' },
{ label: 'America/Aruba', value: 'America/Aruba' },
{ label: 'America/Asuncion', value: 'America/Asuncion' },
{ label: 'America/Bahia', value: 'America/Bahia' },
{ label: 'America/Bahia_Banderas', value: 'America/Bahia_Banderas' },
{ label: 'America/Barbados', value: 'America/Barbados' },
{ label: 'America/Belem', value: 'America/Belem' },
{ label: 'America/Belize', value: 'America/Belize' },
{ label: 'America/Blanc-Sablon', value: 'America/Blanc-Sablon' },
{ label: 'America/Boa_Vista', value: 'America/Boa_Vista' },
{ label: 'America/Bogota', value: 'America/Bogota' },
{ label: 'America/Boise', value: 'America/Boise' },
{ label: 'America/Buenos_Aires', value: 'America/Buenos_Aires' },
{ label: 'America/Cambridge_Bay', value: 'America/Cambridge_Bay' },
{ label: 'America/Campo_Grande', value: 'America/Campo_Grande' },
{ label: 'America/Cancun', value: 'America/Cancun' },
{ label: 'America/Caracas', value: 'America/Caracas' },
{ label: 'America/Catamarca', value: 'America/Catamarca' },
{ label: 'America/Cayenne', value: 'America/Cayenne' },
{ label: 'America/Cayman', value: 'America/Cayman' },
{ label: 'America/Chicago', value: 'America/Chicago' },
{ label: 'America/Chihuahua', value: 'America/Chihuahua' },
{ label: 'America/Ciudad_Juarez', value: 'America/Ciudad_Juarez' },
{ label: 'America/Coral_Harbour', value: 'America/Coral_Harbour' },
{ label: 'America/Cordoba', value: 'America/Cordoba' },
{ label: 'America/Costa_Rica', value: 'America/Costa_Rica' },
{ label: 'America/Creston', value: 'America/Creston' },
{ label: 'America/Cuiaba', value: 'America/Cuiaba' },
{ label: 'America/Curacao', value: 'America/Curacao' },
{ label: 'America/Danmarkshavn', value: 'America/Danmarkshavn' },
{ label: 'America/Dawson', value: 'America/Dawson' },
{ label: 'America/Dawson_Creek', value: 'America/Dawson_Creek' },
{ label: 'America/Denver', value: 'America/Denver' },
{ label: 'America/Detroit', value: 'America/Detroit' },
{ label: 'America/Dominica', value: 'America/Dominica' },
{ label: 'America/Edmonton', value: 'America/Edmonton' },
{ label: 'America/Eirunepe', value: 'America/Eirunepe' },
{ label: 'America/El_Salvador', value: 'America/El_Salvador' },
{ label: 'America/Fort_Nelson', value: 'America/Fort_Nelson' },
{ label: 'America/Fortaleza', value: 'America/Fortaleza' },
{ label: 'America/Glace_Bay', value: 'America/Glace_Bay' },
{ label: 'America/Godthab', value: 'America/Godthab' },
{ label: 'America/Goose_Bay', value: 'America/Goose_Bay' },
{ label: 'America/Grand_Turk', value: 'America/Grand_Turk' },
{ label: 'America/Grenada', value: 'America/Grenada' },
{ label: 'America/Guadeloupe', value: 'America/Guadeloupe' },
{ label: 'America/Guatemala', value: 'America/Guatemala' },
{ label: 'America/Guayaquil', value: 'America/Guayaquil' },
{ label: 'America/Guyana', value: 'America/Guyana' },
{ label: 'America/Halifax', value: 'America/Halifax' },
{ label: 'America/Havana', value: 'America/Havana' },
{ label: 'America/Hermosillo', value: 'America/Hermosillo' },
{ label: 'America/Indiana/Knox', value: 'America/Indiana/Knox' },
{ label: 'America/Indiana/Marengo', value: 'America/Indiana/Marengo' },
{ label: 'America/Indiana/Petersburg', value: 'America/Indiana/Petersburg' },
{ label: 'America/Indiana/Tell_City', value: 'America/Indiana/Tell_City' },
{ label: 'America/Indiana/Vevay', value: 'America/Indiana/Vevay' },
{ label: 'America/Indiana/Vincennes', value: 'America/Indiana/Vincennes' },
{ label: 'America/Indiana/Winamac', value: 'America/Indiana/Winamac' },
{ label: 'America/Indianapolis', value: 'America/Indianapolis' },
{ label: 'America/Inuvik', value: 'America/Inuvik' },
{ label: 'America/Iqaluit', value: 'America/Iqaluit' },
{ label: 'America/Jamaica', value: 'America/Jamaica' },
{ label: 'America/Jujuy', value: 'America/Jujuy' },
{ label: 'America/Juneau', value: 'America/Juneau' },
{
label: 'America/Kentucky/Monticello',
value: 'America/Kentucky/Monticello',
},
{ label: 'America/Kralendijk', value: 'America/Kralendijk' },
{ label: 'America/La_Paz', value: 'America/La_Paz' },
{ label: 'America/Lima', value: 'America/Lima' },
{ label: 'America/Los_Angeles', value: 'America/Los_Angeles' },
{ label: 'America/Louisville', value: 'America/Louisville' },
{ label: 'America/Lower_Princes', value: 'America/Lower_Princes' },
{ label: 'America/Maceio', value: 'America/Maceio' },
{ label: 'America/Managua', value: 'America/Managua' },
{ label: 'America/Manaus', value: 'America/Manaus' },
{ label: 'America/Marigot', value: 'America/Marigot' },
{ label: 'America/Martinique', value: 'America/Martinique' },
{ label: 'America/Matamoros', value: 'America/Matamoros' },
{ label: 'America/Mazatlan', value: 'America/Mazatlan' },
{ label: 'America/Mendoza', value: 'America/Mendoza' },
{ label: 'America/Menominee', value: 'America/Menominee' },
{ label: 'America/Merida', value: 'America/Merida' },
{ label: 'America/Metlakatla', value: 'America/Metlakatla' },
{ label: 'America/Mexico_City', value: 'America/Mexico_City' },
{ label: 'America/Miquelon', value: 'America/Miquelon' },
{ label: 'America/Moncton', value: 'America/Moncton' },
{ label: 'America/Monterrey', value: 'America/Monterrey' },
{ label: 'America/Montevideo', value: 'America/Montevideo' },
{ label: 'America/Montserrat', value: 'America/Montserrat' },
{ label: 'America/Nassau', value: 'America/Nassau' },
{ label: 'America/New_York', value: 'America/New_York' },
{ label: 'America/Nipigon', value: 'America/Nipigon' },
{ label: 'America/Nome', value: 'America/Nome' },
{ label: 'America/Noronha', value: 'America/Noronha' },
{
label: 'America/North_Dakota/Beulah',
value: 'America/North_Dakota/Beulah',
},
{
label: 'America/North_Dakota/Center',
value: 'America/North_Dakota/Center',
},
{
label: 'America/North_Dakota/New_Salem',
value: 'America/North_Dakota/New_Salem',
},
{ label: 'America/Ojinaga', value: 'America/Ojinaga' },
{ label: 'America/Panama', value: 'America/Panama' },
{ label: 'America/Pangnirtung', value: 'America/Pangnirtung' },
{ label: 'America/Paramaribo', value: 'America/Paramaribo' },
{ label: 'America/Phoenix', value: 'America/Phoenix' },
{ label: 'America/Port-au-Prince', value: 'America/Port-au-Prince' },
{ label: 'America/Port_of_Spain', value: 'America/Port_of_Spain' },
{ label: 'America/Porto_Velho', value: 'America/Porto_Velho' },
{ label: 'America/Puerto_Rico', value: 'America/Puerto_Rico' },
{ label: 'America/Punta_Arenas', value: 'America/Punta_Arenas' },
{ label: 'America/Rainy_River', value: 'America/Rainy_River' },
{ label: 'America/Rankin_Inlet', value: 'America/Rankin_Inlet' },
{ label: 'America/Recife', value: 'America/Recife' },
{ label: 'America/Regina', value: 'America/Regina' },
{ label: 'America/Resolute', value: 'America/Resolute' },
{ label: 'America/Rio_Branco', value: 'America/Rio_Branco' },
{ label: 'America/Santa_Isabel', value: 'America/Santa_Isabel' },
{ label: 'America/Santarem', value: 'America/Santarem' },
{ label: 'America/Santiago', value: 'America/Santiago' },
{ label: 'America/Santo_Domingo', value: 'America/Santo_Domingo' },
{ label: 'America/Sao_Paulo', value: 'America/Sao_Paulo' },
{ label: 'America/Scoresbysund', value: 'America/Scoresbysund' },
{ label: 'America/Sitka', value: 'America/Sitka' },
{ label: 'America/St_Barthelemy', value: 'America/St_Barthelemy' },
{ label: 'America/St_Johns', value: 'America/St_Johns' },
{ label: 'America/St_Kitts', value: 'America/St_Kitts' },
{ label: 'America/St_Lucia', value: 'America/St_Lucia' },
{ label: 'America/St_Thomas', value: 'America/St_Thomas' },
{ label: 'America/St_Vincent', value: 'America/St_Vincent' },
{ label: 'America/Swift_Current', value: 'America/Swift_Current' },
{ label: 'America/Tegucigalpa', value: 'America/Tegucigalpa' },
{ label: 'America/Thule', value: 'America/Thule' },
{ label: 'America/Thunder_Bay', value: 'America/Thunder_Bay' },
{ label: 'America/Tijuana', value: 'America/Tijuana' },
{ label: 'America/Toronto', value: 'America/Toronto' },
{ label: 'America/Tortola', value: 'America/Tortola' },
{ label: 'America/Vancouver', value: 'America/Vancouver' },
{ label: 'America/Whitehorse', value: 'America/Whitehorse' },
{ label: 'America/Winnipeg', value: 'America/Winnipeg' },
{ label: 'America/Yakutat', value: 'America/Yakutat' },
{ label: 'America/Yellowknife', value: 'America/Yellowknife' },
{ label: 'Antarctica/Casey', value: 'Antarctica/Casey' },
{ label: 'Antarctica/Davis', value: 'Antarctica/Davis' },
{ label: 'Antarctica/DumontDUrville', value: 'Antarctica/DumontDUrville' },
{ label: 'Antarctica/Macquarie', value: 'Antarctica/Macquarie' },
{ label: 'Antarctica/Mawson', value: 'Antarctica/Mawson' },
{ label: 'Antarctica/McMurdo', value: 'Antarctica/McMurdo' },
{ label: 'Antarctica/Palmer', value: 'Antarctica/Palmer' },
{ label: 'Antarctica/Rothera', value: 'Antarctica/Rothera' },
{ label: 'Antarctica/Syowa', value: 'Antarctica/Syowa' },
{ label: 'Antarctica/Troll', value: 'Antarctica/Troll' },
{ label: 'Antarctica/Vostok', value: 'Antarctica/Vostok' },
{ label: 'Arctic/Longyearbyen', value: 'Arctic/Longyearbyen' },
{ label: 'Asia/Aden', value: 'Asia/Aden' },
{ label: 'Asia/Almaty', value: 'Asia/Almaty' },
{ label: 'Asia/Amman', value: 'Asia/Amman' },
{ label: 'Asia/Anadyr', value: 'Asia/Anadyr' },
{ label: 'Asia/Aqtau', value: 'Asia/Aqtau' },
{ label: 'Asia/Aqtobe', value: 'Asia/Aqtobe' },
{ label: 'Asia/Ashgabat', value: 'Asia/Ashgabat' },
{ label: 'Asia/Atyrau', value: 'Asia/Atyrau' },
{ label: 'Asia/Baghdad', value: 'Asia/Baghdad' },
{ label: 'Asia/Bahrain', value: 'Asia/Bahrain' },
{ label: 'Asia/Baku', value: 'Asia/Baku' },
{ label: 'Asia/Bangkok', value: 'Asia/Bangkok' },
{ label: 'Asia/Barnaul', value: 'Asia/Barnaul' },
{ label: 'Asia/Beirut', value: 'Asia/Beirut' },
{ label: 'Asia/Bishkek', value: 'Asia/Bishkek' },
{ label: 'Asia/Brunei', value: 'Asia/Brunei' },
{ label: 'Asia/Calcutta', value: 'Asia/Calcutta' },
{ label: 'Asia/Chita', value: 'Asia/Chita' },
{ label: 'Asia/Choibalsan', value: 'Asia/Choibalsan' },
{ label: 'Asia/Colombo', value: 'Asia/Colombo' },
{ label: 'Asia/Damascus', value: 'Asia/Damascus' },
{ label: 'Asia/Dhaka', value: 'Asia/Dhaka' },
{ label: 'Asia/Dili', value: 'Asia/Dili' },
{ label: 'Asia/Dubai', value: 'Asia/Dubai' },
{ label: 'Asia/Dushanbe', value: 'Asia/Dushanbe' },
{ label: 'Asia/Famagusta', value: 'Asia/Famagusta' },
{ label: 'Asia/Gaza', value: 'Asia/Gaza' },
{ label: 'Asia/Hebron', value: 'Asia/Hebron' },
{ label: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' },
{ label: 'Asia/Hovd', value: 'Asia/Hovd' },
{ label: 'Asia/Irkutsk', value: 'Asia/Irkutsk' },
{ label: 'Asia/Jakarta', value: 'Asia/Jakarta' },
{ label: 'Asia/Jayapura', value: 'Asia/Jayapura' },
{ label: 'Asia/Jerusalem', value: 'Asia/Jerusalem' },
{ label: 'Asia/Kabul', value: 'Asia/Kabul' },
{ label: 'Asia/Kamchatka', value: 'Asia/Kamchatka' },
{ label: 'Asia/Karachi', value: 'Asia/Karachi' },
{ label: 'Asia/Katmandu', value: 'Asia/Katmandu' },
{ label: 'Asia/Khandyga', value: 'Asia/Khandyga' },
{ label: 'Asia/Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
{ label: 'Asia/Kuala_Lumpur', value: 'Asia/Kuala_Lumpur' },
{ label: 'Asia/Kuching', value: 'Asia/Kuching' },
{ label: 'Asia/Kuwait', value: 'Asia/Kuwait' },
{ label: 'Asia/Macau', value: 'Asia/Macau' },
{ label: 'Asia/Magadan', value: 'Asia/Magadan' },
{ label: 'Asia/Makassar', value: 'Asia/Makassar' },
{ label: 'Asia/Manila', value: 'Asia/Manila' },
{ label: 'Asia/Muscat', value: 'Asia/Muscat' },
{ label: 'Asia/Nicosia', value: 'Asia/Nicosia' },
{ label: 'Asia/Novokuznetsk', value: 'Asia/Novokuznetsk' },
{ label: 'Asia/Novosibirsk', value: 'Asia/Novosibirsk' },
{ label: 'Asia/Omsk', value: 'Asia/Omsk' },
{ label: 'Asia/Oral', value: 'Asia/Oral' },
{ label: 'Asia/Phnom_Penh', value: 'Asia/Phnom_Penh' },
{ label: 'Asia/Pontianak', value: 'Asia/Pontianak' },
{ label: 'Asia/Pyongyang', value: 'Asia/Pyongyang' },
{ label: 'Asia/Qatar', value: 'Asia/Qatar' },
{ label: 'Asia/Qostanay', value: 'Asia/Qostanay' },
{ label: 'Asia/Qyzylorda', value: 'Asia/Qyzylorda' },
{ label: 'Asia/Rangoon', value: 'Asia/Rangoon' },
{ label: 'Asia/Riyadh', value: 'Asia/Riyadh' },
{ label: 'Asia/Saigon', value: 'Asia/Saigon' },
{ label: 'Asia/Sakhalin', value: 'Asia/Sakhalin' },
{ label: 'Asia/Samarkand', value: 'Asia/Samarkand' },
{ label: 'Asia/Seoul', value: 'Asia/Seoul' },
{ label: 'Asia/Shanghai', value: 'Asia/Shanghai' },
{ label: 'Asia/Singapore', value: 'Asia/Singapore' },
{ label: 'Asia/Srednekolymsk', value: 'Asia/Srednekolymsk' },
{ label: 'Asia/Taipei', value: 'Asia/Taipei' },
{ label: 'Asia/Tashkent', value: 'Asia/Tashkent' },
{ label: 'Asia/Tbilisi', value: 'Asia/Tbilisi' },
{ label: 'Asia/Tehran', value: 'Asia/Tehran' },
{ label: 'Asia/Thimphu', value: 'Asia/Thimphu' },
{ label: 'Asia/Tokyo', value: 'Asia/Tokyo' },
{ label: 'Asia/Tomsk', value: 'Asia/Tomsk' },
{ label: 'Asia/Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
{ label: 'Asia/Urumqi', value: 'Asia/Urumqi' },
{ label: 'Asia/Ust-Nera', value: 'Asia/Ust-Nera' },
{ label: 'Asia/Vientiane', value: 'Asia/Vientiane' },
{ label: 'Asia/Vladivostok', value: 'Asia/Vladivostok' },
{ label: 'Asia/Yakutsk', value: 'Asia/Yakutsk' },
{ label: 'Asia/Yekaterinburg', value: 'Asia/Yekaterinburg' },
{ label: 'Asia/Yerevan', value: 'Asia/Yerevan' },
{ label: 'Atlantic/Azores', value: 'Atlantic/Azores' },
{ label: 'Atlantic/Bermuda', value: 'Atlantic/Bermuda' },
{ label: 'Atlantic/Canary', value: 'Atlantic/Canary' },
{ label: 'Atlantic/Cape_Verde', value: 'Atlantic/Cape_Verde' },
{ label: 'Atlantic/Faeroe', value: 'Atlantic/Faeroe' },
{ label: 'Atlantic/Madeira', value: 'Atlantic/Madeira' },
{ label: 'Atlantic/Reykjavik', value: 'Atlantic/Reykjavik' },
{ label: 'Atlantic/South_Georgia', value: 'Atlantic/South_Georgia' },
{ label: 'Atlantic/St_Helena', value: 'Atlantic/St_Helena' },
{ label: 'Atlantic/Stanley', value: 'Atlantic/Stanley' },
{ label: 'Australia/Adelaide', value: 'Australia/Adelaide' },
{ label: 'Australia/Brisbane', value: 'Australia/Brisbane' },
{ label: 'Australia/Broken_Hill', value: 'Australia/Broken_Hill' },
{ label: 'Australia/Currie', value: 'Australia/Currie' },
{ label: 'Australia/Darwin', value: 'Australia/Darwin' },
{ label: 'Australia/Eucla', value: 'Australia/Eucla' },
{ label: 'Australia/Hobart', value: 'Australia/Hobart' },
{ label: 'Australia/Lindeman', value: 'Australia/Lindeman' },
{ label: 'Australia/Lord_Howe', value: 'Australia/Lord_Howe' },
{ label: 'Australia/Melbourne', value: 'Australia/Melbourne' },
{ label: 'Australia/Perth', value: 'Australia/Perth' },
{ label: 'Australia/Sydney', value: 'Australia/Sydney' },
{ label: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
{ label: 'Europe/Andorra', value: 'Europe/Andorra' },
{ label: 'Europe/Astrakhan', value: 'Europe/Astrakhan' },
{ label: 'Europe/Athens', value: 'Europe/Athens' },
{ label: 'Europe/Belgrade', value: 'Europe/Belgrade' },
{ label: 'Europe/Berlin', value: 'Europe/Berlin' },
{ label: 'Europe/Bratislava', value: 'Europe/Bratislava' },
{ label: 'Europe/Brussels', value: 'Europe/Brussels' },
{ label: 'Europe/Bucharest', value: 'Europe/Bucharest' },
{ label: 'Europe/Budapest', value: 'Europe/Budapest' },
{ label: 'Europe/Busingen', value: 'Europe/Busingen' },
{ label: 'Europe/Chisinau', value: 'Europe/Chisinau' },
{ label: 'Europe/Copenhagen', value: 'Europe/Copenhagen' },
{ label: 'Europe/Dublin', value: 'Europe/Dublin' },
{ label: 'Europe/Gibraltar', value: 'Europe/Gibraltar' },
{ label: 'Europe/Guernsey', value: 'Europe/Guernsey' },
{ label: 'Europe/Helsinki', value: 'Europe/Helsinki' },
{ label: 'Europe/Isle_of_Man', value: 'Europe/Isle_of_Man' },
{ label: 'Europe/Istanbul', value: 'Europe/Istanbul' },
{ label: 'Europe/Jersey', value: 'Europe/Jersey' },
{ label: 'Europe/Kaliningrad', value: 'Europe/Kaliningrad' },
{ label: 'Europe/Kiev', value: 'Europe/Kiev' },
{ label: 'Europe/Kirov', value: 'Europe/Kirov' },
{ label: 'Europe/Lisbon', value: 'Europe/Lisbon' },
{ label: 'Europe/Ljubljana', value: 'Europe/Ljubljana' },
{ label: 'Europe/London', value: 'Europe/London' },
{ label: 'Europe/Luxembourg', value: 'Europe/Luxembourg' },
{ label: 'Europe/Madrid', value: 'Europe/Madrid' },
{ label: 'Europe/Malta', value: 'Europe/Malta' },
{ label: 'Europe/Mariehamn', value: 'Europe/Mariehamn' },
{ label: 'Europe/Minsk', value: 'Europe/Minsk' },
{ label: 'Europe/Monaco', value: 'Europe/Monaco' },
{ label: 'Europe/Moscow', value: 'Europe/Moscow' },
{ label: 'Europe/Oslo', value: 'Europe/Oslo' },
{ label: 'Europe/Paris', value: 'Europe/Paris' },
{ label: 'Europe/Podgorica', value: 'Europe/Podgorica' },
{ label: 'Europe/Prague', value: 'Europe/Prague' },
{ label: 'Europe/Riga', value: 'Europe/Riga' },
{ label: 'Europe/Rome', value: 'Europe/Rome' },
{ label: 'Europe/Samara', value: 'Europe/Samara' },
{ label: 'Europe/San_Marino', value: 'Europe/San_Marino' },
{ label: 'Europe/Sarajevo', value: 'Europe/Sarajevo' },
{ label: 'Europe/Saratov', value: 'Europe/Saratov' },
{ label: 'Europe/Simferopol', value: 'Europe/Simferopol' },
{ label: 'Europe/Skopje', value: 'Europe/Skopje' },
{ label: 'Europe/Sofia', value: 'Europe/Sofia' },
{ label: 'Europe/Stockholm', value: 'Europe/Stockholm' },
{ label: 'Europe/Tallinn', value: 'Europe/Tallinn' },
{ label: 'Europe/Tirane', value: 'Europe/Tirane' },
{ label: 'Europe/Ulyanovsk', value: 'Europe/Ulyanovsk' },
{ label: 'Europe/Uzhgorod', value: 'Europe/Uzhgorod' },
{ label: 'Europe/Vaduz', value: 'Europe/Vaduz' },
{ label: 'Europe/Vatican', value: 'Europe/Vatican' },
{ label: 'Europe/Vienna', value: 'Europe/Vienna' },
{ label: 'Europe/Vilnius', value: 'Europe/Vilnius' },
{ label: 'Europe/Volgograd', value: 'Europe/Volgograd' },
{ label: 'Europe/Warsaw', value: 'Europe/Warsaw' },
{ label: 'Europe/Zagreb', value: 'Europe/Zagreb' },
{ label: 'Europe/Zaporozhye', value: 'Europe/Zaporozhye' },
{ label: 'Europe/Zurich', value: 'Europe/Zurich' },
{ label: 'Indian/Antananarivo', value: 'Indian/Antananarivo' },
{ label: 'Indian/Chagos', value: 'Indian/Chagos' },
{ label: 'Indian/Christmas', value: 'Indian/Christmas' },
{ label: 'Indian/Cocos', value: 'Indian/Cocos' },
{ label: 'Indian/Comoro', value: 'Indian/Comoro' },
{ label: 'Indian/Kerguelen', value: 'Indian/Kerguelen' },
{ label: 'Indian/Mahe', value: 'Indian/Mahe' },
{ label: 'Indian/Maldives', value: 'Indian/Maldives' },
{ label: 'Indian/Mauritius', value: 'Indian/Mauritius' },
{ label: 'Indian/Mayotte', value: 'Indian/Mayotte' },
{ label: 'Indian/Reunion', value: 'Indian/Reunion' },
{ label: 'Pacific/Apia', value: 'Pacific/Apia' },
{ label: 'Pacific/Auckland', value: 'Pacific/Auckland' },
{ label: 'Pacific/Bougainville', value: 'Pacific/Bougainville' },
{ label: 'Pacific/Chatham', value: 'Pacific/Chatham' },
{ label: 'Pacific/Easter', value: 'Pacific/Easter' },
{ label: 'Pacific/Efate', value: 'Pacific/Efate' },
{ label: 'Pacific/Enderbury', value: 'Pacific/Enderbury' },
{ label: 'Pacific/Fakaofo', value: 'Pacific/Fakaofo' },
{ label: 'Pacific/Fiji', value: 'Pacific/Fiji' },
{ label: 'Pacific/Funafuti', value: 'Pacific/Funafuti' },
{ label: 'Pacific/Galapagos', value: 'Pacific/Galapagos' },
{ label: 'Pacific/Gambier', value: 'Pacific/Gambier' },
{ label: 'Pacific/Guadalcanal', value: 'Pacific/Guadalcanal' },
{ label: 'Pacific/Guam', value: 'Pacific/Guam' },
{ label: 'Pacific/Honolulu', value: 'Pacific/Honolulu' },
{ label: 'Pacific/Johnston', value: 'Pacific/Johnston' },
{ label: 'Pacific/Kiritimati', value: 'Pacific/Kiritimati' },
{ label: 'Pacific/Kosrae', value: 'Pacific/Kosrae' },
{ label: 'Pacific/Kwajalein', value: 'Pacific/Kwajalein' },
{ label: 'Pacific/Majuro', value: 'Pacific/Majuro' },
{ label: 'Pacific/Marquesas', value: 'Pacific/Marquesas' },
{ label: 'Pacific/Midway', value: 'Pacific/Midway' },
{ label: 'Pacific/Nauru', value: 'Pacific/Nauru' },
{ label: 'Pacific/Niue', value: 'Pacific/Niue' },
{ label: 'Pacific/Norfolk', value: 'Pacific/Norfolk' },
{ label: 'Pacific/Noumea', value: 'Pacific/Noumea' },
{ label: 'Pacific/Pago_Pago', value: 'Pacific/Pago_Pago' },
{ label: 'Pacific/Palau', value: 'Pacific/Palau' },
{ label: 'Pacific/Pitcairn', value: 'Pacific/Pitcairn' },
{ label: 'Pacific/Ponape', value: 'Pacific/Ponape' },
{ label: 'Pacific/Port_Moresby', value: 'Pacific/Port_Moresby' },
{ label: 'Pacific/Rarotonga', value: 'Pacific/Rarotonga' },
{ label: 'Pacific/Saipan', value: 'Pacific/Saipan' },
{ label: 'Pacific/Tahiti', value: 'Pacific/Tahiti' },
{ label: 'Pacific/Tarawa', value: 'Pacific/Tarawa' },
{ label: 'Pacific/Tongatapu', value: 'Pacific/Tongatapu' },
{ label: 'Pacific/Truk', value: 'Pacific/Truk' },
{ label: 'Pacific/Wake', value: 'Pacific/Wake' },
{ label: 'Pacific/Wallis', value: 'Pacific/Wallis' },
];
export default timezoneOptions;

View File

@@ -1,16 +1,36 @@
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 capitalize from './text/capitalize';
import extractEmailAddress from './text/extract-email-address';
import extractNumber from './text/extract-number';
import findArrayItemByProperty from './utilities/find-array-item-by-property';
import formatDateTime from './date-time/format-date-time';
import formatNumber from './numbers/format-number';
import htmlToMarkdown from './text/html-to-markdown';
import lowercase from './text/lowercase';
import markdownToHtml from './text/markdown-to-html';
import performMathOperation from './numbers/perform-math-operation';
import pluralize from './text/pluralize';
import randomNumber from './numbers/random-number';
import replace from './text/replace';
import trimWhitespace from './text/trim-whitespace';
import useDefaultValue from './text/use-default-value';
const options: IJSONObject = {
capitalize,
htmlToMarkdown,
markdownToHtml,
useDefaultValue,
extractEmailAddress,
extractNumber,
findArrayItemByProperty,
formatDateTime,
formatNumber,
htmlToMarkdown,
lowercase,
markdownToHtml,
performMathOperation,
pluralize,
randomNumber,
replace,
trimWhitespace,
useDefaultValue,
};
export default {

View File

@@ -0,0 +1,38 @@
const formatNumber = [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'The number you want to format.',
variables: true,
},
{
label: 'Input Decimal Mark',
key: 'inputDecimalMark',
type: 'dropdown' as const,
required: true,
description: 'The decimal mark of the input number.',
variables: true,
options: [
{ label: 'Comma', value: ',' },
{ label: 'Period', value: '.' },
],
},
{
label: 'To Format',
key: 'toFormat',
type: 'dropdown' as const,
required: true,
description: 'The format you want to convert the number to.',
variables: true,
options: [
{ label: 'Comma for grouping & period for decimal', value: '0' },
{ label: 'Period for grouping & comma for decimal', value: '1' },
{ label: 'Space for grouping & period for decimal', value: '2' },
{ label: 'Space for grouping & comma for decimal', value: '3' },
],
},
];
export default formatNumber;

View File

@@ -0,0 +1,36 @@
const performMathOperation = [
{
label: 'Math Operation',
key: 'mathOperation',
type: 'dropdown' as const,
required: true,
description: 'The math operation to perform.',
variables: true,
options: [
{ label: 'Add', value: 'add' },
{ label: 'Divide', value: 'divide' },
{ label: 'Make Negative', value: 'makeNegative' },
{ label: 'Multiply', value: 'multiply' },
{ label: 'Subtract', value: 'subtract' },
],
},
{
label: 'Values',
key: 'values',
type: 'dynamic' as const,
required: false,
description: 'Add or remove numbers as needed.',
fields: [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'The number to perform the math operation on.',
variables: true,
},
],
},
];
export default performMathOperation;

View File

@@ -0,0 +1,29 @@
const randomNumber = [
{
label: 'Lower range',
key: 'lowerRange',
type: 'string' as const,
required: true,
description: 'The lowest number to generate.',
variables: true,
},
{
label: 'Upper range',
key: 'upperRange',
type: 'string' as const,
required: true,
description: 'The highest number to generate.',
variables: true,
},
{
label: 'Decimal points',
key: 'decimalPoints',
type: 'string' as const,
required: false,
description:
'The number of digits after the decimal point. It can be an integer between 0 and 15.',
variables: true,
},
];
export default randomNumber;

View File

@@ -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;

View File

@@ -0,0 +1,12 @@
const lowercase = [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'Text that will be lowercased.',
variables: true,
},
];
export default lowercase;

View File

@@ -0,0 +1,12 @@
const pluralize = [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'Text that will be pluralized.',
variables: true,
},
];
export default pluralize;

View File

@@ -0,0 +1,28 @@
const replace = [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'Text that you want to search for and replace values.',
variables: true,
},
{
label: 'Find',
key: 'find',
type: 'string' as const,
required: true,
description: 'Text that will be searched for.',
variables: true,
},
{
label: 'Replace',
key: 'replace',
type: 'string' as const,
required: false,
description: 'Text that will replace the found text.',
variables: true,
},
];
export default replace;

View File

@@ -0,0 +1,12 @@
const trimWhitespace = [
{
label: 'Input',
key: 'input',
type: 'string' as const,
required: true,
description: 'Text you want to remove leading and trailing spaces.',
variables: true,
},
];
export default trimWhitespace;

View File

@@ -0,0 +1,28 @@
const findArrayItemByProperty = [
{
label: 'Value',
key: 'value',
type: 'string' as const,
required: true,
description: 'Array of objects that will be searched.',
variables: true,
},
{
label: 'Property Name',
key: 'propertyName',
type: 'string' as const,
required: true,
description: 'Property name that will be searched.',
variables: true,
},
{
label: 'Property Value',
key: 'propertyValue',
type: 'string' as const,
required: true,
description: 'Property value that will be matched.',
variables: true,
},
];
export default findArrayItemByProperty;

View File

@@ -0,0 +1,83 @@
import defineAction from '../../../../helpers/define-action';
export default defineAction({
name: 'Create contact',
key: 'createContact',
description: `Create contact on user's account.`,
arguments: [
{
label: 'Company name',
key: 'company',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Email',
key: 'email',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'First name',
key: 'firstName',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Last name',
key: 'lastName',
type: 'string' as const,
required: false,
description: 'Last name',
variables: true,
},
{
label: 'Phone',
key: 'phone',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Website URL',
key: 'website',
type: 'string' as const,
required: false,
variables: true,
},
{
label: 'Owner ID',
key: 'hubspotOwnerId',
type: 'string' as const,
required: false,
variables: true,
},
],
async run($) {
const company = $.step.parameters.company as string;
const email = $.step.parameters.email as string;
const firstName = $.step.parameters.firstName as string;
const lastName = $.step.parameters.lastName as string;
const phone = $.step.parameters.phone as string;
const website = $.step.parameters.website as string;
const hubspotOwnerId = $.step.parameters.hubspotOwnerId as string;
const response = await $.http.post(`crm/v3/objects/contacts`, {
properties: {
company,
email,
firstname: firstName,
lastname: lastName,
phone,
website,
hubspot_owner_id: hubspotOwnerId,
},
});
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,3 @@
import createContact from './create-contact';
export default [ createContact ];

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="27px" height="28px" viewBox="0 0 27 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#f95c35">
<path d="M19.614233,20.1771162 C17.5228041,20.1771162 15.8274241,18.4993457 15.8274241,16.4299995 C15.8274241,14.3602937 17.5228041,12.6825232 19.614233,12.6825232 C21.7056619,12.6825232 23.4010418,14.3602937 23.4010418,16.4299995 C23.4010418,18.4993457 21.7056619,20.1771162 19.614233,20.1771162 M20.7478775,9.21551429 L20.7478775,5.88190722 C21.6271788,5.47091457 22.243053,4.59067833 22.243053,3.56912967 L22.243053,3.49218091 C22.243053,2.08229273 21.0774338,0.928780545 19.6527478,0.928780545 L19.5753548,0.928780545 C18.1506688,0.928780545 16.9850496,2.08229273 16.9850496,3.49218091 L16.9850496,3.56912967 C16.9850496,4.59067833 17.6009238,5.47127414 18.4802251,5.88226679 L18.4802251,9.21551429 C17.1710836,9.4157968 15.9749432,9.95012321 14.9884545,10.7365107 L5.73944086,3.61659339 C5.80048326,3.3846684 5.84335828,3.14591151 5.84372163,2.89492912 C5.84517502,1.29842223 4.53930368,0.00215931486 2.92531356,1.87311107e-06 C1.31205014,-0.00179599501 0.00181863138,1.29087118 1.8932965e-06,2.88773765 C-0.00181484479,4.48460412 1.30405649,5.78086703 2.91804661,5.7826649 C3.44381061,5.78338405 3.93069642,5.63559929 4.35726652,5.39540411 L13.4551275,12.3995387 C12.6815604,13.5552084 12.2281026,14.9395668 12.2281026,16.4299995 C12.2281026,17.9901894 12.7262522,19.433518 13.5677653,20.6204705 L10.8012365,23.3586237 C10.5825013,23.2935408 10.3557723,23.2482346 10.1152362,23.2482346 C8.78938076,23.2482346 7.71423516,24.3118533 7.71423516,25.6239375 C7.71423516,26.9363812 8.78938076,28 10.1152362,28 C11.441455,28 12.5162373,26.9363812 12.5162373,25.6239375 C12.5162373,25.3866189 12.4704555,25.1618854 12.4046896,24.9454221 L15.1414238,22.2371135 C16.3837093,23.1752411 17.9308435,23.7390526 19.614233,23.7390526 C23.6935367,23.7390526 27,20.466573 27,16.4299995 C27,12.7756527 24.2872467,9.7566726 20.7478775,9.21551429"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,20 @@
import { IField, IGlobalVariable } from '@automatisch/types';
import { URLSearchParams } from 'url';
import scopes from '../common/scopes';
export default async function generateAuthUrl($: IGlobalVariable) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = oauthRedirectUrlField.value as string;
const searchParams = new URLSearchParams({
client_id: $.auth.data.clientId as string,
redirect_uri: callbackUrl,
scope: scopes.join(' '),
});
const url = `https://app.hubspot.com/oauth/authorize?${searchParams.toString()}`;
await $.auth.set({ url });
}

View File

@@ -0,0 +1,48 @@
import generateAuthUrl from './generate-auth-url';
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
import refreshToken from './refresh-token';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string' as const,
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/hubspot/connections/add',
placeholder: null,
description:
'When asked to input an OAuth callback or redirect URL in HubSpot OAuth, enter the URL above.',
clickToCopy: true,
},
{
key: 'clientId',
label: 'Client ID',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'clientSecret',
label: 'Client Secret',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
],
generateAuthUrl,
verifyCredentials,
isStillVerified,
refreshToken,
};

View File

@@ -0,0 +1,10 @@
import { IGlobalVariable } from '@automatisch/types';
import getAccessTokenInfo from '../common/get-access-token-info';
const isStillVerified = async ($: IGlobalVariable) => {
await getAccessTokenInfo($);
return true;
};
export default isStillVerified;

View File

@@ -0,0 +1,28 @@
import { IGlobalVariable, IField } from '@automatisch/types';
import { URLSearchParams } from 'url';
const refreshToken = async ($: IGlobalVariable) => {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = oauthRedirectUrlField.value as string;
const params = new URLSearchParams({
grant_type: 'refresh_token',
client_id: $.auth.data.clientId as string,
client_secret: $.auth.data.clientSecret as string,
redirect_uri: callbackUrl,
refresh_token: $.auth.data.refreshToken as string,
});
const { data } = await $.http.post('/oauth/v1/token', params.toString());
await $.auth.set({
accessToken: data.access_token,
expiresIn: data.expires_in,
refreshToken: data.refresh_token,
});
};
export default refreshToken;

View File

@@ -0,0 +1,52 @@
import { IGlobalVariable, IField } from '@automatisch/types';
import { URLSearchParams } from 'url';
import getAccessTokenInfo from '../common/get-access-token-info';
const verifyCredentials = async ($: IGlobalVariable) => {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = oauthRedirectUrlField.value as string;
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: $.auth.data.clientId as string,
client_secret: $.auth.data.clientSecret as string,
redirect_uri: callbackUrl,
code: $.auth.data.code as string,
});
const { data: verifiedCredentials } = await $.http.post(
'/oauth/v1/token',
params.toString()
);
const {
access_token: accessToken,
refresh_token: refreshToken,
expires_in: expiresIn,
} = verifiedCredentials;
await $.auth.set({
accessToken,
refreshToken,
expiresIn,
});
const accessTokenInfo = await getAccessTokenInfo($);
await $.auth.set({
screenName: accessTokenInfo.user,
hubDomain: accessTokenInfo.hub_domain,
scopes: accessTokenInfo.scopes,
scopeToScopeGroupPks: accessTokenInfo.scope_to_scope_group_pks,
trialScopes: accessTokenInfo.trial_scopes,
trialScopeToScoreGroupPks: accessTokenInfo.trial_scope_to_scope_group_pks,
hubId: accessTokenInfo.hub_id,
appId: accessTokenInfo.app_id,
userId: accessTokenInfo.user_id,
expiresIn: accessTokenInfo.expires_in,
tokenType: accessTokenInfo.token_type,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,14 @@
import { TBeforeRequest } from '@automatisch/types';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthHeader) return requestConfig;
if ($.auth.data?.accessToken) {
const authorizationHeader = `Bearer ${$.auth.data.accessToken}`;
requestConfig.headers.Authorization = authorizationHeader;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,11 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
const getAccessTokenInfo = async ($: IGlobalVariable): Promise<IJSONObject> => {
const response = await $.http.get(
`/oauth/v1/access-tokens/${$.auth.data.accessToken}`
);
return response.data;
};
export default getAccessTokenInfo;

View File

@@ -0,0 +1,3 @@
const scopes = ['crm.objects.contacts.read', 'crm.objects.contacts.write'];
export default scopes;

View File

View File

@@ -0,0 +1,18 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-header';
import actions from './actions';
import auth from './auth';
export default defineApp({
name: 'HubSpot',
key: 'hubspot',
iconUrl: '{BASE_URL}/apps/hubspot/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/hubspot/connection',
supportsConnections: true,
baseUrl: 'https://www.hubspot.com',
apiBaseUrl: 'https://api.hubapi.com',
primaryColor: 'F95C35',
beforeRequest: [addAuthHeader],
auth,
actions,
});

View File

@@ -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));

View File

@@ -3,7 +3,7 @@ import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type Status = {
slug: string;
name: string;
}
};
type Statuses = Record<string, Status>;
export default {
@@ -29,7 +29,7 @@ export default {
statuses.data.push({
value: status.slug,
name: status.name,
})
});
}
return statuses;

View File

@@ -1,3 +1,5 @@
import newComment from './new-comment';
import newPage from './new-page';
import newPost from './new-post';
export default [newPost];
export default [newComment, newPage, newPost];

View File

@@ -0,0 +1,58 @@
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'New comment',
key: 'newComment',
description: 'Triggers when a new comment is created.',
arguments: [
{
label: 'Status',
key: 'status',
type: 'dropdown' as const,
required: true,
variables: true,
options: [
{ label: 'Approve', value: 'approve' },
{ label: 'Unapprove', value: 'hold' },
{ label: 'Spam', value: 'spam' },
{ label: 'Trash', value: 'trash' },
],
},
],
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/comments',
{
params,
}
);
params.page = params.page + 1;
totalPages = Number(headers['x-wp-totalpages']);
if (data.length) {
for (const page of data) {
const dataItem = {
raw: page,
meta: {
internalId: page.id.toString(),
},
};
$.pushTriggerItem(dataItem);
}
}
} while (params.page <= totalPages);
},
});

View File

@@ -0,0 +1,59 @@
import defineTrigger from '../../../../helpers/define-trigger';
export default defineTrigger({
name: 'New page',
key: 'newPage',
description: 'Triggers when a new page 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/pages', {
params,
});
params.page = params.page + 1;
totalPages = Number(headers['x-wp-totalpages']);
if (data.length) {
for (const page of data) {
const dataItem = {
raw: page,
meta: {
internalId: page.id.toString(),
},
};
$.pushTriggerItem(dataItem);
}
}
} while (params.page <= totalPages);
},
});

View File

@@ -1,6 +1,12 @@
import { URL } from 'node:url';
import * as dotenv from 'dotenv';
dotenv.config();
import path from 'path';
if (process.env.APP_ENV === 'test') {
dotenv.config({ path: path.resolve(__dirname, '../../.env.test') });
} else {
dotenv.config();
}
type AppConfig = {
host: string;

View File

@@ -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();
});
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -8,7 +8,11 @@ type Params = {
};
};
const updateConfig = async (_parent: unknown, params: Params, context: Context) => {
const updateConfig = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'Config');
const config = params.input;
@@ -18,22 +22,26 @@ const updateConfig = async (_parent: unknown, params: Params, context: Context)
for (const key of configKeys) {
const newValue = config[key];
const entryUpdate = Config
.query()
.insert({
key,
value: {
data: newValue
}
})
.onConflict('key')
.merge({
value: {
data: newValue
}
});
if (newValue) {
const entryUpdate = Config.query()
.insert({
key,
value: {
data: newValue,
},
})
.onConflict('key')
.merge({
value: {
data: newValue,
},
});
updates.push(entryUpdate);
updates.push(entryUpdate);
} else {
const entryUpdate = Config.query().findOne({ key }).delete();
updates.push(entryUpdate);
}
}
await Promise.all(updates);

View File

@@ -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,
})

View File

@@ -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!');
}
}

View File

@@ -1,6 +1,7 @@
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import SamlAuthProvidersRoleMapping from '../../models/saml-auth-providers-role-mapping.ee';
import Context from '../../types/express/context';
import isEmpty from 'lodash/isEmpty';
type Params = {
input: {
@@ -31,7 +32,7 @@ const upsertSamlAuthProvidersRoleMappings = async (
.$relatedQuery('samlAuthProvidersRoleMappings')
.delete();
if (!params.input.samlAuthProvidersRoleMappings) {
if (isEmpty(params.input.samlAuthProvidersRoleMappings)) {
return [];
}

View 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;

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
import test from 'ava';
const fn = () => 'foo';
test('getUser graphQL query', (t) => {
// TODO: Write a test for getUser graphQL query
t.is(fn(), 'foo');
});

View File

@@ -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);
};

View File

@@ -16,11 +16,13 @@ import getExecutions from './queries/get-executions';
import getFlow from './queries/get-flow';
import getFlows from './queries/get-flows';
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 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';
@@ -50,12 +52,14 @@ const queryResolvers = {
getFlow,
getFlows,
getInvoices,
getNotifications,
getPaddleInfo,
getPaymentPlans,
getPermissionCatalog,
getRole,
getRoles,
getSamlAuthProvider,
getSamlAuthProviderRoleMappings,
getStepWithTestExecutions,
getSubscriptionStatus,
getTrialStatus,

View File

@@ -46,7 +46,9 @@ type Query {
getPermissionCatalog: PermissionCatalog
getRole(id: String!): Role
getRoles: [Role]
getNotifications: [Notification]
getSamlAuthProvider: SamlAuthProvider
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
getSubscriptionStatus: GetSubscriptionStatus
getTrialStatus: GetTrialStatus
getUser(id: String!): User
@@ -329,6 +331,7 @@ type SamlAuthProvider {
emailAttributeName: String
roleAttributeName: String
active: Boolean
defaultRoleId: String
}
type SamlAuthProvidersRoleMapping {
@@ -341,6 +344,7 @@ type SamlAuthProvidersRoleMapping {
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo
totalCount: Int
}
type UserEdge {
@@ -717,6 +721,7 @@ type ListSamlAuthProvider {
id: String
name: String
issuer: String
loginUrl: String
}
type Permission {
@@ -783,6 +788,13 @@ input UpdateAppAuthClientInput {
active: Boolean
}
type Notification {
name: String
createdAt: String
documentationUrl: String
description: String
}
schema {
query: Query
mutation: Mutation

View File

@@ -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'];
@@ -34,15 +34,16 @@ const authentication = shield(
Query: {
'*': isAuthenticated,
getAutomatischInfo: allow,
listSamlAuthProviders: allow,
healthcheck: allow,
getConfig: allow,
getNotifications: allow,
healthcheck: allow,
listSamlAuthProviders: allow,
},
Mutation: {
'*': isAuthenticated,
registerUser: allow,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
},

View File

@@ -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'],

View File

@@ -48,7 +48,7 @@ const findOrCreateUserBySamlIdentity = async (
.join(' '),
email: mappedUser.email as string,
roleId:
samlAuthProviderRoleMapping.roleId || samlAuthProvider.defaultRoleId,
samlAuthProviderRoleMapping?.roleId || samlAuthProvider.defaultRoleId,
identities: [
{
remoteId: mappedUser.id as string,

View File

@@ -1,7 +1,6 @@
// 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

View File

@@ -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,
})),

View File

@@ -75,6 +75,14 @@ class SamlAuthProvider extends Base {
},
});
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`,

View File

@@ -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 }
);

View File

@@ -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,

View File

@@ -1,7 +0,0 @@
import test from 'ava';
const fn = () => 'foo';
test('fn() returns foo', (t) => {
t.is(fn(), 'foo');
});

View File

@@ -0,0 +1,10 @@
import path from 'path';
import fs from 'fs';
const testEnvFile = path.resolve(__dirname, '../../.env.test');
if (!fs.existsSync(testEnvFile)) {
throw new Error(
'Test environment file (.env.test) not found! You can copy .env-example.test to .env.test and fill it with your own values.'
);
}

View File

@@ -0,0 +1,11 @@
import { createDatabaseAndUser } from '../../bin/database/utils';
import logger from '../../src/helpers/logger';
createDatabaseAndUser()
.then(() => {
process.exit(0);
})
.catch((error) => {
logger.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,2 @@
import './check-env-file';
import './create-database';

View File

@@ -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",

View File

@@ -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,

View File

@@ -151,6 +151,15 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/http-request/connection' },
],
},
{
text: 'HubSpot',
collapsible: true,
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/hubspot/actions' },
{ text: 'Connection', link: '/apps/hubspot/connection' },
],
},
{
text: 'Mattermost',
collapsible: true,

View File

@@ -3,6 +3,10 @@ favicon: /favicons/formatter.svg
items:
- name: Text
desc: Transform text data to capitalize, extract emails, apply default value, and much more.
- name: Numbers
desc: Transform numbers to perform math operations, generate random numbers, format numbers, and much more.
- name: Date / Time
desc: Perform date and time related transformations on your data.
---
<script setup>

View File

@@ -7,5 +7,20 @@ Formatter is a built-in app shipped with Automatisch, and it doesn't need to tal
- Capitalize
- Convert HTML to Markdown
- Convert Markdown to HTML
- Use Default Value
- Extract Email Address
- Extract Number
- Lowercase
- Pluralize
- Replace
- Trim Whitespace
- Use Default Value
## Numbers
- Perform Math Operation
- Random Number
- Format Number
## Date / Time
- Format Date / Time

View File

@@ -0,0 +1,12 @@
---
favicon: /favicons/hubspot.svg
items:
- name: Create a contact
desc: Create a contact on user's account.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -0,0 +1,22 @@
# HubSpot
:::info
This page explains the steps you need to follow to set up the Hubspot connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Go to the [HubSpot Developer page](https://developers.hubspot.com/).
2. Login into your developer account.
3. Click on the **Manage apps** button.
4. Click on the **Create app** button.
5. Fill the **Public app name** field with the name of your API app.
6. Go to the **Auth** tab.
7. Fill the **Redirect URL(s)** field with the OAuth Redirect URL from the Automatisch connection creation page.
8. Go to the **Scopes** tab.
9. Select the scopes you want to use with Automatisch.
10. Click on the **Create App** button.
11. Go back to the **Auth** tab.
12. Copy the **Client ID** and **Client Secret** values.
13. Paste the **Client ID** value into Automatisch as **Client ID**, respectively.
14. Paste the **Client Secret** value into Automatisch as **Client Secret**, respectively.
15. Click the **Submit** button on Automatisch.
16. Now, you can start using the HubSpot connection with Automatisch.

View File

@@ -1,6 +1,10 @@
---
favicon: /favicons/wordpress.svg
items:
- name: New comment
desc: Triggers when a new comment is created.
- name: New page
desc: Triggers when a new page is created.
- name: New post
desc: Triggers when a new post is created.
---

View File

@@ -15,6 +15,7 @@ The following integrations are currently supported by Automatisch.
- [Google Forms](/apps/google-forms/triggers)
- [Google Sheets](/apps/google-sheets/triggers)
- [HTTP Request](/apps/http-request/actions)
- [HubSpot](/apps/hubspot/actions)
- [Mattermost](/apps/mattermost/actions)
- [Notion](/apps/notion/triggers)
- [Ntfy](/apps/ntfy/actions)

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