Merge branch 'main' into appbuilder/sprint-14

This commit is contained in:
johnsoncherian 2025-06-23 11:57:26 +05:30
commit 42f81d3e79
33 changed files with 893 additions and 5126 deletions

View file

@ -5,213 +5,214 @@ import { commonText } from "Texts/common";
import { exportAppModalText } from "Texts/exportImport";
import {
clickOnExportButtonAndVerify,
exportAllVersionsAndVerify,
verifyElementsOfExportModal,
clickOnExportButtonAndVerify,
exportAllVersionsAndVerify,
verifyElementsOfExportModal,
} from "Support/utils/exportImport";
import { selectAppCardOption, closeModal } from "Support/utils/common";
describe("App Export", () => {
const TEST_DATA = {
appFiles: {
multiVersion: "cypress/fixtures/templates/three-versions.json",
singleVersion: "cypress/fixtures/templates/one_version.json",
},
};
const TEST_DATA = {
appFiles: {
multiVersion: "cypress/fixtures/templates/three-versions.json",
singleVersion: "cypress/fixtures/templates/one_version.json",
},
};
let data;
let data;
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
beforeEach(() => {
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.exec("mkdir -p ./cypress/downloads/");
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
beforeEach(() => {
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
appName: `${fake.companyName}-IE-App`,
appReName: `${fake.companyName}-${fake.companyName}-IE-App`,
dsName: fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""),
};
cy.exec("mkdir -p ./cypress/downloads/");
cy.wait(3000);
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
});
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
});
it("Verify the elements of export dialog box", () => {
cy.skipWalkthrough();
it("Verify the elements of export dialog box", () => {
cy.skipWalkthrough()
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.get(importSelectors.importOptionInput)
.eq(0)
.selectFile(TEST_DATA.appFiles.multiVersion, { force: true });
cy.wait(2000);
cy.clearAndType(commonSelectors.appNameInput, data.appName);
cy.get(importSelectors.importAppButton).click();
cy.wait(3000);
cy.backToApps();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.get(importSelectors.importOptionInput)
.eq(0)
.selectFile(TEST_DATA.appFiles.multiVersion, {
force: true,
});
cy.wait(1500);
cy.clearAndType(commonSelectors.appNameInput, data.appName);
cy.get(importSelectors.importAppButton).click();
cy.wait(3000);
cy.backToApps();
// Select the app card option to export the app
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
// Select the app card option to export the app
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
// Verify the elements of the export modal
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
// Close the modal
closeModal(exportAppModalText.modalCloseButton);
// Ensure the modal title is no longer visible
cy.get(
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
).should("not.exist");
// Re-open the export modal and click the export button
cy.wait(2000);
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
clickOnExportButtonAndVerify(exportAppModalText.exportAll, data.appName);
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
// Validate the schema for the student table in tooljetdb
const tooljetDatabase = appData.tooljet_database.find(
(db) => db.table_name === "student"
);
expect(tooljetDatabase).to.exist;
expect(tooljetDatabase.schema).to.exist;
// Validate components and queries
const components = appData.app[0].definition.appV2.components;
const text2Component = components.find(
(component) => component.name === "text2"
);
expect(text2Component).to.exist;
expect(text2Component.properties.text.value).to.equal(
"{{constants.pageHeader}}"
);
// Verify the elements of the export modal
verifyElementsOfExportModal("v3", ["v2", "v1"], [true, false, false]);
// Close the modal
closeModal(exportAppModalText.modalCloseButton);
// Ensure the modal title is no longer visible
cy.get(
commonSelectors.modalTitle(exportAppModalText.selectVersionTitle)
).should("not.exist");
// Re-open the export modal and click the export button
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
const textinput1 = components.find(
(component) => component.name === "textinput1"
);
clickOnExportButtonAndVerify(exportAppModalText.exportAll, data.appName);
expect(textinput1).to.exist;
expect(textinput1.properties.value.value).to.include("queries");
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
// Validate the schema for the student table in tooljetdb
const tooljetDatabase = appData.tooljet_database.find(
(db) => db.table_name === "student"
);
expect(tooljetDatabase).to.exist;
expect(tooljetDatabase.schema).to.exist;
// Validate components and queries
const components = appData.app[0].definition.appV2.components;
const text2Component = components.find(
(component) => component.name === "text2"
);
expect(text2Component).to.exist;
expect(text2Component.properties.text.value).to.equal(
"{{constants.pageHeader}}"
);
const textinput1 = components.find(
(component) => component.name === "textinput1"
);
expect(textinput1).to.exist;
expect(textinput1.properties.value.value).to.include("queries");
const textinput2 = components.find(
(component) => component.name === "textinput2"
);
expect(textinput2).to.exist;
expect(textinput2.properties.value.value).to.include("queries");
const textinput3 = components.find(
(component) => component.name === "textinput3"
);
expect(textinput3).to.exist;
expect(textinput3.properties.value.value).to.include("queries");
// Validate the data queries
const dataQueries = appData.app[0].definition.appV2.dataQueries;
const postgresqlQuery = dataQueries.find(
(query) => query.name === "postgresql1"
);
expect(postgresqlQuery).to.exist;
expect(postgresqlQuery.options.query).to.include(
"Select * from {{secrets.db_name}}"
);
const restapiQuery = dataQueries.find(
(query) => query.name === "restapi1"
);
expect(restapiQuery).to.exist;
expect(restapiQuery.options.url).to.equal(
"https://jsonplaceholder.typicode.com/users/1"
);
const tooljetdbQuery = dataQueries.find(
(query) => query.name === "tooljetdb1"
);
expect(tooljetdbQuery).to.exist;
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
// Ensure appVersions exists
const appVersions = appData.app[0].definition.appV2.appVersions;
expect(appVersions).to.exist;
// Map and verify app version names
const versionNames = appVersions.map((version) => version.name);
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
});
});
cy.exec("cd ./cypress/downloads/ && rm -rf *");
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
const textinput2 = components.find(
(component) => component.name === "textinput2"
);
cy.get(`[data-cy="v1-radio-button"]`).check();
cy.get(
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
).click();
expect(textinput2).to.exist;
expect(textinput2.properties.value.value).to.include("queries");
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
const textinput3 = components.find(
(component) => component.name === "textinput3"
);
expect(textinput3).to.exist;
expect(textinput3.properties.value.value).to.include("queries");
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
// Validate the data queries
const dataQueries = appData.app[0].definition.appV2.dataQueries;
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
});
});
const postgresqlQuery = dataQueries.find(
(query) => query.name === "postgresql1"
);
expect(postgresqlQuery).to.exist;
expect(postgresqlQuery.options.query).to.include(
"Select * from {{secrets.db_name}}"
);
const restapiQuery = dataQueries.find(
(query) => query.name === "restapi1"
);
expect(restapiQuery).to.exist;
expect(restapiQuery.options.url).to.equal(
"https://jsonplaceholder.typicode.com/users/1"
);
const tooljetdbQuery = dataQueries.find(
(query) => query.name === "tooljetdb1"
);
expect(tooljetdbQuery).to.exist;
expect(tooljetdbQuery.options.operation).to.equal("list_rows");
// Ensure appVersions exists
const appVersions = appData.app[0].definition.appV2.appVersions;
expect(appVersions).to.exist;
// Map and verify app version names
const versionNames = appVersions.map((version) => version.name);
expect(versionNames).to.include.members(["v1", "v2", "v3"]);
});
});
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
data.appName2 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName2);
cy.openApp(data.appName2);
cy.exec("cd ./cypress/downloads/ && rm -rf *");
cy.dragAndDropWidget("Text Input", 50, 50);
selectAppCardOption(
data.appName,
commonSelectors.appCardOptions(commonText.exportAppOption)
);
cy.get(`[data-cy="v1-radio-button"]`).check();
cy.get(
commonSelectors.buttonSelector(exportAppModalText.exportSelectedVersion)
).click();
cy.get('[data-cy="left-sidebar-settings-button"]').click();
cy.get('[data-cy="button-user-status-change"]').click();
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
const filePath = `./cypress/downloads/${downloadedAppExportFileName}`;
verifyElementsOfExportModal("v1");
// Ensure the file name contains the expected app export name
expect(downloadedAppExportFileName).to.contain(
data.appName.toLowerCase()
);
exportAllVersionsAndVerify(data.appName1, "v1");
// Read and validate the exported JSON file
cy.readFile(filePath).then((appData) => {
// Validate the app name
const appNameFromFile = appData.app[0].definition.appV2.name;
expect(appNameFromFile).to.equal(data.appName);
});
});
});
it.skip("Verify 'Export app' functionality of an application inside app editor", () => {
data.appName2 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName2);
cy.openApp(data.appName2);
cy.dragAndDropWidget("Text Input", 50, 50);
cy.get('[data-cy="left-sidebar-settings-button"]').click();
cy.get('[data-cy="button-user-status-change"]').click();
verifyElementsOfExportModal("v1");
exportAllVersionsAndVerify(data.appName1, "v1");
});
});

View file

@ -0,0 +1,152 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class DeprecateLocalStaticDataSourcesb1745318714733 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Get all organization IDs
const organizationsResult = await queryRunner.query(`SELECT id FROM organizations`);
const organizationIds = organizationsResult.map((row) => row.id);
console.log(`Found ${organizationIds.length} organizations to process`);
// Process each organization
for (const orgId of organizationIds) {
console.log(`Processing organization: ${orgId}`);
// Get all app_version_ids under this organization
const appVersionsResult = await queryRunner.query(
`
SELECT av.id as app_version_id
FROM app_versions av
JOIN apps a ON av.app_id = a.id
WHERE a.organization_id = $1
`,
[orgId]
);
const appVersionIds = appVersionsResult.map((row) => row.app_version_id);
if (appVersionIds.length === 0) {
console.log(`No app versions found for organization: ${orgId}`);
continue;
}
console.log(`Found ${appVersionIds.length} app versions for organization: ${orgId}`);
// Get all distinct kinds for static global data sources associated with these app versions
const kindsResult = await queryRunner.query(
`
SELECT DISTINCT kind
FROM data_sources
WHERE type = 'static' AND scope = 'local' AND app_version_id = ANY($1::uuid[])
`,
[appVersionIds]
);
const kinds = kindsResult.map((row) => row.kind);
if (kinds.length === 0) {
console.log(`No global static data sources found for app versions under organization: ${orgId}`);
continue;
}
console.log(`Found ${kinds.length} different kinds of data sources`);
// Process each kind of data source
for (const kind of kinds) {
console.log(`Processing kind: ${kind}`);
// Get first data source ID of this kind to keep
const primaryResult = await queryRunner.query(
`
SELECT id
FROM data_sources
WHERE type = 'static' AND scope = 'local' AND app_version_id = ANY($1::uuid[]) AND kind = $2
LIMIT 1
`,
[appVersionIds, kind]
);
if (primaryResult.length === 0) {
console.log(`No data sources found for kind: ${kind}`);
continue;
}
const primaryDataSourceId = primaryResult[0].id;
// Ensure the primary data source has scope set to 'global', app_version_id set to null,
// and organization_id set to the current organization
await queryRunner.query(
`
UPDATE data_sources
SET scope = 'global', app_version_id = NULL, organization_id = $1
WHERE id = $2
`,
[orgId, primaryDataSourceId]
);
console.log(
`Updated primary data source ${primaryDataSourceId} scope to 'global', app_version_id to NULL, and organization_id to ${orgId}`
);
// Get all other data source IDs of the same kind to be replaced
const duplicatesResult = await queryRunner.query(
`
SELECT id
FROM data_sources
WHERE type = 'static' AND scope = 'local' AND
app_version_id = ANY($1::uuid[]) AND
kind = $2 AND
id != $3
`,
[appVersionIds, kind, primaryDataSourceId]
);
const duplicateDataSourceIds = duplicatesResult.map((row) => row.id);
if (duplicateDataSourceIds.length === 0) {
console.log(`No duplicates found for kind: ${kind}`);
continue;
}
console.log(`Found ${duplicateDataSourceIds.length} duplicate data sources for kind: ${kind}`);
console.log(`Primary ID: ${primaryDataSourceId}, Duplicates: ${duplicateDataSourceIds.join(', ')}`);
// Update data_queries to use the primary data source
const updateResult = await queryRunner.query(
`
UPDATE data_queries
SET data_source_id = $1
WHERE data_source_id = ANY($2::uuid[])
`,
[primaryDataSourceId, duplicateDataSourceIds]
);
console.log(
`Updated ${updateResult.length || 'multiple'} data_queries to use primary data source: ${primaryDataSourceId}`
);
// Delete the duplicate data sources
const deleteResult = await queryRunner.query(
`
DELETE FROM data_sources
WHERE id = ANY($1::uuid[])
`,
[duplicateDataSourceIds]
);
console.log(`Deleted ${deleteResult.length || duplicateDataSourceIds.length} duplicate data sources`);
}
}
console.log('Data source consolidation complete.');
}
public async down(queryRunner: QueryRunner): Promise<void> {
// This is a data consolidation migration and cannot be easily reverted
console.log(`
WARNING: No down migration available for data consolidation.
This migration consolidates duplicate data sources and cannot be automatically reverted.
Please restore from a backup if needed.
`);
}
}

View file

@ -0,0 +1,83 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMissingStaticDataSources1745409452884 implements MigrationInterface {
private requiredDataSourceKinds = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows'];
public async up(queryRunner: QueryRunner): Promise<void> {
// Get all organization IDs
const organizationsResult = await queryRunner.query(`SELECT id FROM organizations`);
const organizationIds = organizationsResult.map((row) => row.id);
console.log(`Found ${organizationIds.length} organizations to validate`);
// Process each organization
for (const orgId of organizationIds) {
console.log(`Processing organization: ${orgId}`);
// Get existing static data source kinds for this organization
const existingDataSourcesResult = await queryRunner.query(
`
SELECT DISTINCT kind
FROM data_sources
WHERE type = 'static' AND scope = 'global' AND organization_id = $1
`,
[orgId]
);
const existingKinds = existingDataSourcesResult.map((row) => row.kind);
console.log(`Found existing data source kinds: ${existingKinds.join(', ') || 'none'}`);
// Find missing kinds
const missingKinds = this.requiredDataSourceKinds.filter((kind) => !existingKinds.includes(kind));
if (missingKinds.length === 0) {
console.log(`Organization ${orgId} has all required data source kinds`);
continue;
}
console.log(`Missing data source kinds for organization ${orgId}: ${missingKinds.join(', ')}`);
// Add missing data sources
for (const kind of missingKinds) {
const name = this.getDefaultNameForKind(kind);
await queryRunner.query(
`
INSERT INTO data_sources (
name, kind, type, scope, organization_id, created_at, updated_at
) VALUES (
$1, $2, 'static', 'global', $3, NOW(), NOW()
)
`,
[name, kind, orgId]
);
console.log(`Added new data source: ${kind} for organization ${orgId}`);
}
}
console.log('Data source validation and addition complete.');
}
public async down(queryRunner: QueryRunner): Promise<void> {
// This is a data addition migration that adds missing required data sources
console.log(`
NOTE: Down migration is not implemented for data source validation.
This migration adds missing required data sources to organizations.
If needed, you could delete data sources by kind and organization manually.
`);
}
// Helper function to get default names for data source kinds
private getDefaultNameForKind(kind: string): string {
const nameMap: Record<string, string> = {
restapi: 'restapidefault',
runjs: 'runjsdefault',
runpy: 'runpydefault',
tooljetdb: 'tooljetdbdefault',
workflows: 'workflowsdefault',
};
return nameMap[kind] || `${kind}default`;
}
}

View file

@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddDataSourceConstraintsForStatic1745409631920 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
console.log('Starting migration to add constraints to data_sources table');
// Add check constraint to ensure static type data sources have global scope
await queryRunner.query(`
ALTER TABLE data_sources
ADD CONSTRAINT chk_static_type_global_scope
CHECK (type != 'static' OR scope = 'global');
`);
console.log('Added constraint: static type data sources must have global scope');
// Add unique constraint for combination of kind, type, and organization_id
await queryRunner.query(`
CREATE UNIQUE INDEX idx_unique_static_kind_org
ON public.data_sources (kind, type, organization_id)
WHERE type = 'static';
`);
console.log('Added unique constraint on kind, type, and organization_id');
console.log('Migration completed successfully');
}
public async down(queryRunner: QueryRunner): Promise<void> {
console.log('Starting rollback of data_sources constraints');
// Drop the unique constraint
await queryRunner.query(`
ALTER TABLE public.data_sources
DROP CONSTRAINT IF EXISTS idx_unique_static_kind_org;
`);
console.log('Dropped unique constraint on kind, type, and organization_id');
// Drop the check constraint
await queryRunner.query(`
ALTER TABLE public.data_sources
DROP CONSTRAINT IF EXISTS chk_static_type_global_scope;
`);
console.log('Dropped constraint: static type data sources must have global scope');
console.log('Rollback completed successfully');
}
}

View file

@ -15,7 +15,7 @@ import { GuardValidatorModule } from './validators/feature-guard.validator';
import { SentryModule } from '@modules/observability/sentry/module';
export class AppModuleLoader {
static async loadModules(configs: { IS_GET_CONTEXT: boolean }): Promise<(DynamicModule | Type<any>)[]> {
static async loadModules(configs: { IS_GET_CONTEXT: boolean }): Promise<(DynamicModule | typeof GuardValidatorModule)[]> {
// Static imports that are always loaded
const staticModules = [
EventEmitterModule.forRoot({
@ -33,7 +33,7 @@ export class AppModuleLoader {
port: parseInt(process.env.REDIS_PORT) || 6379,
},
}),
ConfigModule.forRoot({
await ConfigModule.forRoot({
isGlobal: true,
envFilePath: [`../.env.${process.env.NODE_ENV}`, '../.env'],
load: [() => getEnvVars()],
@ -111,17 +111,17 @@ export class AppModuleLoader {
*
*
*/
const dynamicModules: Promise<DynamicModule>[] = [];
const dynamicModules: DynamicModule[] = [];
try {
const { LogToFileModule } = await import(`${await getImportPath(configs.IS_GET_CONTEXT)}/log-to-file/module`);
const { AuditLogsModule } = await import(`${await getImportPath(configs.IS_GET_CONTEXT)}/audit-logs/module`);
dynamicModules.push(LogToFileModule.register(configs));
dynamicModules.push(AuditLogsModule.register(configs));
dynamicModules.push(await LogToFileModule.register(configs));
dynamicModules.push(await AuditLogsModule.register(configs));
} catch (error) {
console.error('Error loading dynamic modules:', error);
}
return [...staticModules, ...dynamicModules] as (Type<any> | DynamicModule)[];
return [...staticModules, ...dynamicModules];
}
}

View file

@ -49,7 +49,6 @@ interface AppResourceMappings {
componentsMapping: Record<string, string>;
}
type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';
type DefaultDataSourceName =
| 'restapidefault'
| 'runjsdefault'
@ -76,7 +75,6 @@ const DefaultDataSourceNames: DefaultDataSourceName[] = [
'tooljetdbdefault',
'workflowsdefault',
];
const DefaultDataSourceKinds: DefaultDataSourceKind[] = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows'];
const NewRevampedComponents: NewRevampedComponent[] = [
'Text',
'TextInput',
@ -97,7 +95,6 @@ export class AppImportExportService {
protected dataSourcesRepository: DataSourcesRepository,
protected appEnvironmentUtilService: AppEnvironmentUtilService,
protected usersUtilService: UsersUtilService,
protected readonly entityManager: EntityManager,
protected componentsService: ComponentsService
) {}
@ -261,54 +258,65 @@ export class AppImportExportService {
externalResourceMappings = {},
isGitApp = false,
tooljetVersion = '',
cloning = false
cloning = false,
manager?: EntityManager
): Promise<App> {
if (typeof appParamsObj !== 'object') {
throw new BadRequestException('Invalid params for app import');
}
return await dbTransactionWrap(async (manager: EntityManager) => {
if (typeof appParamsObj !== 'object') {
throw new BadRequestException('Invalid params for app import');
}
let appParams = appParamsObj;
let appParams = appParamsObj;
if (appParams?.appV2) {
appParams = { ...appParams.appV2 };
}
if (appParams?.appV2) {
appParams = { ...appParams.appV2 };
}
if (!appParams?.name) {
throw new BadRequestException('Invalid params for app import');
}
if (!appParams?.name) {
throw new BadRequestException('Invalid params for app import');
}
const schemaUnifiedAppParams = appParams?.schemaDetails?.multiPages
? appParams
: convertSinglePageSchemaToMultiPageSchema(appParams);
schemaUnifiedAppParams.name = appName;
const schemaUnifiedAppParams = appParams?.schemaDetails?.multiPages
? appParams
: convertSinglePageSchemaToMultiPageSchema(appParams);
schemaUnifiedAppParams.name = appName;
const importedAppTooljetVersion = !cloning && extractMajorVersion(tooljetVersion);
const isNormalizedAppDefinitionSchema = cloning
? true
: isTooljetVersionWithNormalizedAppDefinitionSchem(importedAppTooljetVersion);
const importedAppTooljetVersion = !cloning && extractMajorVersion(tooljetVersion);
const isNormalizedAppDefinitionSchema = cloning
? true
: isTooljetVersionWithNormalizedAppDefinitionSchem(importedAppTooljetVersion);
const currentTooljetVersion = !cloning ? tooljetVersion : null;
const currentTooljetVersion = !cloning ? tooljetVersion : null;
const importedApp = await this.createImportedAppForUser(this.entityManager, schemaUnifiedAppParams, user, isGitApp);
const importedApp = await this.createImportedAppForUser(manager, schemaUnifiedAppParams, user, isGitApp);
const resourceMapping = await this.setupImportedAppAssociations(
this.entityManager,
importedApp,
schemaUnifiedAppParams,
user,
externalResourceMappings,
isNormalizedAppDefinitionSchema,
currentTooljetVersion
);
await this.updateEntityReferencesForImportedApp(this.entityManager, resourceMapping);
const resourceMapping = await this.setupImportedAppAssociations(
manager,
importedApp,
schemaUnifiedAppParams,
user,
externalResourceMappings,
isNormalizedAppDefinitionSchema,
currentTooljetVersion
);
await this.updateEntityReferencesForImportedApp(manager, resourceMapping);
// NOTE: App slug updation callback doesn't work while wrapped in transaction
// hence updating slug explicitly
await importedApp.reload();
importedApp.slug = importedApp.id;
await this.entityManager.save(importedApp);
// Update latest version as editing version
const {
importingAppVersions,
} = this.extractImportDataFromAppParams(appParams)
return importedApp;
await this.setEditingVersionAsLatestVersion(manager, resourceMapping.appVersionMapping, importingAppVersions)
// NOTE: App slug updation callback doesn't work while wrapped in transaction
// hence updating slug explicitly
//await importedApp.reload(); -> this will not work as we are using transaction
const newApp = await manager.findOne(App, { where: { id: importedApp.id } });
newApp.slug = importedApp.id;
await manager.save(newApp);
return newApp;
}, manager);
}
async updateEntityReferencesForImportedApp(manager: EntityManager, resourceMapping: AppResourceMappings) {
@ -487,196 +495,194 @@ export class AppImportExportService {
* If an error occurs during the function execution, the transaction will rolled back.
*/
await manager.transaction(async (transactionalEntityManager) => {
appResourceMappings = await this.setupAppVersionAssociations(
transactionalEntityManager,
importingAppVersions,
user,
appResourceMappings,
externalResourceMappings,
importingAppEnvironments,
importingDataSources,
importingDataSourceOptions,
importingDataQueries,
importingDefaultAppEnvironmentId,
importingPages,
importingComponents,
importingEvents,
tooljetVersion
);
appResourceMappings = await this.setupAppVersionAssociations(
manager,
importingAppVersions,
user,
appResourceMappings,
externalResourceMappings,
importingAppEnvironments,
importingDataSources,
importingDataSourceOptions,
importingDataQueries,
importingDefaultAppEnvironmentId,
importingPages,
importingComponents,
importingEvents,
tooljetVersion
);
if (!isNormalizedAppDefinitionSchema) {
for (const importingAppVersion of importingAppVersions) {
const updatedDefinition: DeepPartial<any> = this.replaceDataQueryIdWithinDefinitions(
importingAppVersion.definition,
appResourceMappings.dataQueryMapping
);
if (!isNormalizedAppDefinitionSchema) {
for (const importingAppVersion of importingAppVersions) {
const updatedDefinition: DeepPartial<any> = this.replaceDataQueryIdWithinDefinitions(
importingAppVersion.definition,
appResourceMappings.dataQueryMapping
);
let updateHomepageId = null;
let updateHomepageId = null;
if (updatedDefinition?.pages) {
for (const pageId of Object.keys(updatedDefinition?.pages)) {
const page = updatedDefinition.pages[pageId];
if (updatedDefinition?.pages) {
for (const pageId of Object.keys(updatedDefinition?.pages)) {
const page = updatedDefinition.pages[pageId];
const pageEvents = page.events || [];
const componentEvents = [];
const pageEvents = page.events || [];
const componentEvents = [];
const pagePostionIntheList = Object.keys(updatedDefinition?.pages).indexOf(pageId);
const pagePostionIntheList = Object.keys(updatedDefinition?.pages).indexOf(pageId);
const isHompage = (updatedDefinition['homePageId'] as any) === pageId;
const isHompage = (updatedDefinition['homePageId'] as any) === pageId;
const pageComponents = page.components;
const pageComponents = page.components;
const mappedComponents = transformComponentData(
pageComponents,
componentEvents,
appResourceMappings.componentsMapping,
isNormalizedAppDefinitionSchema,
tooljetVersion
);
const mappedComponents = transformComponentData(
pageComponents,
componentEvents,
appResourceMappings.componentsMapping,
isNormalizedAppDefinitionSchema,
tooljetVersion
);
const componentLayouts = [];
const componentLayouts = [];
const newPage = transactionalEntityManager.create(Page, {
name: page.name,
handle: page.handle,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
index: pagePostionIntheList,
disabled: page.disabled || false,
hidden: page.hidden || false,
autoComputeLayout: page.autoComputeLayout || false,
isPageGroup: page.isPageGroup,
pageGroupIndex: page.pageGroupIndex || null,
icon: page.icon || null,
});
const pageCreated = await transactionalEntityManager.save(newPage);
const newPage = manager.create(Page, {
name: page.name,
handle: page.handle,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
index: pagePostionIntheList,
disabled: page.disabled || false,
hidden: page.hidden || false,
autoComputeLayout: page.autoComputeLayout || false,
isPageGroup: page.isPageGroup,
pageGroupIndex: page.pageGroupIndex || null,
icon: page.icon || null,
});
const pageCreated = await manager.save(newPage);
appResourceMappings.pagesMapping[pageId] = pageCreated.id;
appResourceMappings.pagesMapping[pageId] = pageCreated.id;
mappedComponents.forEach((component) => {
component.page = pageCreated;
});
mappedComponents.forEach((component) => {
component.page = pageCreated;
});
const savedComponents = await transactionalEntityManager.save(Component, mappedComponents);
const savedComponents = await manager.save(Component, mappedComponents);
for (const componentId in pageComponents) {
const componentLayout = pageComponents[componentId]['layouts'];
for (const componentId in pageComponents) {
const componentLayout = pageComponents[componentId]['layouts'];
if (componentLayout && appResourceMappings.componentsMapping[componentId]) {
for (const type in componentLayout) {
const layout = componentLayout[type];
const newLayout = new Layout();
newLayout.type = type;
newLayout.top = layout.top;
newLayout.left =
layout.dimensionUnit !== LayoutDimensionUnits.COUNT
? this.componentsService.resolveGridPositionForComponent(layout.left, type)
: layout.left;
newLayout.dimensionUnit = LayoutDimensionUnits.COUNT;
newLayout.width = layout.width;
newLayout.height = layout.height;
newLayout.componentId = appResourceMappings.componentsMapping[componentId];
if (componentLayout && appResourceMappings.componentsMapping[componentId]) {
for (const type in componentLayout) {
const layout = componentLayout[type];
const newLayout = new Layout();
newLayout.type = type;
newLayout.top = layout.top;
newLayout.left =
layout.dimensionUnit !== LayoutDimensionUnits.COUNT
? this.componentsService.resolveGridPositionForComponent(layout.left, type)
: layout.left;
newLayout.dimensionUnit = LayoutDimensionUnits.COUNT;
newLayout.width = layout.width;
newLayout.height = layout.height;
newLayout.componentId = appResourceMappings.componentsMapping[componentId];
componentLayouts.push(newLayout);
}
componentLayouts.push(newLayout);
}
}
}
await transactionalEntityManager.save(Layout, componentLayouts);
await manager.save(Layout, componentLayouts);
//Event handlers
//Event handlers
if (pageEvents.length > 0) {
pageEvents.forEach(async (event, index) => {
const newEvent = {
name: event.eventId,
sourceId: pageCreated.id,
target: Target.page,
event: event,
index: pageEvents.index || index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
};
if (pageEvents.length > 0) {
await Promise.all(pageEvents.map(async (event, index) => {
const newEvent = {
name: event.eventId,
sourceId: pageCreated.id,
target: Target.page,
event: event,
index: pageEvents.index || index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
};
await transactionalEntityManager.save(EventHandler, newEvent);
await manager.save(EventHandler, newEvent);
}));
}
await Promise.all(componentEvents.map(async (eventObj) => {
if (eventObj.event?.length === 0) return;
await Promise.all(eventObj.event.map(async (event, index) => {
const newEvent = manager.create(EventHandler, {
name: event.eventId,
sourceId: appResourceMappings.componentsMapping[eventObj.componentId],
target: Target.component,
event: event,
index: eventObj.index || index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
}
componentEvents.forEach((eventObj) => {
if (eventObj.event?.length === 0) return;
await manager.save(EventHandler, newEvent);
}));
}));
eventObj.event.forEach(async (event, index) => {
const newEvent = transactionalEntityManager.create(EventHandler, {
name: event.eventId,
sourceId: appResourceMappings.componentsMapping[eventObj.componentId],
target: Target.component,
event: event,
index: eventObj.index || index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
await Promise.all(savedComponents.map(async (component) => {
if (component.type === 'Table') {
const tableActions = component.properties?.actions?.value || [];
const tableColumns = component.properties?.columns?.value || [];
await transactionalEntityManager.save(EventHandler, newEvent);
});
});
const tableActionAndColumnEvents = [];
savedComponents.forEach(async (component) => {
if (component.type === 'Table') {
const tableActions = component.properties?.actions?.value || [];
const tableColumns = component.properties?.columns?.value || [];
tableActions.forEach((action) => {
const actionEvents = action.events || [];
const tableActionAndColumnEvents = [];
tableActions.forEach((action) => {
const actionEvents = action.events || [];
actionEvents.forEach((event, index) => {
tableActionAndColumnEvents.push({
name: event.eventId,
sourceId: component.id,
target: Target.tableAction,
event: { ...event, ref: action.name },
index: event.index ?? index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
actionEvents.forEach((event, index) => {
tableActionAndColumnEvents.push({
name: event.eventId,
sourceId: component.id,
target: Target.tableAction,
event: { ...event, ref: action.name },
index: event.index ?? index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
});
});
tableColumns.forEach((column) => {
if (column?.columnType !== 'toggle') return;
const columnEvents = column.events || [];
tableColumns.forEach((column) => {
if (column?.columnType !== 'toggle') return;
const columnEvents = column.events || [];
columnEvents.forEach((event, index) => {
tableActionAndColumnEvents.push({
name: event.eventId,
sourceId: component.id,
target: Target.tableColumn,
event: { ...event, ref: column.name },
index: event.index ?? index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
columnEvents.forEach((event, index) => {
tableActionAndColumnEvents.push({
name: event.eventId,
sourceId: component.id,
target: Target.tableColumn,
event: { ...event, ref: column.name },
index: event.index ?? index,
appVersionId: appResourceMappings.appVersionMapping[importingAppVersion.id],
});
});
});
await transactionalEntityManager.save(EventHandler, tableActionAndColumnEvents);
}
});
if (isHompage) {
updateHomepageId = pageCreated.id;
await manager.save(EventHandler, tableActionAndColumnEvents);
}
}));
if (isHompage) {
updateHomepageId = pageCreated.id;
}
}
await transactionalEntityManager.update(
AppVersion,
{ id: appResourceMappings.appVersionMapping[importingAppVersion.id] },
{
definition: updatedDefinition,
homePageId: updateHomepageId,
}
);
}
await manager.update(
AppVersion,
{ id: appResourceMappings.appVersionMapping[importingAppVersion.id] },
{
definition: updatedDefinition,
homePageId: updateHomepageId,
}
);
}
});
}
const appVersionIds = Object.values(appResourceMappings.appVersionMapping);
@ -690,8 +696,6 @@ export class AppImportExportService {
);
}
await this.setEditingVersionAsLatestVersion(manager, appResourceMappings.appVersionMapping, importingAppVersions);
return appResourceMappings;
}
@ -748,7 +752,6 @@ export class AppImportExportService {
const dataSourceForAppVersion = await this.findOrCreateDataSourceForAppVersion(
manager,
importingDataSource,
appResourceMappings.appVersionMapping[importingAppVersion.id],
user
);
@ -914,7 +917,7 @@ export class AppImportExportService {
appResourceMappings.componentsMapping[component.id] = savedComponent.id;
const componentLayout = component.layouts;
componentLayout.forEach(async (layout) => {
await Promise.all(componentLayout.map(async (layout) => {
const newLayout = new Layout();
newLayout.type = layout.type;
newLayout.top = layout.top;
@ -928,12 +931,12 @@ export class AppImportExportService {
newLayout.component = savedComponent;
await manager.save(newLayout);
});
}));
const componentEvents = importingEvents.filter((event) => event.sourceId === component.id);
if (componentEvents.length > 0) {
componentEvents.forEach(async (componentEvent) => {
await Promise.all(componentEvents.map(async (componentEvent) => {
const newEvent = await manager.create(EventHandler, {
name: componentEvent.name,
sourceId: savedComponent.id,
@ -944,7 +947,7 @@ export class AppImportExportService {
});
await manager.save(EventHandler, newEvent);
});
}));
}
}
}
@ -952,7 +955,7 @@ export class AppImportExportService {
const pageEvents = importingEvents.filter((event) => event.sourceId === page.id);
if (pageEvents.length > 0) {
pageEvents.forEach(async (pageEvent) => {
await Promise.all(pageEvents.map(async (pageEvent) => {
const newEvent = await manager.create(EventHandler, {
name: pageEvent.name,
sourceId: pageCreated.id,
@ -963,7 +966,7 @@ export class AppImportExportService {
});
await manager.save(EventHandler, newEvent);
});
}));
}
}
@ -990,7 +993,7 @@ export class AppImportExportService {
);
if (importingQueryEvents.length > 0) {
importingQueryEvents.forEach(async (dataQueryEvent) => {
await Promise.all(importingQueryEvents.map(async (dataQueryEvent) => {
const newEvent = await manager.create(EventHandler, {
name: dataQueryEvent.name,
sourceId: mappedNewDataQuery.id,
@ -1001,7 +1004,7 @@ export class AppImportExportService {
});
await manager.save(EventHandler, newEvent);
});
}));
} else {
this.replaceDataQueryOptionsWithNewDataQueryIds(
mappedNewDataQuery?.options,
@ -1012,7 +1015,7 @@ export class AppImportExportService {
delete mappedNewDataQuery?.options?.events;
if (queryEvents.length > 0) {
queryEvents.forEach(async (event, index) => {
await Promise.all(queryEvents.map(async (event, index) => {
const newEvent = await manager.create(EventHandler, {
name: event.eventId,
sourceId: mappedNewDataQuery.id,
@ -1023,7 +1026,7 @@ export class AppImportExportService {
});
await manager.save(EventHandler, newEvent);
});
}));
}
}
@ -1195,12 +1198,7 @@ export class AppImportExportService {
user: User,
appResourceMappings: AppResourceMappings
) {
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
user?.organizationId,
appResourceMappings.appVersionMapping[appVersion.id],
DefaultDataSourceKinds,
manager
);
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(user.organizationId, manager);
appResourceMappings.defaultDataSourceIdMapping[appVersion.id] = defaultDataSourceIds;
return appResourceMappings;
@ -1209,7 +1207,6 @@ export class AppImportExportService {
async findOrCreateDataSourceForAppVersion(
manager: EntityManager,
dataSource: DataSource,
appVersionId: string,
user: User
): Promise<DataSource> {
const isDefaultDatasource = DefaultDataSourceNames.includes(dataSource.name as DefaultDataSourceName);
@ -1218,10 +1215,10 @@ export class AppImportExportService {
if (isDefaultDatasource) {
const createdDefaultDatasource = await manager.findOne(DataSource, {
where: {
appVersionId,
organizationId: user.organizationId,
kind: dataSource.kind,
type: DataSourceTypes.STATIC,
scope: 'local',
scope: DataSourceScopes.GLOBAL,
},
});
@ -1234,8 +1231,8 @@ export class AppImportExportService {
id: dataSource.id,
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
scope: 'global',
organizationId: user?.organizationId,
scope: DataSourceScopes.GLOBAL,
organizationId: user.organizationId,
},
});
};
@ -1245,8 +1242,8 @@ export class AppImportExportService {
name: dataSource.name,
kind: dataSource.kind,
type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]),
scope: 'global',
organizationId: user?.organizationId,
scope: DataSourceScopes.GLOBAL,
organizationId: user.organizationId,
},
});
};
@ -1268,7 +1265,7 @@ export class AppImportExportService {
name: dataSource.name,
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
scope: 'global', // No appVersionId for global data sources
scope: DataSourceScopes.GLOBAL, // No appVersionId for global data sources
pluginId: plugin.id,
});
await manager.save(newDataSource);
@ -1283,7 +1280,7 @@ export class AppImportExportService {
name: dataSource.name,
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
scope: 'global', // No appVersionId for global data sources
scope: DataSourceScopes.GLOBAL, // No appVersionId for global data sources
pluginId: null,
});
await manager.save(newDataSource);
@ -1499,19 +1496,12 @@ export class AppImportExportService {
return appResourceMappings;
}
async createDefaultDataSourceForVersion(
organizationId: string,
versionId: string,
kinds: DefaultDataSourceKind[],
manager: EntityManager
): Promise<any> {
const response = {};
for (const defaultSource of kinds) {
const dataSource = await this.dataSourcesRepository.createDefaultDataSource(defaultSource, versionId, manager);
response[defaultSource] = dataSource.id;
await this.dataSourcesUtilService.createDataSourceInAllEnvironments(organizationId, dataSource.id, manager);
}
return response;
async createDefaultDataSourceForVersion(organizationId: string, manager: EntityManager): Promise<any> {
const dataSources = await this.dataSourcesRepository.getStaticDataSources(organizationId, manager);
return dataSources?.reduce<Record<string, string>>((acc, source) => {
acc[source.kind] = source.id;
return acc;
}, {});
}
async setEditingVersionAsLatestVersion(manager: EntityManager, appVersionMapping: any, appVersions: Array<any>) {
@ -1657,12 +1647,7 @@ export class AppImportExportService {
await manager.save(version);
// Create default data sources
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
user?.organizationId,
version.id,
DefaultDataSourceKinds,
manager
);
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(user.organizationId, manager);
let envIdArray: string[] = [];
const organization: Organization = await manager.findOne(Organization, {
@ -1759,7 +1744,7 @@ export class AppImportExportService {
newQuery.options = newOptions;
await manager.save(newQuery);
queryEvents.forEach(async (event, index) => {
await Promise.all(queryEvents.map(async (event, index) => {
const newEvent = {
name: event.eventId,
sourceId: newQuery.id,
@ -1770,7 +1755,7 @@ export class AppImportExportService {
};
await manager.save(EventHandler, newEvent);
});
}));
}
await manager.update(

View file

@ -34,9 +34,7 @@ import { mergeWith } from 'lodash';
import { isArray } from 'lodash';
import { UserAppsPermissions, UserWorkflowPermissions } from '@modules/ability/types';
import { AbilityService } from '@modules/ability/interfaces/IService';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { IAppsUtilService } from './interfaces/IUtilService';
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
import { APP_TYPES } from './constants';
import { Component } from 'src/entities/component.entity';
@ -52,10 +50,8 @@ export class AppsUtilService implements IAppsUtilService {
protected readonly versionRepository: VersionRepository,
protected readonly licenseTermsService: LicenseTermsService,
protected readonly organizationRepository: OrganizationRepository,
protected readonly abilityService: AbilityService,
protected readonly dataSourceRepository: DataSourcesRepository,
protected readonly dataSourceUtilService: DataSourcesUtilService
) { }
protected readonly abilityService: AbilityService
) {}
async create(name: string, user: User, type: APP_TYPES, manager: EntityManager): Promise<App> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const app = await catchDbException(() => {
@ -77,14 +73,6 @@ export class AppsUtilService implements IAppsUtilService {
const firstPriorityEnv = await this.appEnvironmentUtilService.get(user.organizationId, null, true, manager);
const appVersion = await this.versionRepository.createOne('v1', app.id, firstPriorityEnv.id, null, manager);
for (const defaultSource of ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows']) {
const dataSource = await this.dataSourceRepository.createDefaultDataSource(
defaultSource,
appVersion.id,
manager
);
await this.dataSourceUtilService.createDataSourceInAllEnvironments(user.organizationId, dataSource.id, manager);
}
const defaultHomePage = await manager.save(
manager.create(Page, {
name: 'Home',
@ -157,43 +145,6 @@ export class AppsUtilService implements IAppsUtilService {
}, manager);
}
// async createVersion(
// user: User,
// app: App,
// versionName: string,
// versionFromId: string,
// manager?: EntityManager
// ): Promise<AppVersion> {
// return await dbTransactionWrap(async (manager: EntityManager) => {
// let versionFrom: AppVersion;
// const { organizationId } = user;
// if (versionFromId) {
// versionFrom = await manager.findOneOrFail(AppVersion, {
// where: {
// id: versionFromId,
// app: {
// id: app.id,
// organizationId,
// },
// },
// relations: ['app', 'dataSources', 'dataSources.dataQueries', 'dataSources.dataSourceOptions'],
// });
// }
// const noOfVersions = await manager.count(AppVersion, { where: { appId: app?.id } });
// if (noOfVersions && !versionFrom) {
// throw new BadRequestException('Version from should not be empty');
// }
// if (versionFrom) {
// }
// return appVersion;
// }, manager);
// }
async findAppWithIdOrSlug(slug: string, organizationId: string): Promise<App> {
let app: App;
try {
@ -675,7 +626,7 @@ export class AppsUtilService implements IAppsUtilService {
const tooljetDbDataQueries = await manager
.createQueryBuilder(DataQuery, 'data_queries')
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_queries.app_version_id')
.where('app_versions.app_id = :appId', { appId })
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
.getMany();

View file

@ -1,3 +1,5 @@
import { DefaultDataSourceKind } from '../types';
export enum FEATURE_KEY {
GET = 'GET',
GET_FOR_APP = 'GET_FOR_APP',
@ -23,3 +25,5 @@ export enum DataSourceScopes {
LOCAL = 'local',
GLOBAL = 'global',
}
export const DefaultDataSourceKinds: DefaultDataSourceKind[] = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows'];

View file

@ -19,7 +19,7 @@ export class DataSourcesRepository extends Repository<DataSource> {
organizationId: string,
queryVars: GetQueryVariables
): Promise<DataSource[]> {
const { appVersionId, environmentId } = queryVars;
const { appVersionId, environmentId, types } = queryVars;
// Data source options are attached only if selectedEnvironmentId is passed
// Returns global data sources + sample data sources
// If version Id is passed, then data queries under each are also returned
@ -67,6 +67,9 @@ export class DataSourcesRepository extends Repository<DataSource> {
.andWhere('data_source.organization_id = :organizationId', { organizationId })
.andWhere('data_source.scope = :scope', { scope: DataSourceScopes.GLOBAL });
if (types && types.length > 0) {
query.andWhere('data_source.type IN (:...types)', { types });
}
if (environmentId) {
query.andWhere('data_source_options.environmentId = :environmentId', { environmentId });
}
@ -140,11 +143,12 @@ export class DataSourcesRepository extends Repository<DataSource> {
}, manager || this.manager);
}
async createDefaultDataSource(kind: string, appVersionId: string, manager?: EntityManager): Promise<DataSource> {
async createDefaultDataSource(kind: string, organizationId: string, manager?: EntityManager): Promise<DataSource> {
const newDataSource = manager.create(DataSource, {
name: `${kind}default`,
kind,
appVersionId,
scope: DataSourceScopes.GLOBAL,
organizationId,
type: DataSourceTypes.STATIC,
createdAt: new Date(),
updatedAt: new Date(),
@ -152,6 +156,12 @@ export class DataSourcesRepository extends Repository<DataSource> {
return await manager.save(newDataSource);
}
async getStaticDataSources(organizationId: string, manager?: EntityManager): Promise<DataSource[]> {
return await manager.find(DataSource, {
where: { organizationId, type: DataSourceTypes.STATIC },
});
}
findByQuery(dataQueryId: string, organizationId: string, dataSourceId?: string, manager?: EntityManager) {
return dbTransactionWrap((manager: EntityManager) => {
return manager.findOne(DataSource, {
@ -161,14 +171,6 @@ export class DataSourcesRepository extends Repository<DataSource> {
}, manager || this.manager);
}
getAllStaticDataSources(versionId: string, manager?: EntityManager): Promise<DataSource[]> {
return dbTransactionWrap((manager: EntityManager) => {
return manager.find(DataSource, {
where: { appVersionId: versionId, type: DataSourceTypes.STATIC },
});
}, manager || this.manager);
}
getDatasourceByPluginId(pluginId: string) {
return dbTransactionWrap((manager: EntityManager) => {
return manager.find(DataSource, {

View file

@ -43,14 +43,13 @@ export class DataSourcesService implements IDataSourcesService {
});
const shouldIncludeWorkflows = query.shouldIncludeWorkflows ?? true;
const dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, query ?? {});
let staticDataSources = await this.dataSourcesRepository.getAllStaticDataSources(query.appVersionId);
let dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, query ?? {});
if (!shouldIncludeWorkflows) {
// remove workflowsdefault data source from static data sources
staticDataSources = staticDataSources.filter((dataSource) => dataSource.kind !== 'workflows');
dataSources = dataSources.filter((dataSource) => dataSource.kind !== 'workflows');
}
const decamelizedDatasources = decamelizeKeys([...staticDataSources, ...dataSources]);
const decamelizedDatasources = decamelizeKeys(dataSources);
return { data_sources: decamelizedDatasources };
}
@ -66,6 +65,7 @@ export class DataSourcesService implements IDataSourcesService {
const dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, {
appVersionId: query.appVersionId,
environmentId: selectedEnvironmentId,
types: [DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE],
});
for (const dataSource of dataSources) {
const parseIfNeeded = (data: any) => {

View file

@ -1,4 +1,4 @@
import { FEATURE_KEY } from '../constants';
import { DataSourceTypes, FEATURE_KEY } from '../constants';
import { FeatureConfig } from '@modules/app/types';
import { MODULES } from '@modules/app/constants/modules';
import { QueryError, OAuthUnauthorizedClientError } from '@tooljet/plugins/dist/server';
@ -50,6 +50,7 @@ export { QueryError, OAuthUnauthorizedClientError };
export interface GetQueryVariables {
appVersionId?: string;
environmentId?: string;
types?: DataSourceTypes[];
shouldIncludeWorkflows?: boolean;
}
@ -57,3 +58,5 @@ export interface UpdateOptions {
dataSourceId: string;
environmentId: string;
}
export type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';

View file

@ -712,24 +712,6 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
}
}
async findDefaultDataSource(
kind: string,
appVersionId: string,
organizationId: string,
manager: EntityManager
): Promise<DataSource> {
const defaultDataSource = await manager.findOne(DataSource, {
where: { kind, appVersionId, type: DataSourceTypes.STATIC },
});
if (defaultDataSource) {
return defaultDataSource;
}
const dataSource = await this.dataSourceRepository.createDefaultDataSource(kind, appVersionId, manager);
await this.createDataSourceInAllEnvironments(organizationId, dataSource.id, manager);
return dataSource;
}
async getAuthUrl(getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto): Promise<{ url: string }> {
const { provider, source_options = {}, plugin_id = null } = getDataSourceOauthUrlDto;
const service = await this.pluginsServiceSelector.getService(plugin_id, provider);

View file

@ -9,6 +9,7 @@ import { isEmpty } from 'lodash';
import { InternalTableRepository } from '@modules/tooljet-db/repository';
import { RequestContext } from '@modules/request-context/service';
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
import { dbTransactionWrap } from '@helpers/database.helper';
@Injectable()
export class ImportExportResourcesService {
@ -82,40 +83,43 @@ export class ImportExportResourcesService {
}
}
if (!isEmpty(importResourcesDto.tooljet_database)) {
const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning);
tableNameMapping = res.tableNameMapping;
imports.tooljet_database = res.tooljet_database;
imports.tableNameMapping = tableNameMapping;
}
if (!isEmpty(importResourcesDto.app)) {
for (const appImportDto of importResourcesDto.app) {
user.organizationId = importResourcesDto.organization_id;
const createdApp = await this.appImportExportService.import(
user,
appImportDto.definition,
appImportDto.appName,
{
tooljet_database: tableNameMapping,
},
isGitApp,
importResourcesDto.tooljet_version,
cloning
);
imports.app.push({ id: createdApp.id, name: createdApp.name });
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: createdApp.id,
resourceName: createdApp.name,
});
return await dbTransactionWrap(async (manager) => {
if (!isEmpty(importResourcesDto.tooljet_database)) {
const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning);
tableNameMapping = res.tableNameMapping;
imports.tooljet_database = res.tooljet_database;
imports.tableNameMapping = tableNameMapping;
}
}
return imports;
if (!isEmpty(importResourcesDto.app)) {
for (const appImportDto of importResourcesDto.app) {
user.organizationId = importResourcesDto.organization_id;
const createdApp = await this.appImportExportService.import(
user,
appImportDto.definition,
appImportDto.appName,
{
tooljet_database: tableNameMapping,
},
isGitApp,
importResourcesDto.tooljet_version,
cloning,
manager
);
imports.app.push({ id: createdApp.id, name: createdApp.name });
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: createdApp.id,
resourceName: createdApp.name,
});
}
}
return imports;
});
}
async legacyImport(user: User, templateDefinition: any, appName: string) {

View file

@ -10,6 +10,7 @@ import { ThemesModule } from '@modules/organization-themes/module';
import { SessionModule } from '@modules/session/module';
import { InstanceSettingsModule } from '@modules/instance-settings/module';
import { TooljetDbModule } from '@modules/tooljet-db/module';
import { DataSourcesRepository } from '@modules/data-sources/repository';
export class SetupOrganizationsModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -38,7 +39,7 @@ export class SetupOrganizationsModule {
OrganizationRepository,
OrganizationUsersRepository,
FeatureAbilityFactory,
TooljetDbModule,
DataSourcesRepository,
],
exports: [SetupOrganizationsUtilService],
};

View file

@ -15,6 +15,9 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit
import { SampleDataSourceService } from '@modules/data-sources/services/sample-ds.service';
import { ISetupOrganizationsUtilService } from './interfaces/IUtilService';
import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service';
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { DefaultDataSourceKinds } from '@modules/data-sources/constants';
import { OrganizationInputs } from './types/organization-inputs';
@Injectable()
@ -29,7 +32,9 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer
protected readonly sampleDBService: SampleDataSourceService,
protected readonly licenseOrganizationService: LicenseOrganizationService,
protected readonly licenseUserService: LicenseUserService,
protected readonly organizationUserRepository: OrganizationUsersRepository
protected readonly organizationUserRepository: OrganizationUsersRepository,
protected readonly dataSourceUtilService: DataSourcesUtilService,
protected readonly dataSourcesRepository: DataSourcesRepository
) {}
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
@ -48,6 +53,17 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer
//create default theme for this organization
await this.organizationThemesUtilService.createDefaultTheme(manager, organization.id);
await this.tooljetDbTableOperationsService.createTooljetDbTenantSchemaAndRole(organization.id, manager);
// Create static data sources for the organization
for (const defaultSource of DefaultDataSourceKinds) {
const dataSource = await this.dataSourcesRepository.createDefaultDataSource(
defaultSource,
organization.id,
manager
);
await this.dataSourceUtilService.createDataSourceInAllEnvironments(organization.id, dataSource.id, manager);
}
return organization;
}, manager);
}

View file

@ -0,0 +1,30 @@
# ToolJet DB Postgrest
This module is required to setup ToolJet database
## Install postgrest
https://docs.postgrest.org/en/v12/explanations/install.html
## PostgREST configuration file - postgrest.conf
```
db-uri = "postgres://postgres:postgres@localhost:5432/tooljet_new_db"
db-pre-config = "postgrest.pre_config"
server-port = "3001"
jwt-secret = <add secret>
```
### ToolJet .env settings
```
PGRST_JWT_SECRET=Same value configured as jwt-secret
TOOLJET_DB=tooljet_new_db (Same value configured as db-uri db name)
PGRST_HOST=localhost:3001
TOOLJET_DB_USER=postgres
TOOLJET_DB_PASS=postgres
PGRST_DB_PRE_CONFIG=postgrest.pre_config
```
### Start Postgrest
```
postgrest postgrest.conf
```

View file

@ -0,0 +1,4 @@
db-uri = "postgres://postgres:postgres@localhost:5432/tooljet_new_db"
db-pre-config = "postgrest.pre_config"
server-port = "3001"
jwt-secret = "cba75674be5b6305a18d18e3761d82bb031fe36b45719744a308209db3d6d805"

View file

@ -91,87 +91,44 @@ export class VersionsCreateService implements IVersionsCreateService {
manager
);
if (!versionFrom) {
//create default data sources
for (const defaultSource of ['restapi', 'runjs', 'tooljetdb', 'workflows']) {
const dataSource = await this.dataSourceRepository.createDefaultDataSource(
defaultSource,
appVersion.id,
manager
);
await this.dataSourceUtilService.createDataSourceInAllEnvironments(organizationId, dataSource.id, manager);
}
} else {
const globalQueries: DataQuery[] = await this.dataQueryRepository.getQueriesByVersionId(
versionFrom.id,
DataSourceScopes.GLOBAL,
manager
);
const dataSources = versionFrom?.dataSources.filter((ds) => ds.scope == DataSourceScopes.LOCAL); //Local data sources
const globalDataSources = [...new Map(globalQueries.map((gq) => [gq.dataSource.id, gq.dataSource])).values()];
const globalQueries: DataQuery[] = await this.dataQueryRepository.getQueriesByVersionId(
versionFrom.id,
DataSourceScopes.GLOBAL,
manager
);
const dataSources = versionFrom?.dataSources.filter((ds) => ds.scope == DataSourceScopes.LOCAL); //Local data sources
const globalDataSources = [...new Map(globalQueries.map((gq) => [gq.dataSource.id, gq.dataSource])).values()];
const dataSourceMapping = {};
const newDataQueries = [];
const allEvents = await manager.find(EventHandler, {
where: { appVersionId: versionFrom?.id, target: Target.dataQuery },
});
const dataSourceMapping = {};
const newDataQueries = [];
const allEvents = await manager.find(EventHandler, {
where: { appVersionId: versionFrom?.id, target: Target.dataQuery },
});
if (dataSources?.length > 0 || globalDataSources?.length > 0) {
if (dataSources?.length > 0) {
for (const dataSource of dataSources) {
const dataSourceParams: Partial<DataSource> = {
name: dataSource.name,
kind: dataSource.kind,
type: dataSource.type,
appVersionId: appVersion.id,
};
const newDataSource = await manager.save(manager.create(DataSource, dataSourceParams));
dataSourceMapping[dataSource.id] = newDataSource.id;
if (dataSources?.length > 0 || globalDataSources?.length > 0) {
if (dataSources?.length > 0) {
for (const dataSource of dataSources) {
const dataSourceParams: Partial<DataSource> = {
name: dataSource.name,
kind: dataSource.kind,
type: dataSource.type,
appVersionId: appVersion.id,
};
const newDataSource = await manager.save(manager.create(DataSource, dataSourceParams));
dataSourceMapping[dataSource.id] = newDataSource.id;
const dataQueries = versionFrom?.dataSources?.find((ds) => ds.id === dataSource.id).dataQueries;
const dataQueries = versionFrom?.dataSources?.find((ds) => ds.id === dataSource.id).dataQueries;
for (const dataQuery of dataQueries) {
const dataQueryParams = {
name: dataQuery.name,
options: dataQuery.options,
dataSourceId: newDataSource.id,
appVersionId: appVersion.id,
};
const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
const dataQueryEvents = allEvents.filter((event) => event.sourceId === dataQuery.id);
dataQueryEvents.forEach(async (event, index) => {
const newEvent = new EventHandler();
newEvent.id = uuid.v4();
newEvent.name = event.name;
newEvent.sourceId = newQuery.id;
newEvent.target = event.target;
newEvent.event = event.event;
newEvent.index = event.index ?? index;
newEvent.appVersionId = appVersion.id;
await manager.save(newEvent);
});
oldDataQueryToNewMapping[dataQuery.id] = newQuery.id;
newDataQueries.push(newQuery);
}
}
}
if (globalQueries?.length > 0) {
for (const globalQuery of globalQueries) {
for (const dataQuery of dataQueries) {
const dataQueryParams = {
name: globalQuery.name,
options: globalQuery.options,
dataSourceId: globalQuery.dataSourceId,
name: dataQuery.name,
options: dataQuery.options,
dataSourceId: newDataSource.id,
appVersionId: appVersion.id,
};
const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
const dataQueryEvents = allEvents.filter((event) => event.sourceId === globalQuery.id);
const dataQueryEvents = allEvents.filter((event) => event.sourceId === dataQuery.id);
dataQueryEvents.forEach(async (event, index) => {
const newEvent = new EventHandler();
@ -186,45 +143,70 @@ export class VersionsCreateService implements IVersionsCreateService {
await manager.save(newEvent);
});
oldDataQueryToNewMapping[globalQuery.id] = newQuery.id;
oldDataQueryToNewMapping[dataQuery.id] = newQuery.id;
newDataQueries.push(newQuery);
}
}
}
for (const newQuery of newDataQueries) {
const newOptions = this.replaceDataQueryOptionsWithNewDataQueryIds(
newQuery.options,
oldDataQueryToNewMapping
);
newQuery.options = newOptions;
if (globalQueries?.length > 0) {
for (const globalQuery of globalQueries) {
const dataQueryParams = {
name: globalQuery.name,
options: globalQuery.options,
dataSourceId: globalQuery.dataSourceId,
appVersionId: appVersion.id,
};
await manager.save(newQuery);
const newQuery = await manager.save(manager.create(DataQuery, dataQueryParams));
const dataQueryEvents = allEvents.filter((event) => event.sourceId === globalQuery.id);
dataQueryEvents.forEach(async (event, index) => {
const newEvent = new EventHandler();
newEvent.id = uuid.v4();
newEvent.name = event.name;
newEvent.sourceId = newQuery.id;
newEvent.target = event.target;
newEvent.event = event.event;
newEvent.index = event.index ?? index;
newEvent.appVersionId = appVersion.id;
await manager.save(newEvent);
});
oldDataQueryToNewMapping[globalQuery.id] = newQuery.id;
newDataQueries.push(newQuery);
}
}
appVersion.definition = this.replaceDataQueryIdWithinDefinitions(
appVersion.definition,
oldDataQueryToNewMapping
);
await manager.save(appVersion);
for (const newQuery of newDataQueries) {
const newOptions = this.replaceDataQueryOptionsWithNewDataQueryIds(newQuery.options, oldDataQueryToNewMapping);
newQuery.options = newOptions;
for (const appEnvironment of appEnvironments) {
for (const dataSource of dataSources) {
const dataSourceOption = await manager.findOneOrFail(DataSourceOptions, {
where: { dataSourceId: dataSource.id, environmentId: appEnvironment.id },
});
await manager.save(newQuery);
}
const convertedOptions = this.convertToArrayOfKeyValuePairs(dataSourceOption.options);
const newOptions = await this.dataSourceUtilService.parseOptionsForCreate(convertedOptions, false, manager);
await this.setNewCredentialValueFromOldValue(newOptions, convertedOptions, manager);
appVersion.definition = this.replaceDataQueryIdWithinDefinitions(appVersion.definition, oldDataQueryToNewMapping);
await manager.save(appVersion);
await manager.save(
manager.create(DataSourceOptions, {
options: newOptions,
dataSourceId: dataSourceMapping[dataSource.id],
environmentId: appEnvironment.id,
})
);
}
for (const appEnvironment of appEnvironments) {
for (const dataSource of dataSources) {
const dataSourceOption = await manager.findOneOrFail(DataSourceOptions, {
where: { dataSourceId: dataSource.id, environmentId: appEnvironment.id },
});
const convertedOptions = this.convertToArrayOfKeyValuePairs(dataSourceOption.options);
const newOptions = await this.dataSourceUtilService.parseOptionsForCreate(convertedOptions, false, manager);
await this.setNewCredentialValueFromOldValue(newOptions, convertedOptions, manager);
await manager.save(
manager.create(DataSourceOptions, {
options: newOptions,
dataSourceId: dataSourceMapping[dataSource.id],
environmentId: appEnvironment.id,
})
);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { AppUser } from 'src/entities/app_user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
@Injectable()
export class AppUsersService {
constructor(
@InjectRepository(AppUser)
private appUsersRepository: Repository<AppUser>,
@InjectRepository(OrganizationUser)
private organizationUsersRepository: Repository<AppUser>
) {}
// TODO: remove deprecated
async create(user: User, appId: string, organizationUserId: string, role: string): Promise<AppUser> {
const organizationUser = await this.organizationUsersRepository.findOne({ where: { id: organizationUserId } });
return await this.appUsersRepository.save(
this.appUsersRepository.create({
appId,
userId: organizationUser.userId,
role,
createdAt: new Date(),
updatedAt: new Date(),
})
);
}
}

View file

@ -1,155 +0,0 @@
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Comment } from '../entities/comment.entity';
import { CommentRepository } from '../repositories/comment.repository';
import { CreateCommentDto, UpdateCommentDto } from '../dto/comment.dto';
import { groupBy, head } from 'lodash';
import { DataSource, Repository } from 'typeorm';
import { AppVersion } from 'src/entities/app_version.entity';
import { User } from 'src/entities/user.entity';
import { CommentUsers } from 'src/entities/comment_user.entity';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EMAIL_EVENTS } from '@modules/email/constants';
@Injectable()
export class CommentService {
constructor(
private readonly commentRepository: CommentRepository,
@InjectRepository(AppVersion)
private appVersionsRepository: Repository<AppVersion>,
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(CommentUsers)
private commentUsersRepository: Repository<CommentUsers>,
private readonly _dataSource: DataSource,
private eventEmitter: EventEmitter2
) {}
public async createComment(createCommentDto: CreateCommentDto, user: User): Promise<Comment> {
try {
const comment = await this.commentRepository.createComment(createCommentDto, user.id, user.organizationId);
// todo: move mentioned user emails to a queue service
const [appLink, commentLink, appName] = await this.getAppLinks(createCommentDto.appVersionsId, comment);
for (const userId of createCommentDto.mentionedUsers) {
const mentionedUser = await this.usersRepository.findOne({ where: { id: userId }, relations: ['avatar'] });
if (!mentionedUser) return null; // todo: invite user
this.eventEmitter.emit('emailEvent', {
type: EMAIL_EVENTS.SEND_COMMENT_MENTION_EMAIL,
payload: {
to: mentionedUser.email,
from: user.firstName,
appName: appName,
appLink: appLink,
commentLink: commentLink,
timestamp: comment.createdAt.toUTCString(),
comment: comment.comment,
fromAvatar: mentionedUser.avatar?.data.toString('base64'),
},
});
void this.commentUsersRepository.save(
this.commentUsersRepository.create({ commentId: comment.id, userId: mentionedUser.id })
);
}
return comment;
} catch (error) {
throw new InternalServerErrorException(error);
}
}
private async getAppLinks(appVersionsId: string, comment: Comment) {
const appVersion = await this.appVersionsRepository.findOne({ where: { id: appVersionsId }, relations: ['app'] });
const appLink = `${process.env.TOOLJET_HOST}/apps/${appVersion.app.id}`;
const commentLink = `${appLink}?threadId=${comment.threadId}&commentId=${comment.id}`;
return [appLink, commentLink, appVersion.app.name];
}
public async getComments(threadId: string, appVersionsId: string): Promise<Comment[]> {
return await this._dataSource
.createQueryBuilder(Comment, 'comment')
.innerJoin('comment.user', 'user')
.addSelect(['user.id', 'user.firstName', 'user.lastName'])
.andWhere('comment.threadId = :threadId', {
threadId,
})
.andWhere('comment.appVersionsId = :appVersionsId', {
appVersionsId,
})
.orderBy('comment.createdAt', 'ASC')
.getMany();
}
public async getOrganizationComments(organizationId: string, appVersionsId: string): Promise<Comment[]> {
return await this.commentRepository.find({
where: {
organizationId,
appVersionsId,
},
order: {
createdAt: 'ASC',
},
});
}
public async getNotifications(
appId: string,
userId: string,
isResolved = false,
appVersionsId: string,
pageId: string
): Promise<Comment[]> {
const comments = await this._dataSource
.createQueryBuilder(Comment, 'comment')
.innerJoin('comment.user', 'user')
.addSelect(['user.id', 'user.firstName', 'user.lastName'])
.innerJoin('comment.thread', 'thread')
.addSelect(['thread.id'])
.andWhere('thread.appId = :appId', {
appId,
})
.andWhere('thread.pageId = :pageId', {
pageId,
})
.andWhere('thread.isResolved = :isResolved', {
isResolved,
})
.andWhere('comment.appVersionsId = :appVersionsId', {
appVersionsId,
})
.orderBy('comment.createdAt', 'DESC')
.getMany();
const groupedComments = groupBy(comments, 'threadId');
const _comments = [];
Object.keys(groupedComments).map((k) => {
_comments.push({ comment: head(groupedComments[k]), count: groupedComments[k].length });
});
return _comments;
}
public async getComment(commentId: string): Promise<Comment> {
const foundComment = await this.commentRepository.findOne({ where: { id: commentId } });
if (!foundComment) {
throw new NotFoundException('Comment not found');
}
return foundComment;
}
public async editComment(commentId: string, updateCommentDto: UpdateCommentDto): Promise<Comment> {
const editedComment = await this.commentRepository.findOne({ where: { id: commentId } });
if (!editedComment) {
throw new NotFoundException('Comment not found');
}
return this.commentRepository.editComment(updateCommentDto, editedComment);
}
public async deleteComment(commentId: string): Promise<void> {
await this.commentRepository.delete(commentId);
}
}

View file

@ -1,83 +0,0 @@
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Comment } from '../entities/comment.entity';
import { Repository } from 'typeorm';
import { AppVersion } from 'src/entities/app_version.entity';
import { User } from 'src/entities/user.entity';
import { CommentUsers } from 'src/entities/comment_user.entity';
import { UpdateCommentUserDto } from '@dto/comment-user.dto';
@Injectable()
export class CommentUsersService {
constructor(
@InjectRepository(AppVersion)
private appVersionsRepository: Repository<AppVersion>,
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(CommentUsers)
private commentUsersRepository: Repository<CommentUsers>
) {}
private async getAppLinks(appVersionsId: string, comment: Comment) {
const appVersion = await this.appVersionsRepository.findOne({ where: { id: appVersionsId }, relations: ['app'] });
const appLink = `${process.env.TOOLJET_HOST}/apps/${appVersion.app.id}`;
const commentLink = `${appLink}?threadId=${comment.threadId}&commentId=${comment.id}`;
return [appLink, commentLink, appVersion.app.name];
}
public async findAll(userId: string, isRead = false) {
const notifications = await this.commentUsersRepository.find({
where: {
userId,
isRead,
},
order: {
createdAt: 'DESC',
},
relations: ['comment'],
});
if (!notifications) {
throw new NotFoundException('User notifications not found');
}
try {
const _notifications = notifications.map(async (notification) => {
const [, commentLink] = await this.getAppLinks(notification.comment.appVersionsId, notification.comment);
const user = await this.usersRepository.findOne({
where: { id: notification.comment.userId },
relations: ['avatar'],
});
const creator = {
firstName: user.firstName,
lastName: user.lastName,
avatar: user.avatar?.data.toString('base64'),
};
return {
id: notification.id,
creator,
comment: notification.comment.comment,
createdAt: notification.comment.createdAt,
updatedAt: notification.comment.updatedAt,
commentLink,
isRead: notification.isRead,
};
});
return Promise.all(_notifications);
} catch (error) {
throw new InternalServerErrorException(error);
}
}
public async update(commentUserId: string, body: UpdateCommentUserDto) {
const item = await this.commentUsersRepository.update(commentUserId, { isRead: body.isRead });
return item;
}
public async updateAll(userId: string, body: UpdateCommentUserDto) {
const { isRead } = body;
const item = await this.commentUsersRepository.update({ userId }, { isRead });
return item;
}
}

View file

@ -1,343 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from 'src/entities/user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
import { dbTransactionWrap } from 'src/helpers/database.helper';
import { EntityManager } from 'typeorm';
import { BadRequestException } from '@nestjs/common';
import { CreateUserDto, Status, UpdateGivenWorkspaceDto, UpdateUserDto, WorkspaceDto } from '@dto/external_apis.dto';
@Injectable()
export class ExternalApisService {
constructor() {}
private generateRandomPassword(length: number = 8): string {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let password = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
password += charset[randomIndex];
}
return password;
}
async getAllUsers(id?: string, manager?: EntityManager) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const query = manager
.createQueryBuilder(User, 'user')
.leftJoinAndSelect('user.organizationUsers', 'organizationUser')
.leftJoinAndSelect('organizationUser.organization', 'organization', 'organization.status=:activeStatus', {
activeStatus: 'active',
})
.leftJoinAndSelect('user.userGroupPermissions', 'userGroupPermissions')
.leftJoinAndSelect('userGroupPermissions.groupPermission', 'groupPermissions');
if (id) {
query.andWhere('user.id=:id', { id });
}
const users: User[] = await query.getMany();
const userResponses = users?.map((user) => {
const userResponse = {
id: user.id,
name: `${user.firstName} ${user.lastName}`,
email: user.email,
status: user.status,
workspaces: [],
};
const workspaces = user?.organizationUsers?.map((ou) => {
const workspaceResponse = {
id: ou.organization?.id,
name: ou.organization?.name,
status: ou.organization?.status,
groups: [],
};
const groups = user?.userGroupPermissions
?.filter((ugp) => ugp.groupPermission.organizationId === workspaceResponse.id)
?.map((ugp) => {
return {
id: ugp?.groupPermission?.id,
name: ugp?.groupPermission.group,
};
});
workspaceResponse.groups = groups || [];
return workspaceResponse;
});
userResponse.workspaces = workspaces || [];
return userResponse;
});
return !id ? userResponses || [] : userResponses && userResponses.length > 0 ? userResponses[0] : [];
}, manager);
}
async createUser(userDto: CreateUserDto) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const { name, email, password, status, workspaces } = userDto;
const [firstName, lastName] = name.split(' ');
// Generate a password if not provided
const userPassword = password || this.generateRandomPassword();
const newUser = manager.create(User, {
firstName,
lastName,
email,
password: userPassword,
status: status || Status.ARCHIVED,
});
await manager.save(User, newUser);
for (const workspace of workspaces) {
let organization = null;
if (workspace.id) {
organization = await manager.findOne(Organization, { where: { id: workspace.id } });
} else if (workspace.name) {
organization = await manager.findOne(Organization, { where: { name: workspace.name } });
}
if (!organization) {
throw new BadRequestException(
`The workspaces id or name do not exist: id ${workspace.id}, name ${workspace.name}`
);
}
const organizationUser = manager.create(OrganizationUser, {
userId: newUser.id,
organizationId: organization.id,
status: workspace?.status || Status.ARCHIVED,
role: 'all-users',
});
await manager.save(OrganizationUser, organizationUser);
let groups = workspace.groups;
if (!groups || groups.length === 0) {
groups = [{ name: 'all_users' }];
}
for (const group of groups) {
let groupPermission = null;
if (group.id) {
groupPermission = await manager.findOne(GroupPermission, { where: { id: group.id } });
} else if (group.name) {
groupPermission = await manager.findOne(GroupPermission, {
where: { group: group.name, organizationId: organization.id },
});
}
if (!groupPermission) {
throw new BadRequestException(`Group permission id or name not found: id ${group.id}, name ${group.name}`);
}
// Associate user with group permission
await manager.save(UserGroupPermission, {
userId: newUser.id,
groupPermissionId: groupPermission.id,
});
}
}
return await this.getAllUsers(newUser.id, manager);
});
}
async updateUser(id: string, updateDto: UpdateUserDto) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const user = await manager.findOne(User, { where: { id } });
if (!user) {
throw new NotFoundException('User not found');
}
const { name, email, password, status } = updateDto;
const userUpdateParams: Partial<User> = {};
if (name) {
const [firstName, lastName] = name.split(' ');
userUpdateParams.firstName = firstName;
userUpdateParams.lastName = lastName;
}
if (email) {
userUpdateParams.email = email;
}
if (password) {
userUpdateParams.password = password;
}
if (status) {
userUpdateParams.status = status;
}
await manager.update(User, { id: user.id }, userUpdateParams);
return;
});
}
async replaceUserAllWorkspacesRelations(userId: string, workspacesDto: WorkspaceDto[]) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const user = await manager.findOne(User, { where: { id: userId } });
if (!user) {
throw new NotFoundException('User not found');
}
// Remove existing group permissions for the user from all workspaces
await manager.delete(UserGroupPermission, { userId });
// Remove existing organization user records for the user
await manager.delete(OrganizationUser, { userId });
for (const workspace of workspacesDto) {
let organization = null;
if (workspace.id) {
organization = await manager.findOne(Organization, { where: { id: workspace.id } });
} else if (workspace.name) {
organization = await manager.findOne(Organization, { where: { name: workspace.name } });
}
if (!organization) {
throw new BadRequestException(
`The workspaces id or name do not exist: id ${workspace.id}, name ${workspace.name}`
);
}
const organizationUser = manager.create(OrganizationUser, {
userId: userId,
organizationId: organization.id,
status: workspace.status || Status.ARCHIVED,
role: 'all-users',
});
await manager.save(OrganizationUser, organizationUser);
const groups = !workspace.groups || workspace.groups.length === 0 ? [{ name: 'all_users' }] : workspace.groups;
for (const group of groups) {
let groupPermission = null;
if (group.id) {
groupPermission = await manager.findOne(GroupPermission, {
where: { id: group.id, organizationId: organization.id },
});
} else if (group.name) {
groupPermission = await manager.findOne(GroupPermission, {
where: { group: group.name, organizationId: organization.id },
});
}
if (!groupPermission) {
throw new BadRequestException(`Group permission id or name not found: id ${group.id}, name ${group.name}`);
}
// Associate user with group permission
await manager.save(UserGroupPermission, {
userId: userId,
groupPermissionId: groupPermission.id,
});
}
}
return;
});
}
async replaceUserWorkspaceRelations(userId: string, workspaceId: string, workspaceDto: UpdateGivenWorkspaceDto) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const query = manager
.createQueryBuilder(OrganizationUser, 'organizationUser')
.innerJoin('organizationUser.organization', 'organization', 'organization.status = :active', {
active: 'active',
})
.where('organizationUser.userId = :userId', { userId })
.andWhere('organizationUser.organizationId = :workspaceId', { workspaceId });
const organizationUser = await query.getOne();
if (!organizationUser) {
throw new NotFoundException('User not found');
}
if (workspaceDto.status && organizationUser.status !== workspaceDto.status) {
await manager.update(OrganizationUser, { status: workspaceDto.status }, { id: organizationUser.id });
}
if (workspaceDto.groups && workspaceDto.groups.length > 0) {
// Remove existing group permissions for the user in this workspace
await manager
.createQueryBuilder()
.delete()
.from(UserGroupPermission)
.where('userId = :userId', { userId })
.andWhere('groupPermissionId IN (SELECT id FROM group_permissions WHERE organization_id = :organizationId)', {
organizationId: workspaceId,
})
.execute();
// Add to groups
for (const group of workspaceDto.groups) {
let groupPermission = null;
if (group.id) {
groupPermission = await manager.findOne(GroupPermission, {
where: { id: group.id, organizationId: workspaceId },
});
} else if (group.name) {
groupPermission = await manager.findOne(GroupPermission, {
where: { group: group.name, organizationId: workspaceId },
});
}
if (!groupPermission) {
throw new BadRequestException(`Group permission id or name not found: id ${group.id}, name ${group.name}`);
}
// Associate user with group permission
await manager.save(UserGroupPermission, {
userId: userId,
groupPermissionId: groupPermission.id,
});
}
}
return;
});
}
async getAllWorkspaces() {
return await dbTransactionWrap(async (manager: EntityManager) => {
const workspaces: Organization[] = await manager.find(Organization, {
where: { status: 'active' },
relations: ['groupPermissions'],
});
const workspaceResponses = workspaces.map((workspace: Organization) => {
return {
id: workspace.id,
name: workspace.name,
status: workspace.status,
groups:
workspace?.groupPermissions?.map((groupPermission: GroupPermission) => {
return {
id: groupPermission.id,
name: groupPermission.group,
};
}) || [],
};
});
return workspaceResponses;
});
}
}

View file

@ -1,139 +0,0 @@
// import { Injectable } from '@nestjs/common';
// import { User } from 'src/entities/user.entity';
// import { ExportResourcesDto } from '@dto/export-resources.dto';
// import { AppImportExportService } from './app_import_export.service';
// import { TooljetDbImportExportService } from './tooljet_db_import_export_service';
// import { ImportResourcesDto, ImportTooljetDatabaseDto } from '@dto/import-resources.dto';
// import { AppsService } from './apps.service';
// import { CloneResourcesDto } from '@dto/clone-resources.dto';
// import { isEmpty } from 'lodash';
// import { ActionTypes, ResourceTypes } from 'src/entities/audit_log.entity';
// import { EventEmitter2 } from '@nestjs/event-emitter';
// @Injectable()
// export class ImportExportResourcesService {
// constructor(
// private readonly appImportExportService: AppImportExportService,
// private readonly appsService: AppsService,
// private readonly tooljetDbImportExportService: TooljetDbImportExportService,
// private eventEmitter: EventEmitter2
// ) {}
// async export(
// user: User,
// exportResourcesDto: ExportResourcesDto
// ): Promise<{
// tooljet_database?: Array<ImportTooljetDatabaseDto>;
// app?: Array<Record<string, any>>; // TODO: Define the type for app
// }> {
// const resourcesExport: {
// tooljet_database?: Array<ImportTooljetDatabaseDto>;
// app?: Array<Record<string, unknown>>;
// } = {};
// if (exportResourcesDto.tooljet_database?.length) {
// const exportedDbs: ImportTooljetDatabaseDto[] = [];
// for (const tjdb of exportResourcesDto.tooljet_database) {
// const exportedDb = await this.tooljetDbImportExportService.export(
// exportResourcesDto.organization_id,
// tjdb,
// exportResourcesDto.tooljet_database
// );
// exportedDbs.push(exportedDb);
// }
// if (exportedDbs.length > 0) resourcesExport.tooljet_database = exportedDbs;
// }
// if (exportResourcesDto.app?.length) {
// const exportedApps: Record<string, unknown>[] = [];
// for (const app of exportResourcesDto.app) {
// const exportedApp = {
// definition: await this.appImportExportService.export(user, app.id, app.search_params),
// };
// exportedApps.push(exportedApp);
// }
// if (exportedApps.length > 0) resourcesExport.app = exportedApps;
// }
// return resourcesExport;
// }
// async import(
// user: User,
// importResourcesDto: ImportResourcesDto,
// cloning = false,
// isGitApp = false,
// isTemplateApp = false
// ) {
// let tableNameMapping = {};
// const imports = { app: [], tooljet_database: [], tableNameMapping: {} };
// const importingVersion = importResourcesDto.tooljet_version;
// if (!isEmpty(importResourcesDto.tooljet_database)) {
// const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning);
// tableNameMapping = res.tableNameMapping;
// imports.tooljet_database = res.tooljet_database;
// imports.tableNameMapping = tableNameMapping;
// }
// if (!isEmpty(importResourcesDto.app)) {
// for (const appImportDto of importResourcesDto.app) {
// user.organizationId = importResourcesDto.organization_id;
// const createdApp = await this.appImportExportService.import(
// user,
// appImportDto.definition,
// appImportDto.appName,
// {
// tooljet_database: tableNameMapping,
// },
// isGitApp,
// importResourcesDto.tooljet_version,
// cloning
// );
// imports.app.push({ id: createdApp.id, name: createdApp.name });
// this.eventEmitter.emit('auditLogEntry', {
// userId: user.id,
// organizationId: user.organizationId,
// resourceId: createdApp.id,
// resourceType: ResourceTypes.APP,
// resourceName: createdApp.name,
// actionType: ActionTypes.APP_CREATE,
// });
// }
// }
// return imports;
// }
// async clone(user: User, { organization_id, app: [{ id: appId, name: newAppName }] }: CloneResourcesDto) {
// const tablesForApp = await this.appsService.findTooljetDbTables(appId);
// const exportResourcesDto: ExportResourcesDto = {
// organization_id,
// app: [{ id: appId, search_params: null }],
// tooljet_database: tablesForApp,
// };
// const resourceExport = await this.export(user, exportResourcesDto);
// // TODO: Verify if this is required as we always pass name on imports
// // Without this appImportExportService.import will throw an error
// resourceExport.app[0].definition.appV2.name = newAppName;
// const importResourcesDto: ImportResourcesDto = {
// organization_id,
// tooljet_version: globalThis.TOOLJET_VERSION,
// app: [
// {
// appName: newAppName,
// definition: resourceExport.app[0].definition,
// },
// ],
// tooljet_database: resourceExport.tooljet_database,
// };
// return this.import(user, importResourcesDto, true);
// }
// }

View file

@ -1,140 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { User } from '../entities/user.entity';
import { readFileSync } from 'fs';
import { Logger } from 'nestjs-pino';
import { ImportExportResourcesService } from './import_export_resources.service';
import { ImportResourcesDto } from '@dto/import-resources.dto';
import { AppImportExportService } from './app_import_export.service';
import { isVersionGreaterThanOrEqual } from 'src/helpers/utils.helper';
import { AppsService } from './apps.service';
import { getMaxCopyNumber } from 'src/helpers/utils.helper';
import * as fs from 'fs';
import * as path from 'path';
import { TooljetDbBulkUploadService } from '@services/tooljet_db_bulk_upload.service';
import { PluginsService } from './plugins.service';
@Injectable()
export class LibraryAppCreationService {
constructor(
private readonly importExportResourcesService: ImportExportResourcesService,
private readonly appImportExportService: AppImportExportService,
private readonly appsService: AppsService,
private readonly logger: Logger,
private readonly tooljetDbBulkUploadService: TooljetDbBulkUploadService,
private readonly pluginsService: PluginsService
) {}
async perform(
currentUser: User,
identifier: string,
appName: string,
dependentPluginsForTemplate: Array<string>,
shouldAutoImportPlugin: boolean
) {
const templateDefinition = this.findTemplateDefinition(identifier);
if (dependentPluginsForTemplate.length)
await this.pluginsService.autoInstallPluginsForTemplates(dependentPluginsForTemplate, shouldAutoImportPlugin);
return this.importTemplate(currentUser, templateDefinition, appName, identifier);
}
async createSampleApp(currentUser: User) {
let name = 'Sample app ';
const allSampleApps = await this.appsService.findAll(currentUser?.organizationId, { name });
const existNameList = allSampleApps.map((app) => app.name);
const maxNumber = getMaxCopyNumber(existNameList, ' ');
name = `${name} ${maxNumber}`;
const sampleAppDef = JSON.parse(readFileSync(`templates/sample_app_def.json`, 'utf-8'));
return this.importTemplate(currentUser, sampleAppDef, name);
}
async createSampleOnboardApp(currentUser: User) {
const name = 'Product inventory';
const sampleAppDef = JSON.parse(readFileSync(`templates/onboard_sample_app.json`, 'utf-8'));
return this.importTemplate(currentUser, sampleAppDef, name);
}
async importTemplate(currentUser: User, templateDefinition: any, appName: string, identifier?: string) {
const importDto = new ImportResourcesDto();
importDto.organization_id = currentUser.organizationId;
importDto.app = templateDefinition.app || templateDefinition.appV2;
importDto.tooljet_database = templateDefinition.tooljet_database;
importDto.tooljet_version = templateDefinition.tooljet_version;
if (isVersionGreaterThanOrEqual(templateDefinition.tooljet_version, '2.16.0')) {
importDto.app[0].appName = appName;
const importedResources = await this.importExportResourcesService.import(
currentUser,
importDto,
false,
false,
true
);
const tableNameMapping: { [key: string]: { id: string; table_name: string } } =
importedResources.tableNameMapping;
const entries = Object.entries(tableNameMapping);
for (let i = 0; i < entries.length; i++) {
const [key, { id: tableId }] = entries[i];
const tableIdFromDefinition = key;
const newTableid = tableId;
const tableDetails = templateDefinition.tooljet_database.find(
(table: Record<string, any>) => table.id === tableIdFromDefinition
);
if (tableDetails) {
const tableNameAsPerDefinition = tableDetails.table_name;
this.processCsvFile(identifier, tableNameAsPerDefinition, newTableid, currentUser.organizationId);
}
}
return importedResources;
} else {
const importedApp = await this.appImportExportService.import(currentUser, templateDefinition, appName);
return {
app: [importedApp],
tooljet_database: [],
};
}
}
findTemplateDefinition(identifier: string) {
try {
return JSON.parse(readFileSync(`templates/${identifier}/definition.json`, 'utf-8'));
} catch (err) {
this.logger.error(err);
throw new BadRequestException('App definition not found');
}
}
async processCsvFile(identifier: string, tableName: string, tableId: string, organizationId: string) {
try {
const csvFilePath = path.join('templates', `${identifier}/data/${tableName}/data.csv`);
// Read the CSV file and convert it into a buffer
const fileBuffer = fs.readFileSync(csvFilePath);
return await this.tooljetDbBulkUploadService.bulkUploadCsv(tableId, fileBuffer, organizationId);
} catch (error) {
console.error('Error processing CSV file:', error);
throw new BadRequestException('Failed to process CSV file');
}
}
async findDepedentPluginsFromTemplateDefinition(identifier: string) {
const templateDefinition = this.findTemplateDefinition(identifier);
const importDto = new ImportResourcesDto();
importDto.app = templateDefinition.app || templateDefinition.appV2;
const dataSourcesUsedInApps = [];
importDto.app.forEach((appDefinition) => {
appDefinition.definition?.appV2.dataSources.forEach((dataSource) => {
dataSourcesUsedInApps.push(dataSource);
});
});
const { pluginsToBeInstalled, pluginsListIdToDetailsMap } = await this.pluginsService.checkIfPluginsToBeInstalled(
dataSourcesUsedInApps
);
return { pluginsToBeInstalled, pluginsListIdToDetailsMap };
}
}

View file

@ -1,126 +0,0 @@
import { CreateEnvironmentVariableDto, UpdateEnvironmentVariableDto } from '@dto/environment-variable.dto';
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.entity';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { cleanObject } from 'src/helpers/utils.helper';
import { EncryptionService } from '@modules/encryption/service';
@Injectable()
export class OrgEnvironmentVariablesService {
constructor(
@InjectRepository(OrgEnvironmentVariable)
private orgEnvironmentVariablesRepository: Repository<OrgEnvironmentVariable>,
private encryptionService: EncryptionService
) {}
async fetchVariables(organizationId: string): Promise<OrgEnvironmentVariable[]> {
const variables: OrgEnvironmentVariable[] = await this.orgEnvironmentVariablesRepository.find({
where: { organizationId },
});
await Promise.all(
variables.map(async (variable: OrgEnvironmentVariable) => {
if (variable.variableType === 'server') {
delete variable.value;
} else {
if (variable.encrypted) variable['value'] = await this.decryptSecret(organizationId, variable.value);
}
})
);
return variables;
}
async create(
currentUser: User,
environmentVariableDto: CreateEnvironmentVariableDto
): Promise<OrgEnvironmentVariable> {
const variableToFind = await this.orgEnvironmentVariablesRepository.findOne({
where: {
variableName: environmentVariableDto.variable_name,
variableType: environmentVariableDto.variable_type,
},
});
if (variableToFind) {
throw new ConflictException(
`Variable name already exists in ${environmentVariableDto.variable_type ?? 'environment'} variables`
);
}
const encrypted = environmentVariableDto.variable_type === 'server' ? true : environmentVariableDto.encrypted;
let value: string;
if (encrypted && environmentVariableDto.value) {
value = await this.encryptSecret(currentUser.organizationId, environmentVariableDto.value);
} else {
value = environmentVariableDto.value;
}
return await this.orgEnvironmentVariablesRepository.save(
this.orgEnvironmentVariablesRepository.create({
variableName: environmentVariableDto.variable_name,
value,
variableType: environmentVariableDto.variable_type,
encrypted,
organizationId: currentUser.organizationId,
createdAt: new Date(),
updatedAt: new Date(),
})
);
}
async fetch(organizationId: string, variableId: string) {
return await this.orgEnvironmentVariablesRepository.findOne({
where: {
organizationId,
id: variableId,
},
});
}
async update(organizationId: string, variableId: string, params: UpdateEnvironmentVariableDto) {
const { variable_name } = params;
let value = params.value;
const variable = await this.fetch(organizationId, variableId);
if (variable_name) {
const variableToFind = await this.orgEnvironmentVariablesRepository.findOne({
where: {
variableName: variable_name,
variableType: variable.variableType,
},
});
if (variableToFind && variableToFind.id !== variableId) {
throw new ConflictException(`Variable name already exists in ${variable.variableType} variables`);
}
}
if (variable.encrypted && value) {
value = await this.encryptSecret(organizationId, value);
}
const updateableParams = {
variableName: variable_name,
value,
};
// removing keys with undefined values
cleanObject(updateableParams);
return await this.orgEnvironmentVariablesRepository.update({ organizationId, id: variableId }, updateableParams);
}
async delete(organizationId: string, variableId: string) {
return await this.orgEnvironmentVariablesRepository.delete({ organizationId, id: variableId });
}
private async encryptSecret(workspaceId: string, value: string) {
return await this.encryptionService.encryptColumnValue('org_environment_variables', workspaceId, value);
}
private async decryptSecret(workspaceId: string, value: string) {
return await this.encryptionService.decryptColumnValue('org_environment_variables', workspaceId, value);
}
}

View file

@ -1,254 +0,0 @@
import { Injectable, OnModuleInit, OnApplicationShutdown } from '@nestjs/common';
import { NativeConnection, Worker } from '@temporalio/worker';
import { Connection, Client, ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client';
import * as moment from 'moment';
import { CreateWorkflowExecutionDto } from '@dto/create-workflow-execution.dto';
import { WorkflowSchedule } from '@entities/workflow_schedule.entity';
import { isValidCron } from 'cron-validator';
@Injectable()
export class TemporalService implements OnModuleInit, OnApplicationShutdown {
client: Client;
temporalConnection: Connection = undefined;
workerNativeConnection: NativeConnection = undefined;
worker: Worker = undefined;
async onModuleInit() {
this.#connectToTemporal();
}
async onApplicationShutdown() {
await this.#closeConnection();
await this.#closeWokerNativeConnection();
}
async isTemporalConnected(): Promise<boolean> {
try {
await this.temporalConnection.healthService.check({});
return true;
} catch (exception) {
return false;
}
}
async runWorker() {
try {
console.log(`\x1b[1;33m[INFO] Starting worker connection to Temporal\x1b[0m`);
this.workerNativeConnection = await NativeConnection.connect({
address: process.env.TEMPORAL_SERVER_ADDRESS,
});
this.worker = await Worker.create({
connection: this.workerNativeConnection,
namespace: process.env?.TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE ?? 'default',
taskQueue: process.env?.TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS,
workflowsPath: require.resolve('../temporal/workflows'),
activities: require('../temporal/activities'),
});
console.log(`\x1b[1;33m[INFO] Worker connection to Temporal established.\x1b[0m`);
await this.worker.run();
} catch (error) {
console.log(`\x1b[1;31m[ERROR] Temporal server could not be reached, exiting.\x1b[0m Reason: ${error.message}`);
process.exit(1);
}
}
async createScheduleInTemporal(
workflowScheduleId: string,
settings: any,
schedule: WorkflowSchedule,
environmentId,
timezone,
userId,
paused = true
) {
const workflowExecution = new CreateWorkflowExecutionDto();
workflowExecution.executeUsing = 'version';
workflowExecution.appVersionId = schedule.workflowId;
workflowExecution.environmentId = environmentId;
workflowExecution.params = {};
workflowExecution.userId = userId;
workflowExecution.app = schedule.workflow.appId;
const interval: string =
schedule.type === 'cron'
? `${settings.minute} ${settings.hours} ${settings.dayOfMonth} ${settings.month} ${settings.dayOfWeek}`
: this.#convertWorkflowScheduleSettingsToCronString(settings);
if (isValidCron(interval, { seconds: false }) == false) {
throw Error('Invalid interval configuration ' + interval);
}
const scheduleOptions: ScheduleOptions = {
scheduleId: workflowScheduleId,
action: {
taskQueue: process.env?.TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS ?? 'tooljet-workflows',
type: 'startWorkflow',
workflowType: 'execute',
workflowId: `schedule-${workflowScheduleId}`,
args: [JSON.parse(JSON.stringify(workflowExecution))],
retry: {
maximumAttempts: 1,
},
},
spec: {
cronExpressions: [interval],
timezone,
},
policies: {
overlap: ScheduleOverlapPolicy.SKIP,
},
state: {
paused,
},
};
await this.client.schedule.create(scheduleOptions);
}
async setScheduleState(scheduleId: string, paused: boolean) {
const handle = await this.client.schedule.getHandle(scheduleId);
if (paused) await handle.pause('Paused from ToolJet');
else await handle.unpause('Unpaused from ToolJet');
}
async removeSchedule(scheduleId: string) {
const handle = await this.client.schedule.getHandle(scheduleId);
await handle.delete();
}
async updateSchedule(updatedSchedule: WorkflowSchedule, settings: any, timezone, existingSchedule, userId) {
const handle = this.client.schedule.getHandle(updatedSchedule.id);
await handle.delete();
try {
await this.createScheduleInTemporal(
updatedSchedule.id,
updatedSchedule.details,
updatedSchedule,
updatedSchedule.environmentId,
timezone,
userId,
!existingSchedule.active
);
} catch (error) {
console.log({ error });
await this.createScheduleInTemporal(
existingSchedule.id,
existingSchedule.details,
existingSchedule,
existingSchedule.environmentId,
existingSchedule.timezone,
userId,
!existingSchedule.active
);
throw error;
}
}
#convertWorkflowScheduleSettingsToCronString(settings: any): string {
switch (settings.frequency) {
case 'minute': {
return '* * * * *';
}
case 'hour': {
const { minutes } = settings;
return `${minutes} * * * *`;
}
case 'day': {
const { hour } = settings;
const hourOfTheDay = this.#convertToHourOffset(hour);
return `0 ${hourOfTheDay} * * *`;
}
case 'week': {
const { day, hour } = settings;
const dayOfTheWeek = moment().day(day).day();
const hourOfTheDay = this.#convertToHourOffset(hour);
return `0 ${hourOfTheDay} * * ${dayOfTheWeek}`;
}
case 'month': {
const { date: dayOfMonth, hour } = settings;
const hourOfTheDay = this.#convertToHourOffset(hour);
return `0 ${hourOfTheDay} ${dayOfMonth} * *`;
}
}
}
#convertToHourOffset(timeString) {
const time = moment(timeString, 'h:mm A');
return time.hours() + time.minutes() / 60;
}
#connectToTemporal() {
if (!process.env.WORKER) {
if (process.env.ENABLE_WORKFLOW_SCHEDULING === 'true') {
console.log(`\x1b[1;33m[INFO] Connecting to Temporal server\x1b[0m`);
Connection.connect({
address: process.env.TEMPORAL_SERVER_ADDRESS,
})
.then((connection) => {
this.temporalConnection = connection;
this.client = new Client({
connection,
namespace: process.env?.TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE ?? 'default',
});
console.log(`\x1b[1;32m[INFO] Connected to Temporal server\x1b[0m`);
})
.catch((reason) => {
console.log(
`\x1b[1;33m [WARNING] Temporal server could not be reached, workflow schedules cannot be created, deleted or updated.\x1b[0m Reason: ${reason.message}`
);
});
} else {
console.log(
`\x1b[1;33m[INFO] Not connecting to temporal as ENABLE_WORKFLOW_SCHEDULING is not set to 'true'. \x1b[0m`
);
}
}
}
async #closeConnection() {
if (this.temporalConnection) {
console.log(`\x1b[1;33m[INFO] Closing Temporal connection\x1b[0m`);
this.temporalConnection
.close()
.then(() => {
console.log(`\x1b[1;32m[INFO] Temporal connection closed\x1b[0m`);
})
.catch(() => {
console.log(`\x1b[1;31m[ERROR] Could not close Temporal connection\x1b[0m`);
});
}
}
async #closeWokerNativeConnection() {
if (process.env.WORKER) {
console.log(`\x1b[1;33m[INFO] Closing worker connection to Temporal\x1b[0m`);
try {
await this.workerNativeConnection.close();
console.log(`\x1b[1;32m[INFO] Worker connection to temporal closed\x1b[0m`);
} catch (error) {
console.log(
`\x1b[1;31m[ERROR] Worker connection to temporal failed to close. Reason: ${error.message} \x1b[0m`
);
}
}
}
shutDownWorker() {
this.worker.shutdown();
}
}

View file

@ -1,76 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Thread } from '../entities/thread.entity';
import { CreateThreadDto, UpdateThreadDto } from '../dto/thread.dto';
import { ThreadRepository } from '../repositories/thread.repository';
import { DataSource } from 'typeorm';
@Injectable()
export class ThreadService {
constructor(
@InjectRepository(ThreadRepository)
private threadRepository: ThreadRepository,
private readonly _dataSource: DataSource
) {}
public async createThread(createThreadDto: CreateThreadDto, userId: string, orgId: string): Promise<Thread> {
const thread: Thread = await this.threadRepository.createThread(createThreadDto, userId, orgId);
return (await this.getThreads(thread.appId, thread.organizationId, thread.appVersionsId, thread.id))?.[0];
}
public async getThreads(
appId: string,
organizationId: string,
appVersionsId: string,
threadId?: string
): Promise<Thread[]> {
const query = this._dataSource
.createQueryBuilder(Thread, 'thread')
.innerJoin('thread.user', 'user')
.addSelect(['user.id', 'user.firstName', 'user.lastName'])
.andWhere('thread.appId = :appId', {
appId,
})
.andWhere('thread.organizationId = :organizationId', {
organizationId,
})
.andWhere('thread.appVersionsId = :appVersionsId', {
appVersionsId,
});
if (threadId) {
query.andWhere('thread.id = :threadId', {
threadId,
});
}
return await query.getMany();
}
public async getOrganizationThreads(organizationId: string): Promise<Thread[]> {
return await this.threadRepository.find({
where: {
organizationId,
},
});
}
public async getThread(threadId: string): Promise<Thread> {
const foundThread = await this.threadRepository.findOne({ where: { id: threadId } });
if (!foundThread) {
throw new NotFoundException('Thread not found');
}
return foundThread;
}
public async editThread(threadId: string, updateThreadDto: UpdateThreadDto): Promise<Thread> {
const editedThread = await this.threadRepository.findOne({ where: { id: threadId } });
if (!editedThread) {
throw new NotFoundException('Thread not found');
}
return this.threadRepository.editThread(updateThreadDto, editedThread);
}
public async deleteThread(threadId: string): Promise<void> {
await this.threadRepository.delete(threadId);
}
}

View file

@ -1,19 +0,0 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
@Injectable()
export class UserCommonService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>
) {}
@OnEvent('user.update')
async handleUserUpdate(event: { userId: string; details: Partial<User> }) {
const { userId, details } = event;
return await this.userRepository.update(userId, details);
}
}

View file

@ -1,8 +0,0 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
@Injectable()
export class WorkerService implements OnModuleInit {
onModuleInit() {
console.log(`The module has been initialized.`);
}
}

View file

@ -1,882 +0,0 @@
// import { CreateWorkflowExecutionDto } from '@dto/create-workflow-execution.dto';
// import { Injectable } from '@nestjs/common';
// import { InjectRepository } from '@nestjs/typeorm';
// import { AppVersion } from 'src/entities/app_version.entity';
// import { App } from 'src/entities/app.entity';
// import { WorkflowExecution } from 'src/entities/workflow_execution.entity';
// import { WorkflowExecutionNode } from 'src/entities/workflow_execution_node.entity';
// import { WorkflowExecutionEdge } from 'src/entities/workflow_execution_edge.entity';
// import { dbTransactionWrap } from 'src/helpers/database.helper';
// import { EntityManager, Repository } from 'typeorm';
// import { find } from 'lodash';
// import { DataQueriesService } from '../modules/data-queries/service';
// import { User } from 'src/entities/user.entity';
// import { getQueryVariables, resolveCode } from '../../lib/utils';
// import { Graph, alg } from '@dagrejs/graphlib';
// import * as moment from 'moment';
// import { stringify } from 'flatted';
// import { OrganizationConstantsService } from '../modules/organization-constants/service';
// // import { Organization } from 'src/entities/organization.entity';
// import { Response } from 'express';
// import { cloneDeep } from 'lodash';
// const STATIC_NODE_TYPE_TO_HANDLE_MAPPING = {
// input: 'startTrigger',
// output: 'responseNode',
// 'if-condition': 'ifCondition',
// };
// import { OrganizationConstantType } from 'src/entities/organization_constants.entity';
// import { LicenseTermsService } from '@modules/licensing/interfaces/IService';
// import { LICENSE_FIELD, LICENSE_LIMIT } from '@modules/licensing/constants';
// @Injectable()
// export class WorkflowExecutionsService {
// constructor(
// @InjectRepository(AppVersion)
// private appVersionsRepository: Repository<AppVersion>,
// @InjectRepository(WorkflowExecution)
// private workflowExecutionRepository: Repository<WorkflowExecution>,
// @InjectRepository(WorkflowExecutionEdge)
// private workflowExecutionEdgeRepository: Repository<WorkflowExecutionEdge>,
// @InjectRepository(WorkflowExecutionNode)
// private workflowExecutionNodeRepository: Repository<WorkflowExecutionNode>,
// @InjectRepository(User)
// private userRepository: Repository<User>,
// private dataQueriesService: DataQueriesService,
// private licenseTermsService: LicenseTermsService,
// private organizationConstantsService: OrganizationConstantsService
// ) {}
// workflowExecutionTimeout = parseInt(process.env.WORKFLOW_TIMEOUT_SECONDS);
// async create(createWorkflowExecutionDto: CreateWorkflowExecutionDto): Promise<WorkflowExecution> {
// const workflowExecution = await dbTransactionWrap(async (manager: EntityManager) => {
// const appVersionId =
// createWorkflowExecutionDto?.appVersionId ??
// (
// await manager.findOne(App, {
// where: { id: createWorkflowExecutionDto.appId },
// })
// ).editingVersion.id;
// const workflowExecution = await manager.save(
// WorkflowExecution,
// manager.create(WorkflowExecution, {
// appVersionId: appVersionId,
// createdAt: new Date(),
// updatedAt: new Date(),
// })
// );
// const appVersion = await this.appVersionsRepository.findOne({ where: { id: workflowExecution.appVersionId } });
// const definition = appVersion.definition;
// const queryIdsOnDefinitionToActualQueryIdMapping = Object.fromEntries(
// definition.nodes
// .filter((node) => node.type === 'query')
// .map((node) => node.data.idOnDefinition)
// .map((idOnDefinition) => [
// idOnDefinition,
// find(appVersion.definition.queries, {
// idOnDefinition,
// }).id,
// ])
// );
// const queries = await this.dataQueriesService.findByIds(
// Object.values(queryIdsOnDefinitionToActualQueryIdMapping)
// );
// const nodes = [];
// for (const nodeData of definition.nodes) {
// nodeData.data.handle = this.computeNodeHandle(nodeData, queries, queryIdsOnDefinitionToActualQueryIdMapping);
// const node = await manager.save(
// WorkflowExecutionNode,
// manager.create(WorkflowExecutionNode, {
// type: nodeData.type,
// workflowExecutionId: workflowExecution.id,
// idOnWorkflowDefinition: nodeData.id,
// definition: nodeData.data,
// createdAt: new Date(),
// updatedAt: new Date(),
// })
// );
// nodes.push(node);
// }
// const startNode = find(nodes, (node) => node.definition.nodeType === 'start');
// workflowExecution.startNodeId = startNode.id;
// await manager.update(WorkflowExecution, workflowExecution.id, { startNode });
// const edges = [];
// for (const edgeData of definition.edges) {
// // const sourceNode = find(nodes, (node) => node.idOnWorkflowDefinition === edgeData.source);
// // const targetNode = find(nodes, (node) => node.idOnWorkflowDefinition === edgeData.target);
// const edge = await manager.save(
// WorkflowExecutionEdge,
// manager.create(WorkflowExecutionEdge, {
// workflowExecutionId: workflowExecution.id,
// idOnWorkflowDefinition: edgeData.id,
// sourceWorkflowExecutionNodeId: find(nodes, (node) => node.idOnWorkflowDefinition === edgeData.source).id,
// targetWorkflowExecutionNodeId: find(nodes, (node) => node.idOnWorkflowDefinition === edgeData.target).id,
// sourceHandle: edgeData.sourceHandle,
// createdAt: new Date(),
// updatedAt: new Date(),
// })
// );
// edges.push(edge);
// }
// return workflowExecution;
// });
// return workflowExecution;
// }
// async getStatus(workflowExecutionId: string) {
// const workflowExecution = await this.workflowExecutionRepository.findOne({
// where: { id: workflowExecutionId },
// });
// const workflowExecutionNodes = await this.workflowExecutionNodeRepository.find({
// where: {
// workflowExecutionId: workflowExecution.id,
// },
// });
// const nodes = workflowExecutionNodes.map((node) => ({
// id: node.id,
// idOnDefinition: node.idOnWorkflowDefinition,
// executed: node.executed,
// result: node.result,
// }));
// return {
// logs: workflowExecution.logs,
// status: workflowExecution.executed,
// nodes,
// };
// }
// async getWorkflowExecution(workflowExecutionId: string) {
// const workflowExecution = await this.workflowExecutionRepository.findOne({
// where: { id: workflowExecutionId },
// });
// const nodes = await this.workflowExecutionNodeRepository.find({
// where: { id: workflowExecutionId },
// });
// const appVersion = await this.appVersionsRepository.findOne({
// where: { id: workflowExecution.appVersionId },
// });
// const queries = await this.dataQueriesService.all({ app_version_id: appVersion.id });
// const nodesWithHandles = nodes.map((node) => {
// const queryId = find(appVersion.definition.queries, {
// idOnDefinition: node.definition.idOnDefinition,
// })?.id;
// const query = find(queries, { id: queryId });
// if (query) {
// node.definition.handle = query.name;
// } else {
// node.definition.handle = STATIC_NODE_TYPE_TO_HANDLE_MAPPING[node.type];
// }
// return node;
// });
// workflowExecution.nodes = nodesWithHandles;
// return workflowExecution;
// }
// async listWorkflowExecutions(appVersionId: string) {
// const workflowExecutions = await this.workflowExecutionRepository.find({
// where: {
// appVersionId,
// },
// order: {
// createdAt: 'DESC',
// },
// relations: ['nodes'],
// take: 10,
// });
// return workflowExecutions;
// }
// async execute(
// workflowExecution: WorkflowExecution,
// params: object = {},
// envId = '',
// response: Response
// ): Promise<object> {
// const organization: any = await dbTransactionWrap(async (manager: EntityManager) => {
// return manager
// .createQueryBuilder('organizations', 'organization')
// .innerJoin('apps', 'app', 'app.organization_id = organization.id')
// .innerJoin('app_versions', 'av', 'av.app_id = app.id')
// .innerJoin('workflow_executions', 'we', 'we.app_version_id = av.id')
// .where('we.id = :workflowExecutionId', { workflowExecutionId: workflowExecution.id })
// .getOne();
// });
// const constants = await this.organizationConstantsService.getConstantsForEnvironment(
// organization.id,
// envId,
// OrganizationConstantType.GLOBAL
// );
// const constantsObject = Object.fromEntries(constants.map((constant) => [constant.name, constant.value]));
// const appVersion = await this.appVersionsRepository.findOne({
// where: {
// id: workflowExecution.appVersionId,
// },
// relations: ['app'],
// });
// this.workflowExecutionTimeout =
// (await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.WORKFLOWS))?.execution_timeout ??
// parseInt(process.env.WORKFLOW_TIMEOUT_SECONDS);
// if (envId) appVersion.currentEnvironmentId = envId;
// workflowExecution = await this.workflowExecutionRepository.findOne({
// where: {
// id: workflowExecution.id,
// },
// relations: ['startNode', 'user', 'nodes', 'edges'],
// });
// let finalResult = {};
// const logs = [];
// const queue = [];
// const addLog = (message: string, status = 'normal') =>
// logs.push({ createdAt: moment().utc().format('YYYY-MM-DD HH:mm:ss.SSS'), message, nodeId: undefined, status });
// // FIXME: isMaintenanceOn - Column is used to check whether workflow is enabled or not.
// if (appVersion.app.isMaintenanceOn) {
// queue.push(
// ...this.computeNodesToBeExecuted(workflowExecution.startNode, workflowExecution.nodes, workflowExecution.edges)
// );
// } else {
// addLog('Workflow is disabled', 'failure');
// }
// let executionFailed = false;
// while (queue.length != 0 && executionFailed === false) {
// const nodeToBeExecuted = queue.shift();
// const currentNode = await this.workflowExecutionNodeRepository.findOne({ where: { id: nodeToBeExecuted.id } });
// const addLog = (message: string, queryName: string = undefined, status = 'normal') =>
// logs.push({
// createdAt: moment().utc().format('YYYY-MM-DD HH:mm:ss.SSS'),
// message,
// nodeId: currentNode.idOnWorkflowDefinition,
// kind: currentNode?.definition?.kind ?? STATIC_NODE_TYPE_TO_HANDLE_MAPPING[currentNode.type],
// handle: queryName ? queryName : STATIC_NODE_TYPE_TO_HANDLE_MAPPING[currentNode.type],
// status,
// });
// const currentTime = moment();
// const timeTaken = currentTime.diff(moment(workflowExecution.createdAt));
// if (timeTaken / 1000 > this.workflowExecutionTimeout) {
// addLog('Execution stopped due to timeout', undefined, 'failure');
// break;
// }
// let { state } = await this.getStateAndPreviousNodesExecutionCompletionStatus(currentNode);
// state = { constants: constantsObject, ...state };
// // eslint-disable-next-line no-empty
// if (currentNode.executed) {
// } else {
// switch (currentNode.type) {
// case 'input': {
// const { result } = await this.processStartNode(currentNode, params, addLog);
// if (result?.status === 'failed') executionFailed = true;
// break;
// }
// case 'query': {
// const { result } = await this.processQueryNode(
// currentNode,
// workflowExecution,
// appVersion,
// state,
// addLog,
// response,
// queue
// );
// if (result?.status === 'failed' && !currentNode.definition.errorHandler) executionFailed = true;
// break;
// }
// case 'if-condition': {
// const { result } = await this.processIfConditionNode(
// currentNode,
// workflowExecution,
// appVersion,
// state,
// addLog,
// response,
// queue
// );
// if (result?.status === 'failed') executionFailed = true;
// break;
// }
// case 'output': {
// const { result } = await this.processResponseNode(
// currentNode,
// workflowExecution,
// appVersion,
// state,
// addLog,
// response,
// queue
// );
// finalResult = result?.data ?? {};
// if (result?.status === 'failed') executionFailed = true;
// break;
// }
// }
// }
// }
// await this.saveExecutionStatus({ workflowExecution, logs, executionFailed });
// await this.markWorkflowAsExecuted(workflowExecution);
// await this.saveWorkflowLogs(workflowExecution, logs);
// return finalResult;
// }
// async processStartNode(node: WorkflowExecutionNode, params: object, addLog) {
// addLog('Execution started', undefined, 'success');
// await this.completeNodeExecution(node, '', { startTrigger: { params } });
// const result: any = { status: 'ok' };
// return { result };
// }
// async processQueryNode(
// node: WorkflowExecutionNode,
// execution: WorkflowExecution,
// appVersion: AppVersion,
// state: object,
// basicAddLog: any,
// response: Response,
// queue: WorkflowExecutionNode[]
// ) {
// const queryId = find(appVersion.definition.queries, {
// idOnDefinition: node.definition.idOnDefinition,
// }).id;
// const query = await this.dataQueriesService.findOne(queryId);
// const addLog = (message, status = 'normal') => basicAddLog(message, query.name, status);
// //* beta: workflow execution's environment is "development" by default
// const currentEnvironmentId = appVersion.currentEnvironmentId;
// const user = await this.userRepository.findOne({
// where: {
// id: execution.executingUserId,
// },
// relations: ['organization'],
// });
// user.organizationId = user.organization.id;
// let result: any = {};
// let handleToSkip = 'failure';
// try {
// addLog(`Started execution`);
// if (node.definition.looped) {
// const iterationValues = resolveCode({ code: node.definition?.iterationValuesCode, state, addLog });
// if (!Array.isArray(iterationValues)) throw new Error('Loop array did not resolve into an array');
// let index = 0;
// result = [];
// for (const value of iterationValues) {
// const currentTime = moment();
// const timeTaken = currentTime.diff(moment(execution.createdAt));
// if (timeTaken / 1000 > this.workflowExecutionTimeout) {
// throw new Error('Execution stopped due to timeout');
// }
// const modifiedState = { ...state, value, index };
// const options = getQueryVariables(query.options, modifiedState, addLog);
// result.push(
// query.kind === 'runjs'
// ? resolveCode({ code: query.options?.code, state: modifiedState, addLog })
// : (
// await this.dataQueriesService.runQuery(
// user,
// cloneDeep(query),
// options,
// response,
// currentEnvironmentId
// )
// )['data']
// );
// index++;
// }
// } else {
// const options = getQueryVariables(query.options, state, addLog);
// result =
// query.kind === 'runjs'
// ? resolveCode({ code: query.options?.code, state, addLog })
// : (await this.dataQueriesService.runQuery(user, query, options, response, currentEnvironmentId))['data'];
// }
// const decoratedResult = { status: 'ok', data: result };
// const newState = {
// ...state,
// [query.name]: decoratedResult,
// };
// addLog(`Execution succeeded`, 'success');
// await this.completeNodeExecution(node, stringify(decoratedResult), newState);
// } catch (exception) {
// // if (exception instanceof TypeError) throw exception;
// addLog(`Execution failed: ${exception.message}`, 'failure');
// result = { status: 'failed', exception };
// const newState = {
// ...state,
// [query.name]: result,
// };
// handleToSkip = 'success';
// await this.completeNodeExecution(node, stringify(result), newState);
// }
// execution.edges
// .filter((edge) => edge.sourceWorkflowExecutionNodeId === node.id && edge.sourceHandle == handleToSkip)
// .forEach((edge) => (edge.skipped = true));
// queue.length = 0;
// queue.push(...this.computeNodesToBeExecuted(execution.startNode, execution.nodes, execution.edges));
// return { result };
// }
// async processIfConditionNode(
// currentNode: WorkflowExecutionNode,
// workflowExecution: WorkflowExecution,
// appVersion: AppVersion,
// state: object,
// addLog: any,
// response: Response,
// queue: WorkflowExecutionNode[]
// ) {
// const code = currentNode.definition?.code ?? '';
// let sourceHandleToBeSkipped = 'false';
// let result: any = {};
// try {
// result = { status: 'ok', data: resolveCode({ code, state, isIfCondition: true, addLog }) };
// addLog('If condition evaluated to ' + result);
// sourceHandleToBeSkipped = result.data ? 'false' : 'true';
// await this.completeNodeExecution(currentNode, stringify(result), { ...state });
// } catch (exception) {
// addLog(`Code within if condition failed: ${exception.message}`);
// result = { status: 'failed' };
// await this.completeNodeExecution(currentNode, stringify(result), { ...state });
// }
// workflowExecution.edges
// .filter(
// (edge) => edge.sourceWorkflowExecutionNodeId === currentNode.id && edge.sourceHandle === sourceHandleToBeSkipped
// )
// .forEach((edge) => (edge.skipped = true));
// queue.length = 0;
// queue.push(
// ...this.computeNodesToBeExecuted(workflowExecution.startNode, workflowExecution.nodes, workflowExecution.edges)
// );
// return { result };
// }
// async processResponseNode(
// currentNode: WorkflowExecutionNode,
// workflowExecution: WorkflowExecution,
// appVersion: AppVersion,
// state: object,
// addLog: any,
// response: Response,
// queue: WorkflowExecutionNode[]
// ) {
// const code = currentNode.definition?.code ?? '';
// let result: any = {};
// try {
// result = {
// data: resolveCode({
// code,
// state,
// isIfCondition: false,
// addLog,
// }),
// status: 'ok',
// };
// await this.completeNodeExecution(currentNode, stringify(result), state);
// } catch (exception) {
// result = { status: 'failed' };
// await this.completeNodeExecution(currentNode, stringify(result), state);
// }
// return { result };
// }
// computeNodesToBeExecuted(
// currentNode: WorkflowExecutionNode,
// nodes: WorkflowExecutionNode[],
// edges: WorkflowExecutionEdge[]
// ) {
// const nodeIds = nodes.map((node) => node.id);
// const dag = new Graph({ directed: true });
// nodeIds.forEach((nodeId) => dag.setNode(nodeId));
// edges.forEach((edge) => {
// if (!edge.skipped) {
// dag.setEdge(edge.sourceWorkflowExecutionNodeId, edge.targetWorkflowExecutionNodeId);
// }
// });
// const sortedNodeIds = alg.topsort(dag);
// const traversedNodeIds = alg.postorder(dag, [currentNode.id]);
// const orderedNodes = sortedNodeIds
// .filter((nodeId) => traversedNodeIds.includes(nodeId))
// .map((id) => {
// return find(nodes, { id });
// });
// return orderedNodes;
// }
// async completeNodeExecution(node: WorkflowExecutionNode, result: any, state: object) {
// await dbTransactionWrap(async (manager: EntityManager) => {
// await manager.update(WorkflowExecutionNode, node.id, { executed: true, result, state });
// });
// }
// async markWorkflowAsExecuted(workflow: WorkflowExecution) {
// await dbTransactionWrap(async (manager: EntityManager) => {
// await manager.update(WorkflowExecution, workflow.id, { executed: true });
// });
// }
// async saveWorkflowLogs(workflow: WorkflowExecution, logs: any[]) {
// await dbTransactionWrap(async (manager: EntityManager) => {
// await manager.update(WorkflowExecution, workflow.id, { logs });
// });
// }
// async saveExecutionStatus({ workflowExecution, logs, executionFailed }) {
// await dbTransactionWrap(async (manager: EntityManager) => {
// const status = executionFailed ? 'failure' : 'success';
// await manager.update(WorkflowExecution, workflowExecution.id, { logs, status });
// });
// }
// async getStateAndPreviousNodesExecutionCompletionStatus(node: WorkflowExecutionNode) {
// const incomingEdges = await this.workflowExecutionEdgeRepository.find({
// where: {
// targetWorkflowExecutionNodeId: node.id,
// },
// relations: ['sourceWorkflowExecutionNode'],
// });
// const incomingNodes = await Promise.all(incomingEdges.map((edge) => edge.sourceWorkflowExecutionNode));
// const previousNodesExecutionCompletionStatus = !incomingNodes.map((node) => node.executed).includes(false);
// const state = incomingNodes.reduce((existingState, node) => {
// const nodeState = node.state ?? {};
// return { ...existingState, ...nodeState };
// }, {});
// return { state, previousNodesExecutionCompletionStatus };
// }
// async forwardNodes(
// startNode: WorkflowExecutionNode,
// sourceHandle: string = undefined
// ): Promise<WorkflowExecutionNode[]> {
// const forwardEdges = await this.workflowExecutionEdgeRepository.find({
// where: {
// sourceWorkflowExecutionNode: startNode,
// ...(sourceHandle ? { sourceHandle } : {}),
// },
// });
// const forwardNodeIds = forwardEdges.map((edge) => edge.targetWorkflowExecutionNodeId);
// const forwardNodes = Promise.all(
// forwardNodeIds.map((id) =>
// this.workflowExecutionNodeRepository.findOne({
// where: {
// id,
// },
// })
// )
// );
// return forwardNodes;
// }
// async incomingNodes(startNode: WorkflowExecutionNode): Promise<WorkflowExecutionNode[]> {
// const incomingEdges = await this.workflowExecutionEdgeRepository.find({
// where: {
// targetWorkflowExecutionNode: startNode,
// },
// });
// const incomingNodeIds = incomingEdges.map((edge) => edge.sourceWorkflowExecutionNodeId);
// const receivedNodes = Promise.all(
// incomingNodeIds.map((id) =>
// this.workflowExecutionNodeRepository.findOne({
// where: {
// id,
// },
// })
// )
// );
// return receivedNodes;
// }
// async previewQueryNode(
// queryId: string,
// nodeId: string,
// state: object,
// appVersion: AppVersion,
// user: User,
// response: Response
// ): Promise<any> {
// const query = await this.dataQueriesService.findOne(queryId);
// const node = find(appVersion.definition.nodes, { id: nodeId });
// //* beta: workflow execution's environment is "development" by default
// const currentEnvironmentId = appVersion.currentEnvironmentId;
// const organization: any = await dbTransactionWrap(async (manager: EntityManager) => {
// return manager
// .createQueryBuilder('organizations', 'organization')
// .innerJoin('apps', 'app', 'app.organization_id = organization.id')
// .innerJoin('app_versions', 'av', 'av.app_id = app.id')
// .where('av.id = :appVersionId', { appVersionId: appVersion.id })
// .getOne();
// });
// const constants = await this.organizationConstantsService.getConstantsForEnvironment(
// organization.id,
// currentEnvironmentId,
// OrganizationConstantType.GLOBAL
// );
// const constantsObject = Object.fromEntries(constants.map((constant) => [constant.name, constant.value]));
// state = { ...state, constants: constantsObject };
// // const user = await this.userRepository.findOne(execution.executingUserId, {
// // relations: ['organization'],
// // });
// // user.organizationId = user.organization.id;
// try {
// void getQueryVariables(query.options, state);
// } catch (e) {
// console.log({ e });
// }
// const startingTime = moment();
// let result: any;
// const addLog = () => {};
// try {
// if (node.data.looped) {
// const iterationValues = resolveCode({ code: node.data?.iterationValuesCode, state, addLog });
// if (!Array.isArray(iterationValues)) throw new Error('Loop array did not resolve into an array');
// let index = 0;
// result = [];
// for (const value of iterationValues) {
// const currentTime = moment();
// const timeTaken = currentTime.diff(startingTime);
// if (timeTaken / 1000 > this.workflowExecutionTimeout) {
// throw new Error('Execution stopped due to timeout');
// }
// const modifiedState = { ...state, value, index };
// const options = getQueryVariables(query.options, modifiedState, addLog);
// result.push(
// query.kind === 'runjs'
// ? resolveCode({ code: query.options?.code, state: modifiedState, addLog })
// : (
// await this.dataQueriesService.runQuery(
// user,
// cloneDeep(query),
// options,
// response,
// currentEnvironmentId
// )
// )['data']
// );
// index++;
// }
// result = { status: 'ok', data: result };
// } else {
// const options = getQueryVariables(query.options, state, addLog);
// result =
// query.kind === 'runjs'
// ? { status: 'ok', data: resolveCode({ code: query.options?.code, state, addLog }) }
// : await this.dataQueriesService.runQuery(user, query, options, response, currentEnvironmentId);
// }
// } catch (exception) {
// const result = { status: 'failed', exception };
// return result;
// }
// return result;
// }
// computeNodeHandle(nodeData, queries, queryIdsOnDefinitionToActualQueryIdMapping): string {
// switch (nodeData.type) {
// case 'query': {
// return find(queries, { id: queryIdsOnDefinitionToActualQueryIdMapping[nodeData.data.idOnDefinition] }).name;
// }
// default:
// return STATIC_NODE_TYPE_TO_HANDLE_MAPPING[nodeData.type];
// }
// }
// async canExecuteWorkflow(organizationId: string): Promise<{ allowed: boolean; message: string }> {
// if (!organizationId) {
// return { allowed: false, message: 'WorkspaceId is missing' };
// }
// const workflowsLimit = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.WORKFLOWS);
// if (!workflowsLimit?.workspace || !workflowsLimit?.instance) {
// return { allowed: false, message: 'Workflow is not enabled in the license, contact admin' };
// }
// return await dbTransactionWrap(async (manager) => {
// const dailyWorkspaceCount = (
// await manager.query(
// `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)
// AND DATE(we.created_at) = current_date`,
// [organizationId]
// )
// )[0].count;
// if (
// workflowsLimit.workspace.daily_executions !== LICENSE_LIMIT.UNLIMITED &&
// dailyWorkspaceCount >= workflowsLimit.workspace.daily_executions
// ) {
// return {
// allowed: false,
// message: 'Maximum daily limit for workflow execution has been reached for this workspace',
// };
// }
// // Workspace Level - Monthly Limit
// const monthlyWorkspaceCount = (
// await manager.query(
// `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)`,
// [organizationId]
// )
// )[0].count;
// if (
// workflowsLimit.workspace.monthly_executions !== LICENSE_LIMIT.UNLIMITED &&
// monthlyWorkspaceCount >= workflowsLimit.workspace.monthly_executions
// ) {
// return {
// allowed: false,
// message: 'Maximum monthly limit for workflow execution has been reached for this workspace',
// };
// }
// // Instance Level - Daily Limit
// const dailyInstanceCount = (
// await manager.query(
// `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)
// AND DATE(we.created_at) = current_date`
// )
// )[0].count;
// if (
// workflowsLimit.instance.daily_executions !== LICENSE_LIMIT.UNLIMITED &&
// dailyInstanceCount >= workflowsLimit.instance.daily_executions
// ) {
// return { allowed: false, message: 'Maximum daily limit for workflow execution has been reached' };
// }
// // Instance Level - Monthly Limit
// const monthlyInstanceCount = (
// await manager.query(
// `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;
// if (
// workflowsLimit.instance.monthly_executions !== LICENSE_LIMIT.UNLIMITED &&
// monthlyInstanceCount >= workflowsLimit.instance.monthly_executions
// ) {
// return { allowed: false, message: 'Maximum monthly limit for workflow execution has been reached' };
// }
// return { allowed: true, message: 'Workflow execution allowed' };
// });
// }
// // Workspace Level - Daily Limit
// }

View file

@ -1,74 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkflowSchedule } from '../entities/workflow_schedule.entity';
import { AppVersion } from '../entities/app_version.entity';
@Injectable()
export class WorkflowSchedulesService {
constructor(
@InjectRepository(WorkflowSchedule)
private workflowSchedulesRepository: Repository<WorkflowSchedule>
) {}
async create(createWorkflowScheduleDto: {
workflowId: string;
active: boolean;
environmentId: string;
type: string;
timezone: string;
details: any;
}): Promise<WorkflowSchedule> {
const { workflowId, active, environmentId, type, timezone, details } = createWorkflowScheduleDto;
const scheduleOptions = this.workflowSchedulesRepository.create({
workflow: { id: workflowId } as AppVersion,
active,
environmentId,
type,
timezone,
details,
});
const workflowSchedule = await this.workflowSchedulesRepository.save(scheduleOptions);
return workflowSchedule;
}
async findOne(id: string): Promise<WorkflowSchedule> {
const workflowSchedule = await this.workflowSchedulesRepository.findOne({
where: { id },
relations: ['workflow'],
});
if (!workflowSchedule) {
throw new NotFoundException(`WorkflowSchedule with ID ${id} not found`);
}
return workflowSchedule;
}
async findAll(appVersionId: string): Promise<WorkflowSchedule[]> {
return await this.workflowSchedulesRepository.find({
where: { workflowId: appVersionId },
});
}
async update(
id: string,
updateWorkflowScheduleDto: Partial<{
active: boolean;
environmentId: string;
type: string;
timezone: string;
details: any;
}>
): Promise<WorkflowSchedule> {
const workflowSchedule = await this.findOne(id);
Object.assign(workflowSchedule, updateWorkflowScheduleDto);
return await this.workflowSchedulesRepository.save(workflowSchedule);
}
async remove(id: string): Promise<void> {
const result = await this.workflowSchedulesRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`WorkflowSchedule with ID ${id} not found`);
}
}
}

View file

@ -1,121 +0,0 @@
// import { BadRequestException, HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
// import { EventEmitter2 } from '@nestjs/event-emitter';
// import { DataSource, EntityManager } from 'typeorm';
// import { App } from 'src/entities/app.entity';
// import { AppEnvironment } from 'src/entities/app_environments.entity';
// import { AppVersion } from 'src/entities/app_version.entity';
// import { v4 as uuidv4 } from 'uuid';
// import { WorkflowExecutionsService } from '@services/workflow_executions.service';
// @Injectable()
// export class WorkflowWebhooksService {
// constructor(
// private readonly manager: EntityManager,
// private eventEmitter: EventEmitter2,
// private workflowExecutionsService: WorkflowExecutionsService,
// private readonly _dataSource: DataSource
// ) {}
// async triggerWorkflow(workflowApps, workflowParams, environment, response) {
// // When workflow version is introduced - Query needs to be tweaked
// const appVersion = await this.manager
// .createQueryBuilder(AppVersion, 'av')
// .select(['av.definition'])
// .innerJoinAndSelect(App, 'a', 'av.appId = a.id')
// .where('av.appId = :id', { id: workflowApps.appId })
// .getOne();
// const app = await this.manager
// .createQueryBuilder(App, 'app')
// .where('app.id = :id', { id: workflowApps.appId })
// .getOne();
// const enabled = app.isMaintenanceOn;
// if (!enabled) throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
// // Type validation for input values passed.
// const inputValidators = appVersion?.definition?.webhookParams ?? [];
// if (inputValidators.length) {
// const inputParamsSet = new Set();
// Object.entries(workflowParams).forEach(([key, _value]) => {
// inputParamsSet.add(key);
// });
// inputValidators.forEach((validator: { key: string; dataType: string }) => {
// if (!inputParamsSet.has(validator.key)) throw new BadRequestException(`Params - ${validator.key} is missing`);
// });
// }
// const sanitisedWorkflowParams = {};
// inputValidators.length &&
// Object.entries(workflowParams).forEach(([key, value]) => {
// const condition = inputValidators.find((input) => input.key == key);
// if (condition) {
// const isValidType = this.isValidateInputTypes(value, condition.dataType);
// if (!isValidType) throw new BadRequestException(`${key} has incorrect datatype`);
// if (isValidType) sanitisedWorkflowParams[key] = value;
// }
// });
// const environmentDetails = await this.manager
// .createQueryBuilder(App, 'apps')
// .leftJoinAndSelect(AppEnvironment, 'ae', 'ae.organizationId = apps.organizationId')
// .where('apps.id = :id and ae.name = :envName', { id: workflowApps.appId, envName: environment })
// .select(['apps.id', 'ae.id'])
// .execute();
// if (!environmentDetails.length) throw new HttpException('Invalid environment', 404);
// const webhookEnvironmentId = environmentDetails[0]?.ae_id ?? '';
// const workflowExecution = await this.workflowExecutionsService.create(workflowApps);
// const result = await this.workflowExecutionsService.execute(
// workflowExecution,
// sanitisedWorkflowParams,
// webhookEnvironmentId,
// response
// );
// return result;
// }
// async updateWorkflow(workflowId, workflowValuesToUpdate) {
// if (Object.keys(workflowValuesToUpdate).length === 0) throw new BadRequestException('Values to update is empty');
// if (!workflowId) throw new BadRequestException('Invalid workflowId');
// const { isEnable } = workflowValuesToUpdate;
// const workflowApps = await this._dataSource
// .getRepository(App)
// .createQueryBuilder('apps')
// .where('apps.id = :id', { id: workflowId })
// .getOne();
// if (!workflowApps) throw new NotFoundException("Workflow doesn't exist");
// return this._dataSource
// .createQueryBuilder()
// .update(App)
// .set({
// workflowEnabled: isEnable === 'endPointTrigger',
// ...(!workflowApps?.workflowApiToken && { workflowApiToken: uuidv4() }),
// })
// .where('id = :id', { id: workflowId })
// .execute();
// }
// private isValidateInputTypes(value, type) {
// switch (type) {
// case 'string':
// return typeof value == 'string';
// case 'number':
// return typeof value == 'number';
// case 'array':
// return Array.isArray(value);
// case 'object':
// return typeof value == 'object';
// case 'boolean':
// return typeof value == 'boolean';
// case 'null':
// return value == null;
// default:
// return false;
// }
// }
// }