From a7e0208d524ce583e3a3351fa954efcfc56e7542 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 12 Jun 2023 17:37:39 +0530 Subject: [PATCH] Revert "Revert "refactor: MetaDB LTAR revamp"" --- .../column/LinkedToAnotherRecordOptions.vue | 95 ++--- .../composables/useColumnCreateStore.ts | 5 +- packages/nc-gui/composables/useTable.ts | 6 +- packages/nocodb/src/db/BaseModelSqlv2.ts | 240 ++++++++++- .../db/sql-client/lib/sqlite/SqliteClient.ts | 14 +- .../nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts | 23 +- packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts | 12 +- packages/nocodb/src/meta/meta.service.ts | 1 - .../extract-project-id.middleware.ts | 1 + packages/nocodb/src/models/Column.ts | 16 +- packages/nocodb/src/models/Model.ts | 6 +- packages/nocodb/src/models/Project.ts | 7 +- packages/nocodb/src/models/View.ts | 10 +- .../global/init-meta-service.provider.ts | 2 +- .../nocodb/src/services/columns.service.ts | 215 ++++++---- packages/nocodb/src/services/datas.service.ts | 12 +- .../nocodb/src/services/tables.service.ts | 105 +++-- .../nocodb/src/version-upgrader/NcUpgrader.ts | 2 + .../ncFilterUpgrader_0104004.ts | 384 ++---------------- .../version-upgrader/ncXcdbLTARUpgrader.ts | 164 ++++++++ .../tests/unit/rest/tests/viewRow.test.ts | 10 +- .../playwright/pages/Dashboard/Grid/index.ts | 14 +- tests/playwright/setup/xcdbProject.ts | 40 ++ tests/playwright/tests/db/metaLTAR.spec.ts | 324 +++++++++++++++ 24 files changed, 1146 insertions(+), 562 deletions(-) create mode 100644 packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts create mode 100644 tests/playwright/setup/xcdbProject.ts create mode 100644 tests/playwright/tests/db/metaLTAR.spec.ts diff --git a/packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue b/packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue index b3d175f567..f285026379 100644 --- a/packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue +++ b/packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue @@ -16,7 +16,7 @@ const vModel = useVModel(props, 'value', emit) const meta = $(inject(MetaInj, ref())) -const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi } = useColumnCreateStoreOrThrow() +const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi, isXcdbBase } = useColumnCreateStoreOrThrow() const { tables } = $(storeToRefs(useProject())) @@ -86,54 +86,57 @@ const filterOption = (value: string, option: { key: string }) => option.key.toLo + diff --git a/packages/nc-gui/composables/useColumnCreateStore.ts b/packages/nc-gui/composables/useColumnCreateStore.ts index 978f71fdd4..a6b99847be 100644 --- a/packages/nc-gui/composables/useColumnCreateStore.ts +++ b/packages/nc-gui/composables/useColumnCreateStore.ts @@ -31,7 +31,7 @@ interface ValidationsObj { const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( (meta: Ref, column: Ref) => { const projectStore = useProject() - const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = projectStore + const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc } = projectStore const { project, sqlUis } = storeToRefs(projectStore) const { $api } = useNuxtApp() @@ -52,6 +52,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState const isMssql = computed(() => isMssqlFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0])) + const isXcdbBase = computed(() => isXcdbBaseFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0])) + const idType = null const additionalValidations = ref({}) @@ -289,6 +291,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState isMssql, isPg, isMysql, + isXcdbBase } }, ) diff --git a/packages/nc-gui/composables/useTable.ts b/packages/nc-gui/composables/useTable.ts index cb4bccea52..4596c2ff2c 100644 --- a/packages/nc-gui/composables/useTable.ts +++ b/packages/nc-gui/composables/useTable.ts @@ -30,7 +30,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId? const { getMeta, removeMeta } = useMetas() - const { loadTables } = useProject() + const { loadTables, isXcdbBase } = useProject() const { closeTab } = useTabs() const projectStore = useProject() @@ -88,7 +88,9 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId? const meta = (await getMeta(table.id as string, true)) as TableType const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c)) - if (relationColumns?.length) { + // Check if table has any relation columns and show notification + // skip for xcdb base + if (relationColumns?.length && !isXcdbBase(table.base_id)) { const refColMsgs = await Promise.all( relationColumns.map(async (c, i) => { const refMeta = (await getMeta( diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 1d5c356a2f..b15aa4b355 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -17,9 +17,19 @@ import Validator from 'validator'; import { customAlphabet } from 'nanoid'; import DOMPurify from 'isomorphic-dompurify'; import { v4 as uuidv4 } from 'uuid'; +import { Knex } from 'knex'; import { NcError } from '../helpers/catchError'; import getAst from '../helpers/getAst'; -import { Audit, Column, Filter, Model, Project, Sort, View } from '../models'; +import { + Audit, + Base, + Column, + Filter, + Model, + Project, + Sort, + View, +} from '../models'; import { sanitize, unsanitize } from '../helpers/sqlSanitize'; import { COMPARISON_OPS, @@ -47,8 +57,8 @@ import type { RollupColumn, SelectOption, } from '../models'; -import type { Knex } from 'knex'; import type { SortType } from 'nocodb-sdk'; +import Transaction = Knex.Transaction; dayjs.extend(utc); dayjs.extend(timezone); @@ -1875,18 +1885,80 @@ class BaseModelSqlv2 { } } - async delByPk(id, trx?, cookie?) { + async delByPk(id, _trx?, cookie?) { + let trx: Transaction = _trx; try { // retrieve data for handling params in hook const data = await this.readByPk(id); await this.beforeDelete(id, trx, cookie); - const response = await this.dbDriver(this.tnPath) - .del() - .where(await this._wherePk(id)); + + const execQueries: ((trx: Transaction) => Promise)[] = []; + + for (const column of this.model.columns) { + if (column.uidt !== UITypes.LinkToAnotherRecord) continue; + + const colOptions = + await column.getColOptions(); + + switch (colOptions.type) { + case 'mm': + { + const mmTable = await Model.get(colOptions.fk_mm_model_id); + const mmParentColumn = await Column.get({ + colId: colOptions.fk_mm_child_column_id, + }); + + execQueries.push((trx) => + trx(mmTable.table_name) + .del() + .where(mmParentColumn.column_name, id), + ); + } + break; + case 'hm': + { + // skip if it's an mm table column + const relatedTable = await colOptions.getRelatedTable(); + if (relatedTable.mm) { + break; + } + + const childColumn = await Column.get({ + colId: colOptions.fk_child_column_id, + }); + + execQueries.push((trx) => + trx(relatedTable.table_name) + .update({ + [childColumn.column_name]: null, + }) + .where(childColumn.column_name, id), + ); + } + break; + case 'bt': + { + // nothing to do + } + break; + } + } + const where = await this._wherePk(id); + if (!trx) { + trx = await this.dbDriver.transaction(); + } + + await Promise.all(execQueries.map((q) => q(trx))); + + const response = await trx(this.tnPath).del().where(where); + + if (!_trx) await trx.commit(); + await this.afterDelete(data, trx, cookie); return response; } catch (e) { console.log(e); + if (!_trx) await trx.rollback(); await this.errorDelete(e, id, trx, cookie); throw e; } @@ -2379,8 +2451,71 @@ class BaseModelSqlv2 { res.push(d); } + const execQueries: ((trx: Transaction, ids: any[]) => Promise)[] = + []; + + const base = await Base.get(this.model.base_id); + + for (const column of this.model.columns) { + if (column.uidt !== UITypes.LinkToAnotherRecord) continue; + + const colOptions = + await column.getColOptions(); + + switch (colOptions.type) { + case 'mm': + { + const mmTable = await Model.get(colOptions.fk_mm_model_id); + const mmParentColumn = await Column.get({ + colId: colOptions.fk_mm_child_column_id, + }); + + execQueries.push((trx, ids) => + trx(mmTable.table_name) + .del() + .whereIn(mmParentColumn.column_name, ids), + ); + } + break; + case 'hm': + { + // skip if it's an mm table column + const relatedTable = await colOptions.getRelatedTable(); + if (relatedTable.mm) { + break; + } + + const childColumn = await Column.get({ + colId: colOptions.fk_child_column_id, + }); + + execQueries.push((trx, ids) => + trx(relatedTable.table_name) + .update({ + [childColumn.column_name]: null, + }) + .whereIn(childColumn.column_name, ids), + ); + } + break; + case 'bt': + { + // nothing to do + } + break; + } + } + + const idsVals = res.map((d) => d[this.model.primaryKey.column_name]); + transaction = await this.dbDriver.transaction(); + if (base.is_meta && execQueries.length > 0) { + for (const execQuery of execQueries) { + await execQuery(transaction, idsVals); + } + } + for (const d of res) { await transaction(this.tnPath).del().where(d); } @@ -2401,6 +2536,7 @@ class BaseModelSqlv2 { args: { where?: string; filterArr?: Filter[] } = {}, { cookie }: { cookie?: any } = {}, ) { + let trx: Transaction; try { await this.model.getColumns(); const { where } = this._getListArgs(args); @@ -2425,14 +2561,102 @@ class BaseModelSqlv2 { this.dbDriver, ); - qb.del(); + const execQueries: ((trx: Transaction, qb: any) => Promise)[] = []; + + for (const column of this.model.columns) { + if (column.uidt !== UITypes.LinkToAnotherRecord) continue; + + const colOptions = + await column.getColOptions(); + + if (colOptions.type === 'bt') { + continue; + } - const count = (await qb) as any; + const childColumn = await colOptions.getChildColumn(); + const parentColumn = await colOptions.getParentColumn(); + const parentTable = await parentColumn.getModel(); + const childTable = await childColumn.getModel(); + await childTable.getColumns(); + await parentTable.getColumns(); + + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); + + switch (colOptions.type) { + case 'mm': + { + const vChildCol = await colOptions.getMMChildColumn(); + const vParentCol = await colOptions.getMMParentColumn(); + const vTable = await colOptions.getMMModel(); + + const vTn = this.getTnPath(vTable); + + execQueries.push((trx, qb) => + this.dbDriver(vTn) + .where({ + [vChildCol.column_name]: this.dbDriver(childTn) + .select(childColumn.column_name) + .first(), + }) + .delete(), + ); + } + break; + case 'hm': + { + // skip if it's an mm table column + const relatedTable = await colOptions.getRelatedTable(); + if (relatedTable.mm) { + break; + } + + const childColumn = await Column.get({ + colId: colOptions.fk_child_column_id, + }); + + execQueries.push((trx, qb) => + trx(childTn) + .where({ + [childColumn.column_name]: this.dbDriver.from( + qb + .select(parentColumn.column_name) + // .where(_wherePk(parentTable.primaryKeys, rowId)) + .first() + .as('___cn_alias'), + ), + }) + .update({ + [childColumn.column_name]: null, + }), + ); + } + break; + } + } + + const base = await Base.get(this.model.base_id); + + trx = await this.dbDriver.transaction(); + + // unlink LTAR data + if (base.is_meta) { + for (const execQuery of execQueries) { + await execQuery(trx, qb.clone()); + } + } + + const deleteQb = qb.clone().transacting(trx).del(); + + const count = (await deleteQb) as any; + + await trx.commit(); await this.afterBulkDelete(count, this.dbDriver, cookie, true); return count; } catch (e) { + if (trx) await trx.rollback(); throw e; } } diff --git a/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts b/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts index 118423df7b..00ecca611d 100644 --- a/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts +++ b/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts @@ -22,7 +22,8 @@ class SqliteClient extends KnexClient { // sqlite does not support inserting default values and knex fires a warning without this flag connectionConfig.connection.useNullAsDefault = true; super(connectionConfig); - this.sqlClient = knex(connectionConfig.connection); + this.sqlClient = + connectionConfig?.knex || knex(connectionConfig.connection); this.queries = queries; this._version = {}; } @@ -1557,7 +1558,13 @@ class SqliteClient extends KnexClient { upQuery, ); - await this.sqlClient.raw('PRAGMA foreign_keys = OFF;'); + const fkCheckEnabled = ( + await this.sqlClient.raw('PRAGMA foreign_keys;') + )?.[0]?.foreign_keys; + + if (fkCheckEnabled) + await this.sqlClient.raw('PRAGMA foreign_keys = OFF;'); + await this.sqlClient.raw('PRAGMA legacy_alter_table = ON;'); const trx = await this.sqlClient.transaction(); @@ -1594,7 +1601,8 @@ class SqliteClient extends KnexClient { log.ppe(e, _func); throw e; } finally { - await this.sqlClient.raw('PRAGMA foreign_keys = ON;'); + if (fkCheckEnabled) + await this.sqlClient.raw('PRAGMA foreign_keys = ON;'); await this.sqlClient.raw('PRAGMA legacy_alter_table = OFF;'); } diff --git a/packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts b/packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts index f8f884ea5f..730cbdda59 100644 --- a/packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts +++ b/packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts @@ -1,18 +1,21 @@ -import SqlMgrv2 from './SqlMgrv2'; -import SqlMgrv2Trans from './SqlMgrv2Trans'; +import { MetaService } from '../../../meta/meta.service' +import SqlMgrv2 from './SqlMgrv2' +import SqlMgrv2Trans from './SqlMgrv2Trans' // import type NcMetaIO from '../../../meta/NcMetaIO'; -import type Base from '../../../models/Base'; +import type Base from '../../../models/Base' export default class ProjectMgrv2 { private static sqlMgrMap: { [key: string]: SqlMgrv2; - } = {}; + } = {} + + public static getSqlMgr(project: { id: string }, ncMeta: MetaService = null): SqlMgrv2 { + if (ncMeta) return new SqlMgrv2(project, ncMeta) - public static getSqlMgr(project: { id: string }): SqlMgrv2 { if (!this.sqlMgrMap[project.id]) { - this.sqlMgrMap[project.id] = new SqlMgrv2(project); + this.sqlMgrMap[project.id] = new SqlMgrv2(project) } - return this.sqlMgrMap[project.id]; + return this.sqlMgrMap[project.id] } public static async getSqlMgrTrans( @@ -21,8 +24,8 @@ export default class ProjectMgrv2 { ncMeta: any, base: Base, ): Promise { - const sqlMgr = new SqlMgrv2Trans(project, ncMeta, base); - await sqlMgr.startTransaction(base); - return sqlMgr; + const sqlMgr = new SqlMgrv2Trans(project, ncMeta, base) + await sqlMgr.startTransaction(base) + return sqlMgr } } diff --git a/packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts b/packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts index 17d808e15e..437a79921f 100644 --- a/packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts +++ b/packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts @@ -5,12 +5,14 @@ import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import SqlClientFactory from '../../sql-client/lib/SqlClientFactory'; import KnexMigratorv2 from '../../sql-migrator/lib/KnexMigratorv2'; import Debug from '../../util/Debug'; +import type { MetaService } from '../../../meta/meta.service'; import type Base from '../../../models/Base'; const log = new Debug('SqlMgr'); export default class SqlMgrv2 { protected _migrator: KnexMigratorv2; + protected ncMeta?: MetaService; // @ts-ignore private currentProjectFolder: any; @@ -20,18 +22,18 @@ export default class SqlMgrv2 { * @param {String} args.toolDbPath - path to sqlite file that sql mgr will use * @memberof SqlMgr */ - constructor(args: { id: string }) { + constructor(args: { id: string }, ncMeta = null) { const func = 'constructor'; log.api(`${func}:args:`, args); // this.metaDb = args.metaDb; this._migrator = new KnexMigratorv2(args); - - return this; + this.ncMeta = ncMeta; } public async migrator(_base: Base) { return this._migrator; } + public static async testConnection(args = {}) { const client = await SqlClientFactory.create(args); return client.testConnection(); @@ -119,6 +121,10 @@ export default class SqlMgrv2 { } protected async getSqlClient(base: Base) { + if (base.is_meta && this.ncMeta) { + return NcConnectionMgrv2.getSqlClient(base, this.ncMeta.knex); + } + return NcConnectionMgrv2.getSqlClient(base); } } diff --git a/packages/nocodb/src/meta/meta.service.ts b/packages/nocodb/src/meta/meta.service.ts index 5d30444980..565b921b42 100644 --- a/packages/nocodb/src/meta/meta.service.ts +++ b/packages/nocodb/src/meta/meta.service.ts @@ -532,7 +532,6 @@ export class MetaService { } else { query.where(idOrCondition); } - return query.first(); } diff --git a/packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts b/packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts index d797e146c7..d26858f077 100644 --- a/packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts +++ b/packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts @@ -37,6 +37,7 @@ export class ExtractProjectIdMiddleware implements NestMiddleware, CanActivate { // extract project id based on request path params if (params.projectName) { const project = await Project.getByTitleOrId(params.projectName); + if (!project) NcError.notFound('Project not found'); req.ncProjectId = project.id; res.locals.project = project; } diff --git a/packages/nocodb/src/models/Column.ts b/packages/nocodb/src/models/Column.ts index 22ed522e37..78ae4edc9f 100644 --- a/packages/nocodb/src/models/Column.ts +++ b/packages/nocodb/src/models/Column.ts @@ -65,10 +65,13 @@ export default class Column implements ColumnType { Object.assign(this, data); } - public async getModel(): Promise { - return Model.getByIdOrName({ - id: this.fk_model_id, - }); + public async getModel(ncMeta = Noco.ncMeta): Promise { + return Model.getByIdOrName( + { + id: this.fk_model_id, + }, + ncMeta, + ); } public static async insert( @@ -598,6 +601,11 @@ export default class Column implements ColumnType { static async delete(id, ncMeta = Noco.ncMeta) { const col = await this.get({ colId: id }, ncMeta); + // if column is not found, return + if (!col) { + return; + } + // todo: or instead of delete reset related foreign key value to null and handle in BaseModel // get qr code columns and delete diff --git a/packages/nocodb/src/models/Model.ts b/packages/nocodb/src/models/Model.ts index c29868b4db..8750ef7c9f 100644 --- a/packages/nocodb/src/models/Model.ts +++ b/packages/nocodb/src/models/Model.ts @@ -368,10 +368,10 @@ export default class Model implements TableType { } async delete(ncMeta = Noco.ncMeta, force = false): Promise { - await Audit.deleteRowComments(this.id); + await Audit.deleteRowComments(this.id, ncMeta); - for (const view of await this.getViews(true)) { - await view.delete(); + for (const view of await this.getViews(true, ncMeta)) { + await view.delete(ncMeta); } for (const col of await this.getColumns(ncMeta)) { diff --git a/packages/nocodb/src/models/Project.ts b/packages/nocodb/src/models/Project.ts index b062222f69..e08d5e036a 100644 --- a/packages/nocodb/src/models/Project.ts +++ b/packages/nocodb/src/models/Project.ts @@ -10,7 +10,7 @@ import NocoCache from '../cache/NocoCache'; import Base from './/Base'; import { ProjectUser } from './index'; import type { BoolType, MetaType, ProjectType } from 'nocodb-sdk'; -import type { DB_TYPES } from './/Base'; +import type { DB_TYPES } from './Base'; export default class Project implements ProjectType { public id: string; @@ -309,6 +309,11 @@ export default class Project implements ProjectType { `${CacheScope.PROJECT}:${projectId}`, CacheDelDirection.CHILD_TO_PARENT, ); + + await ncMeta.metaDelete(null, null, MetaTable.AUDIT, { + project_id: projectId, + }); + return await ncMeta.metaDelete(null, null, MetaTable.PROJECT, projectId); } diff --git a/packages/nocodb/src/models/View.ts b/packages/nocodb/src/models/View.ts index 8d03d28564..187a0ee52d 100644 --- a/packages/nocodb/src/models/View.ts +++ b/packages/nocodb/src/models/View.ts @@ -1001,9 +1001,9 @@ export default class View implements ViewType { // @ts-ignore static async delete(viewId, ncMeta = Noco.ncMeta) { - const view = await this.get(viewId); - await Sort.deleteAll(viewId); - await Filter.deleteAll(viewId); + const view = await this.get(viewId, ncMeta); + await Sort.deleteAll(viewId, ncMeta); + await Filter.deleteAll(viewId, ncMeta); const table = this.extractViewTableName(view); const tableScope = this.extractViewTableNameScope(view); const columnTable = this.extractViewColumnsTableName(view); @@ -1273,8 +1273,8 @@ export default class View implements ViewType { ); } - async delete() { - await View.delete(this.id); + async delete(ncMeta = Noco.ncMeta){ + await View.delete(this.id, ncMeta); } static async shareViewList(tableId, ncMeta = Noco.ncMeta) { diff --git a/packages/nocodb/src/modules/global/init-meta-service.provider.ts b/packages/nocodb/src/modules/global/init-meta-service.provider.ts index a86646b302..eab0293639 100644 --- a/packages/nocodb/src/modules/global/init-meta-service.provider.ts +++ b/packages/nocodb/src/modules/global/init-meta-service.provider.ts @@ -26,7 +26,7 @@ export const InitMetaServiceProvider: Provider = { const config = await NcConfig.createByEnv(); // set version - process.env.NC_VERSION = '0107004'; + process.env.NC_VERSION = '0108002'; // init cache await NocoCache.init(); diff --git a/packages/nocodb/src/services/columns.service.ts b/packages/nocodb/src/services/columns.service.ts index aa4fd91da7..f1c8a81620 100644 --- a/packages/nocodb/src/services/columns.service.ts +++ b/packages/nocodb/src/services/columns.service.ts @@ -38,8 +38,8 @@ import { import Noco from '../Noco'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { MetaTable } from '../utils/globals'; +import { MetaService } from '../meta/meta.service'; import type { LinkToAnotherRecordColumn, Project } from '../models'; -import type { MetaService } from '../meta/meta.service'; import type SqlMgrv2 from '../db/sql-mgr/v2/SqlMgrv2'; import type { ColumnReqType, @@ -57,6 +57,8 @@ export enum Altered { @Injectable() export class ColumnsService { + constructor(private readonly metaService: MetaService) {} + async columnUpdate(param: { req?: any; columnId: string; @@ -1142,21 +1144,23 @@ export class ColumnsService { return table; } - async columnDelete(param: { req?: any; columnId: string }) { - const column = await Column.get({ colId: param.columnId }); - const table = await Model.getWithInfo({ - id: column.fk_model_id, - }); - const base = await Base.get(table.base_id); - - // const ncMeta = await Noco.ncMeta.startTransaction(); - // const sql-mgr = await ProjectMgrv2.getSqlMgrTrans( - // { id: base.project_id }, - // ncMeta, - // base - // ); + async columnDelete( + param: { req?: any; columnId: string }, + ncMeta = this.metaService, + ) { + const column = await Column.get({ colId: param.columnId }, ncMeta); + const table = await Model.getWithInfo( + { + id: column.fk_model_id, + }, + ncMeta, + ); + const base = await Base.get(table.base_id, ncMeta); - const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); + const sqlMgr = await ProjectMgrv2.getSqlMgr( + { id: base.project_id }, + ncMeta, + ); switch (column.uidt) { case UITypes.Lookup: @@ -1164,17 +1168,17 @@ export class ColumnsService { case UITypes.QrCode: case UITypes.Barcode: case UITypes.Formula: - await Column.delete(param.columnId); + await Column.delete(param.columnId, ncMeta); break; case UITypes.LinkToAnotherRecord: { const relationColOpt = - await column.getColOptions(); - const childColumn = await relationColOpt.getChildColumn(); - const childTable = await childColumn.getModel(); + await column.getColOptions(ncMeta); + const childColumn = await relationColOpt.getChildColumn(ncMeta); + const childTable = await childColumn.getModel(ncMeta); - const parentColumn = await relationColOpt.getParentColumn(); - const parentTable = await parentColumn.getModel(); + const parentColumn = await relationColOpt.getParentColumn(ncMeta); + const parentTable = await parentColumn.getModel(ncMeta); switch (relationColOpt.type) { case 'bt': @@ -1188,15 +1192,19 @@ export class ColumnsService { parentColumn, parentTable, sqlMgr, - // ncMeta + ncMeta, }); } break; case 'mm': { - const mmTable = await relationColOpt.getMMModel(); - const mmParentCol = await relationColOpt.getMMParentColumn(); - const mmChildCol = await relationColOpt.getMMChildColumn(); + const mmTable = await relationColOpt.getMMModel(ncMeta); + const mmParentCol = await relationColOpt.getMMParentColumn( + ncMeta, + ); + const mmChildCol = await relationColOpt.getMMChildColumn( + ncMeta, + ); await this.deleteHmOrBtRelation( { @@ -1207,7 +1215,7 @@ export class ColumnsService { parentTable: parentTable, childColumn: mmParentCol, base, - // ncMeta + ncMeta, }, true, ); @@ -1221,18 +1229,18 @@ export class ColumnsService { parentTable: childTable, childColumn: mmChildCol, base, - // ncMeta + ncMeta, }, true, ); const columnsInRelatedTable: Column[] = await relationColOpt - .getRelatedTable() - .then((m) => m.getColumns()); + .getRelatedTable(ncMeta) + .then((m) => m.getColumns(ncMeta)); for (const c of columnsInRelatedTable) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(); + await c.getColOptions(ncMeta); if ( colOpt.type === 'mm' && colOpt.fk_parent_column_id === childColumn.id && @@ -1241,55 +1249,55 @@ export class ColumnsService { colOpt.fk_mm_parent_column_id === mmChildCol.id && colOpt.fk_mm_child_column_id === mmParentCol.id ) { - await Column.delete(c.id); + await Column.delete(c.id, ncMeta); break; } } - await Column.delete(relationColOpt.fk_column_id); + await Column.delete(relationColOpt.fk_column_id, ncMeta); // delete bt columns in m2m table - await mmTable.getColumns(); + await mmTable.getColumns(ncMeta); for (const c of mmTable.columns) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(); + await c.getColOptions(ncMeta); if (colOpt.type === 'bt') { - await Column.delete(c.id); + await Column.delete(c.id, ncMeta); } } // delete hm columns in parent table - await parentTable.getColumns(); + await parentTable.getColumns(ncMeta); for (const c of parentTable.columns) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(); + await c.getColOptions(ncMeta); if (colOpt.fk_related_model_id === mmTable.id) { - await Column.delete(c.id); + await Column.delete(c.id, ncMeta); } } // delete hm columns in child table - await childTable.getColumns(); + await childTable.getColumns(ncMeta); for (const c of childTable.columns) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(); + await c.getColOptions(ncMeta); if (colOpt.fk_related_model_id === mmTable.id) { - await Column.delete(c.id); + await Column.delete(c.id, ncMeta); } } // retrieve columns in m2m table again - await mmTable.getColumns(); + await mmTable.getColumns(ncMeta); // ignore deleting table if it has more than 2 columns // the expected 2 columns would be table1_id & table2_id if (mmTable.columns.length === 2) { (mmTable as any).tn = mmTable.table_name; await sqlMgr.sqlOpPlus(base, 'tableDelete', mmTable); - await mmTable.delete(); + await mmTable.delete(ncMeta); } } break; @@ -1337,26 +1345,30 @@ export class ColumnsService { await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - await Column.delete(param.columnId); + await Column.delete(param.columnId, ncMeta); } } - await Audit.insert({ - project_id: base.project_id, - op_type: AuditOperationTypes.TABLE_COLUMN, - op_sub_type: AuditOperationSubTypes.DELETE, - user: param?.req?.user?.email, - description: `The column ${column.column_name} with alias ${column.title} from table ${table.table_name} has been deleted`, - ip: param?.req.clientIp, - }); - - await table.getColumns(); + await Audit.insert( + { + project_id: base.project_id, + op_type: AuditOperationTypes.TABLE_COLUMN, + op_sub_type: AuditOperationSubTypes.DELETE, + user: param?.req?.user?.email, + description: `The column ${column.column_name} with alias ${column.title} from table ${table.table_name} has been deleted`, + ip: param?.req.clientIp, + }, + ncMeta, + ); + + await table.getColumns(ncMeta); const displayValueColumn = mapDefaultDisplayValue(table.columns); if (displayValueColumn) { await Model.updatePrimaryColumn( displayValueColumn.fk_model_id, displayValueColumn.id, + ncMeta, ); } @@ -1394,45 +1406,47 @@ export class ColumnsService { if (!relationColOpt) { foreignKeyName = ( ( - await childTable.getColumns().then((cols) => { - return cols?.find((c) => { - return ( - c.uidt === UITypes.LinkToAnotherRecord && - c.colOptions.fk_related_model_id === parentTable.id && - (c.colOptions as LinkToAnotherRecordType).fk_child_column_id === - childColumn.id && - (c.colOptions as LinkToAnotherRecordType) - .fk_parent_column_id === parentColumn.id - ); - }); + await childTable.getColumns(ncMeta).then(async (cols) => { + for (const col of cols) { + if (col.uidt === UITypes.LinkToAnotherRecord) { + const colOptions = + await col.getColOptions(ncMeta); + console.log(colOptions); + if (colOptions.fk_related_model_id === parentTable.id) { + return { colOptions }; + } + } + } }) - ).colOptions as LinkToAnotherRecordType + )?.colOptions as LinkToAnotherRecordType ).fk_index_name; } else { foreignKeyName = relationColOpt.fk_index_name; } - // todo: handle relation delete exception - try { - await sqlMgr.sqlOpPlus(base, 'relationDelete', { - childColumn: childColumn.column_name, - childTable: childTable.table_name, - parentTable: parentTable.table_name, - parentColumn: parentColumn.column_name, - foreignKeyName, - }); - } catch (e) { - console.log(e); + if (!relationColOpt?.virtual) { + // todo: handle relation delete exception + try { + await sqlMgr.sqlOpPlus(base, 'relationDelete', { + childColumn: childColumn.column_name, + childTable: childTable.table_name, + parentTable: parentTable.table_name, + parentColumn: parentColumn.column_name, + foreignKeyName, + }); + } catch (e) { + console.log(e); + } } if (!relationColOpt) return; const columnsInRelatedTable: Column[] = await relationColOpt - .getRelatedTable() - .then((m) => m.getColumns()); + .getRelatedTable(ncMeta) + .then((m) => m.getColumns(ncMeta)); const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt'; for (const c of columnsInRelatedTable) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; - const colOpt = await c.getColOptions(); + const colOpt = await c.getColOptions(ncMeta); if ( colOpt.fk_parent_column_id === parentColumn.id && colOpt.fk_child_column_id === childColumn.id && @@ -1447,9 +1461,34 @@ export class ColumnsService { await Column.delete(relationColOpt.fk_column_id, ncMeta); if (!ignoreFkDelete) { - const cTable = await Model.getWithInfo({ - id: childTable.id, - }); + const cTable = await Model.getWithInfo( + { + id: childTable.id, + }, + ncMeta, + ); + + // if virtual column delete all index before deleting the column + if (relationColOpt?.virtual) { + const indexes = + ( + await sqlMgr.sqlOp(base, 'indexList', { + tn: cTable.table_name, + }) + )?.data?.list ?? []; + + for (const index of indexes) { + if (index.cn !== childColumn.column_name) continue; + + await sqlMgr.sqlOpPlus(base, 'indexDelete', { + ...index, + tn: cTable.table_name, + columns: [childColumn.column_name], + indexName: index.index_name, + }); + } + } + const tableUpdateBody = { ...cTable, tn: cTable.table_name, @@ -1499,6 +1538,12 @@ export class ColumnsService { const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: param.base.project_id, }); + + // if xcdb base then treat as virtual relation to avoid creating foreign key + if (param.base.is_meta) { + (param.column as LinkToAnotherColumnReqType).virtual = true; + } + if ( (param.column as LinkToAnotherColumnReqType).type === 'hm' || (param.column as LinkToAnotherColumnReqType).type === 'bt' @@ -1570,7 +1615,7 @@ export class ColumnsService { } // todo: create index for virtual relations as well - // create index for foreign key in pg + // create index for foreign key in pg if ( param.base.type === 'pg' || (param.column as LinkToAnotherColumnReqType).virtual @@ -1729,6 +1774,7 @@ export class ColumnsService { fk_mm_child_column_id: childCol.id, fk_mm_parent_column_id: parentCol.id, fk_related_model_id: parent.id, + virtual: (param.column as LinkToAnotherColumnReqType).virtual, }); await Column.insert({ title: getUniqueColumnAliasName( @@ -1748,6 +1794,7 @@ export class ColumnsService { fk_mm_child_column_id: parentCol.id, fk_mm_parent_column_id: childCol.id, fk_related_model_id: child.id, + virtual: (param.column as LinkToAnotherColumnReqType).virtual, }); // todo: create index for virtual relations as well diff --git a/packages/nocodb/src/services/datas.service.ts b/packages/nocodb/src/services/datas.service.ts index 56bdc589a1..d1e1c89705 100644 --- a/packages/nocodb/src/services/datas.service.ts +++ b/packages/nocodb/src/services/datas.service.ts @@ -103,11 +103,15 @@ export class DatasService { dbDriver: await NcConnectionMgrv2.get(base), }); - // todo: Should have error http status code - const message = await baseModel.hasLTARData(param.rowId, model); - if (message.length) { - NcError.badRequest(message); + // if xcdb project skip checking for LTAR + if (!base.is_meta) { + // todo: Should have error http status code + const message = await baseModel.hasLTARData(param.rowId, model); + if (message.length) { + NcError.badRequest(message); + } } + return await baseModel.delByPk(param.rowId, null, param.cookie); } diff --git a/packages/nocodb/src/services/tables.service.ts b/packages/nocodb/src/services/tables.service.ts index 7d86ce4d5d..ae89adb87b 100644 --- a/packages/nocodb/src/services/tables.service.ts +++ b/packages/nocodb/src/services/tables.service.ts @@ -14,9 +14,19 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT'; import getColumnUiType from '../helpers/getColumnUiType'; import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; -import { Audit, Column, Model, ModelRoleVisibility, Project } from '../models'; +import { + Audit, + Base, + Column, + Model, + ModelRoleVisibility, + Project, +} from '../models'; +import Noco from '../Noco'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { validatePayload } from '../helpers'; +import { ColumnsService } from './columns.service'; +import type { MetaService } from '../meta/meta.service'; import type { LinkToAnotherRecordColumn, User, View } from '../models'; import type { ColumnType, @@ -26,6 +36,8 @@ import type { @Injectable() export class TablesService { + constructor(private readonly columnsService: ColumnsService) {} + async tableUpdate(param: { tableId: any; table: TableReqType & { project_id?: string }; @@ -142,11 +154,14 @@ export class TablesService { const table = await Model.getByIdOrName({ id: param.tableId }); await table.getColumns(); + const project = await Project.getWithInfo(table.project_id); + const base = project.bases.find((b) => b.id === table.base_id); + const relationColumns = table.columns.filter( (c) => c.uidt === UITypes.LinkToAnotherRecord, ); - if (relationColumns?.length) { + if (relationColumns?.length && !base.is_meta) { const referredTables = await Promise.all( relationColumns.map(async (c) => c @@ -162,37 +177,69 @@ export class TablesService { ); } - const project = await Project.getWithInfo(table.project_id); - const base = project.bases.find((b) => b.id === table.base_id); - const sqlMgr = await ProjectMgrv2.getSqlMgr(project); - (table as any).tn = table.table_name; - table.columns = table.columns.filter((c) => !isVirtualCol(c)); - table.columns.forEach((c) => { - (c as any).cn = c.column_name; - }); + // start a transaction + const ncMeta = await (Noco.ncMeta as MetaService).startTransaction(); + let result; + try { + // delete all relations + for (const c of relationColumns) { + // skip if column is hasmany relation to mm table + if (c.system) { + continue; + } + + // verify column exist or not and based on that delete the column + if (!(await Column.get({ colId: c.id }, ncMeta))) { + continue; + } + + await this.columnsService.columnDelete( + { + req: param.req, + columnId: c.id, + }, + ncMeta, + ); + } - if (table.type === ModelTypes.TABLE) { - await sqlMgr.sqlOpPlus(base, 'tableDelete', table); - } else if (table.type === ModelTypes.VIEW) { - await sqlMgr.sqlOpPlus(base, 'viewDelete', { - ...table, - view_name: table.table_name, + const sqlMgr = await ProjectMgrv2.getSqlMgr(project, ncMeta); + (table as any).tn = table.table_name; + table.columns = table.columns.filter((c) => !isVirtualCol(c)); + table.columns.forEach((c) => { + (c as any).cn = c.column_name; }); - } - await Audit.insert({ - project_id: project.id, - base_id: base.id, - op_type: AuditOperationTypes.TABLE, - op_sub_type: AuditOperationSubTypes.DELETE, - user: param.user?.email, - description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `, - ip: param.req?.clientIp, - }).then(() => {}); - - T.emit('evt', { evt_type: 'table:deleted' }); + if (table.type === ModelTypes.TABLE) { + await sqlMgr.sqlOpPlus(base, 'tableDelete', table); + } else if (table.type === ModelTypes.VIEW) { + await sqlMgr.sqlOpPlus(base, 'viewDelete', { + ...table, + view_name: table.table_name, + }); + } - return table.delete(); + await Audit.insert( + { + project_id: project.id, + base_id: base.id, + op_type: AuditOperationTypes.TABLE, + op_sub_type: AuditOperationSubTypes.DELETE, + user: param.user?.email, + description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `, + ip: param.req?.clientIp, + }, + ncMeta, + ).then(() => {}); + + T.emit('evt', { evt_type: 'table:deleted' }); + + result = await table.delete(ncMeta); + await ncMeta.commit(); + } catch (e) { + await ncMeta.rollback(); + throw e; + } + return result; } async getTableWithAccessibleViews(param: { tableId: string; user: User }) { diff --git a/packages/nocodb/src/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/version-upgrader/NcUpgrader.ts index 951a93099f..a0969366a1 100644 --- a/packages/nocodb/src/version-upgrader/NcUpgrader.ts +++ b/packages/nocodb/src/version-upgrader/NcUpgrader.ts @@ -14,6 +14,7 @@ import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045'; import ncProjectEnvUpgrader from './ncProjectEnvUpgrader'; import ncHookUpgrader from './ncHookUpgrader'; import ncProjectConfigUpgrader from './ncProjectConfigUpgrader'; +import ncXcdbLTARUpgrader from './ncXcdbLTARUpgrader'; import type { MetaService } from '../meta/meta.service'; import type { NcConfig } from '../interface/config'; @@ -50,6 +51,7 @@ export default class NcUpgrader { { name: '0105003', handler: ncFilterUpgrader_0105003 }, { name: '0105004', handler: ncHookUpgrader }, { name: '0107004', handler: ncProjectConfigUpgrader }, + { name: '0108002', handler: ncXcdbLTARUpgrader }, ]; if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) { return; diff --git a/packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts b/packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts index 1aecb3eb32..f3e69083f8 100644 --- a/packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts +++ b/packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts @@ -1,359 +1,43 @@ -import { UITypes } from 'nocodb-sdk'; +import { OrgUserRoles } from 'nocodb-sdk'; +import { NC_APP_SETTINGS } from '../constants'; +import Store from '../models/Store'; import { MetaTable } from '../utils/globals'; -import Column from '../models/Column'; -import Filter from '../models/Filter'; -import Project from '../models/Project'; -import type { MetaService } from '../meta/meta.service'; import type { NcUpgraderCtx } from './NcUpgrader'; -import type { SelectOptionsType } from 'nocodb-sdk'; -// as of 0.104.3, almost all filter operators are available to all column types -// while some of them aren't supposed to be shown -// this upgrader is to remove those unsupported filters / migrate to the correct filter - -// Change Summary: -// - Text-based columns: -// - remove `>`, `<`, `>=`, `<=` -// - Numeric-based / SingleSelect columns: -// - remove `like` -// - migrate `null`, and `empty` to `blank` -// - Checkbox columns: -// - remove `equal` -// - migrate `empty` and `null` to `notchecked` -// - MultiSelect columns: -// - remove `like` -// - migrate `equal`, `null`, `empty` -// - Attachment columns: -// - remove `>`, `<`, `>=`, `<=`, `equal` -// - migrate `empty`, `null` to `blank` -// - LTAR columns: -// - remove `>`, `<`, `>=`, `<=` -// - migrate `empty`, `null` to `blank` -// - Lookup columns: -// - migrate `empty`, `null` to `blank` -// - Duration columns: -// - remove `like` -// - migrate `empty`, `null` to `blank` - -const removeEqualFilters = (filter, ncMeta) => { - const actions = []; - // remove `is equal`, `is not equal` - if (['eq', 'neq'].includes(filter.comparison_op)) { - actions.push(Filter.delete(filter.id, ncMeta)); - } - return actions; -}; - -const removeArithmeticFilters = (filter, ncMeta) => { - const actions = []; - // remove `>`, `<`, `>=`, `<=` - if (['gt', 'lt', 'gte', 'lte'].includes(filter.comparison_op)) { - actions.push(Filter.delete(filter.id, ncMeta)); - } - return actions; -}; - -const removeLikeFilters = (filter, ncMeta) => { - const actions = []; - // remove `is like`, `is not like` - if (['like', 'nlike'].includes(filter.comparison_op)) { - actions.push(Filter.delete(filter.id, ncMeta)); - } - return actions; -}; - -const migrateNullAndEmptyToBlankFilters = (filter, ncMeta) => { - const actions = []; - if (['empty', 'null'].includes(filter.comparison_op)) { - // migrate to blank - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'blank', - }, - ncMeta, - ), - ); - } else if (['notempty', 'notnull'].includes(filter.comparison_op)) { - // migrate to not blank - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'notblank', - }, - ncMeta, - ), - ); - } - return actions; -}; - -const migrateMultiSelectEq = async (filter, col: Column, ncMeta) => { - // only allow eq / neq - if (!['eq', 'neq'].includes(filter.comparison_op)) return; - // if there is no value -> delete this filter - if (!filter.value) { - return await Filter.delete(filter.id, ncMeta); - } - // options inputted from users - const options = filter.value.split(','); - // retrieve the possible col options - const colOptions = (await col.getColOptions()) as SelectOptionsType; - // only include valid options as the input value becomes dropdown type now - const validOptions = []; - for (const option of options) { - if (colOptions.options.includes(option)) { - validOptions.push(option); - } - } - const newFilterValue = validOptions.join(','); - // if all inputted options are invalid -> delete this filter - if (!newFilterValue) { - return await Filter.delete(filter.id, ncMeta); - } - const actions = []; - if (filter.comparison_op === 'eq') { - // migrate to `contains all of` - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'anyof', - value: newFilterValue, - }, - ncMeta, - ), - ); - } else if (filter.comparison_op === 'neq') { - // migrate to `doesn't contain all of` - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'nanyof', - value: newFilterValue, - }, - ncMeta, - ), - ); - } - return await Promise.all(actions); -}; - -const migrateToCheckboxFilter = (filter, ncMeta) => { - const actions = []; - const possibleTrueValues = ['true', 'True', '1', 'T', 'Y']; - const possibleFalseValues = ['false', 'False', '0', 'F', 'N']; - if (['empty', 'null'].includes(filter.comparison_op)) { - // migrate to not checked - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'notchecked', - }, - ncMeta, - ), - ); - } else if (['notempty', 'notnull'].includes(filter.comparison_op)) { - // migrate to checked - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'checked', - }, - ncMeta, - ), +/** Upgrader for upgrading roles */ +export default async function ({ ncMeta }: NcUpgraderCtx) { + const users = await ncMeta.metaList2(null, null, MetaTable.USERS); + + for (const user of users) { + user.roles = user.roles + .split(',') + .map((r) => { + // update old role names with new roles + if (r === 'user') { + return OrgUserRoles.CREATOR; + } else if (r === 'user-new') { + return OrgUserRoles.VIEWER; + } + return r; + }) + .join(','); + await ncMeta.metaUpdate( + null, + null, + MetaTable.USERS, + { roles: user.roles }, + user.id, ); - } else if (filter.comparison_op === 'eq') { - if (possibleTrueValues.includes(filter.value)) { - // migrate to checked - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'checked', - value: '', - }, - ncMeta, - ), - ); - } else if (possibleFalseValues.includes(filter.value)) { - // migrate to notchecked - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'notchecked', - value: '', - }, - ncMeta, - ), - ); - } else { - // invalid value - good to delete - actions.push(Filter.delete(filter.id, ncMeta)); - } - } else if (filter.comparison_op === 'neq') { - if (possibleFalseValues.includes(filter.value)) { - // migrate to checked - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'checked', - value: '', - }, - ncMeta, - ), - ); - } else if (possibleTrueValues.includes(filter.value)) { - // migrate to not checked - actions.push( - Filter.update( - filter.id, - { - comparison_op: 'notchecked', - value: '', - }, - ncMeta, - ), - ); - } else { - // invalid value - good to delete - actions.push(Filter.delete(filter.id, ncMeta)); - } - } - return actions; -}; - -async function migrateFilters(ncMeta: MetaService) { - const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); - for (const filter of filters) { - if (!filter.fk_column_id || filter.is_group) { - continue; - } - const col = await Column.get({ colId: filter.fk_column_id }, ncMeta); - if ( - [ - UITypes.SingleLineText, - UITypes.LongText, - UITypes.PhoneNumber, - UITypes.Email, - UITypes.URL, - ].includes(col.uidt) - ) { - await Promise.all(removeArithmeticFilters(filter, ncMeta)); - } else if ( - [ - // numeric fields - UITypes.Duration, - UITypes.Currency, - UITypes.Percent, - UITypes.Number, - UITypes.Decimal, - UITypes.Rating, - UITypes.Rollup, - // select fields - UITypes.SingleSelect, - ].includes(col.uidt) - ) { - await Promise.all([ - ...removeLikeFilters(filter, ncMeta), - ...migrateNullAndEmptyToBlankFilters(filter, ncMeta), - ]); - } else if (col.uidt === UITypes.Checkbox) { - await Promise.all(migrateToCheckboxFilter(filter, ncMeta)); - } else if (col.uidt === UITypes.MultiSelect) { - await Promise.all([ - ...removeLikeFilters(filter, ncMeta), - ...migrateNullAndEmptyToBlankFilters(filter, ncMeta), - ]); - await migrateMultiSelectEq(filter, col, ncMeta); - } else if (col.uidt === UITypes.Attachment) { - await Promise.all([ - ...removeArithmeticFilters(filter, ncMeta), - ...removeEqualFilters(filter, ncMeta), - ...migrateNullAndEmptyToBlankFilters(filter, ncMeta), - ]); - } else if (col.uidt === UITypes.LinkToAnotherRecord) { - await Promise.all([ - ...removeArithmeticFilters(filter, ncMeta), - ...migrateNullAndEmptyToBlankFilters(filter, ncMeta), - ]); - } else if (col.uidt === UITypes.Lookup) { - await Promise.all([ - ...removeArithmeticFilters(filter, ncMeta), - ...migrateNullAndEmptyToBlankFilters(filter, ncMeta), - ]); - } else if (col.uidt === UITypes.Duration) { - await Promise.all([ - ...removeLikeFilters(filter, ncMeta), - ...migrateNullAndEmptyToBlankFilters(filter, ncMeta), - ]); - } - } -} - -async function updateProjectMeta(ncMeta: MetaService) { - const projectHasEmptyOrFilters: Record = {}; - - const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); - - const actions = []; - - for (const filter of filters) { - if ( - ['notempty', 'notnull', 'empty', 'null'].includes(filter.comparison_op) - ) { - projectHasEmptyOrFilters[filter.project_id] = true; - } } - const projects = await ncMeta.metaList2(null, null, MetaTable.PROJECT); - - const defaultProjectMeta = { - showNullAndEmptyInFilter: false, - }; - - for (const project of projects) { - const oldProjectMeta = project.meta; - let newProjectMeta = defaultProjectMeta; - try { - newProjectMeta = - (typeof oldProjectMeta === 'string' - ? JSON.parse(oldProjectMeta) - : oldProjectMeta) ?? defaultProjectMeta; - } catch {} - - newProjectMeta = { - ...newProjectMeta, - showNullAndEmptyInFilter: projectHasEmptyOrFilters[project.id] ?? false, - }; - - actions.push( - Project.update( - project.id, - { - meta: JSON.stringify(newProjectMeta), - }, - ncMeta, - ), + // set invite only signup if user have environment variable set + if (process.env.NC_INVITE_ONLY_SIGNUP) { + await Store.saveOrUpdate( + { + value: '{ "invite_only_signup": true }', + key: NC_APP_SETTINGS, + }, + ncMeta, ); } - await Promise.all(actions); -} - -export default async function ({ ncMeta }: NcUpgraderCtx) { - // fix the existing filter behaviours or - // migrate `null` or `empty` filters to `blank` - await migrateFilters(ncMeta); - // enrich `showNullAndEmptyInFilter` in project meta - // if there is empty / null filters in existing projects, - // then set `showNullAndEmptyInFilter` to true - // else set to false - await updateProjectMeta(ncMeta); } diff --git a/packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts b/packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts new file mode 100644 index 0000000000..19d2f94a94 --- /dev/null +++ b/packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts @@ -0,0 +1,164 @@ +import { RelationTypes, UITypes } from 'nocodb-sdk'; +import NocoCache from '../cache/NocoCache'; +import { MetaTable } from '../meta/meta.service'; +import { Base } from '../models'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import { CacheGetType, CacheScope } from '../utils/globals'; +import { Model } from '../models'; +import type { LinkToAnotherRecordColumn } from '../models'; +import type { MetaService } from '../meta/meta.service'; +import type { NcUpgraderCtx } from './NcUpgrader'; + +// An upgrader for upgrading LTAR relations in XCDB bases +// it will delete all the foreign keys and create a new index +// and treat all the LTAR as virtual + +async function upgradeModelRelations({ + model, + relations, + ncMeta, + sqlClient, +}: { + ncMeta: MetaService; + model: Model; + sqlClient: ReturnType< + (typeof NcConnectionMgrv2)['getSqlClient'] + > extends Promise + ? U + : ReturnType<(typeof NcConnectionMgrv2)['getSqlClient']>; + relations: { + tn: string; + rtn: string; + cn: string; + rcn: string; + }[]; +}) { + // Iterate over each column and upgrade LTAR + for (const column of await model.getColumns(ncMeta)) { + if (column.uidt !== UITypes.LinkToAnotherRecord) { + continue; + } + + const colOptions = await column.getColOptions( + ncMeta, + ); + + switch (colOptions.type) { + case RelationTypes.HAS_MANY: + { + // skip if virtual + if (colOptions.virtual) { + break; + } + + const parentCol = await colOptions.getParentColumn(ncMeta); + const childCol = await colOptions.getChildColumn(ncMeta); + + const parentModel = await parentCol.getModel(ncMeta); + const childModel = await childCol.getModel(ncMeta); + + // delete the foreign key constraint if exists + const relation = relations.find((r) => { + return ( + parentCol.column_name === r.rcn && + childCol.column_name === r.cn && + parentModel.table_name === r.rtn && + childModel.table_name === r.tn + ); + }); + + // delete the relation + if (relation) { + await sqlClient.relationDelete({ + parentColumn: relation.rcn, + childColumn: relation.cn, + parentTable: relation.rtn, + childTable: relation.tn, + }); + } + + // skip postgres since we were already creating the index while creating the relation + if (ncMeta.knex.clientType() !== 'pg') { + // create a new index for the column + const indexArgs = { + columns: [relation.cn], + tn: relation.tn, + non_unique: true, + }; + await sqlClient.indexCreate(indexArgs); + } + } + break; + } + + // update the relation as virtual + await ncMeta.metaUpdate( + null, + null, + MetaTable.COL_RELATIONS, + { virtual: true }, + colOptions.id, + ); + + // update the cache as well + const cachedData = await NocoCache.get( + `${CacheScope.COL_RELATION}:${colOptions.fk_column_id}`, + CacheGetType.TYPE_OBJECT, + ); + if (cachedData) { + cachedData.virtual = true; + await NocoCache.set( + `${CacheScope.COL_RELATION}:${colOptions.fk_column_id}`, + cachedData, + ); + } + } +} + +// An upgrader for upgrading any existing relation in xcdb +async function upgradeBaseRelations({ + ncMeta, + base, +}: { + ncMeta: MetaService; + base: any; +}) { + // const sqlMgr = ProjectMgrv2.getSqlMgr({ id: base.project_id }, ncMeta); + + const sqlClient = await NcConnectionMgrv2.getSqlClient(base, ncMeta.knex); + + // get all relations + const relations = (await sqlClient.relationListAll())?.data?.list; + + // get models for the base + const models = await ncMeta.metaList2(null, base.id, MetaTable.MODELS); + + // get all columns and filter out relations and upgrade + for (const model of models) { + await upgradeModelRelations({ + ncMeta, + model: new Model(model), + sqlClient, + relations, + }); + } +} + +// database to virtual relation and create an index for it +export default async function ({ ncMeta }: NcUpgraderCtx) { + // get all xcdb bases + const bases = await ncMeta.metaList2(null, null, MetaTable.BASES, { + condition: { + is_meta: 1, + }, + orderBy: {}, + }); + + // iterate and upgrade each base + for (const base of bases) { + await upgradeBaseRelations({ + ncMeta, + base: new Base(base), + }); + } +} diff --git a/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts b/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts index 7056bde79a..20ec1feb3d 100644 --- a/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts @@ -1339,7 +1339,7 @@ function viewRowTests() { await testDeleteViewRow(ViewTypes.FORM); }); - const testDeleteViewRowWithForiegnKeyConstraint = async ( + const testDeleteViewRowWithForeignKeyConstraint = async ( viewType: ViewTypes, ) => { const table = await createTable(context, project); @@ -1370,7 +1370,7 @@ function viewRowTests() { rowId: row['Id'], }); - const response = await request(context.app) + await request(context.app) .delete( `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`, ) @@ -1390,15 +1390,15 @@ function viewRowTests() { }; it('Delete view row with ltar foreign key constraint GALLERY', async function () { - await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.GALLERY); + await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.GALLERY); }); it('Delete view row with ltar foreign key constraint GRID', async function () { - await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.GRID); + await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.GRID); }); it('Delete view row with ltar foreign key constraint FORM', async function () { - await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.FORM); + await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.FORM); }); const testViewRowExists = async (viewType: ViewTypes) => { diff --git a/tests/playwright/pages/Dashboard/Grid/index.ts b/tests/playwright/pages/Dashboard/Grid/index.ts index 9ab2a0c8cd..3614fcf2ee 100644 --- a/tests/playwright/pages/Dashboard/Grid/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/index.ts @@ -185,6 +185,12 @@ export class GridPage extends BasePage { await (await this.rootPage.locator('.ant-drawer-body').elementHandle())?.waitForElementState('stable'); } + async selectRow(index: number) { + const cell: Locator = await this.get().locator(`td[data-testid="cell-Id-${index}"]`); + await cell.hover(); + await cell.locator('input[type="checkbox"]').check({ force: true }); + } + async selectAll() { await this.get().locator('[data-testid="nc-check-all"]').hover(); @@ -201,8 +207,7 @@ export class GridPage extends BasePage { await this.rootPage.waitForTimeout(300); } - async deleteAll() { - await this.selectAll(); + async deleteSelectedRows() { await this.get().locator('[data-testid="nc-check-all"]').nth(0).click({ button: 'right', }); @@ -210,6 +215,11 @@ export class GridPage extends BasePage { await this.dashboard.waitForLoaderToDisappear(); } + async deleteAll() { + await this.selectAll(); + await this.deleteSelectedRows(); + } + async verifyTotalRowCount({ count }: { count: number }) { // wait for 100 ms and try again : 5 times let i = 0; diff --git a/tests/playwright/setup/xcdbProject.ts b/tests/playwright/setup/xcdbProject.ts new file mode 100644 index 0000000000..7123e80b2c --- /dev/null +++ b/tests/playwright/setup/xcdbProject.ts @@ -0,0 +1,40 @@ +import { Api } from 'nocodb-sdk'; +let api: Api; +async function createXcdb(token?: string) { + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': token, + }, + }); + + const projectList = await api.project.list(); + for (const project of projectList.list) { + // delete project with title 'xcdb' if it exists + if (project.title === 'xcdb') { + await api.project.delete(project.id); + } + } + + const project = await api.project.create({ title: 'xcdb' }); + return project; +} + +async function deleteXcdb(token?: string) { + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': token, + }, + }); + + const projectList = await api.project.list(); + for (const project of projectList.list) { + // delete project with title 'xcdb' if it exists + if (project.title === 'xcdb') { + await api.project.delete(project.id); + } + } +} + +export { createXcdb, deleteXcdb }; diff --git a/tests/playwright/tests/db/metaLTAR.spec.ts b/tests/playwright/tests/db/metaLTAR.spec.ts new file mode 100644 index 0000000000..7205c37c33 --- /dev/null +++ b/tests/playwright/tests/db/metaLTAR.spec.ts @@ -0,0 +1,324 @@ +/* + * + * Meta projects, additional provision for deleting of rows, columns and tables with Link to another record field type + * + * Pre-requisite: + * TableA TableB TableC + * TableA TableD TableE + * TableA TableA : self relation + * TableA TableA : self relation + * Insert some records in TableA, TableB, TableC, TableD, TableE, add some links between them + * + * + * Tests: + * 1. Delete a row from TableA : Verify record status in adjacent tables + * 2. Delete hm link from TableA to TableB : Verify record status in adjacent tables + * 3. Delete mm link from TableA to TableD : Verify record status in adjacent tables + * 4. Delete a self link column from TableA : Verify + * 5. Delete TableA : Verify record status in adjacent tables + * + */ + +import { test } from '@playwright/test'; +import setup from '../../setup'; +import { Api, UITypes } from 'nocodb-sdk'; +import { DashboardPage } from '../../pages/Dashboard'; +import { GridPage } from '../../pages/Dashboard/Grid'; +import { createXcdb, deleteXcdb } from '../../setup/xcdbProject'; +import { ProjectsPage } from '../../pages/ProjectsPage'; +import { isSqlite } from '../../setup/db'; +let api: Api; +const recordCount = 10; + +// serial as all projects end up creating xcdb using same name +// fix me : use worker ID logic for creating unique project name +test.describe.serial('Test table', () => { + let context: any; + let dashboard: DashboardPage; + let grid: GridPage; + const tables = []; + + test.afterEach(async () => { + try { + if (context) { + await deleteXcdb(context.token); + } + } catch (e) { + console.log(e); + } + + // reset tables array + tables.length = 0; + }); + + test.beforeEach(async ({ page }) => { + context = await setup({ page, isEmptyProject: true }); + dashboard = new DashboardPage(page, context.project); + grid = dashboard.grid; + + // create a new xcdb project + const xcdb = await createXcdb(context.token); + await dashboard.clickHome(); + const projectsPage = new ProjectsPage(dashboard.rootPage); + await projectsPage.openProject({ title: 'xcdb', withoutPrefix: true }); + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + const columns = [ + { + column_name: 'Id', + title: 'Id', + uidt: UITypes.ID, + }, + { + column_name: 'Title', + title: 'Title', + uidt: UITypes.SingleLineText, + pv: true, + }, + ]; + + const rows = []; + for (let i = 0; i < recordCount * 10; i++) { + rows.push({ + Id: i + 1, + Title: `${i + 1}`, + }); + } + + for (let i = 0; i < 5; i++) { + const table = await api.base.tableCreate(xcdb.id, xcdb.bases?.[0].id, { + table_name: `Table${i}`, + title: `Table${i}`, + columns: columns, + }); + tables.push(table); + await api.dbTableRow.bulkCreate('noco', xcdb.id, tables[i].id, rows); + } + + // Create links + // TableA TableB TableC + await api.dbTableColumn.create(tables[0].id, { + uidt: UITypes.LinkToAnotherRecord, + title: `TableA:hm:TableB`, + column_name: `TableA:hm:TableB`, + parentId: tables[0].id, + childId: tables[1].id, + type: 'hm', + }); + await api.dbTableColumn.create(tables[1].id, { + uidt: UITypes.LinkToAnotherRecord, + title: `TableB:hm:TableC`, + column_name: `TableB:hm:TableC`, + parentId: tables[1].id, + childId: tables[2].id, + type: 'hm', + }); + + // TableA TableD TableE + await api.dbTableColumn.create(tables[0].id, { + uidt: UITypes.LinkToAnotherRecord, + title: `TableA:mm:TableD`, + column_name: `TableA:mm:TableD`, + parentId: tables[0].id, + childId: tables[3].id, + type: 'mm', + }); + await api.dbTableColumn.create(tables[3].id, { + uidt: UITypes.LinkToAnotherRecord, + title: `TableD:mm:TableE`, + column_name: `TableD:mm:TableE`, + parentId: tables[3].id, + childId: tables[4].id, + type: 'mm', + }); + + // TableA TableA : self relation + await api.dbTableColumn.create(tables[0].id, { + uidt: UITypes.LinkToAnotherRecord, + title: `TableA:hm:TableA`, + column_name: `TableA:hm:TableA`, + parentId: tables[0].id, + childId: tables[0].id, + type: 'hm', + }); + + // TableA TableA : self relation + await api.dbTableColumn.create(tables[0].id, { + uidt: UITypes.LinkToAnotherRecord, + title: `TableA:mm:TableA`, + column_name: `TableA:mm:TableA`, + parentId: tables[0].id, + childId: tables[0].id, + type: 'mm', + }); + + // Add links + // TableA TableB TableC + // Link every record in tableA to 3 records in tableB + for (let i = 1; i <= recordCount; i++) { + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableB', `${i * 3 - 2}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableB', `${i * 3 - 1}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableB', `${i * 3 - 0}`); + } + // Link every record in tableB to 3 records in tableC + for (let i = 1; i <= recordCount; i++) { + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[1].id, i, 'hm', 'TableB:hm:TableC', `${i * 3 - 2}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[1].id, i, 'hm', 'TableB:hm:TableC', `${i * 3 - 1}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[1].id, i, 'hm', 'TableB:hm:TableC', `${i * 3 - 0}`); + } + + // TableA TableD TableE + // Link every record in tableA to 5 records in tableD + for (let i = 1; i <= recordCount; i++) { + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 1}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 2}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 3}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 4}`); + } + // Link every record in tableD to 5 records in tableE + for (let i = 1; i <= recordCount; i++) { + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 1}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 2}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 3}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 4}`); + } + + // TableA TableA : self relation + // Link every record in tableA to 3 records in tableA + for (let i = 1; i <= recordCount; i++) { + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableA', `${i * 3 - 2}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableA', `${i * 3 - 1}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableA', `${i * 3 - 0}`); + } + + // TableA TableA : self relation + // Link every record in tableA to 5 records in tableA + for (let i = 1; i <= recordCount; i++) { + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 1}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 2}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 3}`); + await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 4}`); + } + + // refresh page + await page.reload(); + }); + + test('Delete record - single, over UI', async () => { + await dashboard.treeView.openTable({ title: 'Table0' }); + await grid.deleteRow(0); + + // verify row count + await dashboard.grid.verifyTotalRowCount({ count: 99 }); + + // verify row count in all tables + for (let i = 1; i <= 4; i++) { + await dashboard.treeView.openTable({ title: `Table${i}` }); + await dashboard.grid.verifyTotalRowCount({ count: 100 }); + } + + // has-many removal verification + await dashboard.treeView.openTable({ title: 'Table1' }); + await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0', count: 0, value: [] }); + await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0', count: 0, value: [] }); + await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0', count: 0, value: [] }); + + // many-many removal verification + await dashboard.treeView.openTable({ title: 'Table3' }); + await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0 List', count: 0, value: [] }); + await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0 List', count: 1, value: ['2'] }); + await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0 List', count: 2, value: ['2', '3'] }); + await dashboard.grid.cell.verifyVirtualCell({ + index: 3, + columnHeader: 'Table0 List', + count: 3, + value: ['2', '3', '4'], + }); + await dashboard.grid.cell.verifyVirtualCell({ + index: 4, + columnHeader: 'Table0 List', + count: 4, + value: ['2', '3', '4', '5'], + }); + }); + + test('Delete record - bulk, over UI', async () => { + await dashboard.treeView.openTable({ title: 'Table0' }); + await grid.selectRow(0); + await grid.selectRow(1); + await grid.selectRow(2); + await grid.deleteSelectedRows(); + + // verify row count + await dashboard.grid.verifyTotalRowCount({ count: 97 }); + + // verify row count in all tables + for (let i = 1; i <= 4; i++) { + await dashboard.treeView.openTable({ title: `Table${i}` }); + await dashboard.grid.verifyTotalRowCount({ count: 100 }); + } + }); + + test('Delete column', async () => { + // has-many + await dashboard.treeView.openTable({ title: 'Table0' }); + await dashboard.grid.column.delete({ title: 'TableA:hm:TableB' }); + + // verify + await dashboard.treeView.openTable({ title: 'Table1' }); + await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); + await dashboard.grid.column.verify({ title: 'TableB:hm:TableC', isVisible: true }); + + /////////////////////////////////////////////////////////////////////////////////////////////// + + // many-many + await dashboard.treeView.openTable({ title: 'Table0' }); + await dashboard.grid.column.delete({ title: 'TableA:mm:TableD' }); + + // verify + await dashboard.treeView.openTable({ title: 'Table3' }); + await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false }); + await dashboard.grid.column.verify({ title: 'TableD:mm:TableE', isVisible: true }); + + /////////////////////////////////////////////////////////////////////////////////////////////// + + // has-many self relation + await dashboard.treeView.openTable({ title: 'Table0' }); + await dashboard.grid.column.delete({ title: 'TableA:hm:TableA' }); + + // verify + await dashboard.grid.column.verify({ title: 'TableA:hm:TableA', isVisible: false }); + await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); + + /////////////////////////////////////////////////////////////////////////////////////////////// + + // many-many self relation + await dashboard.treeView.openTable({ title: 'Table0' }); + await dashboard.grid.column.delete({ title: 'TableA:mm:TableA' }); + + // verify + await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false }); + await dashboard.grid.column.verify({ title: 'TableA:mm:TableA', isVisible: false }); + }); + + test('Delete table', async () => { + await dashboard.treeView.deleteTable({ title: 'Table0' }); + await dashboard.treeView.verifyTable({ title: 'Table0', exists: false }); + + // verify + await dashboard.treeView.openTable({ title: 'Table1' }); + await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); + + await dashboard.treeView.openTable({ title: 'Table3' }); + await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false }); + }); +});