Compare commits

..

94 Commits

Author SHA1 Message Date
Rıdvan Akca
dc3695766c feat(bigin-by-zoho-crm): add create task action 2024-02-22 16:10:13 +03:00
Rıdvan Akca
daa38ab846 feat(bigin-by-zoho-crm): add create company action 2024-02-22 14:16:48 +03:00
Rıdvan Akca
00e18cf42d feat(bigin-by-zoho-crm): add create event action 2024-02-21 18:14:22 +03:00
Rıdvan Akca
b3d2a1167c feat(bigin-by-zoho-crm): add create contact action 2024-02-21 15:27:35 +03:00
Rıdvan Akca
43c34fcb7b feat(bigin-by-zoho-crm): add new products trigger 2024-02-20 17:31:30 +03:00
Rıdvan Akca
c98f24338f feat(bigin-by-zoho-crm): add new tasks trigger 2024-02-20 17:26:56 +03:00
Rıdvan Akca
d0748bb7e2 feat(bigin-by-zoho-crm): add new calls trigger 2024-02-20 16:23:51 +03:00
Rıdvan Akca
81e8e4963c feat(bigin-by-zoho-crm): add new companies trigger 2024-02-20 16:22:21 +03:00
Rıdvan Akca
282b2374a2 feat(bigin-by-zoho-crm): add new contacts trigger 2024-02-20 16:19:47 +03:00
Rıdvan Akca
b7e38f9d1c feat(bigin-by-zoho-crm): add bigin by zoho crm integration 2024-02-20 14:21:23 +03:00
Ömer Faruk Aydın
024c7476c7 Merge pull request #1616 from automatisch/node-18-in-devcontainer
refactor: use node 18 in devcontainer
2024-02-19 11:51:59 +01:00
Ali BARIN
30a7ffe93d refactor: use node 18 in devcontainer 2024-02-19 10:17:16 +00:00
kattoczko
e2d803ebf7 feat: do not let users access notifications page when it's turned off (#1583) 2024-02-16 16:15:01 +01:00
kattoczko
be7e67c940 feat: introduce 404 page (#1600) 2024-02-16 15:54:35 +01:00
kattoczko
ead4b13ba5 feat: Show /login directly on / without valid authentication (#1528) 2024-02-16 15:15:25 +01:00
Ömer Faruk Aydın
e02c42ee18 Merge pull request #1605 from automatisch/test-permission-serializer
test: Add tests for permission serializer
2024-02-16 12:41:09 +01:00
Ömer Faruk Aydın
d39886fdf8 Merge pull request #1604 from automatisch/test-role-serializer
test: Add tests for role serializer
2024-02-16 12:40:59 +01:00
Ömer Faruk Aydın
11a425f1de Merge pull request #1603 from automatisch/test-user-serializer
test: Add tests for user serializer
2024-02-16 12:40:48 +01:00
Ömer Faruk Aydın
f0e194e584 Merge pull request #1606 from automatisch/remove-redundant
chore: Remove redundant npm libraries
2024-02-16 12:37:34 +01:00
Faruk AYDIN
d4b9331cf2 chore: Remove redundant npm libraries 2024-02-16 01:01:50 +01:00
Faruk AYDIN
37e1acc5f1 test: Add tests for permission serializer 2024-02-16 00:19:53 +01:00
Faruk AYDIN
ffaf6a577d test: Add tests for role serializer 2024-02-16 00:18:53 +01:00
Faruk AYDIN
afdaf6ba39 test: Add tests for user serializer 2024-02-16 00:10:37 +01:00
Ömer Faruk Aydın
4c49367910 Merge pull request #1602 from automatisch/introduce-serializers
feat: Introduce serializers
2024-02-15 12:46:20 +01:00
Ömer Faruk Aydın
a506c4411d Merge pull request #1601 from automatisch/rest-get-trial
feat: Implement API endpoint for user trial info
2024-02-15 12:45:54 +01:00
Faruk AYDIN
1859c9854e chore: Add permission serializer to serializers 2024-02-15 02:21:26 +01:00
Faruk AYDIN
6ff29b9ae6 refactor: Use serializer for user model instead of formatJson 2024-02-15 02:19:24 +01:00
Faruk AYDIN
3578f6b849 feat: Extend renderer to use serializers 2024-02-15 02:15:44 +01:00
Faruk AYDIN
0347864fde feat: Introduce serializers 2024-02-15 02:15:19 +01:00
Faruk AYDIN
5f9786a2c7 chore: Adjust get user trial file to have .ee extension 2024-02-14 15:55:02 +01:00
Faruk AYDIN
75aeff1898 test: Cover removed user token for authentication tests 2024-02-14 15:48:49 +01:00
Faruk AYDIN
0afcdce6d3 refactor: Do not expose subscription info for get user trial 2024-02-14 14:23:54 +01:00
Faruk AYDIN
a591d0ea87 test: Add tests for get user trial action 2024-02-14 14:18:42 +01:00
Faruk AYDIN
0e111a3532 feat: Implement API endpoint for user trial info 2024-02-14 14:18:28 +01:00
Faruk AYDIN
b599466ffa feat: Add checkIsCloud middleware for routes 2024-02-14 13:06:58 +01:00
Faruk AYDIN
69727e78df fix: Throw not found error for authentication 2024-02-14 13:06:29 +01:00
Ömer Faruk Aydın
02ae67b147 Merge pull request #1597 from automatisch/rest-api-get-users
feat: Implement api/v1/users API endpoint
2024-02-14 11:38:09 +01:00
Faruk AYDIN
a769f78801 test: Add tests for api/v1/users API endpoint 2024-02-14 01:20:29 +01:00
Faruk AYDIN
d583e42428 refactor: Structure api mock data with folders 2024-02-14 01:02:42 +01:00
Faruk AYDIN
da732becb6 feat: Implement api/v1/users API endpoint 2024-02-14 00:52:17 +01:00
Faruk AYDIN
b89a4d58d9 feat: Add pagination for REST endpoints 2024-02-14 00:51:48 +01:00
Faruk AYDIN
09854147d1 feat: Extend renderer functionality to work with pagination 2024-02-14 00:51:16 +01:00
Ömer Faruk Aydın
3648c2bfe3 Merge pull request #1592 from automatisch/rest-api-get-user
feat: Implement api/v1/users/:userId API endpoint
2024-02-13 12:14:37 +01:00
Ömer Faruk Aydın
3f3ee032f6 Merge pull request #1591 from automatisch/rest-api-get-current-user
feat: Implement users/me API endpoint
2024-02-13 11:34:37 +01:00
Ömer Faruk Aydın
68e5d54331 Merge pull request #1590 from automatisch/rest-api-automatisch-version
feat: Implement automatisch version API endpoint
2024-02-13 10:42:44 +01:00
Faruk AYDIN
824c434b0b feat: Implement api/v1/users/:userId API endpoint 2024-02-13 03:44:44 +01:00
Faruk AYDIN
9f0e0ca656 feat: Implement users/me API endpoint 2024-02-13 02:06:24 +01:00
Faruk AYDIN
95f89ba03e refactor: Use objectionjs instead of knex for factories 2024-02-13 01:49:40 +01:00
Faruk AYDIN
697f72ecf4 refactor: Use api/v1 namespace for routes 2024-02-12 23:30:38 +01:00
Faruk AYDIN
4f03f2ab51 feat: Introduce renderer helper 2024-02-12 23:25:09 +01:00
Faruk AYDIN
c81531cb7a feat: Implement automatisch version API endpoint 2024-02-12 22:59:44 +01:00
Ömer Faruk Aydın
7b6e4aa153 Merge pull request #1589 from automatisch/healthcheck-api-endpoint
feat: Implement healthcheck api endpoint
2024-02-12 22:57:58 +01:00
Faruk AYDIN
f21039d19d feat: Implement healthcheck api endpoint 2024-02-12 22:50:57 +01:00
morihoos
8c936a91be fix(csp): remove illegal characters in directive names (#1585) 2024-02-08 17:41:33 +01:00
Ali BARIN
24451892ff feat: add custom additional drawer link (#1586) 2024-02-08 16:33:12 +01:00
morihoos
6bba2c82fe feat(config): add ability to override apiUrl in environment variables (#1581) 2024-02-07 16:17:03 +01:00
Ali BARIN
3320dc6bc4 Merge pull request #1582 from automatisch/toggle-favicon-and-notifications-page
feat: put favicon and notifications page behind feature flags
2024-02-07 14:46:59 +01:00
Ali BARIN
9d42fd9293 test(queries/get-config): incorporate feature flags
cover disableNotificationsPage and disableFavicon feature flags
2024-02-07 13:11:57 +00:00
Ali BARIN
e6b806616f feat: add DISABLE_FAVICON feature flag 2024-02-07 11:51:17 +00:00
Ali BARIN
6ec5872391 feat: add DISABLE_NOTIFICATIONS_PAGE feature flag 2024-02-07 11:47:44 +00:00
Ali BARIN
a26cf932a1 chore(devcontainer): upgrade node to 20 2024-02-07 11:46:12 +00:00
Ali BARIN
38a3e3ab9f Merge pull request #1570 from automatisch/prevent-public-registrations
fix: prevent registration on non-cloud
2024-01-30 14:11:05 +01:00
Ali BARIN
32b17c1418 fix: prevent registration on non-cloud 2024-01-30 13:03:52 +00:00
Ömer Faruk Aydın
44aa6a1579 Merge pull request #1569 from automatisch/compile-email
fix: Adjust dirname for compile email helper
2024-01-29 13:05:58 +01:00
Faruk AYDIN
2369aacd2a fix: Adjust dirname for compile email helper 2024-01-29 12:48:21 +01:00
Ömer Faruk Aydın
7dafc6364b Merge pull request #1552 from automatisch/dependabot/npm_and_yarn/vite-3.2.8
chore(deps): Bump vite from 3.2.7 to 3.2.8
2024-01-29 12:32:10 +01:00
Ömer Faruk Aydın
3d25fa0aeb Merge branch 'main' into dependabot/npm_and_yarn/vite-3.2.8 2024-01-29 11:14:49 +01:00
Ali BARIN
0297b0f296 Merge pull request #1559 from automatisch/base64-to-text
feat(formatter): add base64 to string action
2024-01-26 10:56:54 +01:00
Rıdvan Akca
4c7d09c3d8 feat(formatter): add base64 to string action 2024-01-26 12:41:05 +03:00
Ali BARIN
48a74826e8 Merge pull request #1557 from automatisch/text-to-base64
feat(formatter): add string to base64 action
2024-01-25 16:56:15 +01:00
Rıdvan Akca
ef34068ac4 feat(formatter): add string to base64 action 2024-01-25 18:52:44 +03:00
dependabot[bot]
3987a8db77 chore(deps): Bump vite from 3.2.7 to 3.2.8
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.8/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.8/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 22:40:35 +00:00
Ömer Faruk Aydın
953c5a5b5b Merge pull request #1540 from automatisch/authentication-tests
feat: Add tests for authentication helper
2024-01-15 16:10:36 +01:00
Ömer Faruk Aydın
4313265c00 Merge pull request #1541 from automatisch/remove-cli-from
chore: Remove cli package from linter and build scripts
2024-01-15 16:10:24 +01:00
Faruk AYDIN
9405f267ba chore: Remove build:watch script 2024-01-15 16:06:28 +01:00
Faruk AYDIN
1d29238199 chore: Remove cli package from linter and build scripts 2024-01-15 15:33:21 +01:00
Faruk AYDIN
c5bf66f462 chore: Remove redundant import of authentication rule 2024-01-15 15:31:20 +01:00
Faruk AYDIN
e6180bdfaa chore: No need to export authentication options 2024-01-15 15:31:00 +01:00
Faruk AYDIN
55c391afc8 chore: Remove authentication cases from individual tests 2024-01-15 15:27:30 +01:00
Faruk AYDIN
782fa67320 feat: Add tests for authentication helper 2024-01-15 15:21:04 +01:00
Ömer Faruk Aydın
1e3ab75bb7 Merge pull request #1538 from automatisch/remove-types-package
chore: Remove types package
2024-01-15 13:57:01 +01:00
Faruk AYDIN
5f6dd12a73 chore: Remove types package from web dependencies 2024-01-15 13:41:54 +01:00
Faruk AYDIN
d18c06d2c4 chore: Remove types package 2024-01-15 13:37:58 +01:00
Ömer Faruk Aydın
baf99a9cfe Merge pull request #1537 from automatisch/web/types
chore: Use types from the web package
2024-01-15 13:37:43 +01:00
Faruk AYDIN
159931a6ea chore: Use types from the web package 2024-01-15 13:30:48 +01:00
Ömer Faruk Aydın
7831f2925b Merge pull request #1536 from automatisch/docs/remove-typescript
Use JS for the documentation examples
2024-01-15 13:17:48 +01:00
Faruk AYDIN
8fcb7840de docs: Convert code examples to JS 2024-01-15 13:13:48 +01:00
Faruk AYDIN
9ece9461dc docs: Convert all file imports to ES modules 2024-01-15 13:10:26 +01:00
Faruk AYDIN
b304acaaba docs: Use .js file extension 2024-01-15 13:02:06 +01:00
Faruk AYDIN
5a1960609a docs: Use JS highlighter instead of TS 2024-01-15 12:56:43 +01:00
Faruk AYDIN
476aa6e3aa docs: Remove imports of automatisch types 2024-01-15 12:55:55 +01:00
Ömer Faruk Aydın
aa76007fd0 Merge pull request #1533 from automatisch/remove-cli
chore: Remove cli package
2024-01-14 21:06:46 +01:00
Faruk AYDIN
17a8813c4b chore: Remove build step from CI for cli 2024-01-14 20:13:23 +01:00
Faruk AYDIN
fe79fc9003 chore: Remove cli package 2024-01-14 20:10:23 +01:00
216 changed files with 4637 additions and 20025 deletions

View File

@@ -8,7 +8,7 @@
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": 16
"version": 18
},
"ghcr.io/devcontainers/features/common-utils:1": {
"username": "vscode",

View File

@@ -83,20 +83,3 @@ jobs:
env:
CI: false
- run: echo "🍏 This job's status is ${{ job.status }}."
build-cli:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/cli && yarn build
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -6,8 +6,7 @@
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} lint",
"build:watch": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} build:watch",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
@@ -18,7 +17,6 @@
"**/babel-loader",
"**/webpack",
"**/@automatisch/web",
"**/@automatisch/types",
"**/ajv"
]
},

View File

@@ -1,3 +1,3 @@
import { createUser } from './utils.js';
await createUser();
createUser();

View File

@@ -33,7 +33,6 @@
"axios": "1.6.0",
"bcrypt": "^5.0.1",
"bullmq": "^3.0.0",
"copyfiles": "^2.4.1",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
@@ -45,7 +44,6 @@
"graphql-middleware": "^6.1.15",
"graphql-shield": "^7.5.0",
"graphql-tools": "^8.2.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7",
"http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
@@ -68,7 +66,6 @@
"pluralize": "^8.0.0",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"stripe": "^11.13.0",
"winston": "^3.7.1",
"xmlrpc": "^1.3.2"
},

View File

@@ -0,0 +1,161 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create company',
key: 'createCompany',
description: 'Creates a new company.',
arguments: [
{
label: 'Company Owner',
key: 'companyOwnerId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listContactOwners',
},
],
},
},
{
label: 'Company Name',
key: 'companyName',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Phone',
key: 'phone',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Website',
key: 'website',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Tags',
key: 'tags',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tag',
type: 'string',
required: false,
variables: true,
},
],
},
{
label: 'Description',
key: 'description',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Billing Street',
key: 'billingStreet',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Billing City',
key: 'billingCity',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Billing State',
key: 'billingState',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Billing Country',
key: 'billingCountry',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Billing Code',
key: 'billingCode',
type: 'string',
required: false,
description: '',
variables: true,
},
],
async run($) {
const {
contactOwnerId,
companyName,
phone,
website,
tags,
description,
billingStreet,
billingCity,
billingState,
billingCountry,
billingCode,
} = $.step.parameters;
const allTags = tags.map((tag) => ({
name: tag.tag,
}));
const body = {
data: [
{
Owner: {
id: contactOwnerId,
},
Account_Name: companyName,
Phone: phone,
Website: website,
Tag: allTags,
Description: description,
Billing_Street: billingStreet,
Billing_City: billingCity,
Billing_State: billingState,
Billing_Country: billingCountry,
Billing_Code: billingCode,
},
],
};
const { data } = await $.http.post(`/bigin/v2/Accounts`, body);
$.setActionItem({
raw: data[0],
});
},
});

View File

@@ -0,0 +1,217 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create contact',
key: 'createContact',
description: 'Creates a new contact.',
arguments: [
{
label: 'Contact Owner',
key: 'contactOwnerId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listContactOwners',
},
],
},
},
{
label: 'First Name',
key: 'firstName',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Last Name',
key: 'lastName',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Title',
key: 'title',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Email',
key: 'email',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Company ID',
key: 'companyId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listCompanies',
},
],
},
},
{
label: 'Mobile',
key: 'mobile',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Email Opt Out',
key: 'emailOptOut',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'True', value: true },
{ label: 'False', value: false },
],
},
{
label: 'Tags',
key: 'tags',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tag',
type: 'string',
required: false,
variables: true,
},
],
},
{
label: 'Description',
key: 'description',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Mailing Street',
key: 'mailingStreet',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Mailing City',
key: 'mailingCity',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Mailing State',
key: 'mailingState',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Mailing Country',
key: 'mailingCountry',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Mailing Zip',
key: 'mailingZip',
type: 'string',
required: false,
description: '',
variables: true,
},
],
async run($) {
const {
contactOwnerId,
firstName,
lastName,
title,
email,
companyId,
mobile,
emailOptOut,
tags,
description,
mailingStreet,
mailingCity,
mailingState,
mailingCountry,
mailingZip,
} = $.step.parameters;
const allTags = tags.map((tag) => ({
name: tag.tag,
}));
const body = {
data: [
{
Owner: {
id: contactOwnerId,
},
Account_Name: {
id: companyId,
},
First_Name: firstName,
Last_Name: lastName,
Title: title,
Email: email,
Mobile: mobile,
Email_Opt_Out: emailOptOut,
Tag: allTags,
Description: description,
Mailing_Street: mailingStreet,
Mailing_City: mailingCity,
Mailing_State: mailingState,
Mailing_Country: mailingCountry,
Mailing_Zip: mailingZip,
},
],
};
const { data } = await $.http.post(`/bigin/v2/Contacts`, body);
$.setActionItem({
raw: data[0],
});
},
});

View File

@@ -0,0 +1,293 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create event',
key: 'createEvent',
description: 'Creates a new event.',
arguments: [
{
label: 'Host',
key: 'hostId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listContactOwners',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'From',
key: 'from',
type: 'string',
required: true,
description: 'The date format is ISO8601 (yyyy-mm-ddTHH:mm:ssZ).',
variables: true,
},
{
label: 'To',
key: 'to',
type: 'string',
required: true,
description: 'The date format is ISO8601 (yyyy-mm-ddTHH:mm:ssZ).',
variables: true,
},
{
label: 'All Day',
key: 'allDay',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'True', value: true },
{ label: 'False', value: false },
],
},
{
label: 'Frequency (Recurring Activity)',
key: 'frequency',
type: 'dropdown',
required: false,
description:
'Specifies the frequency of event recurrence. The options include DAILY, WEEKLY, MONTHLY, or YEARLY.',
variables: true,
options: [
{ label: 'Daily Events', value: 'DAILY' },
{ label: 'Weekly Events', value: 'WEEKLY' },
{ label: 'Monthly Events', value: 'MONTHLY' },
],
},
{
label: 'Interval (Recurring Activity)',
key: 'interval',
type: 'string',
required: false,
description:
'Specifies the time difference between individual events. The INTERVAL can be anywhere from 1 to 99. For instance, with a WEEKLY event set at an INTERVAL of 2, there will be a two-week gap between each occurrence.',
variables: true,
},
{
label: 'By Month Day (Recurring Activity)',
key: 'byMonthDay',
type: 'string',
required: false,
description:
'This specifies the date within the month when the event recurs. The BYMONTHDAY value can be any number from 1 to 31. This rule applies exclusively to events that repeat monthly or yearly.',
variables: true,
},
{
label: 'By Week (Recurring Activity)',
key: 'byWeek',
type: 'dropdown',
required: false,
description:
'Only relevant for events that occur on a monthly or yearly basis.',
variables: true,
options: [
{ label: 'First week of the month', value: '1' },
{ label: 'Second week of the month', value: '2' },
{ label: 'Third week of the month', value: '3' },
{ label: 'Fourth week of the month', value: '4' },
{ label: 'Last week of the month', value: '-1' },
],
},
{
label: 'By Day (Recurring Activity)',
key: 'byDay',
type: 'string',
required: false,
description:
'This signifies the weekday when the event recurs. The options include SU, MO, TU, WE, TH, FR, or SA. This rule applies to events that repeat daily, weekly, monthly, and yearly (should not be combined with INTERVAL).',
variables: true,
},
{
label: 'Count (Recurring Activity)',
key: 'count',
type: 'string',
required: false,
description:
'Specifies the number of events you wish to generate. The count value range from 1 to 99.',
variables: true,
},
{
label: 'Until (Recurring Activity)',
key: 'until',
type: 'string',
required: false,
description:
'Specifies the concluding date for the event recurrence. Please input the date in the YYYY-MM-DD format.',
variables: true,
},
{
label: 'Reminder',
key: 'reminder',
type: 'dropdown',
required: false,
description:
'Provide the reminder list to notify or prompt participants prior to the event.',
variables: true,
options: [
{ label: '5 min', value: '5 minutes' },
{ label: '10 min', value: '10 minutes' },
{ label: '15 min', value: '15 minutes' },
{ label: '1 hrs', value: '1 hours' },
{ label: '2 hrs', value: '2 hours' },
{ label: '1 days', value: '1 days' },
{ label: '2 days', value: '2 days' },
{ label: '1 weeks', value: '1 weeks' },
],
},
{
label: 'Location',
key: 'location',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Related Module',
key: 'relatedModule',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Companies', value: 'Accounts' },
{ label: 'Contacts', value: 'Contacts' },
{ label: 'Deals', value: 'Deals' },
],
},
{
label: 'Participants',
key: 'participants',
type: 'dynamic',
required: false,
description: 'Email Address of participants.',
fields: [
{
label: 'Participant',
key: 'participant',
type: 'string',
required: false,
variables: true,
},
],
},
{
label: 'Description',
key: 'description',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Tags',
key: 'tags',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tag',
type: 'string',
required: false,
variables: true,
},
],
},
],
async run($) {
const {
hostId,
title,
from,
to,
allDay,
frequency,
interval,
byMonthDay,
byDay,
byWeek,
count,
until,
reminder,
location,
relatedModule,
participants,
description,
tags,
} = $.step.parameters;
const allTags = tags.map((tag) => ({
name: tag.tag,
}));
const allParticipants = participants.map((participant) => ({
type: 'email',
participant: participant.participant,
}));
const [unit, period] = reminder.split(' ');
let rrule = `FREQ=${frequency};INTERVAL=${interval};`;
if (count) rrule += `COUNT=${count};`;
if (byMonthDay) rrule += `BYMONTHDAY=${byMonthDay};`;
if (byDay) rrule += `BYDAY=${byDay};`;
if (byWeek) rrule += `BYSETPOS=${byWeek};`;
if (until) rrule += `UNTIL=${until};`;
const body = {
data: [
{
Owner: {
id: hostId,
},
Event_Title: title,
Start_DateTime: from,
End_DateTime: to,
All_day: allDay,
$se_module: relatedModule,
Recurring_Activity: {
RRULE: rrule,
},
Remind_At: [
{
unit: Number(unit),
period,
},
],
Venue: location,
Participants: allParticipants,
Description: description,
Tag: allTags,
},
],
};
const { data } = await $.http.post(`/bigin/v2/Events`, body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -0,0 +1,246 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create task',
key: 'createTask',
description: 'Creates a new task.',
arguments: [
{
label: 'Task Owner',
key: 'taskOwnerId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listContactOwners',
},
],
},
},
{
label: 'Subject',
key: 'subject',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Due Date',
key: 'dueDate',
type: 'string',
required: true,
description: 'The date format is yyyy-mm-dd.',
variables: true,
},
{
label: 'Frequency (Recurring Activity)',
key: 'frequency',
type: 'dropdown',
required: false,
description:
'Specifies the frequency of event recurrence. The options include DAILY, WEEKLY, MONTHLY, or YEARLY.',
variables: true,
options: [
{ label: 'None', value: 'NONE' },
{ label: 'Daily Events', value: 'DAILY' },
{ label: 'Weekly Events', value: 'WEEKLY' },
{ label: 'Monthly Events', value: 'MONTHLY' },
],
},
{
label: 'Interval (Recurring Activity)',
key: 'interval',
type: 'string',
required: false,
description:
'Specifies the time difference between individual events. The INTERVAL can be anywhere from 1 to 99. For instance, with a WEEKLY event set at an INTERVAL of 2, there will be a two-week gap between each occurrence.',
variables: true,
},
{
label: 'Start Date (Recurring Activity)',
key: 'startDate',
type: 'string',
required: false,
description: 'The date format is yyyy-mm-dd.',
variables: true,
},
{
label: 'By Month Day (Recurring Activity)',
key: 'byMonthDay',
type: 'string',
required: false,
description:
'This specifies the date within the month when the event recurs. The BYMONTHDAY value can be any number from 1 to 31. This rule applies exclusively to events that repeat monthly or yearly.',
variables: true,
},
{
label: 'By Week (Recurring Activity)',
key: 'byWeek',
type: 'dropdown',
required: false,
description:
'Only relevant for events that occur on a monthly or yearly basis.',
variables: true,
options: [
{ label: 'First week of the month', value: '1' },
{ label: 'Second week of the month', value: '2' },
{ label: 'Third week of the month', value: '3' },
{ label: 'Fourth week of the month', value: '4' },
{ label: 'Last week of the month', value: '-1' },
],
},
{
label: 'By Day (Recurring Activity)',
key: 'byDay',
type: 'dropdown',
required: false,
description:
'This signifies the weekday when the event recurs. This rule applies to events that repeat daily, weekly, monthly, and yearly (should not be combined with INTERVAL).',
variables: true,
options: [
{ label: 'Sunday', value: 'SU' },
{ label: 'Monday', value: 'MO' },
{ label: 'Tuesday', value: 'TU' },
{ label: 'Wednesday', value: 'WE' },
{ label: 'Thursday', value: 'TH' },
{ label: 'Friday', value: 'FR' },
{ label: 'Saturday', value: 'SA' },
],
},
{
label: 'Count (Recurring Activity)',
key: 'count',
type: 'string',
required: false,
description:
'Specifies the number of events you wish to generate. The count value range from 1 to 99.',
variables: true,
},
{
label: 'Until (Recurring Activity)',
key: 'until',
type: 'string',
required: false,
description:
'Specifies the concluding date for the event recurrence. Please input the date in the YYYY-MM-DD format.',
variables: true,
},
{
label: 'Description',
key: 'description',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Priority',
key: 'priority',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'High', value: 'High' },
{ label: 'Normal', value: 'Normal' },
],
},
{
label: 'Status',
key: 'status',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'In Progress', value: 'In Progress' },
{ label: 'Completed', value: 'Completed' },
],
},
{
label: 'Tags',
key: 'tags',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tag',
type: 'string',
required: false,
variables: true,
},
],
},
],
async run($) {
const {
taskOwnerId,
subject,
dueDate,
frequency,
startDate,
interval,
byMonthDay,
byDay,
byWeek,
count,
until,
description,
priority,
status,
tags,
} = $.step.parameters;
const allTags = tags.map((tag) => ({
name: tag.tag,
}));
let rrule;
if (frequency) rrule += `FREQ=${frequency};`;
if (interval) rrule += `INTERVAL=${interval};`;
if (count) rrule += `COUNT=${count};`;
if (byMonthDay) rrule += `BYMONTHDAY=${byMonthDay};`;
if (byDay) rrule += `BYDAY=${byDay};`;
if (byWeek) rrule += `BYSETPOS=${byWeek};`;
if (until) rrule += `UNTIL=${until};`;
if (startDate) rrule += `DTSTART=${startDate}`;
const body = {
data: [
{
Owner: {
id: taskOwnerId,
},
Subject: subject,
Due_Date: dueDate,
Description: description,
Priority: priority,
Status: status,
Tag: allTags,
},
],
};
if (rrule) {
body.data[0].Recurring_Activity = {
RRULE: rrule,
};
}
const { data } = await $.http.post(`/bigin/v2/Tasks`, body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -0,0 +1,6 @@
import createCompany from './create-company/index.js';
import createContact from './create-contact/index.js';
import createEvent from './create-event/index.js';
import createTask from './create-task/index.js';
export default [createCompany, createContact, createEvent, createTask];

View File

@@ -0,0 +1,32 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" width="327.714" height="120" viewBox="0 0 327.714 120">
<defs>
<style>
.d {
fill: #039649;
}
</style>
</defs>
<g id="c" data-name="Layer 1">
<g>
<path class="d" d="m52.39,120c-1.801,0-3.6-.6-5.101-1.5-2.1-1.5-3.6-4.2-3.6-6.9v-24.6L1.09,12.901C-.41,9.9-.41,6.601,1.391,4.2,2.891,1.5,5.89,0,8.89,0h78c2.999,0,5.698,1.5,7.199,4.2,1.801,2.702,1.801,6.3.301,9.002l-5.101,9h10.201c2.999,0,5.698,1.5,7.199,4.2,1.801,2.7,1.801,6.3.301,9l-29.701,51.3v18.599c0,3.899-2.399,6.9-5.999,8.099l-16.2,6.3c-.6.301-1.799.301-2.7.301M8.89,8.4q-.301.301-.301.602l42.301,73.798c1.199,2.1,1.199,3.299,1.199,4.501v23.998s.301.301.6,0l16.2-6.3h.6v-17.999c0-1.199,0-3.299,1.201-4.8l29.4-50.999c0-.301,0-.6-.301-.6l-53.699-.301,14.399,24.901,10.201-17.7c1.199-2.1,3.6-2.7,5.7-1.5,2.1,1.199,2.7,3.6,1.498,5.7l-11.998,20.699c-2.702,4.2-8.701,3.901-11.102.301l-17.4-30.9c-1.199-1.799-1.199-4.2,0-6.3,1.201-2.1,3.301-3.6,5.7-3.6h36.6l7.499-12.899s0-.301-.299-.602H8.89Z"/>
<g>
<path d="m181.725,48.737c0,3.089-.54,5.684-1.618,7.788-1.081,2.104-2.548,3.79-4.407,5.062-1.858,1.27-4.016,2.179-6.476,2.726-2.459.547-5.069.819-7.828.819h-24.591V6.521h23.034c2.676,0,5.198.24,7.561.718,2.364.479,4.427,1.311,6.189,2.5,1.762,1.189,3.149,2.787,4.16,4.795,1.011,2.008,1.517,4.53,1.517,7.562,0,3.17-.786,5.813-2.357,7.93-1.57,2.119-3.913,3.628-7.028,4.53,4.017.819,6.995,2.371,8.935,4.652s2.91,5.458,2.91,9.529Zm-33.813-17.624h10.533c1.612,0,3.033-.136,4.263-.41,1.23-.272,2.268-.738,3.115-1.393.846-.656,1.489-1.55,1.927-2.684.436-1.134.655-2.562.655-4.284,0-1.612-.267-2.923-.799-3.934-.532-1.01-1.25-1.809-2.152-2.398-.902-.587-1.967-.99-3.197-1.209s-2.542-.328-3.934-.328h-10.411v16.64Zm0,26.067h12.09c1.64,0,3.115-.169,4.427-.512,1.311-.342,2.424-.894,3.341-1.66.915-.764,1.612-1.748,2.091-2.951.478-1.202.716-2.663.716-4.385,0-1.802-.294-3.285-.881-4.447-.588-1.161-1.394-2.083-2.419-2.766s-2.227-1.154-3.606-1.414c-1.381-.26-2.863-.39-4.447-.39h-11.312v18.525Z"/>
<path d="m202.257,9.555c0,.847-.151,1.633-.451,2.356-.3.724-.716,1.34-1.25,1.844-.532.507-1.167.902-1.905,1.189s-1.53.431-2.377.431c-.819,0-1.598-.144-2.336-.431s-1.388-.69-1.947-1.209c-.56-.519-.998-1.133-1.311-1.844-.315-.711-.471-1.489-.471-2.336s.156-1.626.471-2.336c.314-.711.744-1.325,1.291-1.845.546-.519,1.187-.922,1.925-1.209s1.53-.431,2.377-.431c.821,0,1.598.144,2.336.431s1.373.69,1.907,1.209c.532.52.955,1.134,1.27,1.845.314.71.471,1.489.471,2.336Zm-.778,12.705v42.871h-10.246V22.261h10.246Z"/>
<path d="m254.186,61.935c0,3.361-.587,6.236-1.762,8.627-1.174,2.391-2.78,4.352-4.815,5.882-2.036,1.529-4.407,2.65-7.111,3.361-2.706.71-5.589,1.065-8.648,1.065-2.023,0-4.037-.157-6.045-.471-2.009-.314-3.908-.861-5.697-1.64-1.79-.778-3.395-1.837-4.816-3.175-1.421-1.34-2.555-3.021-3.402-5.042l8.074-3.525c.519,1.202,1.202,2.193,2.049,2.971.847.779,1.797,1.408,2.848,1.885,1.051.479,2.172.814,3.361,1.005s2.398.287,3.628.287c1.885,0,3.572-.232,5.062-.696,1.489-.466,2.752-1.169,3.79-2.111,1.039-.943,1.838-2.132,2.399-3.566.559-1.434.839-3.107.839-5.02v-6.393c-.71,1.229-1.592,2.336-2.643,3.319-1.053.983-2.207,1.824-3.464,2.52s-2.582,1.237-3.976,1.62c-1.393.383-2.814.574-4.263.574-3.033,0-5.737-.56-8.114-1.681-2.377-1.119-4.379-2.636-6.005-4.55-1.625-1.912-2.862-4.139-3.709-6.68-.847-2.542-1.27-5.233-1.27-8.074,0-2.814.451-5.491,1.353-8.033.901-2.542,2.192-4.775,3.873-6.702,1.68-1.927,3.709-3.464,6.086-4.611,2.376-1.147,5.054-1.721,8.033-1.721,2.923,0,5.621.594,8.094,1.782,2.472,1.189,4.473,3.082,6.004,5.677v-6.557h10.246v39.674Zm-33.198-19.017c0,1.666.252,3.265.758,4.795s1.237,2.876,2.193,4.037c.957,1.162,2.137,2.084,3.545,2.767s3.013,1.025,4.816,1.025c2.021,0,3.784-.355,5.287-1.066,1.502-.711,2.746-1.673,3.729-2.89.985-1.215,1.722-2.643,2.213-4.283.492-1.64.738-3.374.738-5.206,0-1.802-.239-3.49-.716-5.062-.479-1.57-1.203-2.93-2.173-4.077s-2.179-2.056-3.626-2.726c-1.449-.67-3.157-1.005-5.123-1.005-2.049,0-3.812.363-5.287,1.086-1.476.724-2.684,1.709-3.628,2.951-.943,1.243-1.633,2.699-2.069,4.365-.438,1.666-.656,3.429-.656,5.287Z"/>
<path d="m277.096,9.555c0,.847-.151,1.633-.451,2.356-.3.724-.716,1.34-1.25,1.844-.532.507-1.167.902-1.905,1.189s-1.53.431-2.377.431c-.819,0-1.598-.144-2.336-.431s-1.388-.69-1.947-1.209c-.56-.519-.998-1.133-1.311-1.844-.315-.711-.471-1.489-.471-2.336s.156-1.626.471-2.336c.314-.711.744-1.325,1.291-1.845.546-.519,1.187-.922,1.925-1.209s1.53-.431,2.377-.431c.821,0,1.598.144,2.336.431s1.373.69,1.907,1.209c.532.52.955,1.134,1.27,1.845.314.71.471,1.489.471,2.336Zm-.778,12.705v42.871h-10.246V22.261h10.246Z"/>
<path d="m308.451,29.801c-1.585,0-2.999.24-4.243.718-1.243.479-2.288,1.162-3.135,2.049-.847.889-1.496,1.961-1.947,3.217-.451,1.258-.676,2.651-.676,4.181v25.165h-10.246V22.261h10.246v6.803c.683-1.338,1.537-2.492,2.562-3.462,1.025-.97,2.165-1.769,3.422-2.399,1.257-.627,2.59-1.091,3.997-1.393,1.406-.3,2.82-.451,4.241-.451,2.623,0,4.884.431,6.783,1.291s3.464,2.049,4.694,3.565c1.229,1.517,2.131,3.307,2.704,5.37s.861,4.311.861,6.742v26.805h-10.328v-25.822c0-3.005-.711-5.341-2.132-7.008-1.421-1.666-3.688-2.5-6.803-2.5Z"/>
</g>
<g>
<path d="m152.871,102.12c0,1.138-.168,2.221-.507,3.25-.337,1.028-.828,1.935-1.471,2.72s-1.436,1.41-2.379,1.874-2.021.696-3.234.696c-.579,0-1.155-.061-1.723-.183-.569-.121-1.107-.306-1.613-.553-.505-.247-.974-.561-1.407-.941s-.801-.822-1.107-1.328v2.72h-2.625v-24.446h2.625v10.689c.273-.464.632-.882,1.076-1.257.442-.374.93-.696,1.463-.964.531-.27,1.082-.477,1.652-.626.569-.146,1.122-.22,1.66-.22,1.244,0,2.34.227,3.289.679.95.454,1.743,1.068,2.38,1.843s1.117,1.685,1.44,2.728c.321,1.044.482,2.151.482,3.321Zm-13.536.126c0,.917.123,1.756.371,2.515.249.759.614,1.415,1.1,1.968.485.553,1.079.984,1.787,1.289.706.306,1.517.459,2.435.459.991,0,1.813-.178,2.467-.53.653-.354,1.178-.828,1.573-1.424.395-.595.674-1.283.838-2.063.163-.78.245-1.603.245-2.467,0-.801-.104-1.576-.308-2.325-.206-.748-.52-1.415-.941-1.999-.422-.586-.958-1.055-1.605-1.407-.648-.354-1.415-.53-2.3-.53-.981,0-1.827.174-2.538.521-.711.349-1.3.818-1.764,1.409-.464.59-.806,1.28-1.028,2.071s-.332,1.629-.332,2.514Z"/>
<path d="m169.949,93.834l-9.141,22.297h-2.656l3.131-7.464-6.388-14.833h2.814l4.965,11.86,4.586-11.86h2.689Z"/>
<path d="m194.11,108.034v2.34h-16.414v-1.139l13.189-19.228h-12.05v-2.246h15.465v1.139l-12.84,19.134h12.65Z"/>
<path d="m213.229,102.215c0,1.202-.206,2.319-.617,3.352-.411,1.034-.986,1.927-1.723,2.681-.739.753-1.611,1.344-2.618,1.77-1.007.428-2.106.641-3.296.641-1.256,0-2.388-.222-3.4-.665s-1.874-1.053-2.585-1.834-1.257-1.697-1.637-2.752c-.38-1.053-.57-2.192-.57-3.416,0-1.191.201-2.3.601-3.329.4-1.028.964-1.92,1.692-2.68.727-.759,1.594-1.354,2.601-1.787s2.116-.648,3.329-.648c1.254,0,2.391.22,3.408.663s1.881,1.055,2.593,1.835,1.26,1.697,1.644,2.751c.385,1.055.578,2.192.578,3.416Zm-13.726.031c0,.886.117,1.708.349,2.467s.579,1.418,1.043,1.977c.464.558,1.038.995,1.723,1.311.685.317,1.481.476,2.388.476.938,0,1.753-.175,2.443-.522.691-.349,1.263-.816,1.717-1.407.452-.591.79-1.275,1.012-2.056.22-.78.332-1.602.332-2.466,0-.844-.117-1.646-.349-2.404-.232-.759-.579-1.429-1.043-2.008s-1.038-1.038-1.723-1.376c-.685-.337-1.481-.505-2.388-.505-.971,0-1.802.174-2.498.521-.696.349-1.265.82-1.708,1.416-.443.595-.77,1.288-.981,2.078-.21.792-.316,1.624-.316,2.498Z"/>
<path d="m224.423,95.827c-.696,0-1.323.105-1.881.316s-1.034.512-1.423.902c-.391.39-.691.864-.902,1.423s-.316,1.186-.316,1.881v10.026h-2.625v-24.446h2.625v10.531c.253-.516.564-.956.933-1.32s.777-.663,1.226-.902c.447-.237.933-.411,1.454-.521.522-.111,1.057-.166,1.605-.166,1.033,0,1.937.171,2.712.513.775.343,1.423.818,1.945,1.424.522.605.915,1.326,1.178,2.157.263.833.395,1.745.395,2.735v9.994h-2.625v-10.184c0-1.423-.36-2.506-1.083-3.25-.722-.742-1.795-1.114-3.217-1.114Z"/>
<path d="m251.685,102.215c0,1.202-.206,2.319-.617,3.352-.411,1.034-.986,1.927-1.723,2.681-.739.753-1.611,1.344-2.618,1.77-1.007.428-2.106.641-3.296.641-1.256,0-2.388-.222-3.4-.665s-1.874-1.053-2.585-1.834-1.257-1.697-1.637-2.752c-.38-1.053-.57-2.192-.57-3.416,0-1.191.201-2.3.601-3.329.4-1.028.964-1.92,1.692-2.68.727-.759,1.594-1.354,2.601-1.787s2.116-.648,3.329-.648c1.254,0,2.391.22,3.408.663s1.881,1.055,2.593,1.835,1.26,1.697,1.644,2.751c.385,1.055.578,2.192.578,3.416Zm-13.726.031c0,.886.117,1.708.349,2.467s.579,1.418,1.043,1.977c.464.558,1.038.995,1.723,1.311.685.317,1.481.476,2.388.476.938,0,1.753-.175,2.443-.522.691-.349,1.263-.816,1.717-1.407.452-.591.79-1.275,1.012-2.056.22-.78.332-1.602.332-2.466,0-.844-.117-1.646-.349-2.404-.232-.759-.579-1.429-1.043-2.008s-1.038-1.038-1.723-1.376c-.685-.337-1.481-.505-2.388-.505-.971,0-1.802.174-2.498.521-.696.349-1.265.82-1.708,1.416-.443.595-.77,1.288-.981,2.078-.21.792-.316,1.624-.316,2.498Z"/>
<path d="m281.097,103.923c-.38.959-.884,1.85-1.511,2.672-.627.823-1.346,1.534-2.157,2.135-.812.601-1.703,1.073-2.673,1.415-.969.342-1.976.514-3.02.514-1.76,0-3.312-.295-4.656-.886-1.345-.59-2.472-1.407-3.385-2.45-.912-1.044-1.603-2.279-2.072-3.709-.469-1.428-.704-2.98-.704-4.657,0-1.033.114-2.037.341-3.013.227-.974.553-1.889.98-2.743.428-.854.95-1.637,1.565-2.348.617-.711,1.318-1.32,2.104-1.827.785-.505,1.652-.901,2.601-1.186.949-.284,1.961-.426,3.036-.426,1.012,0,2.003.148,2.973.442.971.295,1.866.718,2.689,1.266.822.548,1.55,1.209,2.182,1.984s1.127,1.642,1.486,2.602l-2.246.98c-.295-.768-.673-1.468-1.13-2.095-.459-.627-.989-1.162-1.59-1.604-.601-.443-1.265-.785-1.992-1.029-.728-.242-1.508-.363-2.341-.363-1.359,0-2.532.268-3.518.806s-1.797,1.254-2.435,2.151c-.638.895-1.107,1.919-1.407,3.067-.301,1.149-.451,2.335-.451,3.558,0,1.244.166,2.424.498,3.543.333,1.117.833,2.098,1.503,2.94.669.844,1.507,1.513,2.514,2.008,1.007.496,2.18.744,3.518.744.801,0,1.576-.143,2.325-.428.749-.284,1.44-.671,2.072-1.162.632-.49,1.191-1.064,1.675-1.723.485-.658.864-1.357,1.139-2.095l2.088.917Z"/>
<path d="m287.991,100.539v9.835h-2.751v-22.613h7.431c1.044,0,2.042.111,2.997.332.954.222,1.795.586,2.522,1.092.728.505,1.307,1.168,1.74,1.984.431.818.648,1.822.648,3.013,0,.885-.137,1.673-.411,2.364-.275.691-.66,1.289-1.155,1.795-.496.507-1.086.925-1.771,1.257-.685.333-1.44.583-2.261.752l6.926,10.026h-3.068l-6.736-9.835h-4.112Zm0-2.183h4.871c.727,0,1.393-.075,1.999-.228s1.131-.402,1.574-.744c.442-.342.785-.785,1.028-1.328s.363-1.21.363-2.001c0-.801-.126-1.466-.378-1.992-.254-.527-.601-.943-1.044-1.25-.443-.305-.967-.521-1.573-.648s-1.262-.189-1.968-.189h-4.871v8.38Z"/>
<path d="m308.231,91.335v19.039h-2.529v-22.613h3.826l7.242,17.932,7.148-17.932h3.795v22.613h-2.752v-19.039l-7.653,19.039h-1.17l-7.907-19.039Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,25 @@
import { URLSearchParams } from 'url';
import authScope from '../common/auth-scope.js';
export default async function generateAuthUrl($) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const searchParams = new URLSearchParams({
scope: authScope.join(','),
client_id: $.auth.data.clientId,
response_type: 'code',
access_type: 'offline',
redirect_uri: redirectUri,
});
const domain =
$.auth.data.region !== 'cn' ? 'account.zoho.com' : 'accounts.zoho.com.cn';
const url = `https://${domain}/oauth/v2/auth?${searchParams.toString()}`;
await $.auth.set({
url,
});
}

View File

@@ -0,0 +1,66 @@
import generateAuthUrl from './generate-auth-url.js';
import verifyCredentials from './verify-credentials.js';
import refreshToken from './refresh-token.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string',
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/bigin-by-zoho-crm/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in Bigin By Zoho CRM, enter the URL above.',
clickToCopy: true,
},
{
key: 'region',
label: 'Region',
type: 'dropdown',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: '',
options: [
{ label: 'United States', value: 'us' },
{ label: 'European Union', value: 'eu' },
{ label: 'Australia', value: 'au' },
{ label: 'India', value: 'in' },
{ label: 'China', value: 'cn' },
],
clickToCopy: false,
},
{
key: 'clientId',
label: 'Client ID',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'clientSecret',
label: 'Client Secret',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
],
generateAuthUrl,
verifyCredentials,
isStillVerified,
refreshToken,
};

View File

@@ -0,0 +1,8 @@
import getCurrentOrganization from '../common/get-current-organization.js';
const isStillVerified = async ($) => {
const org = await getCurrentOrganization($);
return !!org.id;
};
export default isStillVerified;

View File

@@ -0,0 +1,34 @@
import { URLSearchParams } from 'node:url';
import authScope from '../common/auth-scope.js';
import { regionUrlMap } from '../common/region-url-map.js';
const refreshToken = async ($) => {
const location = $.auth.data.location;
const params = new URLSearchParams({
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
refresh_token: $.auth.data.refreshToken,
grant_type: 'refresh_token',
});
const { data } = await $.http.post(
`${regionUrlMap[location]}/oauth/v2/token`,
params.toString(),
{
additionalProperties: {
skipAddingBaseUrl: true,
},
}
);
await $.auth.set({
accessToken: data.access_token,
apiDomain: data.api_domain,
scope: authScope.join(','),
tokenType: data.token_type,
expiresIn: data.expires_in,
});
};
export default refreshToken;

View File

@@ -0,0 +1,46 @@
import { URLSearchParams } from 'node:url';
import { regionUrlMap } from '../common/region-url-map.js';
import getCurrentOrganization from '../common/get-current-organization.js';
const verifyCredentials = async ($) => {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const location = $.auth.data.location;
const params = new URLSearchParams({
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
code: $.auth.data.code,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
});
const { data } = await $.http.post(
`${regionUrlMap[location]}/oauth/v2/token`,
params.toString()
);
await $.auth.set({
accessToken: data.access_token,
tokenType: data.token_type,
apiDomain: data.api_domain,
});
const organization = await getCurrentOrganization($);
const screenName = [organization.company_name, organization.primary_email]
.filter(Boolean)
.join(' @ ');
await $.auth.set({
clientId: $.auth.data.clientId,
clientSecret: $.auth.data.clientSecret,
scope: $.auth.data.scope,
expiresIn: data.expires_in,
refreshToken: data.refresh_token,
screenName,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,9 @@
const addAuthHeader = ($, requestConfig) => {
if ($.auth.data?.accessToken) {
requestConfig.headers.Authorization = `Zoho-oauthtoken ${$.auth.data.accessToken}`;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,10 @@
const authScope = [
'ZohoBigin.notifications.ALL',
'ZohoBigin.users.ALL',
'ZohoBigin.modules.ALL',
'ZohoBigin.org.READ',
'ZohoBigin.settings.ALL',
'ZohoBigin.modules.ALL',
];
export default authScope;

View File

@@ -0,0 +1,6 @@
const getCurrentOrganization = async ($) => {
const response = await $.http.get('/bigin/v2/org');
return response.data.org[0];
};
export default getCurrentOrganization;

View File

@@ -0,0 +1,8 @@
export const regionUrlMap = {
us: 'https://accounts.zoho.com',
au: 'https://accounts.zoho.com.au',
eu: 'https://accounts.zoho.eu',
in: 'https://accounts.zoho.in',
cn: 'https://accounts.zoho.com.cn',
jp: 'https://accounts.zoho.jp',
};

View File

@@ -0,0 +1,14 @@
const setBaseUrl = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingBaseUrl)
return requestConfig;
const apiDomain = $.auth.data.apiDomain;
if (apiDomain) {
requestConfig.baseURL = apiDomain;
}
return requestConfig;
};
export default setBaseUrl;

View File

@@ -0,0 +1,5 @@
import listCompanies from './list-companies/index.js';
import listOrganizations from './list-organizations/index.js';
import listContactOwners from './list-contact-owners/index.js';
export default [listCompanies, listOrganizations, listContactOwners];

View File

@@ -0,0 +1,39 @@
export default {
name: 'List companies',
key: 'listCompanies',
async run($) {
const companies = {
data: [],
};
const params = new URLSearchParams({
page: 1,
pageSize: 200,
fields: 'Account_Name',
});
let next = false;
do {
const { data } = await $.http.get('/bigin/v2/Accounts', { params });
if (data.info.more_records) {
params.page = params.page + 1;
next = true;
} else {
next = false;
}
if (data.data) {
for (const account of data.data) {
companies.data.push({
value: account.id,
name: account.Account_Name,
});
}
}
} while (next);
return companies;
},
};

View File

@@ -0,0 +1,39 @@
export default {
name: 'List contact owners',
key: 'listContactOwners',
async run($) {
const contactOwners = {
data: [],
};
const params = {
type: 'AllUsers',
page: 1,
pageSize: 200,
};
let next = false;
do {
const { data } = await $.http.get('/bigin/v2/users', params);
if (data.users.length === params.pageSize) {
next = true;
params.page = params.page + 1;
} else {
next = false;
}
if (data.users) {
for (const user of data.users) {
contactOwners.data.push({
value: user.id,
name: user.full_name,
});
}
}
} while (next);
return contactOwners;
},
};

View File

@@ -0,0 +1,23 @@
export default {
name: 'List organizations',
key: 'listOrganizations',
async run($) {
const organizations = {
data: [],
};
const { data } = await $.http.get('/bigin/v2/org');
if (data.org) {
for (const org of data.org) {
organizations.data.push({
value: org.id,
name: org.company_name,
});
}
}
return organizations;
},
};

View File

@@ -0,0 +1,23 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
import setBaseUrl from './common/set-base-url.js';
import triggers from './triggers/index.js';
import dynamicData from './dynamic-data/index.js';
import actions from './actions/index.js';
export default defineApp({
name: 'Bigin By Zoho CRM',
key: 'bigin-by-zoho-crm',
baseUrl: 'https://www.bigin.com',
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/bigin-by-zoho-crm/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/bigin-by-zoho-crm/connection',
primaryColor: '039649',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,
triggers,
dynamicData,
actions,
});

View File

@@ -0,0 +1,7 @@
import newCalls from './new-calls/index.js';
import newCompanies from './new-companies/index.js';
import newContacts from './new-contacts/index.js';
import newProducts from './new-products/index.js';
import newTasks from './new-tasks/index.js';
export default [newCalls, newCompanies, newContacts, newProducts, newTasks];

View File

@@ -0,0 +1,89 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New calls',
key: 'newCalls',
type: 'webhook',
description: 'Triggers when a new call is added.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const organizationId = $.step.parameters.organizationId;
const sampleEventData = {
ids: ['111111111111111111'],
token: null,
module: 'Calls',
operation: 'insert',
channel_id: organizationId,
server_time: 1708426963120,
query_params: {},
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Calls`,
affected_fields: [],
};
const dataItem = {
raw: sampleEventData,
meta: {
internalId: sampleEventData.channel_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const payload = {
watch: [
{
channel_id: organizationId,
notify_url: $.webhookUrl,
events: ['Calls.create'],
},
],
};
await $.http.post('/bigin/v2/actions/watch', payload);
await $.flow.setRemoteWebhookId(organizationId);
},
async unregisterHook($) {
await $.http.delete(
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
);
},
});

View File

@@ -0,0 +1,89 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New companies',
key: 'newCompanies',
type: 'webhook',
description: 'Triggers when a new company is created.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const organizationId = $.step.parameters.organizationId;
const sampleEventData = {
ids: ['111111111111111111'],
token: null,
module: 'Accounts',
operation: 'insert',
channel_id: organizationId,
server_time: 1708426963120,
query_params: {},
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Accounts`,
affected_fields: [],
};
const dataItem = {
raw: sampleEventData,
meta: {
internalId: sampleEventData.channel_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const payload = {
watch: [
{
channel_id: organizationId,
notify_url: $.webhookUrl,
events: ['Accounts.create'],
},
],
};
await $.http.post('/bigin/v2/actions/watch', payload);
await $.flow.setRemoteWebhookId(organizationId);
},
async unregisterHook($) {
await $.http.delete(
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
);
},
});

View File

@@ -0,0 +1,89 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New contacts',
key: 'newContacts',
type: 'webhook',
description: 'Triggers when a new contact is created.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const organizationId = $.step.parameters.organizationId;
const sampleEventData = {
ids: ['111111111111111111'],
token: null,
module: 'Contacts',
operation: 'insert',
channel_id: organizationId,
server_time: 1708426963120,
query_params: {},
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Contacts`,
affected_fields: [],
};
const dataItem = {
raw: sampleEventData,
meta: {
internalId: sampleEventData.channel_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const payload = {
watch: [
{
channel_id: organizationId,
notify_url: $.webhookUrl,
events: ['Contacts.create'],
},
],
};
await $.http.post('/bigin/v2/actions/watch', payload);
await $.flow.setRemoteWebhookId(organizationId);
},
async unregisterHook($) {
await $.http.delete(
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
);
},
});

View File

@@ -0,0 +1,89 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New products',
key: 'newProducts',
type: 'webhook',
description: 'Triggers when a new product is created.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const organizationId = $.step.parameters.organizationId;
const sampleEventData = {
ids: ['111111111111111111'],
token: null,
module: 'Products',
operation: 'insert',
channel_id: organizationId,
server_time: 1708426963120,
query_params: {},
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Products`,
affected_fields: [],
};
const dataItem = {
raw: sampleEventData,
meta: {
internalId: sampleEventData.channel_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const payload = {
watch: [
{
channel_id: organizationId,
notify_url: $.webhookUrl,
events: ['Products.create'],
},
],
};
await $.http.post('/bigin/v2/actions/watch', payload);
await $.flow.setRemoteWebhookId(organizationId);
},
async unregisterHook($) {
await $.http.delete(
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
);
},
});

View File

@@ -0,0 +1,89 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New tasks',
key: 'newTasks',
type: 'webhook',
description: 'Triggers when a new task is created.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const organizationId = $.step.parameters.organizationId;
const sampleEventData = {
ids: ['111111111111111111'],
token: null,
module: 'Tasks',
operation: 'insert',
channel_id: organizationId,
server_time: 1708426963120,
query_params: {},
resource_uri: `${$.auth.data.apiDomain}/bigin/v1/Tasks`,
affected_fields: [],
};
const dataItem = {
raw: sampleEventData,
meta: {
internalId: sampleEventData.channel_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const payload = {
watch: [
{
channel_id: organizationId,
notify_url: $.webhookUrl,
events: ['Tasks.create'],
},
],
};
await $.http.post('/bigin/v2/actions/watch', payload);
await $.flow.setRemoteWebhookId(organizationId);
},
async unregisterHook($) {
await $.http.delete(
`/bigin/v2/actions/watch?channel_ids=${$.flow.remoteWebhookId}`
);
},
});

View File

@@ -1,5 +1,6 @@
import defineAction from '../../../../helpers/define-action.js';
import base64ToString from './transformers/base64-to-string.js';
import capitalize from './transformers/capitalize.js';
import extractEmailAddress from './transformers/extract-email-address.js';
import extractNumber from './transformers/extract-number.js';
@@ -8,10 +9,12 @@ import lowercase from './transformers/lowercase.js';
import markdownToHtml from './transformers/markdown-to-html.js';
import pluralize from './transformers/pluralize.js';
import replace from './transformers/replace.js';
import stringToBase64 from './transformers/string-to-base64.js';
import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js';
const transformers = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -20,6 +23,7 @@ const transformers = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
};
@@ -37,6 +41,7 @@ export default defineAction({
required: true,
variables: true,
options: [
{ label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
@@ -45,6 +50,7 @@ export default defineAction({
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],

View File

@@ -0,0 +1,8 @@
const base64ToString = ($) => {
const input = $.step.parameters.input;
const decodedString = Buffer.from(input, 'base64').toString('utf8');
return decodedString;
};
export default base64ToString;

View File

@@ -0,0 +1,8 @@
const stringtoBase64 = ($) => {
const input = $.step.parameters.input;
const base64String = Buffer.from(input).toString('base64');
return base64String;
};
export default stringtoBase64;

View File

@@ -1,3 +1,4 @@
import base64ToString from './text/base64-to-string.js';
import capitalize from './text/capitalize.js';
import extractEmailAddress from './text/extract-email-address.js';
import extractNumber from './text/extract-number.js';
@@ -6,6 +7,7 @@ import lowercase from './text/lowercase.js';
import markdownToHtml from './text/markdown-to-html.js';
import pluralize from './text/pluralize.js';
import replace from './text/replace.js';
import stringToBase64 from './text/string-to-base64.js';
import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js';
import performMathOperation from './numbers/perform-math-operation.js';
@@ -15,6 +17,7 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
import formatDateTime from './date-time/format-date-time.js';
const options = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -23,6 +26,7 @@ const options = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
performMathOperation,

View File

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

View File

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

View File

@@ -18,7 +18,9 @@ const port = process.env.PORT || '3000';
const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let apiUrl = new URL(`${protocol}://${host}:${port}`).toString();
let apiUrl = new URL(
process.env.API_URL || `${protocol}://${host}:${port}`
).toString();
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
// use apiUrl by default, which has less priority over the following cases
@@ -88,6 +90,10 @@ const appConfig = {
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
CI: process.env.CI === 'true',
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
};
if (!appConfig.encryptionKey) {

View File

@@ -0,0 +1,6 @@
import appConfig from '../../../../config/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, { version: appConfig.version });
};

View File

@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
describe('GET /api/v1/automatisch/version', () => {
it('should return Automatisch version', async () => {
const response = await request(app)
.get('/api/v1/automatisch/version')
.expect(200);
const expectedPayload = {
data: {
version: '0.10.0',
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,5 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, request.currentUser);
};

View File

@@ -0,0 +1,26 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
describe('GET /api/v1/users/me', () => {
let role, currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
role = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return current user info', async () => {
const response = await request(app)
.get('/api/v1/users/me')
.set('Authorization', token)
.expect(200);
const expectedPayload = getCurrentUserMock(currentUser, role);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,12 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const inTrial = await request.currentUser.inTrial();
const trialInfo = {
inTrial,
expireAt: request.currentUser.trialExpiryDate,
};
renderObject(response, trialInfo);
};

View File

@@ -0,0 +1,38 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js';
import appConfig from '../../../../config/app.js';
import { DateTime } from 'luxon';
import User from '../../../../models/user.js';
describe('GET /api/v1/users/:userId/trial', () => {
let user, token;
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
token = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
describe('should return in trial, active subscription and expire at info', () => {
beforeEach(async () => {
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true);
});
it('should return null', async () => {
const response = await request(app)
.get(`/api/v1/users/${user.id}/trial`)
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUserTrialMock(user);
expect(response.body).toEqual(expectedResponsePayload);
});
});
});

View File

@@ -0,0 +1,16 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
export default async (request, response) => {
const user = await User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.findById(request.params.userId)
.throwIfNotFound();
renderObject(response, user);
};

View File

@@ -0,0 +1,36 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission';
import getUserMock from '../../../../../test/mocks/rest/api/v1/users/get-user';
describe('GET /api/v1/users/:userId', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
anotherUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
anotherUserRole = await anotherUser.$relatedQuery('role');
await createPermission({
roleId: currentUserRole.id,
action: 'read',
subject: 'User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified user info', async () => {
const response = await request(app)
.get(`/api/v1/users/${anotherUser.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,18 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const usersQuery = User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.orderBy('full_name', 'asc');
const users = await paginateRest(usersQuery, request.query.page);
renderObject(response, users);
};

View File

@@ -0,0 +1,56 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../test/factories/role';
import { createPermission } from '../../../../../test/factories/permission';
import { createUser } from '../../../../../test/factories/user';
import getUsersMock from '../../../../../test/mocks/rest/api/v1/users/get-users';
describe('GET /api/v1/users', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole({
key: 'currentUser',
name: 'Current user role',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: currentUserRole.id,
});
currentUser = await createUser({
roleId: currentUserRole.id,
fullName: 'Current User',
});
anotherUserRole = await createRole({
key: 'anotherUser',
name: 'Another user role',
});
anotherUser = await createUser({
roleId: anotherUserRole.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return users data', async () => {
const response = await request(app)
.get('/api/v1/users')
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUsersMock(
[anotherUser, currentUser],
[anotherUserRole, currentUserRole]
);
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -0,0 +1,3 @@
export default async (request, response) => {
response.status(200).end();
};

View File

@@ -0,0 +1,9 @@
import { describe, it } from 'vitest';
import request from 'supertest';
import app from '../../app.js';
describe('GET /healthcheck', () => {
it('should return 200 response with version data', async () => {
await request(app).get('/healthcheck').expect(200);
});
});

View File

@@ -1,7 +1,10 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import Role from '../../models/role.js';
const registerUser = async (_parent, params) => {
if (!appConfig.isCloud) return;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({

View File

@@ -1,9 +1,17 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
@@ -18,7 +26,7 @@ const getConfig = async (_parent, params) => {
computedConfig[key] = value?.data;
return computedConfig;
}, {});
}, defaultConfig);
};
export default getConfig;

View File

@@ -2,6 +2,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
@@ -56,6 +57,10 @@ describe('graphQL getConfig query', () => {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
@@ -82,6 +87,48 @@ describe('graphQL getConfig query', () => {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};

View File

@@ -6,100 +6,74 @@ import { createRole } from '../../../test/factories/role';
import { createUser } from '../../../test/factories/user';
describe('graphQL getCurrentUser query', () => {
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const invalidUserToken = 'invalid-token';
let role, currentUser, token, requestObject;
const query = `
query {
getCurrentUser {
id
email
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidUserToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
currentUser = await createUser({
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
describe('with authenticated user', () => {
let role, currentUser, token, requestObject;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
currentUser = await createUser({
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
it('should return user data', async () => {
const query = `
query {
getCurrentUser {
it('should return user data', async () => {
const query = `
query {
getCurrentUser {
id
email
fullName
email
createdAt
updatedAt
role {
id
email
fullName
email
createdAt
updatedAt
role {
id
name
}
name
}
}
`;
}
`;
const response = await requestObject.send({ query }).expect(200);
const response = await requestObject.send({ query }).expect(200);
const expectedResponsePayload = {
data: {
getCurrentUser: {
createdAt: currentUser.createdAt.getTime().toString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: { id: role.id, name: role.name },
updatedAt: currentUser.updatedAt.getTime().toString(),
},
const expectedResponsePayload = {
data: {
getCurrentUser: {
createdAt: currentUser.createdAt.getTime().toString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: { id: role.id, name: role.name },
updatedAt: currentUser.updatedAt.getTime().toString(),
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
it('should not return user password', async () => {
const query = `
query {
getCurrentUser {
id
email
password
}
it('should not return user password', async () => {
const query = `
query {
getCurrentUser {
id
email
password
}
`;
}
`;
const response = await requestObject.send({ query }).expect(400);
const response = await requestObject.send({ query }).expect(400);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
});

View File

@@ -40,307 +40,291 @@ describe('graphQL getExecutions query', () => {
}
`;
const invalidToken = 'invalid-token';
describe('with unauthenticated user', () => {
describe('and without correct permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
describe('and with correct permission', () => {
let role,
currentUser,
anotherUser,
token,
flowOne,
stepOneForFlowOne,
stepTwoForFlowOne,
executionOne,
flowTwo,
stepOneForFlowTwo,
stepTwoForFlowTwo,
executionTwo,
flowThree,
stepOneForFlowThree,
stepTwoForFlowThree,
executionThree,
expectedResponseForExecutionOne,
expectedResponseForExecutionTwo,
expectedResponseForExecutionThree;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
currentUser = await createUser({
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
flowOne = await createFlow({
userId: currentUser.id,
});
stepOneForFlowOne = await createStep({
flowId: flowOne.id,
});
stepTwoForFlowOne = await createStep({
flowId: flowOne.id,
});
executionOne = await createExecution({
flowId: flowOne.id,
});
await createExecutionStep({
executionId: executionOne.id,
stepId: stepOneForFlowOne.id,
status: 'success',
});
await createExecutionStep({
executionId: executionOne.id,
stepId: stepTwoForFlowOne.id,
status: 'success',
});
flowTwo = await createFlow({
userId: currentUser.id,
});
stepOneForFlowTwo = await createStep({
flowId: flowTwo.id,
});
stepTwoForFlowTwo = await createStep({
flowId: flowTwo.id,
});
executionTwo = await createExecution({
flowId: flowTwo.id,
});
await createExecutionStep({
executionId: executionTwo.id,
stepId: stepOneForFlowTwo.id,
status: 'success',
});
await createExecutionStep({
executionId: executionTwo.id,
stepId: stepTwoForFlowTwo.id,
status: 'failure',
});
flowThree = await createFlow({
userId: anotherUser.id,
});
stepOneForFlowThree = await createStep({
flowId: flowThree.id,
});
stepTwoForFlowThree = await createStep({
flowId: flowThree.id,
});
executionThree = await createExecution({
flowId: flowThree.id,
});
await createExecutionStep({
executionId: executionThree.id,
stepId: stepOneForFlowThree.id,
status: 'success',
});
await createExecutionStep({
executionId: executionThree.id,
stepId: stepTwoForFlowThree.id,
status: 'failure',
});
expectedResponseForExecutionOne = {
node: {
createdAt: executionOne.createdAt.getTime().toString(),
flow: {
active: flowOne.active,
id: flowOne.id,
name: flowOne.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowOne.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowOne.appKey}/assets/favicon.svg`,
},
],
},
id: executionOne.id,
status: 'success',
testRun: executionOne.testRun,
updatedAt: executionOne.updatedAt.getTime().toString(),
},
};
expectedResponseForExecutionTwo = {
node: {
createdAt: executionTwo.createdAt.getTime().toString(),
flow: {
active: flowTwo.active,
id: flowTwo.id,
name: flowTwo.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
},
],
},
id: executionTwo.id,
status: 'failure',
testRun: executionTwo.testRun,
updatedAt: executionTwo.updatedAt.getTime().toString(),
},
};
expectedResponseForExecutionThree = {
node: {
createdAt: executionThree.createdAt.getTime().toString(),
flow: {
active: flowThree.active,
id: flowThree.id,
name: flowThree.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowThree.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowThree.appKey}/assets/favicon.svg`,
},
],
},
id: executionThree.id,
status: 'failure',
testRun: executionThree.testRun,
updatedAt: executionThree.updatedAt.getTime().toString(),
},
};
});
describe('and with isCreator condition', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: ['isCreator'],
});
});
it('should return executions data of the current user', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [
expectedResponseForExecutionTwo,
expectedResponseForExecutionOne,
],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with correct permission', () => {
let role,
currentUser,
anotherUser,
token,
flowOne,
stepOneForFlowOne,
stepTwoForFlowOne,
executionOne,
flowTwo,
stepOneForFlowTwo,
stepTwoForFlowTwo,
executionTwo,
flowThree,
stepOneForFlowThree,
stepTwoForFlowThree,
executionThree,
expectedResponseForExecutionOne,
expectedResponseForExecutionTwo,
expectedResponseForExecutionThree;
describe('and without isCreator condition', () => {
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
currentUser = await createUser({
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
flowOne = await createFlow({
userId: currentUser.id,
});
stepOneForFlowOne = await createStep({
flowId: flowOne.id,
});
stepTwoForFlowOne = await createStep({
flowId: flowOne.id,
});
executionOne = await createExecution({
flowId: flowOne.id,
});
await createExecutionStep({
executionId: executionOne.id,
stepId: stepOneForFlowOne.id,
status: 'success',
});
await createExecutionStep({
executionId: executionOne.id,
stepId: stepTwoForFlowOne.id,
status: 'success',
});
flowTwo = await createFlow({
userId: currentUser.id,
});
stepOneForFlowTwo = await createStep({
flowId: flowTwo.id,
});
stepTwoForFlowTwo = await createStep({
flowId: flowTwo.id,
});
executionTwo = await createExecution({
flowId: flowTwo.id,
});
await createExecutionStep({
executionId: executionTwo.id,
stepId: stepOneForFlowTwo.id,
status: 'success',
});
await createExecutionStep({
executionId: executionTwo.id,
stepId: stepTwoForFlowTwo.id,
status: 'failure',
});
flowThree = await createFlow({
userId: anotherUser.id,
});
stepOneForFlowThree = await createStep({
flowId: flowThree.id,
});
stepTwoForFlowThree = await createStep({
flowId: flowThree.id,
});
executionThree = await createExecution({
flowId: flowThree.id,
});
await createExecutionStep({
executionId: executionThree.id,
stepId: stepOneForFlowThree.id,
status: 'success',
});
await createExecutionStep({
executionId: executionThree.id,
stepId: stepTwoForFlowThree.id,
status: 'failure',
});
expectedResponseForExecutionOne = {
node: {
createdAt: executionOne.createdAt.getTime().toString(),
flow: {
active: flowOne.active,
id: flowOne.id,
name: flowOne.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowOne.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowOne.appKey}/assets/favicon.svg`,
},
],
},
id: executionOne.id,
status: 'success',
testRun: executionOne.testRun,
updatedAt: executionOne.updatedAt.getTime().toString(),
},
};
expectedResponseForExecutionTwo = {
node: {
createdAt: executionTwo.createdAt.getTime().toString(),
flow: {
active: flowTwo.active,
id: flowTwo.id,
name: flowTwo.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
},
],
},
id: executionTwo.id,
status: 'failure',
testRun: executionTwo.testRun,
updatedAt: executionTwo.updatedAt.getTime().toString(),
},
};
expectedResponseForExecutionThree = {
node: {
createdAt: executionThree.createdAt.getTime().toString(),
flow: {
active: flowThree.active,
id: flowThree.id,
name: flowThree.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowThree.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowThree.appKey}/assets/favicon.svg`,
},
],
},
id: executionThree.id,
status: 'failure',
testRun: executionThree.testRun,
updatedAt: executionThree.updatedAt.getTime().toString(),
},
};
});
describe('and with isCreator condition', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: ['isCreator'],
});
});
it('should return executions data of the current user', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [
expectedResponseForExecutionTwo,
expectedResponseForExecutionOne,
],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
conditions: [],
});
});
describe('and without isCreator condition', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: [],
});
});
it('should return executions data of all users', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
it('should return executions data of all users', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [
expectedResponseForExecutionThree,
expectedResponseForExecutionTwo,
expectedResponseForExecutionOne,
],
pageInfo: { currentPage: 1, totalPages: 1 },
},
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [
expectedResponseForExecutionThree,
expectedResponseForExecutionTwo,
expectedResponseForExecutionOne,
],
pageInfo: { currentPage: 1, totalPages: 1 },
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with filters', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: [],
});
});
describe('and with filters', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: [],
});
});
it('should return executions data for the specified flow', async () => {
const query = `
it('should return executions data for the specified flow', async () => {
const query = `
query {
getExecutions(limit: 10, offset: 0, filters: { flowId: "${flowOne.id}" }) {
pageInfo {
@@ -368,26 +352,26 @@ describe('graphQL getExecutions query', () => {
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
it('should return only executions data with success status', async () => {
const query = `
it('should return only executions data with success status', async () => {
const query = `
query {
getExecutions(limit: 10, offset: 0, filters: { status: "success" }) {
pageInfo {
@@ -415,30 +399,30 @@ describe('graphQL getExecutions query', () => {
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
it('should return only executions data within date range', async () => {
const createdAtFrom = executionOne.createdAt.getTime().toString();
it('should return only executions data within date range', async () => {
const createdAtFrom = executionOne.createdAt.getTime().toString();
const createdAtTo = executionOne.createdAt.getTime().toString();
const createdAtTo = executionOne.createdAt.getTime().toString();
const query = `
const query = `
query {
getExecutions(limit: 10, offset: 0, filters: { createdAt: { from: "${createdAtFrom}", to: "${createdAtTo}" }}) {
pageInfo {
@@ -466,23 +450,22 @@ describe('graphQL getExecutions query', () => {
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
});
});

View File

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

View File

@@ -17,7 +17,6 @@ describe('graphQL getRole query', () => {
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions,
invalidToken,
permissionOne,
permissionTwo;
@@ -74,108 +73,91 @@ describe('graphQL getRole query', () => {
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
invalidToken = 'invalid-token';
});
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query: queryWithValidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
});
describe('with authenticated user', () => {
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithoutPermissions)
.send({ query: queryWithValidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithoutPermissions)
.send({ query: queryWithValidRole })
.expect(200);
describe('and correct permissions', () => {
it('should return role data for a valid role id', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithValidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and correct permissions', () => {
it('should return role data for a valid role id', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithValidRole })
.expect(200);
const expectedResponsePayload = {
data: {
getRole: {
description: validRole.description,
id: validRole.id,
isAdmin: validRole.key === 'admin',
key: validRole.key,
name: validRole.name,
permissions: [
{
action: permissionOne.action,
conditions: permissionOne.conditions,
id: permissionOne.id,
subject: permissionOne.subject,
},
{
action: permissionTwo.action,
conditions: permissionTwo.conditions,
id: permissionTwo.id,
subject: permissionTwo.subject,
},
],
},
const expectedResponsePayload = {
data: {
getRole: {
description: validRole.description,
id: validRole.id,
isAdmin: validRole.key === 'admin',
key: validRole.key,
name: validRole.name,
permissions: [
{
action: permissionOne.action,
conditions: permissionOne.conditions,
id: permissionOne.id,
subject: permissionOne.subject,
},
{
action: permissionTwo.action,
conditions: permissionTwo.conditions,
id: permissionTwo.id,
subject: permissionTwo.subject,
},
],
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
it('should return not found for invalid role id', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithInvalidRole })
.expect(200);
it('should return not found for invalid role id', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithInvalidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('NotFoundError');
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('NotFoundError');
});
});
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
describe('and correct permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithInvalidRole })
.expect(200);
describe('and correct permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithInvalidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
});

View File

@@ -15,8 +15,7 @@ describe('graphQL getRoles query', () => {
userWithPermissions,
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions,
invalidToken;
tokenWithoutPermissions;
beforeEach(async () => {
currentUserRole = await createRole({ name: 'Current user role' });
@@ -53,99 +52,82 @@ describe('graphQL getRoles query', () => {
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
invalidToken = 'invalid-token';
});
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
});
describe('with authenticated user', () => {
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithoutPermissions)
.send({ query })
.expect(200);
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithoutPermissions)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and correct permissions', () => {
it('should return roles data', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getRoles: [
{
description: currentUserRole.description,
id: currentUserRole.id,
isAdmin: currentUserRole.key === 'admin',
key: currentUserRole.key,
name: currentUserRole.name,
},
{
description: roleOne.description,
id: roleOne.id,
isAdmin: roleOne.key === 'admin',
key: roleOne.key,
name: roleOne.name,
},
{
description: roleSecond.description,
id: roleSecond.id,
isAdmin: roleSecond.key === 'admin',
key: roleSecond.key,
name: roleSecond.name,
},
],
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
describe('and correct permissions', () => {
it('should return roles data', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getRoles: [
{
description: currentUserRole.description,
id: currentUserRole.id,
isAdmin: currentUserRole.key === 'admin',
key: currentUserRole.key,
name: currentUserRole.name,
},
{
description: roleOne.description,
id: roleOne.id,
isAdmin: roleOne.key === 'admin',
key: roleOne.key,
name: roleOne.name,
},
{
description: roleSecond.description,
id: roleSecond.id,
isAdmin: roleSecond.key === 'admin',
key: roleSecond.key,
name: roleSecond.name,
},
],
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
describe('and correct permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query })
.expect(200);
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
describe('and correct permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
});

View File

@@ -16,34 +16,46 @@ describe('graphQL getTrialStatus query', () => {
}
`;
const invalidToken = 'invalid-token';
let user, userToken;
describe('with unauthenticated user', () => {
it('should throw not authorized error', async () => {
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
userToken = createAuthTokenByUserId(user.id);
});
describe('and with cloud flag disabled', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
});
it('should return null', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', invalidToken)
.set('Authorization', userToken)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
const expectedResponsePayload = {
data: { getTrialStatus: null },
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('with authenticated user', () => {
let user, userToken;
describe('and with cloud flag enabled', () => {
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
userToken = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
describe('and with cloud flag disabled', () => {
describe('and not in trial and has active subscription', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(
true
);
});
it('should return null', async () => {
@@ -61,56 +73,27 @@ describe('graphQL getTrialStatus query', () => {
});
});
describe('and with cloud flag enabled', () => {
describe('and in trial period', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(true);
});
describe('and not in trial and has active subscription', () => {
beforeEach(async () => {
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(
true
);
});
it('should return null', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', userToken)
.send({ query })
.expect(200);
it('should return null', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', userToken)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: { getTrialStatus: null },
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and in trial period', () => {
beforeEach(async () => {
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(true);
});
it('should return null', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', userToken)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getTrialStatus: {
expireAt: new Date(user.trialExpiryDate).getTime().toString(),
},
const expectedResponsePayload = {
data: {
getTrialStatus: {
expireAt: new Date(user.trialExpiryDate).getTime().toString(),
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
});
});

View File

@@ -8,37 +8,12 @@ import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
describe('graphQL getUser query', () => {
describe('with unauthenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const invalidUserId = '123123123';
const userWithoutPermissions = await createUser();
const anotherUser = await createUser();
const query = `
query {
getUser(id: "${invalidUserId}") {
id
email
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', 'invalid-token')
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const anotherUser = await createUser();
const query = `
query {
getUser(id: "${anotherUser.id}") {
id
@@ -47,50 +22,48 @@ describe('graphQL getUser query', () => {
}
`;
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and correct permissions', () => {
let role, currentUser, anotherUser, token, requestObject;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
});
anotherUser = await createUser({
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
describe('and correct permissions', () => {
let role, currentUser, anotherUser, token, requestObject;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
});
anotherUser = await createUser({
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app)
.post('/graphql')
.set('Authorization', token);
});
it('should return user data for a valid user id', async () => {
const query = `
it('should return user data for a valid user id', async () => {
const query = `
query {
getUser(id: "${anotherUser.id}") {
id
@@ -107,26 +80,26 @@ describe('graphQL getUser query', () => {
}
`;
const response = await requestObject.send({ query }).expect(200);
const response = await requestObject.send({ query }).expect(200);
const expectedResponsePayload = {
data: {
getUser: {
createdAt: anotherUser.createdAt.getTime().toString(),
email: anotherUser.email,
fullName: anotherUser.fullName,
id: anotherUser.id,
role: { id: role.id, name: role.name },
updatedAt: anotherUser.updatedAt.getTime().toString(),
},
const expectedResponsePayload = {
data: {
getUser: {
createdAt: anotherUser.createdAt.getTime().toString(),
email: anotherUser.email,
fullName: anotherUser.fullName,
id: anotherUser.id,
role: { id: role.id, name: role.name },
updatedAt: anotherUser.updatedAt.getTime().toString(),
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
it('should not return user password for a valid user id', async () => {
const query = `
it('should not return user password for a valid user id', async () => {
const query = `
query {
getUser(id: "${anotherUser.id}") {
id
@@ -136,18 +109,18 @@ describe('graphQL getUser query', () => {
}
`;
const response = await requestObject.send({ query }).expect(400);
const response = await requestObject.send({ query }).expect(400);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
it('should return not found for invalid user id', async () => {
const invalidUserId = Crypto.randomUUID();
it('should return not found for invalid user id', async () => {
const invalidUserId = Crypto.randomUUID();
const query = `
const query = `
query {
getUser(id: "${invalidUserId}") {
id
@@ -164,11 +137,10 @@ describe('graphQL getUser query', () => {
}
`;
const response = await requestObject.send({ query }).expect(200);
const response = await requestObject.send({ query }).expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('NotFoundError');
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('NotFoundError');
});
});
});

View File

@@ -30,111 +30,95 @@ describe('graphQL getUsers query', () => {
}
`;
describe('with unauthenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', 'invalid-token')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not Authorised!');
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('with authenticated user', () => {
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
describe('and with correct permissions', () => {
let role, currentUser, anotherUser, token, requestObject;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser({
roleId: role.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
describe('and with correct permissions', () => {
let role, currentUser, anotherUser, token, requestObject;
it('should return users data', async () => {
const response = await requestObject.send({ query }).expect(200);
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser({
roleId: role.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app)
.post('/graphql')
.set('Authorization', token);
});
it('should return users data', async () => {
const response = await requestObject.send({ query }).expect(200);
const expectedResponsePayload = {
data: {
getUsers: {
edges: [
{
node: {
email: anotherUser.email,
fullName: anotherUser.fullName,
id: anotherUser.id,
role: {
id: role.id,
name: role.name,
},
const expectedResponsePayload = {
data: {
getUsers: {
edges: [
{
node: {
email: anotherUser.email,
fullName: anotherUser.fullName,
id: anotherUser.id,
role: {
id: role.id,
name: role.name,
},
},
{
node: {
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
id: role.id,
name: role.name,
},
},
},
],
pageInfo: {
currentPage: 1,
totalPages: 1,
},
totalCount: 2,
{
node: {
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
id: role.id,
name: role.name,
},
},
},
],
pageInfo: {
currentPage: 1,
totalPages: 1,
},
totalCount: 2,
},
};
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
expect(response.body).toEqual(expectedResponsePayload);
});
it('should not return users data with password', async () => {
const query = `
it('should not return users data with password', async () => {
const query = `
query {
getUsers(limit: 10, offset: 0) {
pageInfo {
@@ -153,13 +137,12 @@ describe('graphQL getUsers query', () => {
}
`;
const response = await requestObject.send({ query }).expect(400);
const response = await requestObject.send({ query }).expect(400);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
});
});

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import appConfig from '../config/app.js';
import User from '../models/user.js';
const isAuthenticated = rule()(async (_parent, _args, req) => {
export const isAuthenticated = async (_parent, _args, req) => {
const token = req.headers['authorization'];
if (token == null) return false;
@@ -20,35 +20,47 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
.withGraphFetched({
role: true,
permissions: true,
});
})
.throwIfNotFound();
return true;
} catch (error) {
return false;
}
});
};
const authentication = shield(
{
Query: {
'*': isAuthenticated,
getAutomatischInfo: allow,
getConfig: allow,
getNotifications: allow,
healthcheck: allow,
listSamlAuthProviders: allow,
},
Mutation: {
'*': isAuthenticated,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
},
{
allowExternalErrors: true,
export const authenticateUser = async (request, response, next) => {
if (await isAuthenticated(null, null, request)) {
next();
} else {
return response.status(401).end();
}
);
};
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {
Query: {
'*': isAuthenticatedRule,
getAutomatischInfo: allow,
getConfig: allow,
getNotifications: allow,
healthcheck: allow,
listSamlAuthProviders: allow,
},
Mutation: {
'*': isAuthenticatedRule,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
};
const authenticationOptions = {
allowExternalErrors: true,
};
const authentication = shield(authenticationRules, authenticationOptions);
export default authentication;

View File

@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import { allow } from 'graphql-shield';
import { isAuthenticated, authenticationRules } from './authentication.js';
import { createUser } from '../../test/factories/user.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
describe('isAuthenticated', () => {
it('should return false if no token is provided', async () => {
const req = { headers: {} };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return false if token is invalid', async () => {
const req = { headers: { authorization: 'invalidToken' } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return true if token is valid and there is a user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(true);
});
it('should return false if token is valid and but there is no user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
await user.$query().delete();
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
});
describe('authentication rules', () => {
const getQueryAndMutationNames = (rules) => {
const queries = Object.keys(rules.Query || {});
const mutations = Object.keys(rules.Mutation || {});
return { queries, mutations };
};
const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
describe('for queries', () => {
queries.forEach((query) => {
it(`should apply correct rule for query: ${query}`, () => {
const ruleApplied = authenticationRules.Query[query];
if (query === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toEqual(allow);
}
});
});
});
describe('for mutations', () => {
mutations.forEach((mutation) => {
it(`should apply correct rule for mutation: ${mutation}`, () => {
const ruleApplied = authenticationRules.Mutation[mutation];
if (mutation === '*') {
expect(ruleApplied.func).toBe(isAuthenticated);
} else {
expect(ruleApplied).toBe(allow);
}
});
});
});
});

View File

@@ -0,0 +1,22 @@
const authorizationList = {
'/api/v1/users/:userId': {
action: 'read',
subject: 'User',
},
'/api/v1/users/': {
action: 'read',
subject: 'User',
},
};
export const authorizeUser = async (request, response, next) => {
const currentRoute = request.baseUrl + request.route.path;
const currentRouteRule = authorizationList[currentRoute];
try {
request.currentUser.can(currentRouteRule.action, currentRouteRule.subject);
next();
} catch (error) {
return response.status(403).end();
}
};

View File

@@ -0,0 +1,11 @@
import appConfig from '../config/app.js';
export const checkIsCloud = async (request, response, next) => {
if (appConfig.isCloud) {
next();
} else {
return response.status(404).end();
}
};
export default checkIsCloud;

View File

@@ -1,6 +1,9 @@
import * as path from 'path';
import * as fs from 'fs';
import * as handlebars from 'handlebars';
import path from 'path';
import fs from 'fs';
import handlebars from 'handlebars';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compileEmail = (emailPath, replacements = {}) => {
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);

View File

@@ -0,0 +1,25 @@
const paginateRest = async (query, page) => {
const pageSize = 10;
page = parseInt(page, 10);
if (isNaN(page) || page < 1) {
page = 1;
}
const [records, count] = await Promise.all([
query.limit(pageSize).offset((page - 1) * pageSize),
query.resultSize(),
]);
return {
pageInfo: {
currentPage: page,
totalPages: Math.ceil(count / pageSize),
},
totalCount: count,
records,
};
};
export default paginateRest;

View File

@@ -0,0 +1,42 @@
import serializers from '../serializers/index.js';
const isPaginated = (object) =>
object?.pageInfo &&
object?.totalCount !== undefined &&
Array.isArray(object?.records);
const isArray = (object) =>
Array.isArray(object) || Array.isArray(object?.records);
const totalCount = (object) =>
isPaginated(object) ? object.totalCount : isArray(object) ? object.length : 1;
const renderObject = (response, object) => {
let data = isPaginated(object) ? object.records : object;
const type = isPaginated(object)
? object.records[0].constructor.name
: object.constructor.name;
const serializer = serializers[type];
if (serializer) {
data = Array.isArray(data)
? data.map((item) => serializer(item))
: serializer(data);
}
const computedPayload = {
data,
meta: {
type,
count: totalCount(object),
isArray: isArray(object),
currentPage: isPaginated(object) ? object.pageInfo.currentPage : null,
totalPages: isPaginated(object) ? object.pageInfo.totalPages : null,
},
};
return response.json(computedPayload);
};
export { renderObject };

View File

@@ -15,7 +15,7 @@ const webUIHandler = async (app) => {
app.use(express.static(webBuildPath));
app.get('*', (_req, res) => {
res.set('Content-Security-Policy', 'frame-ancestors: none;');
res.set('Content-Security-Policy', 'frame-ancestors \'none\';');
res.set('X-Frame-Options', 'DENY');
res.sendFile(indexHtml);

View File

@@ -0,0 +1,8 @@
import { Router } from 'express';
import versionAction from '../../../controllers/api/v1/automatisch/version.js';
const router = Router();
router.get('/version', versionAction);
export default router;

View File

@@ -0,0 +1,22 @@
import { Router } from 'express';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import checkIsCloud from '../../../helpers/check-is-cloud.js';
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
import getUserAction from '../../../controllers/api/v1/users/get-user.js';
import getUsersAction from '../../../controllers/api/v1/users/get-users.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
const router = Router();
router.get('/', authenticateUser, authorizeUser, getUsersAction);
router.get('/me', authenticateUser, getCurrentUserAction);
router.get('/:userId', authenticateUser, authorizeUser, getUserAction);
router.get(
'/:userId/trial',
authenticateUser,
checkIsCloud,
getUserTrialAction
);
export default router;

View File

@@ -0,0 +1,8 @@
import { Router } from 'express';
import indexAction from '../controllers/healthcheck/index.js';
const router = Router();
router.get('/', indexAction);
export default router;

View File

@@ -2,11 +2,17 @@ import { Router } from 'express';
import graphQLInstance from '../helpers/graphql-instance.js';
import webhooksRouter from './webhooks.js';
import paddleRouter from './paddle.ee.js';
import healthcheckRouter from './healthcheck.js';
import automatischRouter from './api/v1/automatisch.js';
import usersRouter from './api/v1/users.js';
const router = Router();
router.use('/graphql', graphQLInstance);
router.use('/webhooks', webhooksRouter);
router.use('/paddle', paddleRouter);
router.use('/healthcheck', healthcheckRouter);
router.use('/api/v1/automatisch', automatischRouter);
router.use('/api/v1/users', usersRouter);
export default router;

View File

@@ -0,0 +1,11 @@
import userSerializer from './user.js';
import roleSerializer from './role.js';
import permissionSerializer from './permission.js';
const serializers = {
User: userSerializer,
Role: roleSerializer,
Permission: permissionSerializer,
};
export default serializers;

View File

@@ -0,0 +1,13 @@
const permissionSerializer = (permission) => {
return {
id: permission.id,
roleId: permission.roleId,
action: permission.action,
subject: permission.subject,
conditions: permission.conditions,
createdAt: permission.createdAt,
updatedAt: permission.updatedAt,
};
};
export default permissionSerializer;

View File

@@ -0,0 +1,25 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createPermission } from '../../test/factories/permission';
import permissionSerializer from './permission';
describe('permissionSerializer', () => {
let permission;
beforeEach(async () => {
permission = await createPermission();
});
it('should return permission data', async () => {
const expectedPayload = {
id: permission.id,
roleId: permission.roleId,
action: permission.action,
subject: permission.subject,
conditions: permission.conditions,
createdAt: permission.createdAt,
updatedAt: permission.updatedAt,
};
expect(permissionSerializer(permission)).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,13 @@
const roleSerializer = (role) => {
return {
id: role.id,
name: role.name,
key: role.key,
description: role.description,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isAdmin: role.isAdmin,
};
};
export default roleSerializer;

View File

@@ -0,0 +1,25 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createRole } from '../../test/factories/role';
import roleSerializer from './role';
describe('roleSerializer', () => {
let role;
beforeEach(async () => {
role = await createRole();
});
it('should return role data', async () => {
const expectedPayload = {
id: role.id,
name: role.name,
key: role.key,
description: role.description,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isAdmin: role.isAdmin,
};
expect(roleSerializer(role)).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,32 @@
import roleSerializer from './role.js';
import permissionSerializer from './permission.js';
import appConfig from '../config/app.js';
const userSerializer = (user) => {
let userData = {
id: user.id,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
fullName: user.fullName,
roleId: user.roleId,
};
if (user.role) {
userData.role = roleSerializer(user.role);
}
if (user.permissions) {
userData.permissions = user.permissions.map((permission) =>
permissionSerializer(permission)
);
}
if (appConfig.isCloud && user.trialExpiryDate) {
userData.trialExpiryDate = user.trialExpiryDate;
}
return userData;
};
export default userSerializer;

View File

@@ -0,0 +1,76 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { DateTime } from 'luxon';
import appConfig from '../config/app';
import { createUser } from '../../test/factories/user';
import { createPermission } from '../../test/factories/permission';
import userSerializer from './user';
describe('userSerializer', () => {
let user, role, permissionOne, permissionTwo;
beforeEach(async () => {
user = await createUser();
role = await user.$relatedQuery('role');
permissionOne = await createPermission({
roleId: role.id,
action: 'read',
subject: 'User',
});
permissionTwo = await createPermission({
roleId: role.id,
action: 'read',
subject: 'Role',
});
});
it('should return user data', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const expectedPayload = {
createdAt: user.createdAt,
email: user.email,
fullName: user.fullName,
id: user.id,
roleId: user.roleId,
updatedAt: user.updatedAt,
};
expect(userSerializer(user)).toEqual(expectedPayload);
});
it('should return user data with the role', async () => {
user.role = role;
const expectedPayload = {
role,
};
expect(userSerializer(user)).toMatchObject(expectedPayload);
});
it('should return user data with the permissions', async () => {
user.permissions = [permissionOne, permissionTwo];
const expectedPayload = {
permissions: [permissionOne, permissionTwo],
};
expect(userSerializer(user)).toMatchObject(expectedPayload);
});
it('should return user data with trial expiry date', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
await user.$query().patch({
trialExpiryDate: DateTime.now().plus({ days: 30 }).toISODate(),
});
const expectedPayload = {
trialExpiryDate: user.trialExpiryDate,
};
expect(userSerializer(user)).toMatchObject(expectedPayload);
});
});

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import Config from '../../src/models/config';
export const createConfig = async (params = {}) => {
const configData = {
@@ -6,10 +7,7 @@ export const createConfig = async (params = {}) => {
value: params?.value || { data: 'sampleConfig' },
};
const [config] = await global.knex
.table('config')
.insert(configData)
.returning('*');
const config = await Config.query().insert(configData).returning('*');
return config;
};

View File

@@ -1,5 +1,6 @@
import appConfig from '../../src/config/app';
import { AES } from 'crypto-js';
import Connection from '../../src/models/connection';
export const createConnection = async (params = {}) => {
params.key = params?.key || 'deepl';
@@ -16,10 +17,7 @@ export const createConnection = async (params = {}) => {
appConfig.encryptionKey
).toString();
const [connection] = await global.knex
.table('connections')
.insert(params)
.returning('*');
const connection = await Connection.query().insert(params).returning('*');
return connection;
};

View File

@@ -1,3 +1,4 @@
import ExecutionStep from '../../src/models/execution-step';
import { createExecution } from './execution';
import { createStep } from './step';
@@ -8,8 +9,7 @@ export const createExecutionStep = async (params = {}) => {
params.dataIn = params?.dataIn || { dataIn: 'dataIn' };
params.dataOut = params?.dataOut || { dataOut: 'dataOut' };
const [executionStep] = await global.knex
.table('executionSteps')
const executionStep = await ExecutionStep.query()
.insert(params)
.returning('*');

View File

@@ -1,3 +1,4 @@
import Execution from '../../src/models/execution';
import { createFlow } from './flow';
export const createExecution = async (params = {}) => {
@@ -6,10 +7,7 @@ export const createExecution = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const [execution] = await global.knex
.table('executions')
.insert(params)
.returning('*');
const execution = await Execution.query().insert(params).returning('*');
return execution;
};

View File

@@ -1,3 +1,4 @@
import Flow from '../../src/models/flow';
import { createUser } from './user';
export const createFlow = async (params = {}) => {
@@ -6,7 +7,7 @@ export const createFlow = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const [flow] = await global.knex.table('flows').insert(params).returning('*');
const flow = await Flow.query().insert(params).returning('*');
return flow;
};

View File

@@ -1,3 +1,4 @@
import Permission from '../../src/models/permission';
import { createRole } from './role';
export const createPermission = async (params = {}) => {
@@ -6,10 +7,7 @@ export const createPermission = async (params = {}) => {
params.subject = params?.subject || 'User';
params.conditions = params?.conditions || ['isCreator'];
const [permission] = await global.knex
.table('permissions')
.insert(params)
.returning('*');
const permission = await Permission.query().insert(params).returning('*');
return permission;
};

View File

@@ -1,8 +1,10 @@
import Role from '../../src/models/role';
export const createRole = async (params = {}) => {
params.name = params?.name || 'Viewer';
params.key = params?.key || 'viewer';
const [role] = await global.knex.table('roles').insert(params).returning('*');
const role = await Role.query().insert(params).returning('*');
return role;
};

View File

@@ -1,3 +1,4 @@
import Step from '../../src/models/step';
import { createFlow } from './flow';
export const createStep = async (params = {}) => {
@@ -16,7 +17,7 @@ export const createStep = async (params = {}) => {
params.appKey =
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
const [step] = await global.knex.table('steps').insert(params).returning('*');
const step = await Step.query().insert(params).returning('*');
return step;
};

View File

@@ -1,5 +1,6 @@
import { createRole } from './role';
import { faker } from '@faker-js/faker';
import User from '../../src/models/user';
export const createUser = async (params = {}) => {
params.roleId = params?.roleId || (await createRole()).id;
@@ -7,7 +8,7 @@ export const createUser = async (params = {}) => {
params.email = params?.email || faker.internet.email();
params.password = params?.password || faker.internet.password();
const [user] = await global.knex.table('users').insert(params).returning('*');
const user = await User.query().insert(params).returning('*');
return user;
};

View File

@@ -0,0 +1,32 @@
const getCurrentUserMock = (currentUser, role) => {
return {
data: {
createdAt: currentUser.createdAt.toISOString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
permissions: [],
role: {
createdAt: role.createdAt.toISOString(),
description: null,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
},
roleId: role.id,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default getCurrentUserMock;

View File

@@ -0,0 +1,17 @@
const getUserTrialMock = async (currentUser) => {
return {
data: {
inTrial: await currentUser.inTrial(),
expireAt: currentUser.trialExpiryDate.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
};
export default getUserTrialMock;

View File

@@ -0,0 +1,31 @@
const getUserMock = (currentUser, role) => {
return {
data: {
createdAt: currentUser.createdAt.toISOString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
createdAt: role.createdAt.toISOString(),
description: null,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
},
roleId: role.id,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default getUserMock;

View File

@@ -0,0 +1,38 @@
const getUsersMock = async (users, roles) => {
const data = users.map((user) => {
const role = roles.find((r) => r.id === user.roleId);
return {
createdAt: user.createdAt.toISOString(),
email: user.email,
fullName: user.fullName,
id: user.id,
role: role
? {
createdAt: role.createdAt.toISOString(),
description: role.description,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
}
: null, // Fallback to null if role not found
roleId: user.roleId,
trialExpiryDate: user.trialExpiryDate.toISOString(),
updatedAt: user.updatedAt.toISOString(),
};
});
return {
data: data,
meta: {
count: data.length,
currentPage: 1,
isArray: true,
totalPages: 1,
type: 'User',
},
};
};
export default getUsersMock;

View File

@@ -1,11 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -1 +0,0 @@
/dist

View File

@@ -1,16 +0,0 @@
version: 2
updates:
- package-ecosystem: 'npm'
versioning-strategy: increase
directory: '/'
schedule:
interval: 'monthly'
labels:
- 'dependencies'
open-pull-requests-limit: 100
pull-request-branch-name:
separator: '-'
ignore:
- dependency-name: 'fs-extra'
- dependency-name: '*'
update-types: ['version-update:semver-major']

View File

@@ -1,9 +0,0 @@
*-debug.log
*-error.log
/.nyc_output
/dist
/lib
/package-lock.json
/tmp
node_modules
oclif.manifest.json

View File

@@ -1,4 +0,0 @@
# `@automatisch/cli`
The open source Zapier alternative. Build workflow automation without spending
time and money.

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))

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