Compare commits

...

15 Commits

Author SHA1 Message Date
Faruk AYDIN
8156b8b356 Release v0.9.3 2023-09-01 12:35:19 +02:00
Faruk AYDIN
3a2cbae0a0 chore: Update version to 0.9.3 in Dockerfiles 2023-09-01 12:34:25 +02:00
Ömer Faruk Aydın
0ad8da097b fix(rss): get text for internal ID if the guid or id is object (#1257) 2023-09-01 12:10:42 +02:00
Ömer Faruk Aydın
e2dcdd2811 feat(formatter): add extract number transform to text action (#1255) 2023-08-31 16:35:28 +02:00
Ömer Faruk Aydın
8074f9146b Merge pull request #1253 from automatisch/refactor-notifications
refactor: fetch notifications over graphql query
2023-08-29 16:37:37 +02:00
Ali BARIN
df24bac913 refactor: fetch notifications over graphql query 2023-08-28 20:44:55 +00:00
Ali BARIN
4d4091adcc test: write login page tests 2023-08-28 20:11:21 +02:00
Ali BARIN
cac54c41a1 chore: run automatisch in playwright workflow 2023-08-28 20:11:21 +02:00
Ali BARIN
130931d7af fix: use axios with proxy in license check (#1252) 2023-08-28 17:19:19 +02:00
Ömer Faruk Aydın
d35b08b35e Merge pull request #1250 from automatisch/release/0.9.2
Release v0.9.2
2023-08-28 16:54:32 +02:00
Faruk AYDIN
82031da6a6 Release v0.9.2 2023-08-28 16:30:29 +02:00
Faruk AYDIN
9df5ee7b11 chore: Update version to 0.9.2 in Dockerfiles 2023-08-28 16:29:53 +02:00
Ömer Faruk Aydın
2ed1a57cd9 Merge pull request #1249 from automatisch/permission-contions
chore: Convert conditions of permissions to array
2023-08-28 16:27:29 +02:00
Faruk AYDIN
101450cba6 chore: Convert conditions of permissions to array 2023-08-28 16:24:39 +02:00
Ömer Faruk Aydın
6bab5b3f7c Merge pull request #1248 from automatisch/release/0.9.1
Release v0.9.1
2023-08-28 15:15:25 +02:00
38 changed files with 266 additions and 70 deletions

View File

@@ -29,7 +29,6 @@ rm -rf .env
echo "
PORT=$WEB_PORT
REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
REACT_APP_NOTIFICATIONS_URL=https://notifications.automatisch.io
" >> .env
cd $CURRENT_DIR

View File

@@ -1,31 +1,87 @@
name: Automatisch UI Tests
on:
push:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
APP_SECRET_KEY: sample_app_secret_key
POSTGRES_HOST: localhost
POSTGRES_DATABASE: automatisch
POSTGRES_PORT: 5432
POSTGRES_USERNAME: automatisch_user
POSTGRES_PASSWORD: automatisch_password
REDIS_HOST: localhost
APP_ENV: production
LICENSE_KEY: ${{ secrets.E2E_LICENSE_KEY }}
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14.5-alpine
env:
POSTGRES_DB: automatisch
POSTGRES_USER: automatisch_user
POSTGRES_PASSWORD: automatisch_password
options: >-
--health-cmd "pg_isready -U automatisch_user -d automatisch"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7.0.4-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: yarn
run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Build Automatisch
run: yarn lerna run --scope=@*/{web,backend,cli} build
env:
# Keep this until we clean up warnings in build processes
CI: false
- name: Migrate database
working-directory: ./packages/backend
run: yarn db:migrate --migrations-directory ./dist/src/db/migrations
- name: Seed user
working-directory: ./packages/backend
run: yarn db:seed:user &
- name: Run Automatisch
run: yarn start &
working-directory: ./packages/backend
- name: Run Automatisch worker
run: node dist/src/worker.js &
working-directory: ./packages/backend
- name: Run Playwright tests
working-directory: ./packages/e2e-tests
env:
LOGIN_EMAIL: ${{ secrets.LOGIN_EMAIL }}
LOGIN_PASSWORD: ${{ secrets.LOGIN_PASSWORD }}
BASE_URL: ${{ vars.E2E_BASE_URL }}
run: yarn test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: test-results/
path: ./packages/e2e-tests/test-results/**/*
retention-days: 30

View File

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

View File

@@ -4,7 +4,7 @@ WORKDIR /automatisch
RUN \
apk --no-cache add --virtual build-dependencies python3 build-base && \
yarn global add @automatisch/cli@0.9.1 --network-timeout 1000000 && \
yarn global add @automatisch/cli@0.9.3 --network-timeout 1000000 && \
rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM automatischio/automatisch:0.9.1
FROM automatischio/automatisch:0.9.3
WORKDIR /automatisch
RUN apk add --no-cache openssl dos2unix

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/backend",
"version": "0.9.1",
"version": "0.9.3",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": {
@@ -22,7 +22,7 @@
"prebuild": "rm -rf ./dist"
},
"dependencies": {
"@automatisch/web": "^0.9.1",
"@automatisch/web": "^0.9.3",
"@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@graphql-tools/graphql-file-loader": "^7.3.4",
@@ -110,7 +110,7 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@automatisch/types": "^0.9.1",
"@automatisch/types": "^0.9.3",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8",
"@types/cors": "^2.8.12",

View File

@@ -4,6 +4,7 @@ import htmlToMarkdown from './transformers/html-to-markdown';
import markdownToHtml from './transformers/markdown-to-html';
import useDefaultValue from './transformers/use-default-value';
import extractEmailAddress from './transformers/extract-email-address';
import extractNumber from './transformers/extract-number';
const transformers = {
capitalize,
@@ -11,6 +12,7 @@ const transformers = {
markdownToHtml,
useDefaultValue,
extractEmailAddress,
extractNumber,
};
export default defineAction({
@@ -32,6 +34,7 @@ export default defineAction({
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
{ label: 'Extract Email Address', value: 'extractEmailAddress' },
{ label: 'Extract Number', value: 'extractNumber' },
],
additionalFields: {
type: 'query',

View File

@@ -0,0 +1,26 @@
import { IGlobalVariable } from '@automatisch/types';
const extractNumber = ($: IGlobalVariable) => {
const input = $.step.parameters.input as string;
// Example numbers that's supported:
// 123
// -123
// 123456
// -123456
// 121,234
// -121,234
// 121.234
// -121.234
// 1,234,567.89
// -1,234,567.89
// 1.234.567,89
// -1.234.567,89
const numberRegexp = /-?((\d{1,3})+\.?,?)+/g;
const numbers = input.match(numberRegexp);
return numbers ? numbers[0] : '';
};
export default extractNumber;

View File

@@ -4,6 +4,7 @@ import htmlToMarkdown from './options/html-to-markdown';
import markdownToHtml from './options/markdown-to-html';
import useDefaultValue from './options/use-default-value';
import extractEmailAddress from './options/extract-email-address';
import extractNumber from './options/extract-number';
const options: IJSONObject = {
capitalize,
@@ -11,6 +12,7 @@ const options: IJSONObject = {
markdownToHtml,
useDefaultValue,
extractEmailAddress,
extractNumber,
};
export default {

View File

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

View File

@@ -4,9 +4,13 @@ import bcrypt from 'bcrypt';
const getInternalId = async (item: IJSONObject): Promise<string> => {
if (item.guid) {
return item.guid.toString();
return typeof item.guid === 'object'
? (item.guid as IJSONObject)['#text'].toString()
: item.guid.toString();
} else if (item.id) {
return item.id.toString();
return typeof item.id === 'object'
? (item.id as IJSONObject)['#text'].toString()
: item.id.toString();
}
return await hashItem(JSON.stringify(item));

View File

@@ -0,0 +1,11 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex('permissions')
.where(knex.raw('conditions::text'), '=', knex.raw("'{}'::text"))
.update('conditions', JSON.stringify([]));
}
export async function down(): Promise<void> {
// void
}

View File

@@ -0,0 +1,15 @@
import axios from '../../helpers/axios-with-proxy';
const NOTIFICATIONS_URL = 'https://notifications.automatisch.io/notifications.json';
const getNotifications = async () => {
try {
const { data: notifications = [] } = await axios.get(NOTIFICATIONS_URL);
return notifications;
} catch (err) {
return [];
}
};
export default getNotifications;

View File

@@ -16,13 +16,14 @@ import getExecutions from './queries/get-executions';
import getFlow from './queries/get-flow';
import getFlows from './queries/get-flows';
import getInvoices from './queries/get-invoices.ee';
import getNotifications from './queries/get-notifications';
import getPaddleInfo from './queries/get-paddle-info.ee';
import getPaymentPlans from './queries/get-payment-plans.ee';
import getPermissionCatalog from './queries/get-permission-catalog.ee';
import getRole from './queries/get-role.ee';
import getRoles from './queries/get-roles.ee';
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
import getSubscriptionStatus from './queries/get-subscription-status.ee';
import getTrialStatus from './queries/get-trial-status.ee';
@@ -51,6 +52,7 @@ const queryResolvers = {
getFlow,
getFlows,
getInvoices,
getNotifications,
getPaddleInfo,
getPaymentPlans,
getPermissionCatalog,

View File

@@ -46,6 +46,7 @@ type Query {
getPermissionCatalog: PermissionCatalog
getRole(id: String!): Role
getRoles: [Role]
getNotifications: [Notification]
getSamlAuthProvider: SamlAuthProvider
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
getSubscriptionStatus: GetSubscriptionStatus
@@ -787,6 +788,13 @@ input UpdateAppAuthClientInput {
active: Boolean
}
type Notification {
name: String
createdAt: String
documentationUrl: String
description: String
}
schema {
query: Query
mutation: Mutation

View File

@@ -1,7 +1,7 @@
import { rule, shield, allow } from 'graphql-shield';
import { allow, rule, shield } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import User from '../models/user';
import appConfig from '../config/app';
import User from '../models/user';
const isAuthenticated = rule()(async (_parent, _args, req) => {
const token = req.headers['authorization'];
@@ -34,15 +34,16 @@ const authentication = shield(
Query: {
'*': isAuthenticated,
getAutomatischInfo: allow,
listSamlAuthProviders: allow,
healthcheck: allow,
getConfig: allow,
getNotifications: allow,
healthcheck: allow,
listSamlAuthProviders: allow,
},
Mutation: {
'*': isAuthenticated,
registerUser: allow,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
},

View File

@@ -1,7 +1,6 @@
// TODO: replace with axios-with-proxy
import axios from 'axios';
import appConfig from '../config/app';
import memoryCache from 'memory-cache';
import appConfig from '../config/app';
import axios from './axios-with-proxy';
const CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/cli",
"version": "0.9.1",
"version": "0.9.3",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"contributors": [
@@ -33,7 +33,7 @@
"version": "oclif readme && git add README.md"
},
"dependencies": {
"@automatisch/backend": "^0.9.1",
"@automatisch/backend": "^0.9.3",
"@oclif/core": "^1",
"@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^2.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/docs",
"version": "0.9.1",
"version": "0.9.3",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"private": true,

View File

@@ -8,7 +8,11 @@ const { LoginPage } = require('./login-page');
exports.test = test.extend({
page: async ({ page }, use) => {
await new LoginPage(page).login();
const loginPage = new LoginPage(page);
await loginPage.login();
await expect(loginPage.loginButton).not.toBeVisible();
await expect(page).toHaveURL('/flows');
await use(page);
},
@@ -29,6 +33,19 @@ exports.test = test.extend({
},
});
exports.publicTest = test.extend({
page: async ({ page }, use) => {
await use(page);
},
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.open();
await use(loginPage);
},
});
expect.extend({
toBeClickableLink: async (locator) => {
await expect(locator).not.toHaveAttribute('aria-disabled', 'true');

View File

@@ -17,13 +17,18 @@ export class LoginPage extends BasePage {
this.loginButton = this.page.getByTestId('login-button');
}
async login() {
async open() {
return await this.page.goto(this.path);
}
async login(
email = process.env.LOGIN_EMAIL,
password = process.env.LOGIN_PASSWORD
) {
await this.page.goto(this.path);
await this.emailTextField.fill(process.env.LOGIN_EMAIL);
await this.passwordTextField.fill(process.env.LOGIN_PASSWORD);
await this.emailTextField.fill(email);
await this.passwordTextField.fill(password);
await this.loginButton.click();
await expect(this.loginButton).not.toBeVisible();
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/e2e-tests",
"version": "0.9.1",
"version": "0.9.3",
"license": "See LICENSE file",
"private": true,
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",

View File

@@ -16,20 +16,18 @@ module.exports = defineConfig({
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
retries: 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Timeout threshold for each test */
timeout: 120000,
timeout: 30000,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI ? 'github' : 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI
? 'https://sandbox.automatisch.io'
: 'http://localhost:3001',
baseURL: process.env.BASE_URL
|| 'http://localhost:3001',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
@@ -39,7 +37,7 @@ module.exports = defineConfig({
expect: {
/* Timeout threshold for each assertion */
timeout: 30000,
timeout: 10000,
},
/* Configure projects for major browsers */

View File

@@ -6,7 +6,8 @@ test.describe('Apps page', () => {
await applicationsPage.drawerLink.click();
});
test('displays applications', async ({ applicationsPage }) => {
// no connected application exists in an empty account
test.skip('displays no applications', async ({ applicationsPage }) => {
await applicationsPage.page.getByTestId('apps-loader').waitFor({
state: 'detached',
});

View File

@@ -0,0 +1,22 @@
// @ts-check
const { publicTest, test, expect } = require('../../fixtures/index');
publicTest.describe('Login page', () => {
publicTest('shows login form', async ({ loginPage }) => {
await loginPage.emailTextField.waitFor({ state: 'attached' });
await loginPage.passwordTextField.waitFor({ state: 'attached' });
await loginPage.loginButton.waitFor({ state: 'attached' });
});
publicTest('lets user login', async ({ loginPage }) => {
await loginPage.login();
await expect(loginPage.page).toHaveURL('/flows');
});
publicTest(`doesn't let un-existing user login`, async ({ loginPage }) => {
await loginPage.login('nonexisting@automatisch.io', 'sample');
await expect(loginPage.page).toHaveURL('/login');
});
});

View File

@@ -1,7 +1,8 @@
// @ts-check
const { test, expect } = require('../../fixtures/index');
test.describe('Executions page', () => {
// no execution data exists in an empty account
test.describe.skip('Executions page', () => {
test.beforeEach(async ({ page, executionsPage }) => {
await page.getByTestId('executions-page-drawer-link').click();
await page.getByTestId('execution-row').first().click();

View File

@@ -6,7 +6,8 @@ test.describe('Executions page', () => {
await page.getByTestId('executions-page-drawer-link').click();
});
test('displays executions', async ({ page, executionsPage }) => {
// no executions exist in an empty account
test.skip('displays executions', async ({ page, executionsPage }) => {
await page.getByTestId('executions-loader').waitFor({
state: 'detached',
});

View File

@@ -1,7 +1,7 @@
// @ts-check
const { test, expect } = require('../../fixtures/index');
test.describe('User interface page', () => {
test.describe.skip('User interface page', () => {
test.beforeEach(async ({ userInterfacePage }) => {
await userInterfacePage.profileMenuButton.click();
await userInterfacePage.adminMenuItem.click();

View File

@@ -448,6 +448,13 @@ type AppAuthClient = {
formattedAuthDefaults: IJSONObject;
};
type Notification = {
name: string;
createdAt: string;
documentationUrl: string;
description: string;
}
declare module 'axios' {
interface AxiosResponse {
httpError?: IJSONObject;

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/types",
"version": "0.9.1",
"version": "0.9.3",
"license": "See LICENSE file",
"description": "Type definitions for automatisch",
"homepage": "https://github.com/automatisch/automatisch",

View File

@@ -2,4 +2,3 @@ PORT=3001
REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql
# HTTPS=true
REACT_APP_BASE_URL=http://localhost:3001
REACT_APP_NOTIFICATIONS_URL=https://notifications.automatisch.io

View File

@@ -1,11 +1,11 @@
{
"name": "@automatisch/web",
"version": "0.9.1",
"version": "0.9.3",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"dependencies": {
"@apollo/client": "^3.6.9",
"@automatisch/types": "^0.9.1",
"@automatisch/types": "^0.9.3",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@emotion/react": "^11.4.1",

View File

@@ -2,7 +2,6 @@ type Config = {
[key: string]: string;
baseUrl: string;
graphqlUrl: string;
notificationsUrl: string;
chatwootBaseUrl: string;
supportEmailAddress: string;
};
@@ -10,7 +9,6 @@ type Config = {
const config: Config = {
baseUrl: process.env.REACT_APP_BASE_URL as string,
graphqlUrl: process.env.REACT_APP_GRAPHQL_URL as string,
notificationsUrl: process.env.REACT_APP_NOTIFICATIONS_URL as string,
chatwootBaseUrl: 'https://app.chatwoot.com',
supportEmailAddress: 'support@automatisch.io',
};

View File

@@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const GET_NOTIFICATIONS = gql`
query GetNotifications {
getNotifications {
name
createdAt
documentationUrl
description
}
}
`;

View File

@@ -1,26 +1,20 @@
import * as React from 'react';
import appConfig from 'config/app';
import { useQuery } from '@apollo/client';
import type { Notification } from '@automatisch/types';
interface INotification {
name: string;
createdAt: string;
documentationUrl: string;
description: string;
import { GET_NOTIFICATIONS } from 'graphql/queries/get-notifications';
type UseNotificationsReturn = {
notifications: Notification[];
loading: boolean;
}
export default function useNotifications(): INotification[] {
const [notifications, setNotifications] = React.useState<INotification[]>([]);
export default function useNotifications(): UseNotificationsReturn {
const { data, loading } = useQuery(GET_NOTIFICATIONS);
React.useEffect(() => {
fetch(`${appConfig.notificationsUrl}/notifications.json`)
.then((response) => response.json())
.then((notifications) => {
if (Array.isArray(notifications) && notifications.length) {
setNotifications(notifications);
}
})
.catch(console.error);
}, []);
const notifications = data?.getNotifications || [];
return notifications;
return {
loading,
notifications,
};
}

View File

@@ -10,7 +10,7 @@ type TVersionInfo = {
};
export default function useVersion(): TVersionInfo {
const notifications = useNotifications();
const { notifications } = useNotifications();
const { data } = useQuery(HEALTHCHECK, { fetchPolicy: 'cache-and-network' });
const version = data?.healthcheck.version;

View File

@@ -17,7 +17,7 @@ interface INotification {
export default function Updates(): React.ReactElement {
const formatMessage = useFormatMessage();
const notifications = useNotifications();
const { notifications } = useNotifications();
return (
<Box sx={{ py: 3 }}>