- |
+ |
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/components/BaseManageOrgConstants/BaseManageOrgConstants.jsx b/frontend/src/modules/common/components/BaseManageOrgConstants/BaseManageOrgConstants.jsx
index e0dee2a9f5..d9a473a87d 100644
--- a/frontend/src/modules/common/components/BaseManageOrgConstants/BaseManageOrgConstants.jsx
+++ b/frontend/src/modules/common/components/BaseManageOrgConstants/BaseManageOrgConstants.jsx
@@ -473,10 +473,8 @@ const BaseManageOrgConstants = ({
featureAceess={featureAccess}
licenseType={featureAccess?.licenseStatus?.licenseType}
/>
-
-
-
+
diff --git a/frontend/src/modules/common/components/BaseOrganizationList/BaseOrganizationList.jsx b/frontend/src/modules/common/components/BaseOrganizationList/BaseOrganizationList.jsx
index 840ac8aac0..6defb9c9c9 100644
--- a/frontend/src/modules/common/components/BaseOrganizationList/BaseOrganizationList.jsx
+++ b/frontend/src/modules/common/components/BaseOrganizationList/BaseOrganizationList.jsx
@@ -13,7 +13,7 @@ import { WorkspaceDropDown } from '@/modules/dashboard/components';
each workspace related component has organizations list component which can be moved to a single wrapper.
otherwise this component will intiate everytime we switch between pages
*/
-const BaseOrganizationList = function ({ workspacesLimit = null, LicenseBadge = () => null, ...props }) {
+const BaseOrganizationList = ({ workspacesLimit = null, LicenseBadge = () => null, ...props }) => {
const { current_organization_id, admin } = authenticationService.currentSessionValue;
const { fetchOrganizations, organizationList, isGettingOrganizations } = useCurrentSessionStore(
(state) => ({
diff --git a/frontend/src/modules/common/components/UsersTable/UsersTable.jsx b/frontend/src/modules/common/components/UsersTable/UsersTable.jsx
index b04dc74bf0..c2b62efb65 100644
--- a/frontend/src/modules/common/components/UsersTable/UsersTable.jsx
+++ b/frontend/src/modules/common/components/UsersTable/UsersTable.jsx
@@ -68,7 +68,7 @@ const UsersTable = ({
/>
-
+
|
@@ -106,9 +106,7 @@ const UsersTable = ({
{translator('header.organization.menus.manageUsers.workspaces', 'Workspaces')}
|
)}
- |
- |
- |
+ |
{isLoading ? (
@@ -128,7 +126,7 @@ const UsersTable = ({
users.length > 0 &&
users.map((user) => (
- |
+ |
)}
{isLoadingAllUsers && (
- |
+ |
group.name)} />}
{user.status && (
|
@@ -223,7 +221,7 @@ const UsersTable = ({
|
)}
{isLoadingAllUsers && (
-
+ |
|
)}
-
+ |
{
onClick={(e) => {
orderedArray.length > 2 && toggleAllGroupsList(e);
}}
- className={cx('text-muted groups-name-cell', { 'groups-hover': orderedArray.length > 2 })}
+ className={cx('text-muted groups-name-cell !tw-w-[230px] tw-max-w-[230px]', {
+ 'groups-hover': orderedArray.length > 2,
+ })}
>
{orderedArray.length === 0 ? (
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/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx b/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx
index 4251ebf866..65c03aa074 100644
--- a/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx
+++ b/frontend/src/modules/dataSources/components/DataSourceManager/DataSourceManager.jsx
@@ -117,6 +117,9 @@ class DataSourceManagerComponent extends React.Component {
selectedDataSourceIcon: this.props.selectedDataSource?.plugin?.iconFile?.data,
connectionTestError: null,
datasourceName: this.props.selectedDataSource?.name,
+ validationMessages: {},
+ validationError: [],
+ showValidationErrors: false,
});
}
}
@@ -146,6 +149,9 @@ class DataSourceManagerComponent extends React.Component {
dataSourceSchema: source.manifestFile?.data,
selectedDataSourcePluginId: source.id,
datasourceName: source.name,
+ validationMessages: {},
+ validationError: [],
+ showValidationErrors: false,
},
() => this.createDataSource()
);
@@ -413,6 +419,7 @@ class DataSourceManagerComponent extends React.Component {
const ComponentToRender = isPlugin ? SourceComponent : SourceComponents[sourceComponentName] || SourceComponent;
return (
this.setState({ options })}
optionchanged={this.optionchanged}
@@ -988,7 +995,7 @@ class DataSourceManagerComponent extends React.Component {
this.onNameChanged(e.target.value)}
- className="form-control-plaintext form-control-plaintext-sm color-slate12"
+ className="form-control-plaintext form-control-plaintext-sm color-slate12 tw-border-x tw-border-y"
value={decodeEntities(selectedDataSource.name)}
style={{ width: '160px' }}
data-cy="data-source-name-input-field"
diff --git a/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx b/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx
index 9975bf42d4..5127f009af 100644
--- a/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx
+++ b/frontend/src/modules/dataSources/components/GlobalDataSources/index.jsx
@@ -249,7 +249,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
{suggestingDataSource ? (
@@ -307,7 +306,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
}, 100);
};
return (
-
+
diff --git a/frontend/src/modules/dataSources/components/List/index.jsx b/frontend/src/modules/dataSources/components/List/index.jsx
index 3ceb1864dc..25f46fd226 100644
--- a/frontend/src/modules/dataSources/components/List/index.jsx
+++ b/frontend/src/modules/dataSources/components/List/index.jsx
@@ -10,6 +10,7 @@ import { SearchBox } from '@/_components/SearchBox';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
import Modal from '@/HomePage/Modal';
+import { Button } from '@/components/ui/Button/Button';
export const List = ({ updateSelectedDatasource }) => {
const {
@@ -141,15 +142,18 @@ export const List = ({ updateSelectedDatasource }) => {
Data sources added{' '}
{!isLoading && filteredData && filteredData.length > 0 && `(${filteredData.length})`}
- {
setShowInput(true);
}}
- data-cy="added-ds-search-icon"
+ data-cy="create-new-folder-button"
>
-
-
+
+
>
) : (
REST URL to authenticate the requests of the Qdrant instance.",
- "encrypted": true
+ "helpText": "REST URL to authenticate the requests of the Qdrant instance."
},
"apiKey": {
"label": "API Key",
diff --git a/plugins/packages/common/lib/index.ts b/plugins/packages/common/lib/index.ts
index d520a59f10..8f43f8583a 100644
--- a/plugins/packages/common/lib/index.ts
+++ b/plugins/packages/common/lib/index.ts
@@ -6,6 +6,8 @@ import { QueryService } from './query_service.interface';
import {
isEmpty,
cacheConnection,
+ cacheConnectionWithConfiguration,
+ generateSourceOptionsHash,
getCachedConnection,
parseJson,
cleanSensitiveData,
@@ -37,6 +39,8 @@ export {
User,
App,
cacheConnection,
+ generateSourceOptionsHash,
+ cacheConnectionWithConfiguration,
getCachedConnection,
parseJson,
isEmpty,
diff --git a/plugins/packages/common/lib/utils.helper.ts b/plugins/packages/common/lib/utils.helper.ts
index 7b97e17199..7766ec4e76 100644
--- a/plugins/packages/common/lib/utils.helper.ts
+++ b/plugins/packages/common/lib/utils.helper.ts
@@ -1,6 +1,7 @@
import { QueryError } from './query.error';
import * as tls from 'tls';
import { readFileSync } from 'fs';
+import crypto from 'crypto';
const CACHED_CONNECTIONS: any = {};
@@ -17,8 +18,29 @@ export function cacheConnection(dataSourceId: string, connection: any): any {
CACHED_CONNECTIONS[dataSourceId] = { connection, updatedAt };
}
-export function getCachedConnection(dataSourceId: string | number, dataSourceUpdatedAt: any): any {
- const cachedData = CACHED_CONNECTIONS[dataSourceId];
+export function generateSourceOptionsHash(sourceOptions) {
+ const sortedEntries = Object.entries(sourceOptions)
+ .filter(([_, value]) => value !== undefined && value !== null && value !== '')
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([key, value]) => `${key}:${value}`)
+ .join('|');
+
+ return crypto.createHash('sha256').update(sortedEntries).digest('hex').substring(0, 16);
+}
+
+export function cacheConnectionWithConfiguration(dataSourceId: string, enhancedCacheKey: string, connection: any): any {
+ const updatedAt = new Date();
+ const allKeys = Object.keys(CACHED_CONNECTIONS);
+ const oldKeysForThisDatasource = allKeys.filter(
+ (key) => key.startsWith(`${dataSourceId}_`) && key !== enhancedCacheKey
+ );
+ oldKeysForThisDatasource.forEach((oldKey) => delete CACHED_CONNECTIONS[oldKey]);
+
+ CACHED_CONNECTIONS[enhancedCacheKey] = { connection, updatedAt };
+}
+
+export function getCachedConnection(cacheKey: string | number, dataSourceUpdatedAt: any): any {
+ const cachedData = CACHED_CONNECTIONS[cacheKey];
if (cachedData) {
const updatedAt = new Date(dataSourceUpdatedAt || null);
diff --git a/plugins/packages/mssql/lib/index.ts b/plugins/packages/mssql/lib/index.ts
index 73973d3fc5..aa7a93e506 100644
--- a/plugins/packages/mssql/lib/index.ts
+++ b/plugins/packages/mssql/lib/index.ts
@@ -4,7 +4,8 @@ import {
QueryError,
QueryResult,
QueryService,
- cacheConnection,
+ cacheConnectionWithConfiguration,
+ generateSourceOptionsHash,
getCachedConnection,
} from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
@@ -143,13 +144,15 @@ export default class MssqlQueryService implements QueryService {
dataSourceUpdatedAt?: string
): Promise {
if (checkCache) {
- let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
+ const optionsHash = generateSourceOptionsHash(sourceOptions);
+ const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
+ let connection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (connection) {
return connection;
} else {
connection = await this.buildConnection(sourceOptions);
- dataSourceId && cacheConnection(dataSourceId, connection);
+ cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection;
}
} else {
diff --git a/plugins/packages/mysql/lib/index.ts b/plugins/packages/mysql/lib/index.ts
index 2c3bbe80bb..395e51be71 100644
--- a/plugins/packages/mysql/lib/index.ts
+++ b/plugins/packages/mysql/lib/index.ts
@@ -1,6 +1,7 @@
import knex, { Knex } from 'knex';
import {
- cacheConnection,
+ cacheConnectionWithConfiguration,
+ generateSourceOptionsHash,
getCachedConnection,
ConnectionTestResult,
QueryService,
@@ -145,13 +146,17 @@ export default class MysqlQueryService implements QueryService {
dataSourceUpdatedAt?: string
): Promise {
if (checkCache) {
- const cachedConnection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
+ const optionsHash = generateSourceOptionsHash(sourceOptions);
+ const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
+ const cachedConnection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (cachedConnection) return cachedConnection;
+
+ const connection = await this.buildConnection(sourceOptions);
+ cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
+ return connection;
}
- const connection = await this.buildConnection(sourceOptions);
- if (checkCache && dataSourceId) cacheConnection(dataSourceId, connection);
- return connection;
+ return await this.buildConnection(sourceOptions);
}
buildBulkUpdateQuery(queryOptions: QueryOptions): string {
diff --git a/plugins/packages/oracledb/lib/index.ts b/plugins/packages/oracledb/lib/index.ts
index 735dfe5441..b8add969bc 100644
--- a/plugins/packages/oracledb/lib/index.ts
+++ b/plugins/packages/oracledb/lib/index.ts
@@ -1,7 +1,8 @@
import { Knex, knex } from 'knex';
import oracledb from 'oracledb';
import {
- cacheConnection,
+ cacheConnectionWithConfiguration,
+ generateSourceOptionsHash,
getCachedConnection,
ConnectionTestResult,
QueryService,
@@ -118,13 +119,15 @@ export default class OracledbQueryService implements QueryService {
dataSourceUpdatedAt?: string
): Promise {
if (checkCache) {
- let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
+ const optionsHash = generateSourceOptionsHash(sourceOptions);
+ const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
+ let connection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (connection) {
return connection;
} else {
connection = await this.buildConnection(sourceOptions);
- dataSourceId && cacheConnection(dataSourceId, connection);
+ cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection;
}
} else {
diff --git a/plugins/packages/postgresql/lib/index.ts b/plugins/packages/postgresql/lib/index.ts
index fbadc12723..eb36e9d7f6 100644
--- a/plugins/packages/postgresql/lib/index.ts
+++ b/plugins/packages/postgresql/lib/index.ts
@@ -1,6 +1,7 @@
import {
ConnectionTestResult,
- cacheConnection,
+ cacheConnectionWithConfiguration,
+ generateSourceOptionsHash,
getCachedConnection,
QueryService,
QueryResult,
@@ -145,13 +146,17 @@ export default class PostgresqlQueryService implements QueryService {
dataSourceUpdatedAt?: string
): Promise {
if (checkCache) {
- const cachedConnection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
+ const optionsHash = generateSourceOptionsHash(sourceOptions);
+ const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
+ const cachedConnection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (cachedConnection) return cachedConnection;
+
+ const connection = await this.buildConnection(sourceOptions);
+ cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
+ return connection;
}
- const connection = await this.buildConnection(sourceOptions);
- if (checkCache && dataSourceId) cacheConnection(dataSourceId, connection);
- return connection;
+ return await this.buildConnection(sourceOptions);
}
buildBulkUpdateQuery(queryOptions: QueryOptions): string {
diff --git a/plugins/packages/snowflake/lib/index.ts b/plugins/packages/snowflake/lib/index.ts
index a7fc47172f..1c1a0409d2 100644
--- a/plugins/packages/snowflake/lib/index.ts
+++ b/plugins/packages/snowflake/lib/index.ts
@@ -3,7 +3,8 @@ import {
QueryResult,
QueryService,
ConnectionTestResult,
- cacheConnection,
+ cacheConnectionWithConfiguration,
+ generateSourceOptionsHash,
getCachedConnection,
} from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
@@ -93,13 +94,15 @@ export default class Snowflake implements QueryService {
dataSourceUpdatedAt?: string
): Promise {
if (checkCache) {
- let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
+ const optionsHash = generateSourceOptionsHash(sourceOptions);
+ const enhancedCacheKey = `${dataSourceId}_${optionsHash}`;
+ let connection = await getCachedConnection(enhancedCacheKey, dataSourceUpdatedAt);
if (connection && (await connection.isValidAsync())) {
return connection;
} else {
connection = await this.buildConnection(sourceOptions);
- await cacheConnection(dataSourceId, connection);
+ cacheConnectionWithConfiguration(dataSourceId, enhancedCacheKey, connection);
return connection;
}
} else {
diff --git a/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts b/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts
index bc5be2b2ec..8f0b5a98ae 100644
--- a/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts
+++ b/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts
@@ -5,6 +5,7 @@ import { InternalTable } from '@entities/internal_table.entity';
import { MigrationProgress, processDataInBatches } from '@helpers/migration.helper';
import { getEnvVars } from 'scripts/database-config-utils';
import { EncryptionService } from '@modules/encryption/service';
+import { TOOLJET_EDITIONS } from '@modules/app/constants';
import {
createNewTjdbRole,
createAndGrantSchemaPrivilege,
@@ -20,6 +21,12 @@ const crypto = require('crypto');
export class MoveToolJetDatabaseTablesFromPublicToTenantSchema1721236971725 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
const envData = getEnvVars();
+ const isSqlModeDisabled = envData.TJDB_SQL_MODE_DISABLE == 'true';
+ const isCloud = envData.TOOLJET_EDITION == TOOLJET_EDITIONS.Cloud;
+ if (isSqlModeDisabled || isCloud) {
+ console.log('Skipping TJDB schema migration for SQL mode');
+ return;
+ }
const batchSize = 100;
const entityManager = queryRunner.manager;
const tooljetDbConnection = new DataSource({
diff --git a/server/ee b/server/ee
index 1aaf0d63c9..8c0e6dec37 160000
--- a/server/ee
+++ b/server/ee
@@ -1 +1 @@
-Subproject commit 1aaf0d63c9b55034503696a746167b3f32152360
+Subproject commit 8c0e6dec37f1b0bb7fb5552d8eef4db3ddc18b31
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/helpers/tooljet_db.helper.ts b/server/src/helpers/tooljet_db.helper.ts
index 67c9d8f3af..c53f0d5d0a 100644
--- a/server/src/helpers/tooljet_db.helper.ts
+++ b/server/src/helpers/tooljet_db.helper.ts
@@ -3,6 +3,8 @@ import { tooljetDbOrmconfig } from 'ormconfig';
import { OrganizationTjdbConfigurations } from 'src/entities/organization_tjdb_configurations.entity';
import { EntityManager, DataSource } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
+import { getTooljetEdition } from '@helpers/utils.helper';
+import { TOOLJET_EDITIONS } from '@modules/app/constants';
/**
* Creates a custom tooljet database connection using a tenant user, for the respective workspace.
@@ -40,6 +42,10 @@ export async function createTooljetDatabaseConnection(
}
export async function decryptTooljetDatabasePassword(password: string) {
+ if (isSQLModeDisabled()) {
+ return process.env.TOOLJET_DB_PASS;
+ }
+
const encryptionService = new EncryptionService();
const decryptedvalue = await encryptionService.decryptColumnValue(
'organization_tjdb_configurations',
@@ -60,9 +66,20 @@ export async function encryptTooljetDatabasePassword(password: string) {
}
export function findTenantSchema(organisationId: string): string {
+ if (isSQLModeDisabled()) {
+ return 'public';
+ }
+
return `workspace_${organisationId}`;
}
+// TODO: Cloud TJDB SQL mode is disabled: Use public schema for cloud edition
+// This is because Postgrest doesn't handle loading large amount of schemas in memory
+// We need to migrate to use Table based access control instead of schema based access control
+export function isSQLModeDisabled(): boolean {
+ return process.env.TJDB_SQL_MODE_DISABLE === 'true' || getTooljetEdition() === TOOLJET_EDITIONS.Cloud;
+}
+
export function concatSchemaAndTableName(schema: string, tableName: string) {
return `${schema}` + '.' + `${tableName}`;
}
@@ -244,7 +261,7 @@ export function validateTjdbJSONBColumnInputs(jsonbColumnList: Array, in
} else {
inValidValueColumnsList.push(key);
}
- } catch (error) {
+ } catch {
inValidValueColumnsList.push(key);
}
}
diff --git a/server/src/modules/ai/interfaces/IAgentsService.ts b/server/src/modules/ai/interfaces/IAgentsService.ts
index 924ada16ed..f31947f7a4 100644
--- a/server/src/modules/ai/interfaces/IAgentsService.ts
+++ b/server/src/modules/ai/interfaces/IAgentsService.ts
@@ -1,16 +1,4 @@
export interface IAgentsService {
- createComponent(prompt: string, organizationId: string): Promise;
-
- createQuery(prompt: string, tableName: string, columns: string, organizationId: string): Promise;
-
- createEvent(prompt: string, pageId: string[], organizationId: string): Promise;
-
- Agentic(prompt: string, organizationId: string): Promise;
-
- PromptEnrichment(prd_data: { content: string; metadata?: any }, organizationId: string): Promise;
-
- PromptEnrichmentChat(prompt: string, oldContext: any[], organizationId: string): Promise;
-
CreateTable(organizationId: string, tables: any): Promise;
docs(prompt: string, organizationId: string): Promise;
diff --git a/server/src/modules/ai/interfaces/IUtilService.ts b/server/src/modules/ai/interfaces/IUtilService.ts
index 83dd53d357..cb98cfdb16 100644
--- a/server/src/modules/ai/interfaces/IUtilService.ts
+++ b/server/src/modules/ai/interfaces/IUtilService.ts
@@ -34,10 +34,6 @@ export interface IAiUtilService {
getQueriesfromsteps(steps: any): Promise;
- createQuerySteps(prd: string, lld: string, tableName: any, components: any, organizationId: any): Promise;
-
- createEventSteps(prd: string, Query: any, components: any, organizationId: any): Promise;
-
convertToSteps(jsonData: any): Promise;
getColorScheme(prd: any): any;
diff --git a/server/src/modules/ai/module.ts b/server/src/modules/ai/module.ts
index af11dcb518..108ccf6c53 100644
--- a/server/src/modules/ai/module.ts
+++ b/server/src/modules/ai/module.ts
@@ -12,6 +12,7 @@ import { AppPermissionsModule } from '@modules/app-permissions/module';
import { ImportExportResourcesModule } from '@modules/import-export-resources/module';
import { ArtifactRepository } from './repositories/artifact.repository';
import { SubModule } from '@modules/app/sub-module';
+import { DataQueryRepository } from '@modules/data-queries/repository';
export class AiModule extends SubModule {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise {
@@ -47,6 +48,7 @@ export class AiModule extends SubModule {
AiResponseVoteRepository,
FeatureAbilityFactory,
ArtifactRepository,
+ DataQueryRepository,
EventsService,
],
diff --git a/server/src/modules/ai/services/agents.service.ts b/server/src/modules/ai/services/agents.service.ts
index 67fb9dd963..ad6059363b 100644
--- a/server/src/modules/ai/services/agents.service.ts
+++ b/server/src/modules/ai/services/agents.service.ts
@@ -4,30 +4,6 @@ import { IAgentsService } from '../interfaces/IAgentsService';
@Injectable()
export class AgentsService implements IAgentsService {
constructor() {}
- // Agents methods
- async createComponent(prompt: string, organizationId): Promise {
- throw new Error('Method not implemented.');
- }
-
- async createQuery(prompt: string, tableName: string, columns: string, organizationId): Promise {
- throw new Error('Method not implemented.');
- }
-
- async createEvent(prompt: string, pageId: string[], organizationId): Promise {
- throw new Error('Method not implemented.');
- }
-
- async Agentic(prompt: string, organizationId): Promise {
- throw new Error('Method not implemented.');
- }
-
- async PromptEnrichment(prd_data: { content: string; metadata?: any }, organizationId: string): Promise {
- throw new Error('Method not implemented.');
- }
-
- async PromptEnrichmentChat(prompt: string, oldContext: any[], organizationId): Promise {
- throw new Error('Method not implemented.');
- }
async CreateTable(organizationId: string, tables): Promise {
throw new Error('Method not implemented.');
diff --git a/server/src/modules/ai/util.service.ts b/server/src/modules/ai/util.service.ts
index e90233d5e2..157fb97396 100644
--- a/server/src/modules/ai/util.service.ts
+++ b/server/src/modules/ai/util.service.ts
@@ -2,7 +2,7 @@ import { IAiUtilService } from './interfaces/IUtilService';
export class AiUtilService implements IAiUtilService {
constructor() {}
-
+
public getAgentAssetPath(filename) {
throw new Error('Method not implemented.');
}
@@ -35,14 +35,6 @@ export class AiUtilService implements IAiUtilService {
throw new Error('Method not implemented.');
}
- async createQuerySteps(prd: string, lld: string, tableName, components, organizationId) {
- throw new Error('Method not implemented.');
- }
-
- async createEventSteps(prd: string, Query: any, components: any, organizationId: any): Promise {
- throw new Error('Method not implemented.');
- }
-
async convertToSteps(jsonData: any): Promise {
throw new Error('Method not implemented.');
}
diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts
index 24c78aaaa5..aae67da7ba 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';
@@ -54,6 +56,7 @@ import { SessionScheduler } from '@modules/session/scheduler';
import { AuditLogsClearScheduler } from '@modules/audit-logs/scheduler';
import { ModulesModule } from '@modules/modules/module';
import { EmailListenerModule } from '@modules/email-listener/module';
+import { InMemoryCacheModule } from '@modules/inMemoryCache/module';
export class AppModule implements OnModuleInit {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise {
// Load static and dynamic modules
@@ -68,7 +71,7 @@ export class AppModule implements OnModuleInit {
* â–ˆ â–ˆ
* ████████████████████████████████████████████████████████████████████
*/
- const imports = [
+ const baseImports = [
await AbilityModule.forRoot(configs),
await LicenseModule.forRoot(configs),
await FilesModule.register(configs),
@@ -103,7 +106,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),
@@ -115,8 +117,16 @@ export class AppModule implements OnModuleInit {
await CrmModule.register(configs),
await OrganizationPaymentModule.register(configs),
await EmailListenerModule.register(configs),
+ await InMemoryCacheModule.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..4a4d0dde56 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[]
@@ -1849,6 +1897,7 @@ export class AppImportExportService {
key: key,
value: options[key]['value'],
encrypted: options[key]['encrypted'],
+ workspace_constant: options[key]['workspace_constant'],
};
});
}
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/module.ts b/server/src/modules/data-queries/module.ts
index aad66d3218..e749cd7959 100644
--- a/server/src/modules/data-queries/module.ts
+++ b/server/src/modules/data-queries/module.ts
@@ -37,7 +37,7 @@ export class DataQueriesModule extends SubModule {
AppFeatureAbilityFactory,
DataSourceFeatureAbilityFactory,
],
- exports: [DataQueriesUtilService, DataQueriesService],
+ exports: [DataQueriesUtilService],
controllers: [DataQueriesController],
};
}
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-queries/service.ts b/server/src/modules/data-queries/service.ts
index 34df6c8cd4..2ec6ea2e08 100644
--- a/server/src/modules/data-queries/service.ts
+++ b/server/src/modules/data-queries/service.ts
@@ -3,7 +3,6 @@ import { EntityManager, In } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { DataSource } from 'src/entities/data_source.entity';
import { dbTransactionWrap } from 'src/helpers/database.helper';
-import { DataSourceTypes } from '@modules/data-sources/constants';
import { Response } from 'express';
import { DataQueryRepository } from './repository';
import { decode } from 'js-base64';
@@ -22,14 +21,7 @@ export class DataQueriesService implements IDataQueriesService {
protected readonly dataQueryRepository: DataQueryRepository,
protected readonly dataQueryUtilService: DataQueriesUtilService,
protected readonly dataSourceRepository: DataSourcesRepository
- ) { }
-
- async findOne(dataQueryId: string): Promise {
- return await this.dataQueryRepository.findOne({
- where: { id: dataQueryId },
- relations: ['dataSource', 'apps', 'dataSource.apps', 'plugins'],
- });
- }
+ ) {}
async getAll(user: User, versionId: string, mode?: string) {
const queries = await this.dataQueryRepository.getAll(versionId);
diff --git a/server/src/modules/data-sources/module.ts b/server/src/modules/data-sources/module.ts
index 31c1e50463..38e7ad0ef7 100644
--- a/server/src/modules/data-sources/module.ts
+++ b/server/src/modules/data-sources/module.ts
@@ -12,6 +12,7 @@ import { TooljetDbModule } from '@modules/tooljet-db/module';
import { OrganizationRepository } from '@modules/organizations/repository';
import { SessionModule } from '@modules/session/module';
import { SubModule } from '@modules/app/sub-module';
+import { InMemoryCacheModule } from '@modules/inMemoryCache/module';
export class DataSourcesModule extends SubModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise {
@@ -40,6 +41,7 @@ export class DataSourcesModule extends SubModule {
await InstanceSettingsModule.register(configs),
await TooljetDbModule.register(configs),
await SessionModule.register(configs),
+ await InMemoryCacheModule.register(configs),
],
providers: [
DataSourcesService,
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/data-sources/util.service.ts b/server/src/modules/data-sources/util.service.ts
index fe3601e823..77c251b352 100644
--- a/server/src/modules/data-sources/util.service.ts
+++ b/server/src/modules/data-sources/util.service.ts
@@ -21,6 +21,7 @@ import { PluginsServiceSelector } from './services/plugin-selector.service';
import { OrganizationConstantsUtilService } from '@modules/organization-constants/util.service';
import { DataSourceOptions } from '@entities/data_source_options.entity';
import { IDataSourcesUtilService } from './interfaces/IUtilService';
+import { InMemoryCacheService } from '@modules/inMemoryCache/in-memory-cache.service';
@Injectable()
export class DataSourcesUtilService implements IDataSourcesUtilService {
@@ -31,7 +32,8 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
protected readonly licenseTermsService: LicenseTermsService,
protected readonly encryptionService: EncryptionService,
protected readonly pluginsServiceSelector: PluginsServiceSelector,
- protected readonly organizationConstantsUtilService: OrganizationConstantsUtilService
+ protected readonly organizationConstantsUtilService: OrganizationConstantsUtilService,
+ protected readonly inMemoryCacheService: InMemoryCacheService
) {}
async create(createArgumentsDto: CreateArgumentsDto, user: User): Promise {
return await dbTransactionWrap(async (manager: EntityManager) => {
@@ -103,15 +105,25 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
for (const option of optionsWithOauth) {
if (option['encrypted']) {
- const credential = await this.credentialService.create(
- resetSecureData ? '' : option['value'] || '',
- entityManager
- );
+ if (option['workspace_constant']) {
+ const credential = await this.credentialService.create(option['workspace_constant'], entityManager);
- parsedOptions[option['key']] = {
- credential_id: credential.id,
- encrypted: option['encrypted'],
- };
+ parsedOptions[option['key']] = {
+ credential_id: credential.id,
+ workspace_constant: option['workspace_constant'],
+ encrypted: option['encrypted'],
+ };
+ } else {
+ const credential = await this.credentialService.create(
+ resetSecureData ? '' : option['value'] || '',
+ entityManager
+ );
+
+ parsedOptions[option['key']] = {
+ credential_id: credential.id,
+ encrypted: option['encrypted'],
+ };
+ }
} else {
parsedOptions[option['key']] = {
value: option['value'],
@@ -135,7 +147,17 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
const queryService = await this.pluginsServiceSelector.getService(plugin_id, provider);
// const queryService = new allPlugins[provider]();
- const accessDetails = await queryService.accessDetailsFrom(authCode, options, resetSecureData);
+ let accessDetailsPromise: Promise;
+
+ const cacheKey = `${provider}_${authCode}`;
+
+ if (this.inMemoryCacheService.has(cacheKey)) {
+ accessDetailsPromise = this.inMemoryCacheService.get(cacheKey);
+ } else {
+ accessDetailsPromise = queryService.accessDetailsFrom(authCode, options, resetSecureData);
+ this.inMemoryCacheService.set(cacheKey, accessDetailsPromise);
+ }
+ const accessDetails = await accessDetailsPromise;
for (const row of accessDetails) {
const option = {};
@@ -165,52 +187,58 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
throw new BadRequestException('Cannot update configuration of sample data source');
}
- await dbTransactionWrap(async (manager: EntityManager) => {
- const isMultiEnvEnabled = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT, organizationId);
- const envToUpdate = await this.appEnvironmentUtilService.get(organizationId, environmentId, false, manager);
+ try {
+ await dbTransactionWrap(async (manager: EntityManager) => {
+ const isMultiEnvEnabled = await this.licenseTermsService.getLicenseTerms(
+ LICENSE_FIELD.MULTI_ENVIRONMENT,
+ organizationId
+ );
+ const envToUpdate = await this.appEnvironmentUtilService.get(organizationId, environmentId, false, manager);
+ // if datasource is restapi then reset the token data
+ if (dataSource.kind === 'restapi')
+ options.push({
+ key: 'tokenData',
+ value: undefined,
+ encrypted: false,
+ });
- // if datasource is restapi then reset the token data
- if (dataSource.kind === 'restapi')
- options.push({
- key: 'tokenData',
- value: undefined,
- encrypted: false,
- });
-
- if (isMultiEnvEnabled) {
- dataSource.options = (
- await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, envToUpdate.id)
- ).options;
-
- const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
- await this.appEnvironmentUtilService.updateOptions(newOptions, envToUpdate.id, dataSource.id, manager);
- } else {
- const allEnvs = await this.appEnvironmentUtilService.getAll(organizationId);
- /*
- Basic plan customer. lets update all environment options.
- this will help us to run the queries successfully when the user buys enterprise plan
- */
-
- const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
- for (const env of allEnvs) {
+ if (isMultiEnvEnabled) {
dataSource.options = (
- await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, env.id)
+ await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, envToUpdate.id)
).options;
- await this.appEnvironmentUtilService.updateOptions(newOptions, env.id, dataSource.id, manager);
+ const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
+ await this.appEnvironmentUtilService.updateOptions(newOptions, envToUpdate.id, dataSource.id, manager);
+ } else {
+ const allEnvs = await this.appEnvironmentUtilService.getAll(organizationId);
+ /*
+ Basic plan customer. lets update all environment options.
+ this will help us to run the queries successfully when the user buys enterprise plan
+ */
+
+ for (const env of allEnvs) {
+ dataSource.options = (
+ await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, env.id)
+ ).options;
+ const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
+
+ await this.appEnvironmentUtilService.updateOptions(newOptions, env.id, dataSource.id, manager);
+ }
}
- }
- const updatableParams = {
- id: dataSourceId,
- name,
- updatedAt: new Date(),
- };
+ const updatableParams = {
+ id: dataSourceId,
+ name,
+ updatedAt: new Date(),
+ };
- // Remove keys with undefined values
- cleanObject(updatableParams);
+ // Remove keys with undefined values
+ cleanObject(updatableParams);
- await manager.save(DataSource, updatableParams);
- });
+ await manager.save(DataSource, updatableParams);
+ });
+ } finally {
+ this.inMemoryCacheService.clear();
+ }
}
async decrypt(options: Record) {
@@ -233,33 +261,67 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
const optionsWithOauth = await this.parseOptionsForOauthDataSource(options);
const parsedOptions = {};
+
+ if (dataSource?.options) {
+ for (const key in dataSource.options) {
+ if (dataSource.options[key]?.workspace_constant) {
+ parsedOptions[key] = {
+ workspace_constant: dataSource.options[key].workspace_constant,
+ credential_id: dataSource.options[key].credential_id,
+ encrypted: dataSource.options[key].encrypted,
+ };
+ }
+ }
+ }
+
return await dbTransactionWrap(async (entityManager: EntityManager) => {
for (const option of optionsWithOauth) {
+ const key = option['key'];
+ const credentialValue = option['value'];
+
if (option['encrypted']) {
const existingCredentialId =
- dataSource?.options &&
- dataSource.options[option['key']] &&
- dataSource.options[option['key']]['credential_id'];
+ dataSource?.options && dataSource.options[key] && dataSource.options[key]['credential_id'];
+
+ if (credentialValue && (credentialValue.includes('{{constants') || credentialValue.includes('{{secrets'))) {
+ if (!parsedOptions[key]) {
+ parsedOptions[key] = {};
+ }
+ parsedOptions[key].workspace_constant = credentialValue;
+ } else {
+ if (
+ existingCredentialId &&
+ credentialValue !== undefined &&
+ credentialValue !== (await this.credentialService.getValue(existingCredentialId))
+ ) {
+ if (parsedOptions[key]) {
+ delete parsedOptions[key].workspace_constant;
+ }
+ }
+ }
if (existingCredentialId) {
- (option['value'] || option['value'] === '') &&
- (await this.credentialService.update(existingCredentialId, option['value'] || ''));
+ if (credentialValue !== undefined) {
+ await this.credentialService.update(existingCredentialId, credentialValue || '');
+ }
- parsedOptions[option['key']] = {
- credential_id: existingCredentialId,
- encrypted: option['encrypted'],
- };
+ if (!parsedOptions[key]) {
+ parsedOptions[key] = {};
+ }
+ parsedOptions[key].credential_id = existingCredentialId;
+ parsedOptions[key].encrypted = option['encrypted'];
} else {
- const credential = await this.credentialService.create(option['value'] || '', entityManager);
+ const credential = await this.credentialService.create(credentialValue || '', entityManager);
- parsedOptions[option['key']] = {
- credential_id: credential.id,
- encrypted: option['encrypted'],
- };
+ if (!parsedOptions[key]) {
+ parsedOptions[key] = {};
+ }
+ parsedOptions[key].credential_id = credential.id;
+ parsedOptions[key].encrypted = option['encrypted'];
}
} else {
- parsedOptions[option['key']] = {
- value: option['value'],
+ parsedOptions[key] = {
+ value: credentialValue,
encrypted: false,
};
}
@@ -412,6 +474,7 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
const credentialId = parsedOptions[key]?.['credential_id'];
if (credentialId) {
const encryptedKeyValue = await this.credentialService.getValue(credentialId);
+ constantMatcher.lastIndex = 0;
//check if encrypted key value is a constant
if (constantMatcher.test(encryptedKeyValue)) {
@@ -485,7 +548,10 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
const envToUpdate = await this.appEnvironmentUtilService.get(organizationId, environmentId, false, manager);
const oldOptions = dataSource.options || {};
const updatedOptions = { ...oldOptions, ...parsedOptions };
- const isMultiEnvEnabled = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT,organizationId);
+ const isMultiEnvEnabled = await this.licenseTermsService.getLicenseTerms(
+ LICENSE_FIELD.MULTI_ENVIRONMENT,
+ organizationId
+ );
if (isMultiEnvEnabled) {
await this.appEnvironmentUtilService.updateOptions(updatedOptions, envToUpdate.id, dataSourceId, manager);
@@ -581,7 +647,7 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
let errorObj = {};
try {
errorObj = JSON.parse(error);
- } catch (err) {
+ } catch (error) {
errorObj['error_details'] = error;
}
@@ -612,6 +678,7 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
for (const key of Object.keys(options)) {
const currentOption = options[key]?.['value'];
+ constantMatcher.lastIndex = 0;
//! request options are nested arrays with constants and variables
if (Array.isArray(currentOption)) {
@@ -690,9 +757,9 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
if (existingAccessTokenCredentialId) {
await this.credentialService.update(existingAccessTokenCredentialId, accessTokenDetails['access_token']);
- existingRefreshTokenCredentialId &&
- accessTokenDetails['refresh_token'] &&
- (await this.credentialService.update(existingRefreshTokenCredentialId, accessTokenDetails['refresh_token']));
+ if (existingRefreshTokenCredentialId && accessTokenDetails['refresh_token']) {
+ await this.credentialService.update(existingRefreshTokenCredentialId, accessTokenDetails['refresh_token']);
+ }
} else if (dataSourceId) {
const isMultiAuthEnabled = dataSourceOptions['multiple_auth_enabled']?.value;
const updatedTokenData = this.changeCurrentToken(
diff --git a/server/src/modules/inMemoryCache/in-memory-cache.service.ts b/server/src/modules/inMemoryCache/in-memory-cache.service.ts
new file mode 100644
index 0000000000..ab378edb9a
--- /dev/null
+++ b/server/src/modules/inMemoryCache/in-memory-cache.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { ICacheService } from './interfaces/IUtilService';
+
+@Injectable()
+export class InMemoryCacheService implements ICacheService {
+ private static cacheStore: Map> = new Map();
+
+ set(key: string, value: Promise): void {
+ InMemoryCacheService.cacheStore.set(key, value);
+ }
+
+ get(key: string): Promise | undefined {
+ return InMemoryCacheService.cacheStore.get(key);
+ }
+
+ has(key: string): boolean {
+ return InMemoryCacheService.cacheStore.has(key);
+ }
+
+ clear(): void {
+ InMemoryCacheService.cacheStore.clear();
+ }
+}
diff --git a/server/src/modules/inMemoryCache/interfaces/IUtilService.ts b/server/src/modules/inMemoryCache/interfaces/IUtilService.ts
new file mode 100644
index 0000000000..ce774ebb82
--- /dev/null
+++ b/server/src/modules/inMemoryCache/interfaces/IUtilService.ts
@@ -0,0 +1,27 @@
+export interface ICacheService {
+ /**
+ * Store a promise value in the cache with the given key
+ * @param key - The cache key
+ * @param value - The promise to cache
+ */
+ set(key: string, value: Promise): void;
+
+ /**
+ * Retrieve a cached promise by key
+ * @param key - The cache key
+ * @returns The cached promise or undefined if not found
+ */
+ get(key: string): Promise | undefined;
+
+ /**
+ * Check if a key exists in the cache
+ * @param key - The cache key
+ * @returns True if the key exists, false otherwise
+ */
+ has(key: string): boolean;
+
+ /**
+ * Clear all cached entries
+ */
+ clear(): void;
+}
diff --git a/server/src/modules/inMemoryCache/module.ts b/server/src/modules/inMemoryCache/module.ts
new file mode 100644
index 0000000000..69b897ee61
--- /dev/null
+++ b/server/src/modules/inMemoryCache/module.ts
@@ -0,0 +1,15 @@
+import { SubModule } from '@modules/app/sub-module';
+import { DynamicModule } from '@nestjs/common';
+
+export class InMemoryCacheModule extends SubModule {
+ static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise {
+ const { InMemoryCacheService } = await this.getProviders(configs, 'inMemoryCache', ['in-memory-cache.service']);
+
+ return {
+ module: InMemoryCacheModule,
+ controllers: [],
+ providers: [InMemoryCacheService],
+ exports: [InMemoryCacheService],
+ };
+ }
+}
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/tooljet-db/helper.ts b/server/src/modules/tooljet-db/helper.ts
index 5dab99a0d2..02642892b6 100644
--- a/server/src/modules/tooljet-db/helper.ts
+++ b/server/src/modules/tooljet-db/helper.ts
@@ -27,3 +27,31 @@ export async function reconfigurePostgrest(
throw error;
}
}
+
+/**
+ * Cloud TJDB SQL Disabled: Postgrest configuration without schema synchronization
+ * Postgres schema for each workspace is not loaded into Postgrest.
+ */
+export async function reconfigurePostgrestWithoutSchemaSync(
+ tooljetDbManager: EntityManager,
+ options: { user: string; enableAggregates: boolean; statementTimeoutInSecs: number }
+) {
+ try {
+ await tooljetDbManager.transaction(async (transactionalEntityManager) => {
+ await transactionalEntityManager.queryRunner.query('CREATE SCHEMA IF NOT EXISTS postgrest');
+ await transactionalEntityManager.queryRunner.query(`GRANT USAGE ON SCHEMA postgrest to ${options.user}`);
+ await transactionalEntityManager.queryRunner.query(`create or replace function postgrest.pre_config()
+ returns void as $$
+ select
+ set_config('pgrst.db_aggregates_enabled', '${options.enableAggregates}', false);
+ $$ language sql;
+ `);
+ await transactionalEntityManager.queryRunner.query(
+ `ALTER ROLE ${options.user} SET statement_timeout TO '${options.statementTimeoutInSecs}s'`
+ );
+ });
+ } catch (error) {
+ console.error('The tooljet database reconfiguration process encountered an error.', error);
+ throw error;
+ }
+}
diff --git a/server/src/modules/tooljet-db/module.ts b/server/src/modules/tooljet-db/module.ts
index f0ccb9f602..a28c62f6dd 100644
--- a/server/src/modules/tooljet-db/module.ts
+++ b/server/src/modules/tooljet-db/module.ts
@@ -6,12 +6,15 @@ import { Logger } from 'nestjs-pino';
import { Credential } from '../../../src/entities/credential.entity';
import { InternalTable } from 'src/entities/internal_table.entity';
import { AppUser } from 'src/entities/app_user.entity';
-import { reconfigurePostgrest } from './helper';
+import { reconfigurePostgrest, reconfigurePostgrestWithoutSchemaSync } from './helper';
+import { getTooljetEdition } from '@helpers/utils.helper';
+import { TOOLJET_EDITIONS } from '@modules/app/constants';
import { TableCountGuard } from '@modules/licensing/guards/table.guard';
import { AbilityUtilService } from '@modules/ability/util.service';
import { RolesRepository } from '@modules/roles/repository';
import { FeatureAbilityFactory } from './ability';
import { SubModule } from '@modules/app/sub-module';
+import { isSQLModeDisabled } from '@helpers/tooljet_db.helper';
export class TooljetDbModule extends SubModule implements OnModuleInit {
constructor(
@@ -69,11 +72,20 @@ export class TooljetDbModule extends SubModule implements OnModuleInit {
const statementTimeout = this.configService.get('TOOLJET_DB_STATEMENT_TIMEOUT') || 60000;
const statementTimeoutInSecs = Number.isNaN(Number(statementTimeout)) ? 60 : Number(statementTimeout) / 1000;
- await reconfigurePostgrest(this.tooljetDbManager, {
- user: tooljtDbUser,
- enableAggregates: true,
- statementTimeoutInSecs: statementTimeoutInSecs,
- });
+ if (isSQLModeDisabled()) {
+ await reconfigurePostgrestWithoutSchemaSync(this.tooljetDbManager, {
+ user: tooljtDbUser,
+ enableAggregates: true,
+ statementTimeoutInSecs: statementTimeoutInSecs,
+ });
+ } else {
+ await reconfigurePostgrest(this.tooljetDbManager, {
+ user: tooljtDbUser,
+ enableAggregates: true,
+ statementTimeoutInSecs: statementTimeoutInSecs,
+ });
+ }
+
await this.tooljetDbManager.query("NOTIFY pgrst, 'reload schema'");
}
}
diff --git a/server/src/modules/tooljet-db/services/postgrest-proxy.service.ts b/server/src/modules/tooljet-db/services/postgrest-proxy.service.ts
index 3743d8ad38..0103595d96 100644
--- a/server/src/modules/tooljet-db/services/postgrest-proxy.service.ts
+++ b/server/src/modules/tooljet-db/services/postgrest-proxy.service.ts
@@ -8,7 +8,7 @@ import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import got from 'got';
import { TooljetDbTableOperationsService } from './tooljet-db-table-operations.service';
-import { validateTjdbJSONBColumnInputs } from 'src/helpers/tooljet_db.helper';
+import { isSQLModeDisabled, validateTjdbJSONBColumnInputs } from 'src/helpers/tooljet_db.helper';
import { QueryError } from '@modules/data-sources/types';
import { PostgrestError, TooljetDatabaseError, TooljetDbActions } from '../types';
import { maybeSetSubPath } from '@helpers/utils.helper';
@@ -28,8 +28,16 @@ export class PostgrestProxyService {
async proxy(req, res, next) {
const organizationId = req.headers['tj-workspace-id'] || req.dataQuery?.app?.organizationId;
- const dbUser = `user_${organizationId}`;
- const dbSchema = `workspace_${organizationId}`;
+ const { dbUser, dbSchema } = isSQLModeDisabled()
+ ? {
+ dbUser: this.configService.get('TOOLJET_DB_USER'),
+ dbSchema: 'public',
+ }
+ : {
+ dbUser: `user_${organizationId}`,
+ dbSchema: `workspace_${organizationId}`,
+ };
+
const authToken = 'Bearer ' + this.signJwtPayload(dbUser);
req.url = await this.replaceTableNamesAtPlaceholder(req.url, organizationId);
@@ -92,8 +100,16 @@ export class PostgrestProxyService {
body: Record = {}
) {
try {
- const dbUser = `user_${headers['tj-workspace-id']}`;
- const dbSchema = `workspace_${headers['tj-workspace-id']}`;
+ const { dbUser, dbSchema } = isSQLModeDisabled()
+ ? {
+ dbUser: this.configService.get('TOOLJET_DB_USER'),
+ dbSchema: 'public',
+ }
+ : {
+ dbUser: `user_${headers['tj-workspace-id']}`,
+ dbSchema: `workspace_${headers['tj-workspace-id']}`,
+ };
+
const authToken = 'Bearer ' + this.signJwtPayload(dbUser);
const updatedPath = replaceUrlForPostgrest(url);
let postgrestUrl = (this.configService.get('PGRST_HOST') || 'http://localhost:3001') + updatedPath;
diff --git a/server/src/modules/tooljet-db/services/tooljet-db-data-operations.service.ts b/server/src/modules/tooljet-db/services/tooljet-db-data-operations.service.ts
index b0ef19d4a2..5aaca3f841 100644
--- a/server/src/modules/tooljet-db/services/tooljet-db-data-operations.service.ts
+++ b/server/src/modules/tooljet-db/services/tooljet-db-data-operations.service.ts
@@ -4,11 +4,13 @@ import { QueryService, QueryResult, QueryError } from '@tooljet/plugins/dist/pac
import { TooljetDbTableOperationsService } from './tooljet-db-table-operations.service';
import { isEmpty } from 'lodash';
import { maybeSetSubPath } from 'src/helpers/utils.helper';
+
import { AST, Parser } from 'node-sql-parser/build/postgresql';
import {
createTooljetDatabaseConnection,
decryptTooljetDatabasePassword,
findTenantSchema,
+ isSQLModeDisabled,
modifyTjdbErrorObject,
} from 'src/helpers/tooljet_db.helper';
import { EntityManager, In, QueryFailedError } from 'typeorm';
@@ -169,10 +171,10 @@ export class TooljetDbDataOperationsService implements QueryService {
);
if (groupByAndAggregateQueryList.length) query.push(`select=${groupByAndAggregateQueryList.join(',')}`);
}
- !isEmpty(whereQuery) && query.push(whereQuery);
- !isEmpty(orderQuery) && query.push(orderQuery);
- !isEmpty(limit) && query.push(`limit=${limit}`);
- !isEmpty(offset) && query.push(`offset=${offset}`);
+ if (!isEmpty(whereQuery)) query.push(whereQuery);
+ if (!isEmpty(orderQuery)) query.push(orderQuery);
+ if (!isEmpty(limit)) query.push(`limit=${limit}`);
+ if (!isEmpty(offset)) query.push(`offset=${offset}`);
}
const headers = { 'data-query-id': queryOptions.id, 'tj-workspace-id': organizationId };
@@ -218,7 +220,7 @@ export class TooljetDbDataOperationsService implements QueryService {
return Object.assign(acc, { [colOpts.column]: colOpts.value });
}, {});
- !isEmpty(whereQuery) && query.push(whereQuery);
+ if (!isEmpty(whereQuery)) query.push(whereQuery);
const headers = { 'data-query-id': queryOptions.id, 'tj-workspace-id': organizationId };
const url = maybeSetSubPath(`/api/tooljet-db/proxy/${tableId}?` + query.join('&') + '&order=id');
@@ -251,8 +253,8 @@ export class TooljetDbDataOperationsService implements QueryService {
throw new QueryError('An incorrect limit value.', 'Limit should be a valid integer', {});
}
- !isEmpty(whereQuery) && query.push(whereQuery);
- limit && limit !== '' && query.push(`limit=${limit}&order=id`);
+ if (!isEmpty(whereQuery)) query.push(whereQuery);
+ if (limit && limit !== '') query.push(`limit=${limit}&order=id`);
const headers = { 'data-query-id': queryOptions.id, 'tj-workspace-id': organizationId };
const url = maybeSetSubPath(`/api/tooljet-db/proxy/${tableId}?` + query.join('&'));
@@ -325,7 +327,7 @@ export class TooljetDbDataOperationsService implements QueryService {
}
async sqlExecution(queryOptions, context): Promise {
- if (this.configService.get('TJDB_SQL_MODE_DISABLE') === 'true')
+ if (isSQLModeDisabled())
throw new QueryError('SQL execution is disabled', 'Contact Admin to enable SQL execution', {});
const { organization_id: organizationId } = context.app;
diff --git a/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts b/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts
index add57838ae..47ae973e4d 100644
--- a/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts
+++ b/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts
@@ -27,6 +27,7 @@ import {
createTooljetDatabaseConnection,
decryptTooljetDatabasePassword,
grantTenantRoleToTjdbAdminRole,
+ isSQLModeDisabled,
} from 'src/helpers/tooljet_db.helper';
import { OrganizationTjdbConfigurations } from 'src/entities/organization_tjdb_configurations.entity';
const crypto = require('crypto');
@@ -831,10 +832,10 @@ export class TooljetDbTableOperationsService {
}
async getTablesLimit(organizationId: string) {
- const licenseTerms = await this.licenseTermsService.getLicenseTerms([
- LICENSE_FIELD.TABLE_COUNT,
- LICENSE_FIELD.STATUS,
- ], organizationId);
+ const licenseTerms = await this.licenseTermsService.getLicenseTerms(
+ [LICENSE_FIELD.TABLE_COUNT, LICENSE_FIELD.STATUS],
+ organizationId
+ );
return {
tablesCount: generatePayloadForLimits(
licenseTerms[LICENSE_FIELD.TABLE_COUNT] !== LICENSE_LIMIT.UNLIMITED
@@ -851,9 +852,15 @@ export class TooljetDbTableOperationsService {
const { joinQueryJson, dataQuery, user } = params;
if (!Object.keys(joinQueryJson).length) throw new BadRequestException("Input can't be empty");
- const tjdbTenantConfigs = await this.manager.findOne(OrganizationTjdbConfigurations, {
- where: { organizationId },
- });
+ const tjdbTenantConfigs = isSQLModeDisabled()
+ ? {
+ pgUser: this.configService.get('TOOLJET_DB_USER'),
+ pgPassword: this.configService.get('TOOLJET_DB_PASS'),
+ }
+ : await this.manager.findOne(OrganizationTjdbConfigurations, {
+ where: { organizationId },
+ });
+
if (!tjdbTenantConfigs) throw new NotFoundException(`Tooljet database schema configuration doesn't exists`);
// Gathering tables used, from Join coditions
@@ -931,7 +938,7 @@ export class TooljetDbTableOperationsService {
protected buildJoinQuery(
queryJson,
internalTableIdToNameMap,
- // eslint-disable-next-line
+
tooljetDbTenantConnection: Connection
): SelectQueryBuilder {
const queryBuilder: SelectQueryBuilder = tooljetDbTenantConnection.createQueryBuilder();
@@ -1524,6 +1531,8 @@ export class TooljetDbTableOperationsService {
}
async createTooljetDbTenantSchemaAndRole(organizationId: string, entityManager: EntityManager) {
+ if (isSQLModeDisabled()) return;
+
const dbUser = `user_${organizationId}`;
const dbSchema = `workspace_${organizationId}`;
const dbPassword = crypto.randomBytes(8).toString('hex');
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/versions/controllers/components.controller.ts b/server/src/modules/versions/controllers/components.controller.ts
index e91e30e791..22c7c22e91 100644
--- a/server/src/modules/versions/controllers/components.controller.ts
+++ b/server/src/modules/versions/controllers/components.controller.ts
@@ -24,7 +24,7 @@ import { IComponentsController } from '../interfaces/controllers/IComponentsCont
version: '2',
})
export class ComponentsController implements IComponentsController {
- constructor(protected readonly componentsService: ComponentsService) {}
+ constructor(protected readonly componentsService: ComponentsService) { }
@InitFeature(FEATURE_KEY.CREATE_COMPONENTS)
@UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard)
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;
| | |