diff --git a/frontend/ee b/frontend/ee index 1a14db61e1..eb63fab1fd 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 1a14db61e117145def63bf66e9225e049a2f5818 +Subproject commit eb63fab1fdbd022ad0c7b8028c63a25293cfa4cf diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8d15bf0995..325e956494 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35241,4 +35241,4 @@ } } } -} \ No newline at end of file +} diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index d3cda91e3f..20f54e3f54 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -38,6 +38,7 @@ import { getDataSourcesRoutes, getAuditLogsRoutes, } from '@/modules'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import { shallow } from 'zustand/shallow'; import useStore from '@/AppBuilder/_stores/store'; import { checkIfToolJetCloud } from '@/_helpers/utils'; @@ -278,7 +279,7 @@ class AppComponent extends React.Component { } /> - {window.public_config?.ENABLE_WORKFLOWS_FEATURE === 'true' && ( + {isWorkflowsFeatureEnabled() && ( } > - }> + + } + > diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx index f0a3bbe811..218ffc992d 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourceSelect.jsx @@ -15,6 +15,7 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm import { canCreateDataSource } from '@/_helpers'; import './../queryManager.theme.scss'; import { DATA_SOURCE_TYPE } from '@/_helpers/constants'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) { @@ -40,7 +41,7 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc closePopup(); }; - const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; + const workflowsEnabled = isWorkflowsFeatureEnabled(); const staticDataSources = workflowsEnabled ? staticDatasources : staticDatasources.filter((ds) => ds?.kind !== 'workflows'); diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx index 547aa5452c..e5a9849c35 100644 --- a/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx +++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/Restapi/index.jsx @@ -9,6 +9,7 @@ import { BaseUrl } from './BaseUrl'; import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; import CodeHinter from '@/AppBuilder/CodeEditor'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; +import './styles.css'; class Restapi extends React.Component { constructor(props) { @@ -287,14 +288,15 @@ class Restapi extends React.Component { const { options } = this.state; const dataSourceURL = this.props.selectedDataSource?.options?.url?.value; const queryName = this.props.queryName; + const isWorkflowNode = queryName === 'workflowNode'; const currentValue = { label: options.method?.toUpperCase(), value: options.method }; return ( -
+
{this.props.selectedDataSource?.scope == 'global' &&
}{' '}
-
-
+
+

-
-
+
+
- - -
- )} +   + {appType !== 'workflow' + ? t('blankPage.importApplication', 'Import an app') + : t('blankPage.importWorkflow', 'Import a workflow')} + + + +
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 55d6bd42df..7aedd29910 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -12,7 +12,7 @@ import { } from '@/_services'; import { ConfirmDialog, AppModal, ToolTip } from '@/_components'; import Select from '@/_ui/Select'; -import _, { sample, isEmpty } from 'lodash'; +import _, { sample, isEmpty, capitalize, has } from 'lodash'; import { Folders } from './Folders'; import { BlankPage } from './BlankPage'; import { toast } from 'react-hot-toast'; @@ -48,6 +48,7 @@ import { } from '@/modules/dashboard/components'; import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils'; import EmptyModuleSvg from '../../assets/images/icons/empty-modules.svg'; const { iconList, defaultIcon } = configs; @@ -256,7 +257,11 @@ class HomePageComponent extends React.Component { }; getAppType = () => { - return this.props.appType === 'module' ? 'Module' : this.props.appType === 'workflow' ? 'Workflow' : 'App'; + const { appType } = this.props; + if (appType === 'front-end') return 'App'; + if (appType === 'workflow') return 'Workflow'; + if (appType === 'module') return 'Module'; + return 'app'; }; createApp = async (appName) => { @@ -337,6 +342,66 @@ class HomePageComponent extends React.Component { this.setState({ isExportingApp: true, app: app }); }; + exportAppDirectly = async (app) => { + try { + const fetchVersions = await appsService.getVersions(app.id); + const { versions } = fetchVersions; + + const currentEditingVersion = versions?.filter((version) => version?.isCurrentEditingVersion)[0]; + if (!currentEditingVersion) { + toast.error('Could not find current editing version.', { + position: 'top-center', + }); + return; + } + + // Export all TJDB tables used by default + const fetchTables = await appsService.getTables(app.id); + const { tables: allTables } = fetchTables; + + const versionId = currentEditingVersion.id; + const exportTjDb = true; + const exportTables = allTables; + + const appOpts = { + app: [ + { + id: app.id, + search_params: { version_id: versionId }, + }, + ], + }; + + const requestBody = { + ...appOpts, + ...(exportTjDb && { tooljet_database: exportTables }), + organization_id: app.organization_id, + }; + + const data = await appsService.exportResource(requestBody); + + const appName = app.name.replace(/\s+/g, '-').toLowerCase(); + const fileName = `${appName}-export-${new Date().getTime()}`; + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = fileName + '.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + toast.success('Workflow exported successfully!', { + position: 'top-center', + }); + } catch (error) { + toast.error(`Could not export workflow: ${error?.data?.message || error.message}`, { + position: 'top-center', + }); + } + }; + readAndImport = (event) => { try { const file = event.target.files[0]; @@ -451,7 +516,7 @@ class HomePageComponent extends React.Component { this.setState({ isImportingApp: false }); if (error.statusCode === 409) return false; - toast.error(error?.error || error?.message || 'App import failed'); + toast.error(error?.error || error?.message || `${capitalize(this.getAppType())} import failed`); } }; @@ -483,7 +548,7 @@ class HomePageComponent extends React.Component { }; canViewWorkflow = () => { - return this.canUserPerform(this.state.currentUser, 'view'); + return this.canUserPerform(this.state.currentUser, 'view') && isWorkflowsFeatureEnabled(); }; canUserPerform(user, action, app) { @@ -951,6 +1016,53 @@ class HomePageComponent extends React.Component { importingGitAppOperations: validationMessage, }); }; + + // Helper functions for workflow limit checks + hasWorkflowLimitReached = () => { + const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state; + + const instanceLimitReached = + workflowInstanceLevelLimit.total === 0 || workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total; + const workspaceLimitReached = + workflowWorkspaceLevelLimit.total === 0 || + workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total; + + return instanceLimitReached || workspaceLimitReached; + }; + + hasWorkflowLimitWarning = () => { + const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state; + return this.hasInstanceLimitWarning() || this.hasWorkspaceLimitWarning(); + }; + + hasInstanceLimitWarning = () => { + const { workflowInstanceLevelLimit } = this.state; + const percentage = workflowInstanceLevelLimit.percentage; + + return ( + workflowInstanceLevelLimit.current >= workflowInstanceLevelLimit.total || + (percentage >= 90 && percentage < 100) || + workflowInstanceLevelLimit.current === workflowInstanceLevelLimit.total - 1 + ); + }; + + hasWorkspaceLimitWarning = () => { + const { workflowWorkspaceLevelLimit } = this.state; + const percentage = workflowWorkspaceLevelLimit.percentage; + + return ( + workflowWorkspaceLevelLimit.current >= workflowWorkspaceLevelLimit.total || + (percentage >= 90 && percentage < 100) || + workflowWorkspaceLevelLimit.current === workflowWorkspaceLevelLimit.total - 1 + ); + }; + + getWorkflowLimit = () => { + return this.hasInstanceLimitWarning() + ? this.state.workflowInstanceLevelLimit + : this.state.workflowWorkspaceLevelLimit; + }; + render() { const { apps, @@ -1010,7 +1122,7 @@ class HomePageComponent extends React.Component { } else if (this.props.appType === 'front-end') { return appsLimit?.percentage >= 100; } else { - return workflowInstanceLevelLimit.percentage >= 100 || workflowWorkspaceLevelLimit.percentage >= 100; + return this.hasWorkflowLimitReached(); } }; const modalConfigs = { @@ -1462,15 +1574,12 @@ class HomePageComponent extends React.Component { )} - - {this.props.appType !== 'workflow' && ( - - )} + + + diff --git a/frontend/src/WorkflowEditor/LogsPanel/icons/triangle-right.svg b/frontend/src/WorkflowEditor/LogsPanel/icons/triangle-right.svg new file mode 100644 index 0000000000..9f8648e692 --- /dev/null +++ b/frontend/src/WorkflowEditor/LogsPanel/icons/triangle-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/_services/dataquery.service.js b/frontend/src/_services/dataquery.service.js index 3dc5e26593..c69b125237 100644 --- a/frontend/src/_services/dataquery.service.js +++ b/frontend/src/_services/dataquery.service.js @@ -11,6 +11,7 @@ export const dataqueryService = { changeQueryDataSource, updateStatus, bulkUpdateQueryOptions, + createWorkflowQuery, }; function getAll(appVersionId, mode) { @@ -36,6 +37,21 @@ function create(app_id, app_version_id, name, kind, options, data_source_id, plu ).then(handleResponse); } +function createWorkflowQuery(app_id, app_version_id, name, kind, options, data_source_id, plugin_id) { + const body = { + app_id, + app_version_id, + name, + kind, + options, + data_source_id, + plugin_id, + }; + + const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) }; + return fetch(`${config.apiUrl}/data-queries/workflow-node`, requestOptions).then(handleResponse); +} + function update(id, versionId, name, options, dataSourceId) { const body = { options, diff --git a/frontend/src/_services/workflow_executions.service.js b/frontend/src/_services/workflow_executions.service.js index 8eac985252..8abcdfe569 100644 --- a/frontend/src/_services/workflow_executions.service.js +++ b/frontend/src/_services/workflow_executions.service.js @@ -10,11 +10,15 @@ export const workflowExecutionsService = { all, enableWebhook, previewQueryNode, + getPaginatedExecutions, + getPaginatedNodes, + trigger, + streamSSE, }; -function previewQueryNode(queryId, appVersionId, nodeId) { +function previewQueryNode(queryId, appVersionId, nodeId, state = {}) { const currentSession = authenticationService.currentSessionValue; - const body = { appVersionId, userId: currentSession.current_user?.id, queryId, nodeId }; + const body = { appVersionId, userId: currentSession.current_user?.id, queryId, nodeId, state }; const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' }; return fetch(`${config.apiUrl}/workflow_executions/previewQueryNode`, requestOptions).then(handleResponse); } @@ -70,3 +74,40 @@ function enableWebhook(appId, value) { const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' }; return fetch(`${config.apiUrl}/v2/webhooks/workflows/${appId}`, requestOptions).then(handleResponse); } + +function getPaginatedExecutions(appVersionId, page = 1, perPage = 10) { + const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; + return fetch( + `${config.apiUrl}/workflow_executions?appVersionId=${appVersionId}&page=${page}&per_page=${perPage}`, + requestOptions + ).then(handleResponse); +} + +function getPaginatedNodes(executionId, page = 1, perPage = 20) { + const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; + return fetch( + `${config.apiUrl}/workflow_executions/${executionId}/nodes?page=${page}&per_page=${perPage}`, + requestOptions + ).then(handleResponse); +} + +function trigger(workflowAppId, params, environmentId) { + const currentSession = authenticationService.currentSessionValue; + const body = { + appId: workflowAppId, + userId: currentSession.current_user?.id, + executeUsing: 'app', + params: Array.isArray(params) + ? Object.fromEntries(params.filter((param) => param.key !== '').map((param) => [param.key, param.value])) + : params || {}, + environmentId, + }; + const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body), credentials: 'include' }; + return fetch(`${config.apiUrl}/workflow_executions/${workflowAppId}/trigger`, requestOptions).then(handleResponse); +} + +function streamSSE(workflowExecutionId) { + return new EventSource(`${config.apiUrl}/workflow_executions/${workflowExecutionId}/stream`, { + withCredentials: true, + }); +} diff --git a/frontend/src/_stores/workflowStore.js b/frontend/src/_stores/workflowStore.js new file mode 100644 index 0000000000..89ba9d0c8a --- /dev/null +++ b/frontend/src/_stores/workflowStore.js @@ -0,0 +1,8 @@ +import create from 'zustand'; + +const useWorkflowStore = create((set) => ({ + workflowId: null, + setWorkflowId: (id) => set({ workflowId: id }), +})); + +export default useWorkflowStore; diff --git a/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx b/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx index 7178a19bdf..85a6c25b8d 100644 --- a/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx +++ b/frontend/src/modules/common/components/BaseImportAppMenu/BaseImportAppMenu.jsx @@ -10,13 +10,14 @@ const BaseImportAppMenu = ({ showCloudMenuItems = false, CloudMenuComponent = () => null, darkMode = false, + appType = 'front-end', ...props }) => { const fileInput = React.createRef(); const { t } = useTranslation(); return ( - {props.appType !== 'module' && ( + {appType !== 'wzorkflow' && appType !== 'module' && ( { const { admin } = authenticationService?.currentSessionValue ?? {}; const isWorkflows = type === 'workflows'; - const workflowsEnabled = admin && window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true'; + const workflowsEnabled = admin && isWorkflowsFeatureEnabled(); const handleBackClick = (e) => { e.preventDefault(); diff --git a/frontend/src/modules/common/helpers/utils.js b/frontend/src/modules/common/helpers/utils.js index 882ab17cff..d7418b60d3 100644 --- a/frontend/src/modules/common/helpers/utils.js +++ b/frontend/src/modules/common/helpers/utils.js @@ -18,4 +18,9 @@ const fetchEdition = () => { return config.TOOLJET_EDITION?.toLowerCase() || 'ce'; }; -export { processErrorMessage, clearPageHistory, fetchEdition }; +const isWorkflowsFeatureEnabled = () => { + if (fetchEdition() === 'ee') return true; + return false; +}; + +export { processErrorMessage, clearPageHistory, fetchEdition, isWorkflowsFeatureEnabled }; diff --git a/server/ee b/server/ee index cc864000dd..49945c6f57 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit cc864000dd03cc345e53ae9fc43821d3174f4c64 +Subproject commit 49945c6f57df4739b2898298ed703c9f1adf8173 diff --git a/server/lib/utils.js b/server/lib/utils.js index b1ecded5e0..59b3d922a1 100644 --- a/server/lib/utils.js +++ b/server/lib/utils.js @@ -32,11 +32,11 @@ export function resolveCode(codeContext) { ...Object.fromEntries(reservedKeyword.map((keyWord) => [keyWord, null])), }; const codeToExecute = getFunctionWrappedCode( - 'const console = { log: __reserved_keyword_log };\n' + code, + 'const console = { log: (...args) => __reserved_keyword_log(args.join(\', \'), \'normal\') };\n' + code, globalState, isIfCondition ); - const isolate = new ivm.Isolate({ memoryLimit: parseInt(process.env?.WORKFLOWS_JS_MEMORY_LIMIT ?? '20') }); + const isolate = new ivm.Isolate({ memoryLimit: parseInt(process.env?.WORKFLOW_JS_MEMORY_LIMIT_MB) || 20 }); const context = isolate.createContextSync(); Object.entries(globalState).forEach(([key, value]) => { context.global.setSync(key, new ivm.ExternalCopy(value).copyInto({ release: true })); @@ -57,7 +57,13 @@ export function resolveCode(codeContext) { // }, 1); // Monitor every 100ms // try { - result = script.runSync(context, { release: true, timeout: 100, copy: true }); + result = script.runSync( + context, + { + release: true, + timeout: parseInt(process.env?.WORKFLOW_JS_TIMEOUT_MS) || 100, + copy: true + }); // const stats = isolate.getHeapStatisticsSync(); // addLog("Used heap size: " + stats.used_heap_size); // addLog("heap size limit: " + stats.heap_size_limit); diff --git a/server/src/dto/preview-workflow-node.dto.ts b/server/src/dto/preview-workflow-node.dto.ts index 0268f9652c..63c367fba0 100644 --- a/server/src/dto/preview-workflow-node.dto.ts +++ b/server/src/dto/preview-workflow-node.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator'; export class PreviewWorkflowNodeDto { @IsString() @@ -20,4 +20,8 @@ export class PreviewWorkflowNodeDto { @IsString() @IsOptional() appEnvId?: string; + + @IsObject() + @IsOptional() + state?: Record; } diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts index 24c78aaaa5..0ffa2fff27 100644 --- a/server/src/modules/app/module.ts +++ b/server/src/modules/app/module.ts @@ -3,6 +3,8 @@ import { GetConnection } from './database/getConnection'; import { ShutdownHook } from './schedulers/shut-down.hook'; import { AppModuleLoader } from './loader'; import * as Sentry from '@sentry/node'; +import { getTooljetEdition } from '@helpers/utils.helper'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; import { InstanceSettingsModule } from '@modules/instance-settings/module'; import { AbilityModule } from '@modules/ability/module'; import { LicenseModule } from '@modules/licensing/module'; @@ -68,7 +70,7 @@ export class AppModule implements OnModuleInit { * █ █ * ████████████████████████████████████████████████████████████████████ */ - const imports = [ + const baseImports = [ await AbilityModule.forRoot(configs), await LicenseModule.forRoot(configs), await FilesModule.register(configs), @@ -103,7 +105,6 @@ export class AppModule implements OnModuleInit { await ImportExportResourcesModule.register(configs), await TemplatesModule.register(configs), await TooljetDbModule.register(configs), - await WorkflowsModule.register(configs), await ModulesModule.register(configs), await AiModule.register(configs), await CustomStylesModule.register(configs), @@ -117,6 +118,13 @@ export class AppModule implements OnModuleInit { await EmailListenerModule.register(configs), ]; + const conditionalImports = []; + if (getTooljetEdition() !== TOOLJET_EDITIONS.Cloud) { + conditionalImports.push(await WorkflowsModule.register(configs)); + } + + const imports = [...baseImports, ...conditionalImports]; + return { module: AppModule, imports: [...modules, ...imports], diff --git a/server/src/modules/apps/controllers/workflow.controller.ts b/server/src/modules/apps/controllers/workflow.controller.ts index cc91daf852..32f724cfd0 100644 --- a/server/src/modules/apps/controllers/workflow.controller.ts +++ b/server/src/modules/apps/controllers/workflow.controller.ts @@ -9,12 +9,15 @@ import { ValidAppGuard } from '../guards/valid-app.guard'; import { AppDecorator as App } from '@modules/app/decorators/app.decorator'; import { WorkflowService } from '../services/workflow.service'; import { IWorkflowController } from '../interfaces/IControllerWorkflow'; +import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; +import { FEATURE_KEY } from '../constants'; @InitModule(MODULES.APP) @Controller('apps') export class WorkflowController implements IWorkflowController { constructor(protected readonly workflowService: WorkflowService) {} + @InitFeature(FEATURE_KEY.GET) @UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard) @Get(':id/workflows') async fetchWorkflows(@App() app: AppEntity) { diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 7bb2c2e10c..f30e91cd11 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -30,20 +30,24 @@ export class AppsModule extends SubModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { const { AppsController, + WorkflowController, AppsService, AppsUtilService, PageService, EventsService, ComponentsService, + WorkflowService, AppImportExportService, PageHelperService, } = await this.getProviders(configs, 'apps', [ 'controller', + 'controllers/workflow.controller', 'service', 'util.service', 'services/page.service', 'services/event.service', 'services/component.service', + 'services/workflow.service', 'services/app-import-export.service', 'services/page.util.service', ]); @@ -63,9 +67,10 @@ export class AppsModule extends SubModule { await UsersModule.register(configs), await AppEnvironmentsModule.register(configs), ], - controllers: [AppsController], + controllers: [AppsController, WorkflowController], providers: [ AppsService, + WorkflowService, VersionRepository, AppsRepository, AppGitRepository, diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts index 4651f7fa2f..ef2e75fc82 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -433,13 +433,7 @@ export class AppImportExportService { const currentTooljetVersion = !cloning ? tooljetVersion : null; - const importedApp = await this.createImportedAppForUser( - manager, - schemaUnifiedAppParams, - user, - isGitApp, - appParams?.type - ); + const importedApp = await this.createImportedAppForUser(manager, schemaUnifiedAppParams, user, isGitApp); const resourceMapping = await this.setupImportedAppAssociations( manager, @@ -527,23 +521,22 @@ export class AppImportExportService { await manager.update(AppVersion, { id: appVersion.id }, { globalSettings: updatedGlobalSettings }); } } + + if (appVersionIds.length > 0) { + await this.updateWorkflowDefinitionQueryReferences(manager, appVersionIds, resourceMapping); + } } - async createImportedAppForUser( - manager: EntityManager, - appParams: any, - user: User, - isGitApp = false, - type?: APP_TYPES - ): Promise { + async createImportedAppForUser(manager: EntityManager, appParams: any, user: User, isGitApp = false): Promise { return await catchDbException(async () => { const importedApp = manager.create(App, { name: appParams.name, + type: appParams.type || APP_TYPES.FRONT_END, + isMaintenanceOn: appParams.isMaintenanceOn || false, organizationId: user?.organizationId, userId: user.id, //fetch super admin user id for EE slug: null, icon: appParams.icon, - type: type || APP_TYPES.FRONT_END, creationMode: `${isGitApp ? 'GIT' : 'DEFAULT'}`, isPublic: false, createdAt: new Date(), @@ -605,7 +598,7 @@ export class AppImportExportService { isNormalizedAppDefinitionSchema: boolean, tooljetVersion: string | null, moduleResourceMappings?: Record - ) { + ): Promise { // Old version without app version // Handle exports prior to 0.12.0 // TODO: have version based conditional based on app versions @@ -1270,6 +1263,61 @@ export class AppImportExportService { return appResourceMappings; } + /** + * Updates workflow definition query references with newly created query IDs during app import. + * + * Note: For workflow apps, the entire workflow definition (including nodes, edges, and query mappings) + * is stored as JSON in the app_versions.definition column. Unlike regular apps where queries are + * stored as separate entities, workflow queries are referenced within this JSON structure through + * a queries array that maps workflow node IDs (idOnDefinition) to actual data query IDs. + * + * During import, new data queries are created with different IDs, so we need to update the + * workflow definition's queries array to reference these new IDs while preserving the + * idOnDefinition values that link to workflow nodes. + */ + private async updateWorkflowDefinitionQueryReferences( + manager: EntityManager, + appVersionIds: string[], + resourceMapping: AppResourceMappings + ): Promise { + // Get the app versions with their definitions and associated apps + const appVersionsWithDefinitions = await manager + .createQueryBuilder(AppVersion, 'appVersion') + .leftJoinAndSelect('appVersion.app', 'app') + .where('appVersion.id IN(:...appVersionIds)', { appVersionIds }) + .select(['appVersion.id', 'appVersion.definition', 'app.type']) + .getMany(); + + const workflowAppVersions = appVersionsWithDefinitions.filter( + (appVersion) => appVersion.app?.type === 'workflow' && appVersion.definition?.queries + ); + + if (workflowAppVersions.length > 0) { + for (const appVersion of workflowAppVersions) { + const definition = appVersion.definition; + let definitionUpdated = false; + + // Update query IDs in the workflow definition + if (definition.queries && Array.isArray(definition.queries)) { + definition.queries = definition.queries.map((query) => { + if (query.id && resourceMapping.dataQueryMapping[query.id]) { + definitionUpdated = true; + return { + ...query, + id: resourceMapping.dataQueryMapping[query.id], + }; + } + return query; + }); + } + + if (definitionUpdated) { + await manager.update(AppVersion, { id: appVersion.id }, { definition }); + } + } + } + } + async rejectMarketplacePluginsNotInstalled( manager: EntityManager, importingDataSources: DataSource[] diff --git a/server/src/modules/auth/workflow-sse-auth.guard.ts b/server/src/modules/auth/workflow-sse-auth.guard.ts new file mode 100644 index 0000000000..26fb28092c --- /dev/null +++ b/server/src/modules/auth/workflow-sse-auth.guard.ts @@ -0,0 +1,12 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class WorkflowSseAuthGuard extends AuthGuard('jwt') { + canActivate(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + request.isUserNotMandatory = true; + + return request; + } +} diff --git a/server/src/modules/data-queries/repository.ts b/server/src/modules/data-queries/repository.ts index 7ba62a974a..c939f3044b 100644 --- a/server/src/modules/data-queries/repository.ts +++ b/server/src/modules/data-queries/repository.ts @@ -114,7 +114,7 @@ export class DataQueryRepository extends Repository { findOptions: FindOptionsWhere, relations?: string[], manager?: EntityManager - ): Promise { + ): Promise { return dbTransactionWrap(async (manager: EntityManager) => { return manager.find(DataQuery, { where: { ...(findOptions ? findOptions : {}) }, diff --git a/server/src/modules/data-sources/repository.ts b/server/src/modules/data-sources/repository.ts index a14f3dbe90..746a5a18b7 100644 --- a/server/src/modules/data-sources/repository.ts +++ b/server/src/modules/data-sources/repository.ts @@ -5,7 +5,7 @@ import { UserPermissions } from '@modules/ability/types'; import { MODULES } from '@modules/app/constants/modules'; import { dbTransactionWrap } from '@helpers/database.helper'; import { DataSourceScopes, DataSourceTypes } from './constants'; -import { GetQueryVariables } from './types'; +import { DefaultDataSourceKind, GetQueryVariables } from './types'; import { decode } from 'js-base64'; @Injectable() @@ -162,6 +162,14 @@ export class DataSourcesRepository extends Repository { }); } + async getStaticDataSourceByKind(organizationId: string, kind: DefaultDataSourceKind, manager?: EntityManager): Promise { + return dbTransactionWrap((manager: EntityManager) => { + return manager.findOneOrFail(DataSource, { + where: { organizationId, type: DataSourceTypes.STATIC, kind }, + }); + }, manager || this.manager); + } + findByQuery(dataQueryId: string, organizationId: string, dataSourceId?: string, manager?: EntityManager) { return dbTransactionWrap((manager: EntityManager) => { return manager.findOne(DataSource, { diff --git a/server/src/modules/licensing/guards/webhook.guard.ts b/server/src/modules/licensing/guards/webhook.guard.ts index 91f94928c7..f589bc44dc 100644 --- a/server/src/modules/licensing/guards/webhook.guard.ts +++ b/server/src/modules/licensing/guards/webhook.guard.ts @@ -5,6 +5,7 @@ import { LicenseTermsService } from '../interfaces/IService'; import { LICENSE_FIELD, LICENSE_LIMIT } from '../constants'; import { AppsRepository } from '@modules/apps/repository'; import { APP_TYPES } from '@modules/apps/constants'; +import { isUUID } from 'class-validator'; @Injectable() export class WebhookGuard implements CanActivate { @@ -23,14 +24,16 @@ export class WebhookGuard implements CanActivate { : request.headers['tj-workspace-id']; const workflowsLimit = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.WORKFLOWS, organizationId); + const isUuid = isUUID(request?.params?.idOrName); const workflowApp = await this.appsRepository.findOne({ where: { - id: request?.params?.id, + [isUuid ? 'id' : 'name']: request?.params?.idOrName, type: APP_TYPES.WORKFLOW, }, }); if (!workflowApp) throw new HttpException(`Workflow doesn't exists`, 404); + request.tj_app = workflowApp; // Webhook API token validation if (request.headers.authorization.split(' ')[1] !== workflowApp.workflowApiToken) throw new UnauthorizedException(); @@ -39,71 +42,80 @@ export class WebhookGuard implements CanActivate { if (!workflowApp.workflowEnabled) throw new HttpException(`Webhook endpoint disabled or doesn't exists`, 404); // Workspace Level - - // Daily Limit - if ( - workflowsLimit.workspace.daily_executions !== LICENSE_LIMIT.UNLIMITED && - ( - await this.manager.query( - `SELECT COUNT(*) + if (workflowsLimit.workspace) { + // Daily Limit + const workspaceDailyExecutionsQuery = ` + SELECT COUNT(*) FROM apps a INNER JOIN app_versions av on av.app_id = a.id INNER JOIN workflow_executions we on we.app_version_id = av.id WHERE a.organization_id = $1 - AND DATE(we.created_at) = current_date`, - [workflowApp.organizationId] - ) - )[0].count >= workflowsLimit.workspace.daily_executions - ) { - throw new HttpException('Maximum daily limit for workflow execution has reached for this workspace', 451); - } + AND DATE(we.created_at) = current_date + `; - // Monthly Limit - if ( - workflowsLimit.workspace.monthly_executions !== LICENSE_LIMIT.UNLIMITED && - ( - await this.manager.query( - `SELECT COUNT(*) + if ( + workflowsLimit.workspace.daily_executions !== LICENSE_LIMIT.UNLIMITED && + (await this.manager.query(workspaceDailyExecutionsQuery, [workflowApp.organizationId]))[0].count >= + workflowsLimit.workspace.daily_executions + ) { + throw new HttpException('Maximum daily limit for workflow execution has reached for this workspace', 451); + } + + // Monthly Limit + const workspaceMonthlyExecutionsQuery = ` + SELECT COUNT(*) FROM apps a INNER JOIN app_versions av on av.app_id = a.id INNER JOIN workflow_executions we on we.app_version_id = av.id WHERE a.organization_id = $1 AND extract (year from we.created_at) = extract (year from current_date) - AND extract (month from we.created_at) = extract (month from current_date)`, - [workflowApp.organizationId] - ) - )[0].count >= workflowsLimit.workspace.monthly_executions - ) { - throw new HttpException('Maximum monthly limit for workflow execution has reached for this workspace', 451); + AND extract (month from we.created_at) = extract (month from current_date) + `; + + if ( + workflowsLimit.workspace.monthly_executions !== LICENSE_LIMIT.UNLIMITED && + (await this.manager.query(workspaceMonthlyExecutionsQuery, [workflowApp.organizationId]))[0].count >= + workflowsLimit.workspace.monthly_executions + ) { + throw new HttpException('Maximum monthly limit for workflow execution has reached for this workspace', 451); + } } // Instance Level - - // Daily Limit - if ( - workflowsLimit.instance.daily_executions !== LICENSE_LIMIT.UNLIMITED && - ( - await this.manager.query(`SELECT COUNT(*) + if (workflowsLimit.instance) { + // Daily Limit + const instanceDailyExecutionsQuery = ` + SELECT COUNT(*) FROM apps a INNER JOIN app_versions av on av.app_id = a.id INNER JOIN workflow_executions we on we.app_version_id = av.id - WHERE DATE(we.created_at) = current_date`) - )[0].count >= workflowsLimit.instance.daily_executions - ) { - throw new HttpException('Maximum daily limit for workflow execution has been reached', 451); - } + WHERE DATE(we.created_at) = current_date + `; - // Monthly Limit - if ( - workflowsLimit.instance.monthly_executions !== LICENSE_LIMIT.UNLIMITED && - ( - await this.manager.query(`SELECT COUNT(*) + if ( + workflowsLimit.instance.daily_executions !== LICENSE_LIMIT.UNLIMITED && + (await this.manager.query(instanceDailyExecutionsQuery))[0].count >= workflowsLimit.instance.daily_executions + ) { + throw new HttpException('Maximum daily limit for workflow execution has been reached', 451); + } + + // Monthly Limit + const instanceMonthlyExecutionsQuery = ` + SELECT COUNT(*) FROM apps a INNER JOIN app_versions av on av.app_id = a.id INNER JOIN workflow_executions we on we.app_version_id = av.id WHERE extract (year from we.created_at) = extract (year from current_date) - AND extract (month from we.created_at) = extract (month from current_date)`) - )[0].count >= workflowsLimit.instance.monthly_executions - ) { - throw new HttpException('Maximum monthly limit for workflow execution has been reached', 451); + AND extract (month from we.created_at) = extract (month from current_date) + `; + + if ( + workflowsLimit.instance.monthly_executions !== LICENSE_LIMIT.UNLIMITED && + (await this.manager.query(instanceMonthlyExecutionsQuery))[0].count >= + workflowsLimit.instance.monthly_executions + ) { + throw new HttpException('Maximum monthly limit for workflow execution has been reached', 451); + } } return true; diff --git a/server/src/modules/versions/ability/workflow-version.ability.ts b/server/src/modules/versions/ability/workflow-version.ability.ts index 16c54f8e92..e3733a5b3a 100644 --- a/server/src/modules/versions/ability/workflow-version.ability.ts +++ b/server/src/modules/versions/ability/workflow-version.ability.ts @@ -22,6 +22,7 @@ export function defineWorkflowVersionAbility( FEATURE_KEY.UPDATE, FEATURE_KEY.UPDATE_SETTINGS, FEATURE_KEY.PROMOTE, + FEATURE_KEY.APP_VERSION_UPDATE, ], App ); @@ -41,6 +42,7 @@ export function defineWorkflowVersionAbility( FEATURE_KEY.UPDATE, FEATURE_KEY.UPDATE_SETTINGS, FEATURE_KEY.PROMOTE, + FEATURE_KEY.APP_VERSION_UPDATE, ], App ); @@ -58,6 +60,7 @@ export function defineWorkflowVersionAbility( FEATURE_KEY.UPDATE, FEATURE_KEY.UPDATE_SETTINGS, FEATURE_KEY.PROMOTE, + FEATURE_KEY.APP_VERSION_UPDATE, ], App ); diff --git a/server/src/modules/workflows/constants/feature.ts b/server/src/modules/workflows/constants/feature.ts index d45f218569..b127bcb934 100644 --- a/server/src/modules/workflows/constants/feature.ts +++ b/server/src/modules/workflows/constants/feature.ts @@ -8,6 +8,8 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.WORKFLOW_EXECUTION_STATUS]: {}, [FEATURE_KEY.WORKFLOW_EXECUTION_DETAILS]: {}, //Basic plan users can access worfklows [FEATURE_KEY.LIST_WORKFLOW_EXECUTIONS]: {}, + [FEATURE_KEY.FETCH_EXECUTION_LOGS]: {}, + [FEATURE_KEY.FETCH_EXECUTION_NODES]: {}, [FEATURE_KEY.PREVIEW_QUERY_NODE]: {}, [FEATURE_KEY.CREATE_WORKFLOW_SCHEDULE]: {}, diff --git a/server/src/modules/workflows/constants/index.ts b/server/src/modules/workflows/constants/index.ts index a55692df04..196bcc9bfa 100644 --- a/server/src/modules/workflows/constants/index.ts +++ b/server/src/modules/workflows/constants/index.ts @@ -3,6 +3,8 @@ export enum FEATURE_KEY { WORKFLOW_EXECUTION_STATUS = 'workflow_execution_status', WORKFLOW_EXECUTION_DETAILS = 'workflow_execution_details', LIST_WORKFLOW_EXECUTIONS = 'list_workflow_executions', + FETCH_EXECUTION_LOGS = 'fetch_execution_logs', + FETCH_EXECUTION_NODES = 'fetch_execution_nodes', PREVIEW_QUERY_NODE = 'preview_query_node', CREATE_WORKFLOW_SCHEDULE = 'create_workflow_schedule', diff --git a/server/src/modules/workflows/controllers/workflow-executions.controller.ts b/server/src/modules/workflows/controllers/workflow-executions.controller.ts index 6de5ced93f..c4ff8cdcfe 100644 --- a/server/src/modules/workflows/controllers/workflow-executions.controller.ts +++ b/server/src/modules/workflows/controllers/workflow-executions.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, Res, Sse } from '@nestjs/common'; import { Response } from 'express'; import { IWorkflowExecutionController } from '../interfaces/IWorkflowExecutionController'; import { CreateWorkflowExecutionDto } from '@dto/create-workflow-execution.dto'; @@ -9,6 +9,7 @@ import { InitModule } from '@modules/app/decorators/init-module'; import { MODULES } from '@modules/app/constants/modules'; import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; import { FEATURE_KEY } from '@modules/workflows/constants'; +import { Observable } from 'rxjs'; @InitModule(MODULES.WORKFLOWS) @Controller('workflow_executions') @@ -43,6 +44,28 @@ export class WorkflowExecutionsController implements IWorkflowExecutionControlle throw new Error('Method not implemented.'); } + @InitFeature(FEATURE_KEY.FETCH_EXECUTION_LOGS) + @Get() + async getExecutions( + @Query('appVersionId') appVersionId: string, + @Query('page') page = '1', + @Query('per_page') perPage = '10', + @User() user + ): Promise { + throw new Error('Method not implemented.'); + } + + @InitFeature(FEATURE_KEY.FETCH_EXECUTION_NODES) + @Get(':id/nodes') + async getExecutionNodes( + @Param('id') id: string, + @Query('page') page = '1', + @Query('per_page') perPage = '10', + @User() user + ): Promise { + throw new Error('Method not implemented.'); + } + @InitFeature(FEATURE_KEY.PREVIEW_QUERY_NODE) @Post('previewQueryNode') async previewQueryNode( @@ -52,4 +75,21 @@ export class WorkflowExecutionsController implements IWorkflowExecutionControlle ): Promise<{ result: any }> { throw new Error('Method not implemented.'); } + + @InitFeature(FEATURE_KEY.EXECUTE_WORKFLOW) + @Post(':id/trigger') + async trigger( + @Param('id') id: string, + @Body() createWorkflowExecutionDto: CreateWorkflowExecutionDto, + @User() user, + @Res({ passthrough: true }) response: Response + ): Promise<{ result: any }> { + throw new Error('Method not implemented.'); + } + + @InitFeature(FEATURE_KEY.WORKFLOW_EXECUTION_STATUS) + @Sse(':id/stream') + async streamWorkflowExecution(@Param('id') id: string): Promise> { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/workflows/controllers/workflow-webhooks.controller.ts b/server/src/modules/workflows/controllers/workflow-webhooks.controller.ts index 4558327f6b..727788cb12 100644 --- a/server/src/modules/workflows/controllers/workflow-webhooks.controller.ts +++ b/server/src/modules/workflows/controllers/workflow-webhooks.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Param, Body, Patch, Query, Res } from '@nestjs/common'; +import { Controller, Post, Param, Body, Patch, Query, Res, Get, Sse, Req } from '@nestjs/common'; import { Response } from 'express'; import { IWorkflowWebhooksController } from '../interfaces/IWorkflowWebhooksController'; import { InitModule } from '@modules/app/decorators/init-module'; @@ -20,11 +20,36 @@ export class WorkflowWebhooksController implements IWorkflowWebhooksController { @Param('id') id: any, @Body() workflowParams, @Query('environment') environment: string, - @Res({ passthrough: true }) response: Response + @Res({ passthrough: true }) response: Response, + @Req() req: Request ): Promise { throw new Error('Method not implemented.'); } + @InitFeature(FEATURE_KEY.WEBHOOK_TRIGGER_WORKFLOW) + @Post('workflows/:idOrName/trigger-async') + async triggerWorkflowAsync( + @Param('app') app: any, + @Param('idOrName') idOrName: string, + @Body() workflowParams: Record, + @Query('environment') environment: string, + @Req() req: Request + ): Promise { + throw new Error('Method not implemented.'); + } + + @InitFeature(FEATURE_KEY.WEBHOOK_TRIGGER_WORKFLOW) + @Get('workflows/:idOrName/status/:executionId') + async getExecutionStatus(@Param('executionId') executionId: string): Promise { + throw new Error('Method not implemented.'); + } + + @InitFeature(FEATURE_KEY.WEBHOOK_TRIGGER_WORKFLOW) + @Sse('workflows/:idOrName/execution/:executionId/stream') + async triggerWorkflowStream(@Param('executionId') executionId: string): Promise { + throw new Error('Method not implemented.'); + } + @InitFeature(FEATURE_KEY.UPDATE_WORKFLOW_WEBHOOK_DETAILS) @Patch('workflows/:id') async updateWorkflow(@Param('id') id, @Body() workflowValuesToUpdate): Promise { diff --git a/server/src/modules/workflows/interfaces/IWorkflowExecutionController.ts b/server/src/modules/workflows/interfaces/IWorkflowExecutionController.ts index 01399f46be..61547bb682 100644 --- a/server/src/modules/workflows/interfaces/IWorkflowExecutionController.ts +++ b/server/src/modules/workflows/interfaces/IWorkflowExecutionController.ts @@ -16,5 +16,9 @@ export interface IWorkflowExecutionController { index(appVersionId: any, user: any): Promise; + getExecutions(appVersionId: string, page: any, perPage: any, user: any): Promise; + + getExecutionNodes(id: string, user: any, page: any, perPage: any): Promise; + previewQueryNode(user: any, previewNodeDto: PreviewWorkflowNodeDto, response: Response): Promise<{ result: any }>; } diff --git a/server/src/modules/workflows/interfaces/IWorkflowExecutionsService.ts b/server/src/modules/workflows/interfaces/IWorkflowExecutionsService.ts index 7eeb057902..8ec5b78fe2 100644 --- a/server/src/modules/workflows/interfaces/IWorkflowExecutionsService.ts +++ b/server/src/modules/workflows/interfaces/IWorkflowExecutionsService.ts @@ -1,23 +1,62 @@ import { CreateWorkflowExecutionDto } from '@dto/create-workflow-execution.dto'; import { WorkflowExecution } from 'src/entities/workflow_execution.entity'; +import { AppVersion } from 'src/entities/app_version.entity'; +import { User } from 'src/entities/user.entity'; +import { Response } from 'express'; +import { QueryResult } from '@tooljet/plugins/dist/packages/common/lib'; +import { WorkflowExecutionNode } from 'src/entities/workflow_execution_node.entity'; export interface IWorkflowExecutionsService { create(createWorkflowExecutionDto: CreateWorkflowExecutionDto): Promise; - execute(workflowExecution: WorkflowExecution, params: any, envId: string, response: Response): Promise; + execute( + workflowExecution: WorkflowExecution, + params: Record, + envId: string, + response: Response, + throwOnError?: boolean, + executionStartTime?: Date + ): Promise; - getStatus(id: string): Promise<{ logs: string[]; status: boolean; nodes: any[] }>; + getStatus(id: string): Promise<{ + logs: unknown; + status: boolean; + nodes: Array<{ + id: string; + idOnDefinition: string; + executed: boolean; + result: unknown; + }>; + }>; getWorkflowExecution(id: string): Promise; listWorkflowExecutions(appVersionId: string): Promise; + findOne(id: string, relations?: string[]): Promise; + previewQueryNode( queryId: string, nodeId: string, - params: any, - appVersion: any, - user: any, - response: any + state: Record, + appVersion: AppVersion, + user: User, + response: Response ): Promise; + + getWorkflowExecutionsLogs(appVersionId: string, page?: number, limit?: number): Promise<{ + data: WorkflowExecution[]; + page: number; + per_page: number; + total: number; + total_pages: number; + }>; + + getWorkflowExecutionNodes(workflowExecutionId: string, page?: number, limit?: number): Promise<{ + data: WorkflowExecutionNode[]; + page: number; + per_page: number; + total: number; + total_pages: number; + }>; } diff --git a/server/src/modules/workflows/interfaces/IWorkflowWebhooksController.ts b/server/src/modules/workflows/interfaces/IWorkflowWebhooksController.ts index 5b896e6fb7..1cab4642b9 100644 --- a/server/src/modules/workflows/interfaces/IWorkflowWebhooksController.ts +++ b/server/src/modules/workflows/interfaces/IWorkflowWebhooksController.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; export interface IWorkflowWebhooksController { - triggerWorkflow(id: any, workflowParams: any, environment: string, response: Response): Promise; + triggerWorkflow(id: any, workflowParams: any, environment: string, response: Response, req: Request): Promise; updateWorkflow(id: any, workflowValuesToUpdate: any): Promise; } diff --git a/server/src/modules/workflows/listeners/workflow-triggers.listener.ts b/server/src/modules/workflows/listeners/workflow-triggers.listener.ts new file mode 100644 index 0000000000..6e19c6b379 --- /dev/null +++ b/server/src/modules/workflows/listeners/workflow-triggers.listener.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { WorkflowExecutionsService } from '../services/workflow-executions.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Logger } from 'nestjs-pino'; +import { CreateWorkflowExecutionDto } from '@dto/create-workflow-execution.dto'; +import { WorkflowExecution } from '@entities/workflow_execution.entity'; +import { Response } from 'express'; +import { AppVersion } from '@entities/app_version.entity'; +import { App } from '@entities/app.entity'; +import { EntityManager } from 'typeorm'; + +export const WORKFLOW_EXECUTION_STATUS = { + TRIGGERED: 'workflow_execution_triggered', + RUNNING: 'workflow_execution_running', + COMPLETED: 'workflow_execution_completed', + ERROR: 'workflow_execution_error', +}; + +@Injectable() +export class WorkflowTriggersListener { + constructor( + protected workflowExecutionsService: WorkflowExecutionsService, + protected readonly logger: Logger, + protected readonly eventEmitter: EventEmitter2 + ) {} + + @OnEvent('triggerWorkflow') + async handleTriggerWorkflow({ + createWorkflowExecutionDto, + workflowExecution, + response, + }: { + createWorkflowExecutionDto: CreateWorkflowExecutionDto; + workflowExecution: WorkflowExecution; + response: Response; + }): Promise { + throw new Error('Not implemented.'); + } +} diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 944866621c..c5a70a85f8 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -44,7 +44,9 @@ export class WorkflowsModule extends SubModule { WorkflowSchedulesService, TemporalService, WorkflowWebhooksListener, + WorkflowTriggersListener, FeatureAbilityFactory, + WorkflowStreamService, } = await this.getProviders(configs, 'workflows', [ 'services/workflow-executions.service', 'controllers/workflow-executions.controller', @@ -55,7 +57,9 @@ export class WorkflowsModule extends SubModule { 'services/workflow-schedules.service', 'services/temporal.service', 'listeners/workflow-webhooks.listener', + 'listeners/workflow-triggers.listener', 'ability/app', + 'services/workflow-stream.service', ]); // Get apps related providers @@ -126,6 +130,8 @@ export class WorkflowsModule extends SubModule { PageService, EventsService, WorkflowExecutionsService, + WorkflowStreamService, + WorkflowTriggersListener, WorkflowWebhooksListener, WorkflowWebhooksService, OrganizationConstantsService, diff --git a/server/src/modules/workflows/services/workflow-executions.service.ts b/server/src/modules/workflows/services/workflow-executions.service.ts index a81f2db6ff..ea39f8a488 100644 --- a/server/src/modules/workflows/services/workflow-executions.service.ts +++ b/server/src/modules/workflows/services/workflow-executions.service.ts @@ -5,6 +5,8 @@ import { User } from 'src/entities/user.entity'; import { Response } from 'express'; import { IWorkflowExecutionsService } from '../interfaces/IWorkflowExecutionsService'; import { CreateWorkflowExecutionDto } from '@dto/create-workflow-execution.dto'; +import { QueryResult } from '@tooljet/plugins/dist/packages/common/lib'; +import { WorkflowExecutionNode } from 'src/entities/workflow_execution_node.entity'; @Injectable() export class WorkflowExecutionsService implements IWorkflowExecutionsService { @@ -14,11 +16,27 @@ export class WorkflowExecutionsService implements IWorkflowExecutionsService { throw new Error('Method not implemented.'); } - async execute(workflowExecution: WorkflowExecution, params: any, envId: string, response: any): Promise { + async execute( + workflowExecution: WorkflowExecution, + params: Record, + envId: string, + response: Response, + throwOnError?: boolean, + executionStartTime?: Date + ): Promise { throw new Error('Method not implemented.'); } - async getStatus(workflowExecutionId: string): Promise<{ logs: string[]; status: boolean; nodes: any[] }> { + async getStatus(workflowExecutionId: string): Promise<{ + logs: unknown; + status: boolean; + nodes: Array<{ + id: string; + idOnDefinition: string; + executed: boolean; + result: unknown; + }>; + }> { throw new Error('Method not implemented.'); } @@ -33,11 +51,35 @@ export class WorkflowExecutionsService implements IWorkflowExecutionsService { async previewQueryNode( queryId: string, nodeId: string, - state: object, + state: Record, appVersion: AppVersion, user: User, response: Response ): Promise { throw new Error('Method not implemented.'); } + + async findOne(id: string, relations?: string[]): Promise { + throw new Error('Method not implemented.'); + } + + async getWorkflowExecutionsLogs(appVersionId: string, page: number = 1, limit: number = 10): Promise<{ + data: WorkflowExecution[]; + page: number; + per_page: number; + total: number; + total_pages: number; + }> { + throw new Error('Method not implemented.'); + } + + async getWorkflowExecutionNodes(workflowExecutionId: string, page: number = 1, limit: number = 10): Promise<{ + data: WorkflowExecutionNode[]; + page: number; + per_page: number; + total: number; + total_pages: number; + }> { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/workflows/services/workflow-stream.service.ts b/server/src/modules/workflows/services/workflow-stream.service.ts new file mode 100644 index 0000000000..d0901c9a4e --- /dev/null +++ b/server/src/modules/workflows/services/workflow-stream.service.ts @@ -0,0 +1,31 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { OnEvent } from '@nestjs/event-emitter'; + +export const WORKFLOW_CONNECTION_TYPES = { + INITIALIZED: 'workflow_connection_initialized', + STREAMING: 'workflow_connection_streaming', + ERROR: 'workflow_connection_error', + CLOSE: 'workflow_connection_close', +}; + +// Base WorkflowStreamService class for CE +// This provides the interface but throws "Not implemented" errors for all methods +// EE version will extend this class and provide actual implementations +@Injectable() +export class WorkflowStreamService implements OnModuleInit { + constructor() {} + + onModuleInit() { + // CE version - no implementation needed + } + + @OnEvent('workflow.status') + handleWorkflowStatus({ executionId, status }: { executionId: string; status: any }) { + throw new Error('Method not implemented.'); + } + + getStream(executionId: string): Observable { + throw new Error('Method not implemented.'); + } +} diff --git a/server/src/modules/workflows/types/index.ts b/server/src/modules/workflows/types/index.ts index 5d71a8b0e6..3aaba83ec1 100644 --- a/server/src/modules/workflows/types/index.ts +++ b/server/src/modules/workflows/types/index.ts @@ -7,6 +7,8 @@ interface Features { [FEATURE_KEY.WORKFLOW_EXECUTION_STATUS]: FeatureConfig; [FEATURE_KEY.WORKFLOW_EXECUTION_DETAILS]: FeatureConfig; [FEATURE_KEY.LIST_WORKFLOW_EXECUTIONS]: FeatureConfig; + [FEATURE_KEY.FETCH_EXECUTION_LOGS]: FeatureConfig; + [FEATURE_KEY.FETCH_EXECUTION_NODES]: FeatureConfig; [FEATURE_KEY.PREVIEW_QUERY_NODE]: FeatureConfig; [FEATURE_KEY.CREATE_WORKFLOW_SCHEDULE]: FeatureConfig; [FEATURE_KEY.LIST_WORKFLOW_SCHEDULES]: FeatureConfig;