chore: introduce @automatisch/types

This commit is contained in:
Ali BARIN
2022-03-01 22:56:19 +01:00
committed by Ömer Faruk Aydın
parent bbb6f0b0ff
commit 3391578655
54 changed files with 377 additions and 297 deletions

View File

@@ -15,7 +15,8 @@
"nohoist": [
"**/babel-loader",
"**/webpack",
"**/@automatisch/web"
"**/@automatisch/web",
"**/@automatisch/types"
]
},
"devDependencies": {

View File

@@ -18,7 +18,6 @@
"dependencies": {
"@automatisch/web": "0.1.0",
"@octokit/oauth-methods": "^1.2.6",
"@types/lodash.get": "^4.4.6",
"axios": "0.24.0",
"bcrypt": "^5.0.1",
"cors": "^2.8.5",
@@ -66,11 +65,13 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@automatisch/types": "0.1.0",
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.12",
"@types/crypto-js": "^4.0.2",
"@types/express": "^4.17.13",
"@types/http-errors": "^1.8.1",
"@types/lodash.get": "^4.4.6",
"@types/morgan": "^1.9.3",
"@types/node": "^16.10.2",
"@types/nodemailer": "^6.4.4",

View File

@@ -1,6 +1,6 @@
import type { IField } from '@automatisch/types';
import { URLSearchParams } from 'url';
import axios, { AxiosInstance } from 'axios';
import Field from '../../types/field';
export default class Authentication {
appData: any;
@@ -18,7 +18,7 @@ export default class Authentication {
get oauthRedirectUrl() {
return this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
).value;
}

View File

@@ -1,13 +1,15 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import { google as GoogleApi } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import Field from '../../types/field';
import AppInfo from '../../types/app-info';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: OAuth2Client;
scopes: string[] = [
@@ -17,7 +19,7 @@ export default class Authentication implements AuthenticationInterface {
'profile',
];
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.appData = appData;
this.connectionData = connectionData;
@@ -32,7 +34,7 @@ export default class Authentication implements AuthenticationInterface {
get oauthRedirectUrl() {
return this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
).value;
}

View File

@@ -1,16 +1,18 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import FlickrApi from 'flickr-sdk';
import AppInfo from '../../types/app-info';
import Field from '../../types/field';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: typeof FlickrApi;
oauthClient: typeof FlickrApi;
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.oauthClient = new FlickrApi.OAuth(
connectionData.consumerKey,
connectionData.consumerSecret
@@ -33,7 +35,7 @@ export default class Authentication implements AuthenticationInterface {
async createAuthData() {
const appFields = this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = appFields.value;

View File

@@ -1,16 +1,18 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import {
getWebFlowAuthorizationUrl,
exchangeWebFlowCode,
checkToken,
} from '@octokit/oauth-methods';
import AppInfo from '../../types/app-info';
import Field from '../../types/field';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
scopes: string[] = ['repo'];
client: {
getWebFlowAuthorizationUrl: typeof getWebFlowAuthorizationUrl;
@@ -18,7 +20,7 @@ export default class Authentication implements AuthenticationInterface {
checkToken: typeof checkToken;
};
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
@@ -31,7 +33,7 @@ export default class Authentication implements AuthenticationInterface {
get oauthRedirectUrl(): string {
return this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
).value;
}

View File

@@ -1,14 +1,17 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import { Client } from 'pg';
import AppInfo from '../../types/app-info';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: Client;
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.client = new Client({
host: connectionData.host as string,
port: connectionData.port as number,

View File

@@ -1,13 +1,16 @@
import nodemailer, { Transporter, TransportOptions } from 'nodemailer';
import AppInfo from '../../types/app-info';
import JSONObject from '../../types/interfaces/json-object';
import type {
IAuthentication,
IApp,
IJSONObject,
} from '@automatisch/types';
export default class Authentication {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: Transporter;
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.client = nodemailer.createTransport({
host: connectionData.host,
port: connectionData.port,

View File

@@ -1,14 +1,16 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IJSONObject,
} from '@automatisch/types';
import TwilioApi from 'twilio';
import AppInfo from '../../types/app-info';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: TwilioApi.Twilio;
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.client = TwilioApi(
connectionData.accountSid as string,
connectionData.authToken as string

View File

@@ -1,9 +1,11 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import TwitchApi, { TwitchJsOptions } from 'twitch-js';
import fetchUtil from 'twitch-js/lib/utils/fetch';
import AppInfo from '../../types/app-info';
import Field from '../../types/field';
import JSONObject from '../../types/interfaces/json-object';
type TwitchTokenResponse = {
accessToken: string;
@@ -12,12 +14,12 @@ type TwitchTokenResponse = {
tokenType: string;
};
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: TwitchApi;
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
@@ -36,7 +38,7 @@ export default class Authentication implements AuthenticationInterface {
get oauthRedirectUrl() {
return this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
).value;
}

View File

@@ -1,15 +1,17 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
import AppInfo from '../../types/app-info';
import Field from '../../types/field';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: TwitterApi;
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.appData = appData;
this.connectionData = connectionData;
@@ -25,7 +27,7 @@ export default class Authentication implements AuthenticationInterface {
async createAuthData() {
const appFields = this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = appFields.value;

View File

@@ -1,13 +1,15 @@
import AuthenticationInterface from '../../types/interfaces/authentication-interface';
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import { URLSearchParams } from 'url';
import axios, { AxiosInstance } from 'axios';
import AppInfo from '../../types/app-info';
import Field from '../../types/field';
import JSONObject from '../../types/interfaces/json-object';
export default class Authentication implements AuthenticationInterface {
appData: AppInfo;
connectionData: JSONObject;
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: AxiosInstance = axios.create({
baseURL: 'https://api.typeform.com',
});
@@ -22,14 +24,14 @@ export default class Authentication implements AuthenticationInterface {
'workspaces:read',
];
constructor(appData: AppInfo, connectionData: JSONObject) {
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
}
get oauthRedirectUrl() {
return this.appData.fields.find(
(field: Field) => field.key == 'oAuthRedirectUrl'
(field: IField) => field.key == 'oAuthRedirectUrl'
).value;
}

View File

@@ -1,7 +1,6 @@
import Step from '../../models/step';
import flowType, { flowInputType } from '../types/flow';
import RequestWithCurrentUser from '../../types/express/request-with-current-user';
import { StepType } from '../../types/step';
type Params = {
input: {
@@ -21,7 +20,7 @@ const createFlowResolver = async (
await Step.query().insert({
flowId: flow.id,
type: StepType.Trigger,
type: 'trigger',
position: 1,
appKey,
});

View File

@@ -1,7 +1,6 @@
import { GraphQLNonNull } from 'graphql';
import stepType, { stepInputType } from '../types/step';
import RequestWithCurrentUser from '../../types/express/request-with-current-user';
import { StepType } from '../../types/step';
type Params = {
input: {
@@ -42,7 +41,7 @@ const createStepResolver = async (
const step = await flow.$relatedQuery('steps').insertAndFetch({
key: input.key,
appKey: input.appKey,
type: StepType.Action,
type: 'action',
position: previousStep.position + 1,
parameters: {},
});

View File

@@ -1,11 +1,11 @@
import type { IApp } from '@automatisch/types';
import { GraphQLEnumType } from 'graphql';
import App from '../../models/app';
import appInfoType from '../../types/app-info'
const apps = App.findAll();
const availableAppEnumValues: any = {}
apps.forEach((app: appInfoType) => {
apps.forEach((app: IApp) => {
availableAppEnumValues[app.key] = { value: app.key }
})

View File

@@ -1,11 +1,11 @@
import AppInfoType from '../types/app-info';
import type { IApp } from '@automatisch/types';
import appConfig from '../config/app';
const appInfoConverter = (rawAppData: string) => {
let computedRawData = rawAppData.replace('{BASE_URL}', appConfig.baseUrl);
computedRawData = computedRawData.replace('{WEB_APP_URL}', appConfig.webAppUrl);
const computedJSONData: AppInfoType = JSON.parse(computedRawData)
const computedJSONData: IApp = JSON.parse(computedRawData)
return computedJSONData;
}

View File

@@ -6,10 +6,10 @@ import User from './user';
import appConfig from '../config/app';
class Connection extends Base {
id!: number;
id!: string;
key!: string;
data!: any;
userId!: number;
userId!: string;
verified: boolean;
count: number;

View File

@@ -4,8 +4,8 @@ import Step from './step';
class ExecutionStep extends Base {
id!: string;
executionId!: number;
stepId!: number;
executionId!: string;
stepId!: string;
dataIn!: any;
dataOut!: any;
status: string;
@@ -17,8 +17,8 @@ class ExecutionStep extends Base {
properties: {
id: { type: 'string' },
executionId: { type: 'integer' },
stepId: { type: 'integer' },
executionId: { type: 'string' },
stepId: { type: 'string' },
dataIn: { type: 'object' },
dataOut: { type: 'object' },
status: { type: 'string', enum: ['success', 'failure'] },

View File

@@ -1,11 +1,11 @@
import { ModelOptions, QueryContext, ValidationError } from 'objection';
import { ValidationError } from 'objection';
import Base from './base';
import Step from './step';
class Flow extends Base {
id!: string;
name: string;
userId!: number;
userId!: string;
active: boolean;
steps?: [Step];
@@ -17,7 +17,7 @@ class Flow extends Base {
properties: {
id: { type: 'string' },
name: { type: 'string' },
userId: { type: 'integer' },
userId: { type: 'string' },
active: { type: 'boolean' },
},
};

View File

@@ -2,14 +2,14 @@ import Base from './base';
import Flow from './flow';
import Connection from './connection';
import ExecutionStep from './execution-step';
import { StepType } from '../types/step';
import type { IStep } from '@automatisch/types';
class Step extends Base {
id!: number;
id!: string;
flowId!: string;
key: string;
appKey: string;
type!: StepType;
type!: IStep["type"];
connectionId?: string;
status: string;
position: number;
@@ -25,7 +25,7 @@ class Step extends Base {
required: ['type'],
properties: {
id: { type: 'integer' },
id: { type: 'string' },
flowId: { type: 'string' },
key: { type: ['string', 'null'] },
appKey: { type: ['string', 'null'], minLength: 1, maxLength: 255 },

View File

@@ -6,7 +6,7 @@ import Step from './step';
import bcrypt from 'bcrypt';
class User extends Base {
id!: number;
id!: string;
email!: string;
password!: string;
connections?: [Connection];
@@ -20,7 +20,7 @@ class User extends Base {
required: ['email', 'password'],
properties: {
id: { type: 'integer' },
id: { type: 'string' },
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
password: { type: 'string', minLength: 1, maxLength: 255 },
},

View File

@@ -4,7 +4,6 @@ import Flow from '../models/flow';
import Step from '../models/step';
import Execution from '../models/execution';
import ExecutionStep from '../models/execution-step';
import { StepType } from '../types/step';
type ExecutionSteps = Record<string, ExecutionStep>;
@@ -44,7 +43,7 @@ class Processor {
parameters: rawParameters = {},
id
} = step;
const isTrigger = type === StepType.Trigger;
const isTrigger = type === 'trigger';
const AppClass = (await import(`../apps/${appKey}`)).default;
const computedParameters = Processor.computeParameters(rawParameters, priorExecutionSteps);
const appInstance = new AppClass(appData, connection.data, computedParameters);

View File

@@ -1,10 +1,10 @@
import AuthenticationStepField from '../types/authentication-step-field';
import type { IAuthenticationStepField } from '@automatisch/types';
type AuthenticationStep = {
step: number,
type: string,
name: string,
fields: AuthenticationStepField[];
fields: IAuthenticationStepField[];
}
export default AuthenticationStep;

View File

@@ -1,9 +1,9 @@
import appInfoType from '../../types/app-info';
import type { IApp } from '@automatisch/types';
import JSONObject from './json-object';
export default interface AuthenticationInterface {
appData: appInfoType;
connectionData: JSONObject;
appData: IApp;
connectionData: IJSONObject;
client: unknown;
verifyCredentials(): Promise<JSONObject>;
isStillVerified(): Promise<boolean>;

View File

@@ -9,9 +9,18 @@
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "src/types/*"]
"*": [
"node_modules/*",
"src/types/*"
]
},
"typeRoots": ["node_modules/@types", "./src/types", "./src/apps/*/types"]
"typeRoots": [
"node_modules/@types",
"./src/types",
"./src/apps/*/types"
]
},
"include": ["src/**/*"]
"include": [
"src/**/*"
]
}

11
packages/types/README.md Normal file
View File

@@ -0,0 +1,11 @@
# `@automatisch/types`
> TODO: description
## Usage
```
const types = require('@automatisch/types');
// TODO: DEMONSTRATE API
```

127
packages/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,127 @@
// Type definitions for automatisch
export type IJSONValue = string | number | boolean | JSONObject | JSONArray;
export type IJSONArray = Array<JSONValue>;
export interface IJSONObject {
[x: string]: JSONValue;
}
export interface IConnection<D extends IJSONObject | string> {
id: string;
key: string;
data: D;
userId: string;
verified: boolean;
count: number;
createdAt: string;
}
export interface IExecutionStep {
id: string;
executionId: string;
stepId: string;
dataIn: IJSONObject;
dataOut: IJSONObject;
status: string;
}
export interface IExecution {
id: string;
flowId: string;
testRun: boolean;
executionSteps: IExecutionStep[];
}
export interface IStep {
id: string;
name: string;
flowId: string;
key: string;
appKey: string;
type: 'action' | 'trigger';
connectionId: string;
status: string;
position: number;
parameters: Record<string, unknown>;
connection: IConnection;
flow: IFlow;
executionSteps: IExecutionStep[];
// FIXME: remove this property once execution steps are properly exposed via queries
output: IJSONObject;
}
export interface IFlow {
id: string;
name: string;
userId: string;
active: boolean;
steps: IStep[];
}
export interface IUser {
id: string;
email: string;
password: string;
connections: IConnection[];
flows: IFlow[];
steps: IStep[];
}
export interface IField {
key: string;
label: string;
type: string;
required: boolean;
readOnly: boolean;
value: string;
placeholder: string | null;
description: string;
docUrl: string;
clickToCopy: boolean;
name: string;
variables: boolean;
}
export interface IAuthenticationStepField {
name: string;
value: string | null;
properties: {
name: string;
value: string;
}[];
}
export interface IAuthenticationStep {
step: number;
type: 'mutation' | 'openWithPopup';
name: string;
arguments: IAuthenticationStepField[];
}
export interface IApp {
name: string;
key: string;
iconUrl: string;
docUrl: string;
primaryColor: string;
fields: IField[];
authenticationSteps: IAuthenticationStep[];
reconnectionSteps: IAuthenticationStep[];
connectionCount: number;
triggers: any[];
actions: any[];
connections: IConnection[];
}
export interface IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: unknown;
verifyCredentials(): Promise<JSONObject>;
isStillVerified(): Promise<boolean>;
}
export interface ISubstep {
name: string;
arguments: IField[];
}

View File

@@ -0,0 +1,17 @@
{
"name": "@automatisch/types",
"version": "0.1.0",
"description": "Type definitions for automatisch",
"homepage": "https://github.com/automatisch/automatisch",
"types": "./index.d.ts",
"scripts": {},
"dependencies": {},
"typeScriptVersion": "4.1",
"repository": {
"type": "git",
"url": "git+https://github.com/automatisch/automatisch.git"
},
"bugs": {
"url": "https://github.com/automatisch/automatisch/issues"
}
}

View File

@@ -4,6 +4,7 @@
"description": "> TODO: description",
"dependencies": {
"@apollo/client": "^3.4.15",
"@automatisch/types": "0.1.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.1",

View File

@@ -10,12 +10,12 @@ import useFormatMessage from 'hooks/useFormatMessage';
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
import { processStep } from 'helpers/authenticationSteps';
import InputCreator from 'components/InputCreator';
import type { App } from 'types/app';
import type { IApp, IField } from '@automatisch/types';
import { Form } from './style';
type AddAppConnectionProps = {
onClose: () => void;
application: App;
application: IApp;
connectionId?: string;
};
@@ -73,7 +73,7 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
<DialogContent>
<DialogContentText tabIndex={-1} component="div">
<Form onSubmit={submitHandler}>
{fields?.map(field => (<InputCreator key={field.key} schema={field} />))}
{fields?.map((field: IField) => (<InputCreator key={field.key} schema={field} />))}
<LoadingButton
type="submit"

View File

@@ -16,10 +16,10 @@ import ListItemText from '@mui/material/ListItemText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
import type { IApp } from '@automatisch/types';
import * as URLS from 'config/urls';
import AppIcon from 'components/AppIcon';
import type { App } from 'types/app';
import { GET_APPS } from 'graphql/queries/get-apps';
import useFormatMessage from 'hooks/useFormatMessage';
@@ -68,7 +68,7 @@ export default function AddNewAppConnection(props: AddNewAppConnectionProps): Re
</FormControl>
<List sx={{ pt: 2 }}>
{data?.getApps?.map((app: App) => (
{data?.getApps?.map((app: IApp) => (
<ListItem disablePadding key={app.name}>
<ListItemButton component={Link} to={URLS.APP_ADD_CONNECTION(app.name.toLowerCase())}>
<ListItemIcon sx={{ minWidth: 74 }}>

View File

@@ -2,15 +2,15 @@ import { useQuery } from '@apollo/client';
import { GET_FLOWS } from 'graphql/queries/get-flows';
import AppFlowRow from 'components/AppFlowRow';
import type { Flow } from 'types/flow';
import type { IFlow } from '@automatisch/types';
export default function AppFlows(): React.ReactElement {
const { data } = useQuery(GET_FLOWS);
const appFlows: Flow[] = data?.getFlows || [];
const appFlows: IFlow[] = data?.getFlows || [];
return (
<>
{appFlows.map((appFlow: Flow) => (
{appFlows.map((appFlow: IFlow) => (
<AppFlowRow key={appFlow.id} flow={appFlow} />
))}
</>

View File

@@ -8,11 +8,12 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import useFormatMessage from 'hooks/useFormatMessage';
import AppIcon from 'components/AppIcon';
import * as URLS from 'config/urls';
import type { App } from 'types/app';
import type { IApp } from '@automatisch/types';
import { CardContent, Typography } from './style';
type AppRowProps = {
application: App;
application: IApp;
}
const countTranslation = (value: React.ReactNode) => (

View File

@@ -7,22 +7,21 @@ import ListItem from '@mui/material/ListItem';
import Autocomplete from '@mui/material/Autocomplete';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import type { App, AppConnection } from 'types/app';
import type { Step, Substep } from 'types/step';
import type { IApp, IConnection, IStep, ISubstep, IJSONObject } from '@automatisch/types';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
type ChooseAccountSubstepProps = {
substep: Substep,
substep: ISubstep,
expanded?: boolean;
onExpand: () => void;
onCollapse: () => void;
onChange: ({ step }: { step: Step}) => void;
onChange: ({ step }: { step: IStep }) => void;
onSubmit: () => void;
step: Step;
step: IStep;
};
const optionGenerator = (connection: AppConnection): { label: string; value: string; } => ({
const optionGenerator = (connection: IConnection<IJSONObject>): { label: string; value: string; } => ({
label: connection?.data?.screenName as string ?? 'Unnamed',
value: connection?.id as string,
});
@@ -62,7 +61,7 @@ function ChooseAccountSubstep(props: ChooseAccountSubstepProps): React.ReactElem
// intentionally no dependencies for initial test
}, []);
const connectionOptions = React.useMemo(() => (data?.getApp as App)?.connections?.map((connection) => optionGenerator(connection)) || [], [data]);
const connectionOptions = React.useMemo(() => (data?.getApp as IApp)?.connections?.map((connection) => optionGenerator(connection)) || [], [data]);
const { name } = substep;

View File

@@ -10,26 +10,24 @@ import Autocomplete from '@mui/material/Autocomplete';
import { GET_APPS } from 'graphql/queries/get-apps';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import type { App } from 'types/app';
import type { Step, Substep } from 'types/step';
import { StepType } from 'types/step';
import type { IApp, IStep, ISubstep } from '@automatisch/types';
type ChooseAppAndEventSubstepProps = {
substep: Substep,
substep: ISubstep,
expanded?: boolean;
onExpand: () => void;
onCollapse: () => void;
onChange: ({ step }: { step: Step}) => void;
onChange: ({ step }: { step: IStep }) => void;
onSubmit: () => void;
step: Step;
step: IStep;
};
const optionGenerator = (app: Record<string, unknown>): { label: string; value: string; } => ({
const optionGenerator = (app: IApp): { label: string; value: string; } => ({
label: app.name as string,
value: app.key as string,
});
const getOption = (options: Record<string, unknown>[], appKey: unknown) => options.find(app => app.value === appKey as string) || null;
const getOption = (options: Record<string, unknown>[], appKey: IApp["key"]) => options.find(option => option.value === appKey as string) || null;
function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.ReactElement {
const {
@@ -42,11 +40,11 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R
onChange,
} = props;
const isTrigger = step.type === StepType.Trigger;
const isTrigger = step.type === 'trigger';
const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }});
const apps: App[] = data?.getApps;
const app = apps?.find((currentApp: App) => currentApp.key === step.appKey);
const apps: IApp[] = data?.getApps;
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
const appOptions = React.useMemo(() => apps?.map((app) => optionGenerator(app)), [apps]);
const actionsOrTriggers = isTrigger ? app?.triggers : app?.actions;
@@ -88,7 +86,7 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R
onChange({
step: {
...step,
key: null,
key: '',
appKey,
},
});

View File

@@ -8,10 +8,10 @@ import { GET_FLOW } from 'graphql/queries/get-flow';
import { CREATE_STEP } from 'graphql/mutations/create-step';
import { UPDATE_STEP } from 'graphql/mutations/update-step';
import FlowStep from 'components/FlowStep';
import type { Flow } from 'types/flow';
import type { IFlow } from '@automatisch/types';
type EditorProps = {
flow: Flow;
flow: IFlow;
};
function updateHandlerFactory(flowId: string, previousStepId: string) {

View File

@@ -14,7 +14,7 @@ import Editor from 'components/Editor';
import useFormatMessage from 'hooks/useFormatMessage';
import { UPDATE_FLOW } from 'graphql/mutations/update-flow';
import { GET_FLOW } from 'graphql/queries/get-flow';
import type { Flow } from 'types/flow';
import type { IFlow } from '@automatisch/types';
import * as URLS from 'config/urls';
export default function EditorLayout(): React.ReactElement {
@@ -22,7 +22,7 @@ export default function EditorLayout(): React.ReactElement {
const formatMessage = useFormatMessage();
const [updateFlow] = useMutation(UPDATE_FLOW);
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId }});
const flow: Flow = data?.getFlow;
const flow: IFlow = data?.getFlow;
const onFlowNameUpdate = React.useCallback(async (name: string) => {
await updateFlow({

View File

@@ -5,12 +5,12 @@ import Box from '@mui/material/Box';
import CardActionArea from '@mui/material/CardActionArea';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import type { Flow } from 'types/flow';
import type { IFlow } from '@automatisch/types';
import * as URLS from 'config/urls';
import { CardContent, Typography } from './style';
type FlowRowProps = {
flow: Flow;
flow: IFlow;
}
export default function FlowRow(props: FlowRowProps): React.ReactElement {

View File

@@ -10,6 +10,7 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import IconButton from '@mui/material/IconButton';
import ErrorIcon from '@mui/icons-material/Error';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import type { IApp, IField, IStep } from '@automatisch/types';
import { StepExecutionsProvider } from 'contexts/StepExecutions';
import TestSubstep from 'components/TestSubstep';
@@ -22,18 +23,15 @@ import AppIcon from 'components/AppIcon';
import { GET_APPS } from 'graphql/queries/get-apps';
import { GET_STEP_WITH_TEST_EXECUTIONS } from 'graphql/queries/get-step-with-test-executions';
import useFormatMessage from 'hooks/useFormatMessage';
import type { App, AppFields } from 'types/app';
import type { Step } from 'types/step';
import { StepType } from 'types/step';
import { AppIconWrapper, AppIconStatusIconWrapper, Content, Header, Wrapper } from './style';
type FlowStepProps = {
collapsed?: boolean;
step: Step;
step: IStep;
index?: number;
onOpen?: () => void;
onClose?: () => void;
onChange: (step: Step) => void;
onChange: (step: IStep) => void;
}
const validIcon = <CheckCircleIcon color="success" />;
@@ -42,9 +40,9 @@ const errorIcon = <ErrorIcon color="error" />;
export default function FlowStep(props: FlowStepProps): React.ReactElement | null {
const { collapsed, index, onChange } = props;
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
const step: Step = props.step;
const step: IStep = props.step;
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const isTrigger = step.type === StepType.Trigger;
const isTrigger = step.type === 'trigger';
const formatMessage = useFormatMessage();
const [currentSubstep, setCurrentSubstep] = React.useState<number | null>(2);
const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }});
@@ -70,13 +68,13 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
}
}, [collapsed, stepWithTestExecutionsCalled, getStepWithTestExecutions, step.id, isTrigger]);
const apps: App[] = data?.getApps;
const app = apps?.find((currentApp: App) => currentApp.key === step.appKey);
const apps: IApp[] = data?.getApps;
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
const actionsOrTriggers = isTrigger ? app?.triggers : app?.actions;
const substeps = React.useMemo(() => actionsOrTriggers?.find(({ key }) => key === step.key)?.subSteps || [], [actionsOrTriggers, step?.key]);
const handleChange = React.useCallback(({ step }: { step: Step }) => {
const handleChange = React.useCallback(({ step }: { step: IStep }) => {
onChange(step);
}, [])
@@ -85,7 +83,7 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
}, []);
const handleSubmit = (val: any) => {
handleChange({ step: val as Step });
handleChange({ step: val as IStep });
}
if (!apps) return null;
@@ -143,7 +141,7 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
<Collapse in={!collapsed} unmountOnExit>
<Content>
<List>
<StepExecutionsProvider value={stepWithTestExecutionsData?.getStepWithTestExecutions as Step[]}>
<StepExecutionsProvider value={stepWithTestExecutionsData?.getStepWithTestExecutions as IStep[]}>
<Form defaultValues={step} onSubmit={handleSubmit}>
<ChooseAppAndEventSubstep
expanded={currentSubstep === 0}
@@ -155,7 +153,7 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
step={step}
/>
{substeps?.length > 0 && substeps.map((substep: { name: string, key: string, arguments: AppFields[] }, index: number) => (
{substeps?.length > 0 && substeps.map((substep: { name: string, key: string, arguments: IField[] }, index: number) => (
<React.Fragment key={`${substep?.name}-${index}`}>
{substep.key === 'chooseAccount' && (
<ChooseAccountSubstep

View File

@@ -6,23 +6,22 @@ import Button from '@mui/material/Button';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import InputCreator from 'components/InputCreator';
import type { Step, Substep } from 'types/step';
import type { AppFields } from 'types/app';
import type { IField, IStep, ISubstep } from '@automatisch/types';
type FlowSubstepProps = {
substep: Substep,
substep: ISubstep,
expanded?: boolean;
onExpand: () => void;
onCollapse: () => void;
onChange: ({ step }: { step: Step }) => void;
onChange: ({ step }: { step: IStep }) => void;
onSubmit: () => void;
step: Step;
step: IStep;
};
const validateSubstep = (substep: Substep, step: Step) => {
const validateSubstep = (substep: ISubstep, step: IStep) => {
if (!substep) return true;
const args: AppFields[] = substep.arguments || [];
const args: IField[] = substep.arguments || [];
return args.every(arg => {
if (arg.required === false) { return true; }
@@ -50,7 +49,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
} = substep;
const formContext = useFormContext();
const [validationStatus, setValidationStatus] = React.useState<boolean | null>(validateSubstep(substep, formContext.getValues() as Step));
const [validationStatus, setValidationStatus] = React.useState<boolean | null>(validateSubstep(substep, formContext.getValues() as IStep));
const handleChangeOnBlur = React.useCallback((key: string) => {
@@ -73,7 +72,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
React.useEffect(() => {
function validate (step: unknown) {
const validationResult = validateSubstep(substep, step as Step);
const validationResult = validateSubstep(substep, step as IStep);
setValidationStatus(validationResult);
};
const subscription = formContext.watch(validate);

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { useFormContext } from 'react-hook-form';
import type { AppFields } from 'types/app';
import type { IField } from '@automatisch/types';
import PowerInput from 'components/PowerInput';
import TextField from 'components/TextField';
@@ -8,7 +8,7 @@ import TextField from 'components/TextField';
type InputCreatorProps = {
onChange?: React.ChangeEventHandler;
onBlur?: React.FocusEventHandler;
schema: AppFields;
schema: IField;
namePrefix?: string;
};

View File

@@ -10,7 +10,7 @@ import Collapse from '@mui/material/Collapse';
import Typography from '@mui/material/Typography';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { Step } from 'types/step';
import type { IStep } from '@automatisch/types';
const ListItemText = styled(MuiListItemText)``;
@@ -61,7 +61,7 @@ const Suggestions = (props: SuggestionsProps) => {
<List
disablePadding
>
{data.map((option: Step, index: number) => (
{data.map((option: IStep, index: number) => (
<>
<ListItemButton
divider

View File

@@ -1,4 +1,4 @@
import { Step } from 'types/step';
import type { IStep } from '@automatisch/types';
const joinBy = (delimiter = '.', ...args: string[]) => args.filter(Boolean).join(delimiter);
@@ -32,10 +32,10 @@ const process = (data: any, parentKey?: any, index?: number): any[] => {
});
};
export const processStepWithExecutions = (steps: Step[]): any[] => {
export const processStepWithExecutions = (steps: IStep[]): any[] => {
if (!steps) return [];
return steps.map((step: Step, index: number) => ({
return steps.map((step: IStep, index: number) => ({
id: step.id,
// TODO: replace with step.name once introduced
name: `${index + 1}. ${step.appKey}`,

View File

@@ -6,17 +6,16 @@ import Button from '@mui/material/Button';
import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import type { Step, Substep } from 'types/step';
import type { AppFields } from 'types/app';
import type { IStep, ISubstep } from '@automatisch/types';
type TestSubstepProps = {
substep: Substep,
substep: ISubstep,
expanded?: boolean;
onExpand: () => void;
onCollapse: () => void;
onChange?: ({ step }: { step: Step }) => void;
onChange?: ({ step }: { step: IStep }) => void;
onSubmit?: () => void;
step: Step;
step: IStep;
};
function TestSubstep(props: TestSubstepProps): React.ReactElement {

View File

@@ -1,11 +1,11 @@
import * as React from 'react';
import { Step } from 'types/step';
import type { IStep } from '@automatisch/types';
export const StepExecutionsContext = React.createContext<Step[]>([]);
export const StepExecutionsContext = React.createContext<IStep[]>([]);
type StepExecutionsProviderProps = {
children: React.ReactNode;
value: Step[];
value: IStep[];
}
export const StepExecutionsProvider = (props: StepExecutionsProviderProps): React.ReactElement => {

View File

@@ -8,6 +8,7 @@ export const CREATE_STEP = gql`
key
appKey
parameters
status
connection {
id
}

View File

@@ -1,3 +1,4 @@
import type { IAuthenticationStep } from '@automatisch/types';
import apolloClient from 'graphql/client';
import MUTATIONS from 'graphql/mutations';
import appConfig from 'config/app';
@@ -7,14 +8,7 @@ enum AuthenticationSteps {
OpenWithPopup = 'openWithPopup',
}
type Step = {
name: string;
variables: Record<string, unknown>;
process: (step: any, variables: Record<string, unknown>) => Promise<any>;
type: AuthenticationSteps.Mutation | AuthenticationSteps.OpenWithPopup;
};
const processMutation = async (step: Step, variables: Record<string, unknown>) => {
const processMutation = async (step: IAuthenticationStep, variables: Record<string, unknown>) => {
const mutation = MUTATIONS[step.name];
const mutationResponse = await apolloClient.mutate({ mutation, variables });
const responseData = mutationResponse.data[step.name];
@@ -38,7 +32,7 @@ function getObjectOfEntries(iterator: any) {
return result;
}
const processOpenWithPopup = (step: Step, variables: Record<string, string>) => {
const processOpenWithPopup = (step: IAuthenticationStep, variables: Record<string, string>) => {
return new Promise((resolve) => {
const windowFeatures = 'toolbar=no, titlebar=no, menubar=no, width=500, height=700, top=100, left=100';
const url = variables.url;
@@ -62,7 +56,7 @@ const processOpenWithPopup = (step: Step, variables: Record<string, string>) =>
});
};
export const processStep = async (step: Step, variables: Record<string, string>): Promise<any> => {
export const processStep = async (step: IAuthenticationStep, variables: Record<string, string>): Promise<any> => {
if (step.type === AuthenticationSteps.Mutation) {
return processMutation(step, variables);
} else if (step.type === AuthenticationSteps.OpenWithPopup) {

View File

@@ -1,4 +1,5 @@
import template from 'lodash.template';
import type { IAuthenticationStepField, IJSONObject } from '@automatisch/types';
const interpolate = /{([\s\S]+?)}/g;
@@ -6,18 +7,9 @@ type Variables = {
[key: string]: any
}
type VariableSchema = {
properties: VariableSchema[];
name: string;
type: 'string' | 'integer';
value: string;
}
type IVariable = Omit<IAuthenticationStepField, "properties"> & Partial<Pick<IAuthenticationStepField, "properties">>;
type AggregatedData = {
[key: string]: Record<string, unknown> | string;
}
const computeAuthStepVariables = (variableSchema: VariableSchema[], aggregatedData: AggregatedData): Variables => {
const computeAuthStepVariables = (variableSchema: IVariable[], aggregatedData: IJSONObject): IJSONObject => {
const variables: Variables = {};
for (const variable of variableSchema) {
@@ -27,11 +19,9 @@ const computeAuthStepVariables = (variableSchema: VariableSchema[], aggregatedDa
continue;
}
if (variable.value) {
const computedVariable = template(variable.value, { interpolate })(aggregatedData);
if (variable.type === 'integer') {
variables[variable.name] = parseInt(computedVariable, 10);
} else {
variables[variable.name] = computedVariable;
}
}

View File

@@ -5,6 +5,7 @@ import { useQuery } from '@apollo/client';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import AddIcon from '@mui/icons-material/Add';
import type { IApp } from '@automatisch/types';
import ConditionalIconButton from 'components/ConditionalIconButton';
import Container from 'components/Container';
@@ -15,7 +16,6 @@ import SearchInput from 'components/SearchInput';
import useFormatMessage from 'hooks/useFormatMessage'
import { GET_CONNECTED_APPS } from 'graphql/queries/get-connected-apps';
import * as URLS from 'config/urls';
import type { App } from 'types/app';
export default function Applications(): React.ReactElement {
const navigate = useNavigate();
@@ -69,7 +69,7 @@ export default function Applications(): React.ReactElement {
</Grid>
</Grid>
{data?.getConnectedApps?.map((app: App) => (
{data?.getConnectedApps?.map((app: IApp) => (
<AppRow key={app.name} application={app} />
))}

View File

@@ -5,6 +5,7 @@ import { useQuery } from '@apollo/client';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import AddIcon from '@mui/icons-material/Add';
import type { IFlow } from '@automatisch/types';
import FlowRow from 'components/FlowRow';
import ConditionalIconButton from 'components/ConditionalIconButton';
@@ -14,14 +15,13 @@ import SearchInput from 'components/SearchInput';
import useFormatMessage from 'hooks/useFormatMessage'
import { GET_FLOWS } from 'graphql/queries/get-flows';
import * as URLS from 'config/urls';
import type { Flow } from 'types/flow';
export default function Flows(): React.ReactElement {
const formatMessage = useFormatMessage();
const [flowName, setFlowName] = React.useState('');
const { data } = useQuery(GET_FLOWS);
const flows: Flow[] = data?.getFlows?.filter((flow: Flow) => flow.name?.toLowerCase().includes(flowName.toLowerCase()));
const flows: IFlow[] = data?.getFlows?.filter((flow: IFlow) => flow.name?.toLowerCase().includes(flowName.toLowerCase()));
const onSearchChange = React.useCallback((event) => {
setFlowName(event.target.value);

View File

@@ -1,40 +0,0 @@
type AppFields = {
key: string;
name: string;
label: string;
type: string;
required: boolean;
readOnly: boolean;
value: string;
description: string;
docUrl: string;
clickToCopy: boolean;
variables?: boolean;
};
type AppConnection = {
id: string;
key: string;
verified: boolean;
createdAt: string;
data: {
[key: string]: any;
};
};
type App = {
key: string;
name: string;
connectionCount: number;
iconUrl: string;
docUrl: string;
primaryColor: string;
fields: AppFields[];
authenticationSteps: any[];
reconnectionSteps: any[];
triggers: any[];
actions: any[];
connections: AppConnection[];
};
export type { App, AppFields, AppConnection };

View File

@@ -1,13 +1,3 @@
type ConnectionData = {
screenName: string;
}
import type { IConnection, IJSONObject } from '@automatisch/types';
type Connection = {
id: string;
key: string;
data: ConnectionData;
verified?: boolean;
createdAt: string;
};
export type { Connection, ConnectionData };
export type Connection = IConnection<IJSONObject>;

View File

@@ -1,8 +0,0 @@
import type { Step } from './step';
export type Flow = {
id: string;
name: string;
steps: Step[];
active: boolean;
};

View File

@@ -1,25 +0,0 @@
import type { AppFields } from './app';
import type { Connection } from './connection';
export enum StepType {
Trigger = 'trigger',
Action = 'action',
}
export type Step = {
id: string;
key: string | null;
name: string;
appKey: string | null;
type: StepType;
previousStepId: string | null;
parameters: Record<string, unknown>;
connection: Pick<Connection, 'id' | 'verified'>;
status: 'completed' | 'incomplete';
output: Record<string, unknown>;
};
export type Substep = {
name: string;
arguments: AppFields[];
};