diff --git a/.dockerignore b/.dockerignore index f7dd160e..d9e386e7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,6 +28,7 @@ TODO.md apps/api-gateway-e2e apps/web-e2e +.cache .git *.log *.md diff --git a/.eslintignore b/.eslintignore index 3c3629e6..6bc41343 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules +.cache diff --git a/.gitignore b/.gitignore index 80eaf90a..6c3d6031 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,6 @@ Thumbs.db docs/examples/** +.cache .nx/cache .verdacio/prod/htpasswd diff --git a/.verdaccio/prod/docker-compose.verdacio.yml b/.verdaccio/prod/docker-compose.verdacio.yml index 3bff0d4f..08847ac0 100644 --- a/.verdaccio/prod/docker-compose.verdacio.yml +++ b/.verdaccio/prod/docker-compose.verdacio.yml @@ -1,5 +1,3 @@ -version: '3.9' - services: verdaccio: image: verdaccio/verdaccio diff --git a/apps/web/app/app/flows/editor/[[...id]]/page.tsx b/apps/web/app/app/flows/editor/[[...id]]/page.tsx index 8695ab1f..7b468117 100644 --- a/apps/web/app/app/flows/editor/[[...id]]/page.tsx +++ b/apps/web/app/app/flows/editor/[[...id]]/page.tsx @@ -1,20 +1,184 @@ 'use client' -import { Heading } from '../../../../(landing)/components/Heading' -import { HeroImage } from '../../../../(landing)/components/HeroImage' -import { PageContainer } from '../../../components/PageContainer' +import { ConnectorMetadataSummary } from '@linkerry/connectors-framework' +import { + ActionType, + CustomError, + ErrorCode, + FlowVersion, + TriggerType, + assertNotNullOrUndefined, + isCustomHttpExceptionAxios, + isQuotaErrorCode, +} from '@linkerry/shared' +import { useToast } from '@linkerry/ui-components/client' +import { useRouter } from 'next/navigation' +import { useCallback, useEffect } from 'react' +import { Edge } from 'reactflow' +import { useClientQuery } from '../../../../../libs/react-query' +import { useSubscriptions } from '../../../../../modules/billing/subscriptions/useSubscriptions' +import { useReachLimitDialog } from '../../../../../modules/billing/useReachLimitDialog' +import { CustomNode, Editor, useEditor } from '../../../../../modules/editor' +import { + actionNodeFactory, + nodeConfigs, + selectTriggerNodeFactory, + testFlowNodeFactory, + triggerNodeFactory, +} from '../../../../../modules/editor/common/nodeFactory' +import { defaultEdgeFactory } from '../../../../../modules/editor/edges/edgesFactory' +import { connectorsMetadataQueryConfig } from '../../../../../modules/flows/connectors/api/query-configs' +import { FlowApi } from '../../../../../modules/flows/flows/api' +import { ErrorInfo, Spinner } from '../../../../../shared/components' + +const renderFlow = (flowVersion: FlowVersion, connectorsMetadata: ConnectorMetadataSummary[]) => { + const nodes: CustomNode[] = [ + testFlowNodeFactory({ + position: { + x: nodeConfigs.BaseNode.width / 2 - nodeConfigs.TestFlowNode.width / 2, + y: -60, + }, + }), + ] + const edges: Edge[] = [] + + let parentNode: Pick & { + height: number + width: number + } = { + id: '', + position: { + x: 0, + y: 0, + }, + height: nodeConfigs.BaseNode.height, + width: nodeConfigs.BaseNode.width, + } + + for (const trigger of flowVersion.triggers) { + switch (trigger.type) { + case TriggerType.EMPTY: + nodes.push(selectTriggerNodeFactory({ trigger })) + parentNode = nodes[nodes.length - 1] + break + case TriggerType.CONNECTOR: { + const connectorMetadata = connectorsMetadata.find((metadata) => trigger.settings.connectorName === metadata.name) + assertNotNullOrUndefined(connectorMetadata, 'connectorMetadata') + nodes.push(triggerNodeFactory({ trigger, connectorMetadata })) + parentNode = nodes[nodes.length - 1] + break + } + default: + throw new Error(`Can not find trigger type: ${trigger}`) + } + } + + assertNotNullOrUndefined(parentNode, 'parentNode') + + for (const action of flowVersion.actions) { + switch (action.type) { + case ActionType.BRANCH: + // case ActionType.LOOP_ON_ITEMS: + // case ActionType.MERGE_BRANCH: + throw new CustomError(`Unsuported action type`, ErrorCode.INVALID_TYPE, { + action, + }) + case ActionType.CONNECTOR: { + const connectorMetadata = connectorsMetadata.find((metadata) => action.settings.connectorName === metadata.name) + assertNotNullOrUndefined(connectorMetadata, 'connectorMetadata') + + const newNode = actionNodeFactory({ + action, + connectorMetadata, + position: { + x: parentNode.position.x, + y: parentNode.position.y + parentNode.height + nodeConfigs.gap.y, + }, + }) + nodes.push(newNode) + + edges.push( + defaultEdgeFactory({ + sourceNodeId: parentNode.id, + targetNodeId: newNode.id, + }), + ) + + parentNode = newNode + } + } + } + + return { nodes, edges } +} export default function Page({ params }: { params: { id: string } }) { + const { data: connectorsMetadata, status } = useClientQuery(connectorsMetadataQueryConfig.getSummaryMany()) + const { loadFlow, setFlow, setEdges, setNodes } = useEditor() + const { toast } = useToast() + const { showDialogBasedOnErrorCode } = useReachLimitDialog() + const { currentPlan, subscriptionsStatus, subscriptionsError } = useSubscriptions() + const { push } = useRouter() + + const initEditor = useCallback(async () => { + if (!connectorsMetadata?.length) throw new Error('Can not retrive connectors metadata') + const id = params?.id?.[0] + + // Fetch and render if exists + if (id) { + const flow = await loadFlow(id) + if (flow) { + const { nodes, edges } = renderFlow(flow.version, connectorsMetadata) + setNodes(nodes) + setEdges(edges) + return + } + } + // Create new flow + try { + const { data } = await FlowApi.create() + setFlow(data) + + push(`/app/flows/editor/${data._id}`) + + setNodes([selectTriggerNodeFactory({ trigger: data.version.triggers[0] })]) + setEdges([]) + } catch (error) { + let errorDescription = 'We can not retrive error message. Please inform our Team' + + if (isCustomHttpExceptionAxios(error)) { + if (isQuotaErrorCode(error.response.data.code)) return showDialogBasedOnErrorCode(error.response.data.code) + else errorDescription = error.response.data.message + } + + toast({ + title: 'Can not create new Flow', + description: errorDescription, + variant: 'destructive', + }) + } + }, [params.id, connectorsMetadata]) + + useEffect(() => { + if (status !== 'success') return + ;(async () => { + await initEditor() + })() + }, [status]) + + if (subscriptionsStatus === 'pending') return + if (subscriptionsStatus === 'error') return + if (!currentPlan) return + return ( - - Will be avaible in 1-2 weeks 🎉 -
- -
-
- + ) } diff --git a/apps/web/app/app/flows/editor/[[...id]]/pagePROD.tsx b/apps/web/app/app/flows/editor/[[...id]]/pagePROD.tsx deleted file mode 100644 index ca1c4496..00000000 --- a/apps/web/app/app/flows/editor/[[...id]]/pagePROD.tsx +++ /dev/null @@ -1,184 +0,0 @@ -'use client' - -import { - ActionType, - CustomError, - ErrorCode, - FlowVersion, - TriggerType, - assertNotNullOrUndefined, - isCustomHttpExceptionAxios, - isQuotaErrorCode, -} from '@linkerry/shared' -import { useToast } from '@linkerry/ui-components/client' -import { ConnectorMetadataSummary } from '@linkerry/connectors-framework' -import { useRouter } from 'next/navigation' -import { useCallback, useEffect } from 'react' -import { Edge } from 'reactflow' -import { useClientQuery } from '../../../../../libs/react-query' -import { useSubscriptions } from '../../../../../modules/billing/subscriptions/useSubscriptions' -import { useReachLimitDialog } from '../../../../../modules/billing/useReachLimitDialog' -import { CustomNode, Editor, useEditor } from '../../../../../modules/editor' -import { - actionNodeFactory, - nodeConfigs, - selectTriggerNodeFactory, - testFlowNodeFactory, - triggerNodeFactory, -} from '../../../../../modules/editor/common/nodeFactory' -import { defaultEdgeFactory } from '../../../../../modules/editor/edges/edgesFactory' -import { connectorsMetadataQueryConfig } from '../../../../../modules/flows/connectors/api/query-configs' -import { FlowApi } from '../../../../../modules/flows/flows/api' -import { ErrorInfo, Spinner } from '../../../../../shared/components' - -const renderFlow = (flowVersion: FlowVersion, connectorsMetadata: ConnectorMetadataSummary[]) => { - const nodes: CustomNode[] = [ - testFlowNodeFactory({ - position: { - x: nodeConfigs.BaseNode.width / 2 - nodeConfigs.TestFlowNode.width / 2, - y: -60, - }, - }), - ] - const edges: Edge[] = [] - - let parentNode: Pick & { - height: number - width: number - } = { - id: '', - position: { - x: 0, - y: 0, - }, - height: nodeConfigs.BaseNode.height, - width: nodeConfigs.BaseNode.width, - } - - for (const trigger of flowVersion.triggers) { - switch (trigger.type) { - case TriggerType.EMPTY: - nodes.push(selectTriggerNodeFactory({ trigger })) - parentNode = nodes[nodes.length - 1] - break - case TriggerType.CONNECTOR: { - const connectorMetadata = connectorsMetadata.find((metadata) => trigger.settings.connectorName === metadata.name) - assertNotNullOrUndefined(connectorMetadata, 'connectorMetadata') - nodes.push(triggerNodeFactory({ trigger, connectorMetadata })) - parentNode = nodes[nodes.length - 1] - break - } - default: - throw new Error(`Can not find trigger type: ${trigger}`) - } - } - - assertNotNullOrUndefined(parentNode, 'parentNode') - - for (const action of flowVersion.actions) { - switch (action.type) { - case ActionType.BRANCH: - // case ActionType.LOOP_ON_ITEMS: - // case ActionType.MERGE_BRANCH: - throw new CustomError(`Unsuported action type`, ErrorCode.INVALID_TYPE, { - action, - }) - case ActionType.CONNECTOR: { - const connectorMetadata = connectorsMetadata.find((metadata) => action.settings.connectorName === metadata.name) - assertNotNullOrUndefined(connectorMetadata, 'connectorMetadata') - - const newNode = actionNodeFactory({ - action, - connectorMetadata, - position: { - x: parentNode.position.x, - y: parentNode.position.y + parentNode.height + nodeConfigs.gap.y, - }, - }) - nodes.push(newNode) - - edges.push( - defaultEdgeFactory({ - sourceNodeId: parentNode.id, - targetNodeId: newNode.id, - }), - ) - - parentNode = newNode - } - } - } - - return { nodes, edges } -} - -export default function Page({ params }: { params: { id: string } }) { - const { data: connectorsMetadata, status } = useClientQuery(connectorsMetadataQueryConfig.getSummaryMany()) - const { loadFlow, setFlow, setEdges, setNodes } = useEditor() - const { toast } = useToast() - const { showDialogBasedOnErrorCode } = useReachLimitDialog() - const { currentPlan, subscriptionsStatus, subscriptionsError } = useSubscriptions() - const { push } = useRouter() - - const initEditor = useCallback(async () => { - if (!connectorsMetadata?.length) throw new Error('Can not retrive connectors metadata') - const id = params?.id?.[0] - - // Fetch and render if exists - if (id) { - const flow = await loadFlow(id) - if (flow) { - const { nodes, edges } = renderFlow(flow.version, connectorsMetadata) - setNodes(nodes) - setEdges(edges) - return - } - } - // Create new flow - try { - const { data } = await FlowApi.create() - setFlow(data) - - push(`/app/flows/editor/${data._id}`) - - setNodes([selectTriggerNodeFactory({ trigger: data.version.triggers[0] })]) - setEdges([]) - } catch (error) { - let errorDescription = 'We can not retrive error message. Please inform our Team' - - if (isCustomHttpExceptionAxios(error)) { - if (isQuotaErrorCode(error.response.data.code)) return showDialogBasedOnErrorCode(error.response.data.code) - else errorDescription = error.response.data.message - } - - toast({ - title: 'Can not create new Flow', - description: errorDescription, - variant: 'destructive', - }) - } - }, [params.id, connectorsMetadata]) - - useEffect(() => { - if (status !== 'success') return - ;(async () => { - await initEditor() - })() - }, [status]) - - if (subscriptionsStatus === 'pending') return - if (subscriptionsStatus === 'error') return - if (!currentPlan) return - - return ( - - ) -} diff --git a/apps/web/app/app/flows/editor/[[...id]]/pageTEMP.tsx b/apps/web/app/app/flows/editor/[[...id]]/pageTEMP.tsx new file mode 100644 index 00000000..8695ab1f --- /dev/null +++ b/apps/web/app/app/flows/editor/[[...id]]/pageTEMP.tsx @@ -0,0 +1,20 @@ +'use client' + +import { Heading } from '../../../../(landing)/components/Heading' +import { HeroImage } from '../../../../(landing)/components/HeroImage' +import { PageContainer } from '../../../components/PageContainer' + +export default function Page({ params }: { params: { id: string } }) { + return ( + + Will be avaible in 1-2 weeks 🎉 +
+ +
+
+ + ) +} diff --git a/apps/web/libs/api-client.ts b/apps/web/libs/api-client.ts index 19728a91..4a4e8c7a 100644 --- a/apps/web/libs/api-client.ts +++ b/apps/web/libs/api-client.ts @@ -4,8 +4,7 @@ import { redirect } from 'next/navigation' // import { store } from '../../features/common/store'; // import fingerprint from '../../utils/fingerprint' -// export const API_URL = process.env.NEXT_PUBLIC_API_HOST -export const API_URL = 'https://api.linkerry.com' +export const API_URL = process.env.NEXT_PUBLIC_API_HOST ?? 'https://api.linkerry.com' const apiClient = axios.create({ withCredentials: true, diff --git a/apps/web/libs/api-server-client.ts b/apps/web/libs/api-server-client.ts index f7bba211..8b06a7a8 100644 --- a/apps/web/libs/api-server-client.ts +++ b/apps/web/libs/api-server-client.ts @@ -5,7 +5,7 @@ import { redirect } from 'next/navigation' // import { store } from '../../features/common/store'; // import fingerprint from '../../utils/fingerprint' -export const API_URL = 'https://api.linkerry.com' +export const API_URL = process.env.NEXT_PUBLIC_API_HOST ?? 'https://api.linkerry.com' const apiServerClient = axios.create({ withCredentials: true, diff --git a/libs/connectors/framework/src/lib/processors/processors.ts b/libs/connectors/framework/src/lib/processors/processors.ts index f4de913d..c8874e94 100644 --- a/libs/connectors/framework/src/lib/processors/processors.ts +++ b/libs/connectors/framework/src/lib/processors/processors.ts @@ -17,7 +17,8 @@ export class Processors { } return JSON.parse(value) } catch (error) { - console.error(error) + // if not required, probably it is a empty string + if (property.required) console.error(error) return undefined } } diff --git a/libs/nest-core/src/modules/workers/sandbox/cache/sandbox-cache.ts b/libs/nest-core/src/modules/workers/sandbox/cache/sandbox-cache.ts index bb4f6bb4..c0e45b69 100644 --- a/libs/nest-core/src/modules/workers/sandbox/cache/sandbox-cache.ts +++ b/libs/nest-core/src/modules/workers/sandbox/cache/sandbox-cache.ts @@ -8,7 +8,7 @@ import { connectorManager } from '../../../flows/connectors/connector-manager' export class CachedSandbox { private readonly logger = new Logger(CachedSandbox.name) - private static readonly cachePath = process.env['CACHE_PATH'] || resolve('dist', 'cache') + private static readonly CACHE_PATH = process.env['CACHE_PATH'] || resolve('dist', 'cache') private _state = CachedSandboxState.CREATED private _activeSandboxCount = 0 private _lastUsedAt = dayjs() @@ -16,7 +16,7 @@ export class CachedSandbox { constructor(public readonly key: string) {} path(): string { - return `${CachedSandbox.cachePath}/sandbox/${this.key}` + return `${CachedSandbox.CACHE_PATH}/sandbox/${this.key}` } lastUsedAt(): dayjs.Dayjs { diff --git a/libs/nest-core/src/modules/workers/sandbox/sandboxes/file-sandbox.ts b/libs/nest-core/src/modules/workers/sandbox/sandboxes/file-sandbox.ts index 1858e895..8dbeb2c6 100644 --- a/libs/nest-core/src/modules/workers/sandbox/sandboxes/file-sandbox.ts +++ b/libs/nest-core/src/modules/workers/sandbox/sandboxes/file-sandbox.ts @@ -2,7 +2,7 @@ import { EngineResponseStatus } from '@linkerry/shared' import { Logger } from '@nestjs/common' import { spawn } from 'node:child_process' import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import path from 'node:path' +import path, { resolve } from 'node:path' import { AbstractSandbox, ExecuteSandboxResult, SandboxCtorParams } from './abstract-sandbox' export class FileSandbox extends AbstractSandbox { @@ -62,7 +62,8 @@ export class FileSandbox extends AbstractSandbox { } public override getSandboxFolderPath(): string { - return path.join(__dirname, `../../sandbox/${this.boxId}`) + const systemCache = process.env['CACHE_PATH'] ?? resolve('dist', 'cache') + return path.join(systemCache, 'sandbox', `${this.boxId}`) } protected override async setupCache(): Promise { diff --git a/libs/shared/src/lib/modules/workers/sandbox.ts b/libs/shared/src/lib/modules/workers/sandbox.ts index c1c5b7ff..783fc94b 100644 --- a/libs/shared/src/lib/modules/workers/sandbox.ts +++ b/libs/shared/src/lib/modules/workers/sandbox.ts @@ -42,11 +42,11 @@ const extractFlowCacheKey = ({ flowVersionId }: FlowProvisionCacheInfo): string const extractNoneCacheKey = (_params: NoneProvisionCacheInfo): string => { // return `NONE-apId-${apId()}` - return `NONE-mcId` + return `NONE-linkerryId` } const extractConnectorCacheKey = ({ connectorName, connectorVersion }: ConnectorProvisionCacheInfo): string => { - return `CONNECOR-connectorName-${connectorName}-connectorVersion-${connectorVersion}` + return `CONNECTOR-connectorName-${connectorName}-connectorVersion-${connectorVersion}` } type BaseProvisionCacheInfo = {