ToolJet/server/test/services/app_import_export.service.spec.ts
Akshay c6fe0aa45e
Feature: Import export tjdb schema (#5752)
* add ability to import export app and tjdb schema

* init

* feat ::global settings popover new ui

* feat :: ui for version export modal

* fix :: import export modal

* cleanup

* ui updates

* header footer style fixes

* closing settings modal while showing export modal

* style fix header

* feat :: added button to download table schema

* fix :: styling for fx

* add ability to import and export apps with tjdb schema

* handle duplicate table in workspace

* fix table rename

* fix selected table on edit and delete

* fix invalid toast on table delete

* fix column default value

* handle exports to strip '::' and quotes

* make import/export backward compatible

* handle page redirects based on resource import

* handle import without tjdb schema

* fix column delete and addition

* make data migrations to be run per organizations

* wip

* update migration

* fix credentials to be included

* fix specific version export

* make use of apps ability for import export resource

* fix import navigation

* fix lint

* fix failing tests

* fix lint

* enable tjdb for public apps

* update export error message on tjdb table blank

* fix table not selected after creation

* fix :: styling for imp exp modal , and functionality bug fixes after dev merge

* fixes blank slate and columns selection

* fix table delete

* fix invalid toast on table edit

* fix column information missing tjdb query manager

* make ds imports to either reuse global or create

* export only unique table ids

* create default datasources if not present in export data

* reuse existing table on imports

* add timestamp to table name if name already exists

* add ability to clone with tjdb

* make imports work with marketplace plugin

* skip dataqueries for which plugins are not installed

* fix filter input width

* fix failing spec

* fix marketplace plugin installation in diff workspaces

* fix check for plugin installed in workspace

* fix export when table name is empty

---------

Co-authored-by: stepinfwd <stepinfwd@gmail.com>
2023-08-28 21:23:15 +05:30

313 lines
12 KiB
TypeScript

import {
clearDB,
createUser,
createNestAppInstance,
createApplication,
createApplicationVersion,
createDataQuery,
createDataSource,
generateAppDefaults,
createAppEnvironments,
getAppWithAllDetails,
} from '../test.helper';
import { INestApplication } from '@nestjs/common';
import { getManager, In } from 'typeorm';
import { App } from 'src/entities/app.entity';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { AppImportExportService } from '@services/app_import_export.service';
import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
describe('AppImportExportService', () => {
let nestApp: INestApplication;
let service: AppImportExportService;
beforeEach(async () => {
await clearDB();
});
beforeAll(async () => {
nestApp = await createNestAppInstance();
service = nestApp.get<AppImportExportService>(AppImportExportService);
});
describe('.export', () => {
it('should export app with empty related associations', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const adminUser = adminUserData.user;
const { application: app } = await generateAppDefaults(nestApp, adminUserData.user, {
isAppPublic: true,
isDataSourceNeeded: false,
isQueryNeeded: false,
});
const { appV2: result } = await service.export(adminUser, app.id);
expect(result.id).toBe(app.id);
expect(result.name).toBe(app.name);
expect(result.isPublic).toBe(app.isPublic);
expect(result.organizationId).toBe(app.organizationId);
expect(result.currentVersionId).toBe(null);
expect(result['dataQueries']).toEqual([]);
expect(result['dataSources']).toEqual([]);
});
it('should export app', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const adminUser = adminUserData.user;
const { application } = await generateAppDefaults(nestApp, adminUserData.user, {
isAppPublic: true,
});
const exportedApp = await getAppWithAllDetails(application.id);
const { appV2: result } = await service.export(adminUser, exportedApp.id);
expect(result.id).toBe(exportedApp.id);
expect(result.name).toBe(exportedApp.name);
expect(result.isPublic).toBe(exportedApp.isPublic);
expect(result.organizationId).toBe(exportedApp.organizationId);
expect(result.currentVersionId).toBe(null);
expect(result.appVersions).toEqual(exportedApp.appVersions);
expect(result['dataQueries']).toEqual(exportedApp['dataQueries']);
expect(result['dataSources']).toEqual(exportedApp['dataSources']);
});
it('should export app with filtered version', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const adminUser = adminUserData.user;
const application = await createApplication(
nestApp,
{
user: adminUser,
name: 'sample app',
isPublic: true,
},
false
);
await createAppEnvironments(nestApp, adminUser.organizationId);
const appVersion1 = await createApplicationVersion(nestApp, application, { name: 'v1', definition: {} });
const dataSource1 = await createDataSource(nestApp, {
appVersion: appVersion1,
kind: 'test_kind',
name: 'test_name_1',
});
const dataQuery1 = await createDataQuery(nestApp, {
dataSource: dataSource1,
appVersion: appVersion1,
name: 'test_query_1',
kind: 'test_kind',
});
const appVersion2 = await createApplicationVersion(nestApp, application, {
name: 'v2',
definition: { hello: 'world' },
});
const dataSource2 = await createDataSource(nestApp, {
appVersion: appVersion2,
kind: 'test_kind',
name: 'test_name_2',
});
const dataQuery2 = await createDataQuery(nestApp, {
appVersion: appVersion2,
dataSource: dataSource2,
name: 'test_query_2',
});
const exportedApp = await getManager().findOneOrFail(App, {
where: { id: application.id },
});
let { appV2: result } = await service.export(adminUser, exportedApp.id, { version_id: appVersion1.id });
expect(result.id).toBe(exportedApp.id);
expect(result.name).toBe(exportedApp.name);
expect(result.isPublic).toBe(exportedApp.isPublic);
expect(result.organizationId).toBe(exportedApp.organizationId);
expect(result.currentVersionId).toBe(null);
expect(result['dataQueries'].length).toBe(1);
expect(result['dataQueries'][0].name).toEqual(dataQuery1.name);
expect(result['dataSources'].length).toBe(1);
expect(result['dataSources'][0].name).toEqual(dataSource1.name);
expect(result.appVersions.length).toBe(1);
expect(result.appVersions[0].name).toEqual(appVersion1.name);
const res = await service.export(adminUser, exportedApp.id, { version_id: appVersion2.id });
result = res.appV2;
expect(result.id).toBe(exportedApp.id);
expect(result.name).toBe(exportedApp.name);
expect(result.isPublic).toBe(exportedApp.isPublic);
expect(result.organizationId).toBe(exportedApp.organizationId);
expect(result.currentVersionId).toBe(null);
expect(result['dataQueries'].length).toBe(1);
expect(result['dataQueries'][0].name).toEqual(dataQuery2.name);
expect(result['dataSources'].length).toBe(1);
expect(result['dataSources'][0].name).toEqual(dataSource2.name);
expect(result.appVersions.length).toBe(1);
expect(result.appVersions[0].name).toEqual(appVersion2.name);
});
});
describe('.import', () => {
it('should throw error with invalid params', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const adminUser = adminUserData.user;
await expect(service.import(adminUser, 'hello world')).rejects.toThrow('Invalid params for app import');
});
it('should import app with empty related associations', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const adminUser = adminUserData.user;
const { application: app } = await generateAppDefaults(nestApp, adminUserData.user, {
isAppPublic: true,
isDataSourceNeeded: false,
isQueryNeeded: false,
});
const { appV2: exportedApp } = await service.export(adminUser, app.id);
const result = await service.import(adminUser, exportedApp);
const importedApp = await getAppWithAllDetails(result.id);
expect(importedApp.id == exportedApp.id).toBeFalsy();
expect(importedApp.name).toContain(exportedApp.name);
expect(importedApp.isPublic).toBeFalsy();
expect(importedApp.organizationId).toBe(exportedApp.organizationId);
expect(importedApp.currentVersionId).toBe(null);
expect(importedApp['dataQueries']).toEqual([]);
// there will be 3 data sources created automatically when a user creates a new app.
expect(importedApp['dataSources'].length).toEqual(4);
// assert group permissions are valid
const appGroupPermissions = await getManager().find(AppGroupPermission, {
appId: importedApp.id,
});
const groupPermissionIds = appGroupPermissions.map((agp) => agp.groupPermissionId);
const groupPermissions = await getManager().find(GroupPermission, {
id: In(groupPermissionIds),
});
expect(new Set(groupPermissions.map((gp) => gp.organizationId))).toEqual(new Set([adminUser.organizationId]));
expect(new Set(groupPermissions.map((gp) => gp.group))).toEqual(new Set(['admin']));
});
it('should import app with related associations', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const adminUser = adminUserData.user;
const { application, appVersion: applicationVersion } = await generateAppDefaults(nestApp, adminUserData.user, {
isDataSourceNeeded: false,
isQueryNeeded: false,
});
//create default 3 datasources
const firstDs = await createDataSource(nestApp, {
name: 'runpydefault',
kind: 'runpy',
type: 'static',
appVersion: applicationVersion,
});
await createDataSource(nestApp, {
name: 'restapidefault',
kind: 'restapi',
type: 'static',
appVersion: applicationVersion,
});
await createDataSource(nestApp, {
name: 'runjsdefault',
kind: 'runjs',
type: 'static',
appVersion: applicationVersion,
});
await createDataSource(nestApp, {
name: 'tooljetdbdefault',
kind: 'tooljetdb',
type: 'static',
appVersion: applicationVersion,
});
//create default dataQuery
await createDataQuery(nestApp, {
dataSource: firstDs,
appVersion: applicationVersion,
options: {},
});
const { appV2: exportedApp } = await service.export(adminUser, application.id);
const result = await service.import(adminUser, exportedApp);
const importedApp = await getAppWithAllDetails(result.id);
expect(importedApp.id == exportedApp.id).toBeFalsy();
expect(importedApp.name).toContain(exportedApp.name);
expect(importedApp.isPublic).toBeFalsy();
expect(importedApp.organizationId).toBe(exportedApp.organizationId);
expect(importedApp.currentVersionId).toBe(null);
// assert relations
const appVersion = importedApp.appVersions[0];
expect(appVersion.appId).toEqual(importedApp.id);
const dataQuery = importedApp['dataQueries'][0];
const dataSourceForTheDataQuery = importedApp['dataSources'].find((ds) => ds.id === dataQuery.dataSourceId);
expect(dataSourceForTheDataQuery).toBeDefined();
// assert all fields except primary keys, foreign keys and timestamps are same
const deleteFieldsNotToCheck = (entity) => {
delete entity.id;
delete entity.appId;
delete entity.dataSourceId;
delete entity.appVersionId;
delete entity.createdAt;
delete entity.updatedAt;
return entity;
};
const importedAppVersions = importedApp.appVersions.map((version) => deleteFieldsNotToCheck(version));
const exportedAppVersions = exportedApp.appVersions.map((version) => deleteFieldsNotToCheck(version));
const importedDataSources = importedApp['dataSources'].map((source) => deleteFieldsNotToCheck(source));
const exportedDataSources = exportedApp['dataSources'].map((source) => deleteFieldsNotToCheck(source));
const importedDataQueries = importedApp['dataQueries'].map((query) => deleteFieldsNotToCheck(query));
const exportedDataQueries = exportedApp['dataQueries'].map((query) => deleteFieldsNotToCheck(query));
expect(new Set(importedAppVersions)).toEqual(new Set(exportedAppVersions));
expect(new Set(importedDataSources)).toEqual(new Set(exportedDataSources));
expect(new Set(importedDataQueries)).toEqual(new Set(exportedDataQueries));
// assert group permissions are valid
const appGroupPermissions = await getManager().find(AppGroupPermission, {
appId: importedApp.id,
});
const groupPermissionIds = appGroupPermissions.map((agp) => agp.groupPermissionId);
const groupPermissions = await getManager().find(GroupPermission, {
id: In(groupPermissionIds),
});
expect(new Set(groupPermissions.map((gp) => gp.organizationId))).toEqual(new Set([adminUser.organizationId]));
expect(new Set(groupPermissions.map((gp) => gp.group))).toEqual(new Set(['admin']));
});
});
afterAll(async () => {
await nestApp.close();
});
});