From d93eaef27bce08d9914a48bd83345962867486b7 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 12 Jun 2023 17:37:04 +0530 Subject: [PATCH] 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/tableRow.test.ts | 15 +- .../tests/unit/rest/tests/viewRow.test.ts | 148 +++---- .../playwright/pages/Dashboard/Grid/index.ts | 14 +- tests/playwright/setup/xcdbProject.ts | 40 -- tests/playwright/tests/db/metaLTAR.spec.ts | 324 --------------- 25 files changed, 645 insertions(+), 1216 deletions(-) delete mode 100644 packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts delete mode 100644 tests/playwright/setup/xcdbProject.ts delete 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 f285026379..b3d175f567 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, isXcdbBase } = useColumnCreateStoreOrThrow() +const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi } = useColumnCreateStoreOrThrow() const { tables } = $(storeToRefs(useProject())) @@ -86,57 +86,54 @@ 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 a6b99847be..978f71fdd4 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, isXcdbBase: isXcdbBaseFunc } = projectStore + const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc } = projectStore const { project, sqlUis } = storeToRefs(projectStore) const { $api } = useNuxtApp() @@ -52,8 +52,6 @@ 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({}) @@ -291,7 +289,6 @@ 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 4596c2ff2c..cb4bccea52 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, isXcdbBase } = useProject() + const { loadTables } = useProject() const { closeTab } = useTabs() const projectStore = useProject() @@ -88,9 +88,7 @@ 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)) - // Check if table has any relation columns and show notification - // skip for xcdb base - if (relationColumns?.length && !isXcdbBase(table.base_id)) { + if (relationColumns?.length) { 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 b15aa4b355..1d5c356a2f 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -17,19 +17,9 @@ 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, - Base, - Column, - Filter, - Model, - Project, - Sort, - View, -} from '../models'; +import { Audit, Column, Filter, Model, Project, Sort, View } from '../models'; import { sanitize, unsanitize } from '../helpers/sqlSanitize'; import { COMPARISON_OPS, @@ -57,8 +47,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); @@ -1885,80 +1875,18 @@ class BaseModelSqlv2 { } } - async delByPk(id, _trx?, cookie?) { - let trx: Transaction = _trx; + async delByPk(id, trx?, cookie?) { try { // retrieve data for handling params in hook const data = await this.readByPk(id); await this.beforeDelete(id, trx, cookie); - - 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(); - + const response = await this.dbDriver(this.tnPath) + .del() + .where(await this._wherePk(id)); 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; } @@ -2451,71 +2379,8 @@ 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); } @@ -2536,7 +2401,6 @@ class BaseModelSqlv2 { args: { where?: string; filterArr?: Filter[] } = {}, { cookie }: { cookie?: any } = {}, ) { - let trx: Transaction; try { await this.model.getColumns(); const { where } = this._getListArgs(args); @@ -2561,102 +2425,14 @@ class BaseModelSqlv2 { this.dbDriver, ); - 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; - } + qb.del(); - 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(); + const count = (await qb) as any; 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 00ecca611d..118423df7b 100644 --- a/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts +++ b/packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts @@ -22,8 +22,7 @@ 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 = - connectionConfig?.knex || knex(connectionConfig.connection); + this.sqlClient = knex(connectionConfig.connection); this.queries = queries; this._version = {}; } @@ -1558,13 +1557,7 @@ class SqliteClient extends KnexClient { upQuery, ); - 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 foreign_keys = OFF;'); await this.sqlClient.raw('PRAGMA legacy_alter_table = ON;'); const trx = await this.sqlClient.transaction(); @@ -1601,8 +1594,7 @@ class SqliteClient extends KnexClient { log.ppe(e, _func); throw e; } finally { - if (fkCheckEnabled) - await this.sqlClient.raw('PRAGMA foreign_keys = ON;'); + 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 730cbdda59..f8f884ea5f 100644 --- a/packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts +++ b/packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts @@ -1,21 +1,18 @@ -import { MetaService } from '../../../meta/meta.service' -import SqlMgrv2 from './SqlMgrv2' -import SqlMgrv2Trans from './SqlMgrv2Trans' +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( @@ -24,8 +21,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 437a79921f..17d808e15e 100644 --- a/packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts +++ b/packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts @@ -5,14 +5,12 @@ 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; @@ -22,18 +20,18 @@ export default class SqlMgrv2 { * @param {String} args.toolDbPath - path to sqlite file that sql mgr will use * @memberof SqlMgr */ - constructor(args: { id: string }, ncMeta = null) { + constructor(args: { id: string }) { const func = 'constructor'; log.api(`${func}:args:`, args); // this.metaDb = args.metaDb; this._migrator = new KnexMigratorv2(args); - this.ncMeta = ncMeta; + + return this; } public async migrator(_base: Base) { return this._migrator; } - public static async testConnection(args = {}) { const client = await SqlClientFactory.create(args); return client.testConnection(); @@ -121,10 +119,6 @@ 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 565b921b42..5d30444980 100644 --- a/packages/nocodb/src/meta/meta.service.ts +++ b/packages/nocodb/src/meta/meta.service.ts @@ -532,6 +532,7 @@ 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 d26858f077..d797e146c7 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,7 +37,6 @@ 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 78ae4edc9f..22ed522e37 100644 --- a/packages/nocodb/src/models/Column.ts +++ b/packages/nocodb/src/models/Column.ts @@ -65,13 +65,10 @@ export default class Column implements ColumnType { Object.assign(this, data); } - public async getModel(ncMeta = Noco.ncMeta): Promise { - return Model.getByIdOrName( - { - id: this.fk_model_id, - }, - ncMeta, - ); + public async getModel(): Promise { + return Model.getByIdOrName({ + id: this.fk_model_id, + }); } public static async insert( @@ -601,11 +598,6 @@ 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 8750ef7c9f..c29868b4db 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, ncMeta); + await Audit.deleteRowComments(this.id); - for (const view of await this.getViews(true, ncMeta)) { - await view.delete(ncMeta); + for (const view of await this.getViews(true)) { + await view.delete(); } 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 e08d5e036a..b062222f69 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,11 +309,6 @@ 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 187a0ee52d..8d03d28564 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, ncMeta); - await Sort.deleteAll(viewId, ncMeta); - await Filter.deleteAll(viewId, ncMeta); + const view = await this.get(viewId); + await Sort.deleteAll(viewId); + await Filter.deleteAll(viewId); 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(ncMeta = Noco.ncMeta){ - await View.delete(this.id, ncMeta); + async delete() { + await View.delete(this.id); } 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 eab0293639..a86646b302 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 = '0108002'; + process.env.NC_VERSION = '0107004'; // init cache await NocoCache.init(); diff --git a/packages/nocodb/src/services/columns.service.ts b/packages/nocodb/src/services/columns.service.ts index f1c8a81620..aa4fd91da7 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,8 +57,6 @@ export enum Altered { @Injectable() export class ColumnsService { - constructor(private readonly metaService: MetaService) {} - async columnUpdate(param: { req?: any; columnId: string; @@ -1144,23 +1142,21 @@ export class ColumnsService { return table; } - 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); + 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 + // ); - const sqlMgr = await ProjectMgrv2.getSqlMgr( - { id: base.project_id }, - ncMeta, - ); + const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id }); switch (column.uidt) { case UITypes.Lookup: @@ -1168,17 +1164,17 @@ export class ColumnsService { case UITypes.QrCode: case UITypes.Barcode: case UITypes.Formula: - await Column.delete(param.columnId, ncMeta); + await Column.delete(param.columnId); break; case UITypes.LinkToAnotherRecord: { const relationColOpt = - await column.getColOptions(ncMeta); - const childColumn = await relationColOpt.getChildColumn(ncMeta); - const childTable = await childColumn.getModel(ncMeta); + await column.getColOptions(); + const childColumn = await relationColOpt.getChildColumn(); + const childTable = await childColumn.getModel(); - const parentColumn = await relationColOpt.getParentColumn(ncMeta); - const parentTable = await parentColumn.getModel(ncMeta); + const parentColumn = await relationColOpt.getParentColumn(); + const parentTable = await parentColumn.getModel(); switch (relationColOpt.type) { case 'bt': @@ -1192,19 +1188,15 @@ export class ColumnsService { parentColumn, parentTable, sqlMgr, - ncMeta, + // ncMeta }); } break; case 'mm': { - const mmTable = await relationColOpt.getMMModel(ncMeta); - const mmParentCol = await relationColOpt.getMMParentColumn( - ncMeta, - ); - const mmChildCol = await relationColOpt.getMMChildColumn( - ncMeta, - ); + const mmTable = await relationColOpt.getMMModel(); + const mmParentCol = await relationColOpt.getMMParentColumn(); + const mmChildCol = await relationColOpt.getMMChildColumn(); await this.deleteHmOrBtRelation( { @@ -1215,7 +1207,7 @@ export class ColumnsService { parentTable: parentTable, childColumn: mmParentCol, base, - ncMeta, + // ncMeta }, true, ); @@ -1229,18 +1221,18 @@ export class ColumnsService { parentTable: childTable, childColumn: mmChildCol, base, - ncMeta, + // ncMeta }, true, ); const columnsInRelatedTable: Column[] = await relationColOpt - .getRelatedTable(ncMeta) - .then((m) => m.getColumns(ncMeta)); + .getRelatedTable() + .then((m) => m.getColumns()); for (const c of columnsInRelatedTable) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(ncMeta); + await c.getColOptions(); if ( colOpt.type === 'mm' && colOpt.fk_parent_column_id === childColumn.id && @@ -1249,55 +1241,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, ncMeta); + await Column.delete(c.id); break; } } - await Column.delete(relationColOpt.fk_column_id, ncMeta); + await Column.delete(relationColOpt.fk_column_id); // delete bt columns in m2m table - await mmTable.getColumns(ncMeta); + await mmTable.getColumns(); for (const c of mmTable.columns) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(ncMeta); + await c.getColOptions(); if (colOpt.type === 'bt') { - await Column.delete(c.id, ncMeta); + await Column.delete(c.id); } } // delete hm columns in parent table - await parentTable.getColumns(ncMeta); + await parentTable.getColumns(); for (const c of parentTable.columns) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(ncMeta); + await c.getColOptions(); if (colOpt.fk_related_model_id === mmTable.id) { - await Column.delete(c.id, ncMeta); + await Column.delete(c.id); } } // delete hm columns in child table - await childTable.getColumns(ncMeta); + await childTable.getColumns(); for (const c of childTable.columns) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; const colOpt = - await c.getColOptions(ncMeta); + await c.getColOptions(); if (colOpt.fk_related_model_id === mmTable.id) { - await Column.delete(c.id, ncMeta); + await Column.delete(c.id); } } // retrieve columns in m2m table again - await mmTable.getColumns(ncMeta); + await mmTable.getColumns(); // 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(ncMeta); + await mmTable.delete(); } } break; @@ -1345,30 +1337,26 @@ export class ColumnsService { await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody); - await Column.delete(param.columnId, ncMeta); + await Column.delete(param.columnId); } } - 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); + 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(); const displayValueColumn = mapDefaultDisplayValue(table.columns); if (displayValueColumn) { await Model.updatePrimaryColumn( displayValueColumn.fk_model_id, displayValueColumn.id, - ncMeta, ); } @@ -1406,47 +1394,45 @@ export class ColumnsService { if (!relationColOpt) { foreignKeyName = ( ( - 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 }; - } - } - } + 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 + ); + }); }) - )?.colOptions as LinkToAnotherRecordType + ).colOptions as LinkToAnotherRecordType ).fk_index_name; } else { foreignKeyName = relationColOpt.fk_index_name; } - 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); - } + // 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(ncMeta) - .then((m) => m.getColumns(ncMeta)); + .getRelatedTable() + .then((m) => m.getColumns()); const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt'; for (const c of columnsInRelatedTable) { if (c.uidt !== UITypes.LinkToAnotherRecord) continue; - const colOpt = await c.getColOptions(ncMeta); + const colOpt = await c.getColOptions(); if ( colOpt.fk_parent_column_id === parentColumn.id && colOpt.fk_child_column_id === childColumn.id && @@ -1461,34 +1447,9 @@ export class ColumnsService { await Column.delete(relationColOpt.fk_column_id, ncMeta); if (!ignoreFkDelete) { - 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 cTable = await Model.getWithInfo({ + id: childTable.id, + }); const tableUpdateBody = { ...cTable, tn: cTable.table_name, @@ -1538,12 +1499,6 @@ 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' @@ -1615,7 +1570,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 @@ -1774,7 +1729,6 @@ 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( @@ -1794,7 +1748,6 @@ 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 cf82a9d585..c41609f6c0 100644 --- a/packages/nocodb/src/services/datas.service.ts +++ b/packages/nocodb/src/services/datas.service.ts @@ -103,15 +103,11 @@ export class DatasService { dbDriver: await NcConnectionMgrv2.get(base), }); - // 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) { - return { message }; - } + // todo: Should have error http status code + const message = await baseModel.hasLTARData(param.rowId, model); + if (message.length) { + return { 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 ae89adb87b..7d86ce4d5d 100644 --- a/packages/nocodb/src/services/tables.service.ts +++ b/packages/nocodb/src/services/tables.service.ts @@ -14,19 +14,9 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT'; import getColumnUiType from '../helpers/getColumnUiType'; import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue'; -import { - Audit, - Base, - Column, - Model, - ModelRoleVisibility, - Project, -} from '../models'; -import Noco from '../Noco'; +import { Audit, Column, Model, ModelRoleVisibility, Project } from '../models'; 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, @@ -36,8 +26,6 @@ import type { @Injectable() export class TablesService { - constructor(private readonly columnsService: ColumnsService) {} - async tableUpdate(param: { tableId: any; table: TableReqType & { project_id?: string }; @@ -154,14 +142,11 @@ 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 && !base.is_meta) { + if (relationColumns?.length) { const referredTables = await Promise.all( relationColumns.map(async (c) => c @@ -177,69 +162,37 @@ export class TablesService { ); } - // 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, - ); - } + 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; + }); - 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; + 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, }); + } - 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, - }); - } + 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(() => {}); - 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; + T.emit('evt', { evt_type: 'table:deleted' }); + + return table.delete(); } 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 a0969366a1..951a93099f 100644 --- a/packages/nocodb/src/version-upgrader/NcUpgrader.ts +++ b/packages/nocodb/src/version-upgrader/NcUpgrader.ts @@ -14,7 +14,6 @@ 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'; @@ -51,7 +50,6 @@ 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 f3e69083f8..1aecb3eb32 100644 --- a/packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts +++ b/packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts @@ -1,43 +1,359 @@ -import { OrgUserRoles } from 'nocodb-sdk'; -import { NC_APP_SETTINGS } from '../constants'; -import Store from '../models/Store'; +import { UITypes } from 'nocodb-sdk'; 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'; -/** 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, +// 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, + ), ); + } 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; + } } - // 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, + 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, + ), ); } + 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 deleted file mode 100644 index 19d2f94a94..0000000000 --- a/packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts +++ /dev/null @@ -1,164 +0,0 @@ -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/tableRow.test.ts b/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts index aec23a2a0a..488bb50493 100644 --- a/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts @@ -1422,15 +1422,22 @@ function tableTest() { rowId: row['Id'], }); - await request(context.app) + const response = await request(context.app) .delete(`/api/v1/db/data/noco/${project.id}/${table.id}/${row['Id']}`) .set('xc-auth', context.token) .expect(200); const deleteRow = await getRow(context, { project, table, id: row['Id'] }); - if (deleteRow && Object.keys(deleteRow).length > 0) { - console.log(deleteRow); - throw new Error('Wrong delete'); + if (!deleteRow) { + throw new Error('Should not delete'); + } + + if ( + !(response.body.message[0] as string).includes( + 'is a LinkToAnotherRecord of', + ) + ) { + throw new Error('Should give ltar foreign key error'); } }); diff --git a/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts b/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts index 481eaafa33..1cf5501855 100644 --- a/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts @@ -1,11 +1,12 @@ import 'mocha'; -import { isString } from 'util'; -import request from 'supertest'; -import { UITypes, ViewTypes } from 'nocodb-sdk'; -import { expect } from 'chai'; import init from '../../init'; import { createProject, createSakilaProject } from '../../factory/project'; +import request from 'supertest'; +import Project from '../../../../src/models/Project'; +import Model from '../../../../src/models/Model'; import { createTable, getTable } from '../../factory/table'; +import View from '../../../../src/models/View'; +import { ColumnType, UITypes, ViewTypes } from 'nocodb-sdk'; import { createView } from '../../factory/view'; import { createColumn, @@ -20,11 +21,9 @@ import { getOneRow, getRow, } from '../../factory/row'; +import { expect } from 'chai'; import { isPg } from '../../init/db'; -import type { ColumnType } from 'nocodb-sdk'; -import type View from '../../../../src/models/View'; -import type Model from '../../../../src/models/Model'; -import type Project from '../../../../src/models/Project'; +import { isString } from 'util'; // Test case list // 1. Get view row list g @@ -161,7 +160,7 @@ function viewRowTests() { const testGetViewRowList = async (view: View) => { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .expect(200); @@ -180,7 +179,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}` ) .set('xc-auth', context.token) .expect(200); @@ -226,7 +225,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -245,7 +244,7 @@ function viewRowTests() { requiredColumns.map((c: ColumnType) => ({ title: c.title, uidt: c.uidt, - })), + })) ); throw new Error('Wrong columns'); } @@ -272,7 +271,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}` ) .set('xc-auth', context.token) .query({ @@ -288,7 +287,7 @@ function viewRowTests() { expect( Object.keys(response.body.find((e) => e.key === 'NC-17').value.list[0]) .sort() - .join(','), + .join(',') ).to.equal('FilmId,Title'); }; @@ -298,14 +297,14 @@ function viewRowTests() { const testDescSortedViewDataList = async (view: View) => { const firstNameColumn = customerColumns.find( - (col) => col.title === 'FirstName', + (col) => col.title === 'FirstName' ); const visibleColumns = [firstNameColumn]; const sortInfo = [{ fk_column_id: firstNameColumn.id, direction: 'desc' }]; const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -333,7 +332,7 @@ function viewRowTests() { Math.trunc(pageInfo.totalRows / pageInfo.pageSize) * pageInfo.pageSize; const lastPageResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -376,7 +375,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}` ) .set('xc-auth', context.token) .query({ @@ -391,7 +390,7 @@ function viewRowTests() { expect(response.body).to.be.have.length(6); expect( - response.body.find((e) => e.key === 'PG').value.list[0].Title, + response.body.find((e) => e.key === 'PG').value.list[0].Title ).to.equal('WORST BANGER'); }; @@ -401,14 +400,14 @@ function viewRowTests() { const testAscSortedViewDataList = async (view: View) => { const firstNameColumn = customerColumns.find( - (col) => col.title === 'FirstName', + (col) => col.title === 'FirstName' ); const visibleColumns = [firstNameColumn]; const sortInfo = [{ fk_column_id: firstNameColumn.id, direction: 'asc' }]; const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -436,7 +435,7 @@ function viewRowTests() { Math.trunc(pageInfo.totalRows / pageInfo.pageSize) * pageInfo.pageSize; const lastPageResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -479,7 +478,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/views/${view.id}/group/${ratingColumn.id}` ) .set('xc-auth', context.token) .query({ @@ -494,7 +493,7 @@ function viewRowTests() { expect(response.body).to.be.have.length(6); expect( - response.body.find((e) => e.key === 'PG').value.list[0].Title, + response.body.find((e) => e.key === 'PG').value.list[0].Title ).to.equal('ACADEMY DINOSAUR'); }; @@ -503,7 +502,7 @@ function viewRowTests() { }); const testGetViewDataListWithRequiredColumnsAndFilter = async ( - viewType: ViewTypes, + viewType: ViewTypes ) => { const rentalTable = await getTable({ project: sakilaProject, @@ -524,7 +523,7 @@ function viewRowTests() { }); const paymentListColumn = (await rentalTable.getColumns()).find( - (c) => c.title === 'Payment List', + (c) => c.title === 'Payment List' ); const nestedFilter = { @@ -550,7 +549,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -561,7 +560,7 @@ function viewRowTests() { const ascResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -580,7 +579,7 @@ function viewRowTests() { const descResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -607,7 +606,7 @@ function viewRowTests() { }); const testGetNestedSortedFilteredTableDataListWithLookupColumn = async ( - viewType: ViewTypes, + viewType: ViewTypes ) => { const view = await createView(context, { title: 'View', @@ -625,11 +624,11 @@ function viewRowTests() { }); const paymentListColumn = (await customerTable.getColumns()).find( - (c) => c.title === 'Payment List', + (c) => c.title === 'Payment List' ); const activeColumn = (await customerTable.getColumns()).find( - (c) => c.title === 'Active', + (c) => c.title === 'Active' ); const nestedFields = { @@ -682,7 +681,7 @@ function viewRowTests() { const ascResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .query({ @@ -704,7 +703,7 @@ function viewRowTests() { } const nestedRentalResponse = Object.keys( - ascResponse.body.list[0]['Rental List'][0], + ascResponse.body.list[0]['Rental List'][0] ); if ( @@ -720,7 +719,7 @@ function viewRowTests() { it('Get nested sorted filtered table with nested fields data list with a rollup column in customer table view grid', async () => { await testGetNestedSortedFilteredTableDataListWithLookupColumn( - ViewTypes.GRID, + ViewTypes.GRID ); }); @@ -775,7 +774,7 @@ function viewRowTests() { await request(context.app) .post( - `/api/v1/db/data/noco/${project.id}/${table.id}/views/${nonRelatedView.id}`, + `/api/v1/db/data/noco/${project.id}/${table.id}/views/${nonRelatedView.id}` ) .set('xc-auth', context.token) .send({ @@ -803,7 +802,7 @@ function viewRowTests() { // todo: Test that all the columns needed to be shown in the view are returned const testFindOneSortedDataWithRequiredColumns = async ( - viewType: ViewTypes, + viewType: ViewTypes ) => { const view = await createView(context, { title: 'View', @@ -811,13 +810,13 @@ function viewRowTests() { type: viewType, }); const firstNameColumn = customerColumns.find( - (col) => col.title === 'FirstName', + (col) => col.title === 'FirstName' ); const visibleColumns = [firstNameColumn]; let response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/find-one`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/find-one` ) .set('xc-auth', context.token) .query({ @@ -838,7 +837,7 @@ function viewRowTests() { response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/find-one`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/find-one` ) .set('xc-auth', context.token) .query({ @@ -871,7 +870,7 @@ function viewRowTests() { }); const testFindOneSortedFilteredNestedFieldsDataWithRollup = async ( - viewType: ViewTypes, + viewType: ViewTypes ) => { const rollupColumn = await createRollupColumn(context, { project: sakilaProject, @@ -894,11 +893,11 @@ function viewRowTests() { }); const paymentListColumn = (await customerTable.getColumns()).find( - (c) => c.title === 'Payment List', + (c) => c.title === 'Payment List' ); const activeColumn = (await customerTable.getColumns()).find( - (c) => c.title === 'Active', + (c) => c.title === 'Active' ); const nestedFields = { @@ -951,7 +950,7 @@ function viewRowTests() { const ascResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/find-one`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/find-one` ) .set('xc-auth', context.token) .query({ @@ -996,7 +995,7 @@ function viewRowTests() { type: viewType, }); const firstNameColumn = customerColumns.find( - (col) => col.title === 'FirstName', + (col) => col.title === 'FirstName' ); const rollupColumn = await createRollupColumn(context, { @@ -1013,7 +1012,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/groupby`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/groupby` ) .set('xc-auth', context.token) .query({ @@ -1050,7 +1049,7 @@ function viewRowTests() { }); const firstNameColumn = customerColumns.find( - (col) => col.title === 'FirstName', + (col) => col.title === 'FirstName' ); const rollupColumn = await createRollupColumn(context, { @@ -1067,7 +1066,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/groupby`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/groupby` ) .set('xc-auth', context.token) .query({ @@ -1106,7 +1105,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/count`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/count` ) .set('xc-auth', context.token) .expect(200); @@ -1137,7 +1136,7 @@ function viewRowTests() { const listResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}` ) .set('xc-auth', context.token) .expect(200); @@ -1146,7 +1145,7 @@ function viewRowTests() { const readResponse = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/${row['CustomerId']}`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/${row['CustomerId']}` ) .set('xc-auth', context.token) .expect(200); @@ -1182,7 +1181,7 @@ function viewRowTests() { const updateResponse = await request(context.app) .patch( - `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`, + `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}` ) .set('xc-auth', context.token) .send({ @@ -1208,7 +1207,7 @@ function viewRowTests() { }); const testUpdateViewRowWithValidationAndInvalidData = async ( - viewType: ViewTypes, + viewType: ViewTypes ) => { const table = await createTable(context, project); const emailColumn = await createColumn(context, table, { @@ -1229,7 +1228,7 @@ function viewRowTests() { await request(context.app) .patch( - `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`, + `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}` ) .set('xc-auth', context.token) .send({ @@ -1254,7 +1253,7 @@ function viewRowTests() { // todo: Test with form view const testUpdateViewRowWithValidationAndValidData = async ( - viewType: ViewTypes, + viewType: ViewTypes ) => { const table = await createTable(context, project); const emailColumn = await createColumn(context, table, { @@ -1274,7 +1273,7 @@ function viewRowTests() { const response = await request(context.app) .patch( - `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`, + `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}` ) .set('xc-auth', context.token) .send({ @@ -1315,7 +1314,7 @@ function viewRowTests() { await request(context.app) .delete( - `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`, + `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}` ) .set('xc-auth', context.token) .expect(200); @@ -1339,8 +1338,8 @@ function viewRowTests() { await testDeleteViewRow(ViewTypes.FORM); }); - const testDeleteViewRowWithForeignKeyConstraint = async ( - viewType: ViewTypes, + const testDeleteViewRowWithForiegnKeyConstraint = async ( + viewType: ViewTypes ) => { const table = await createTable(context, project); const relatedTable = await createTable(context, project, { @@ -1370,30 +1369,37 @@ function viewRowTests() { rowId: row['Id'], }); - await request(context.app) + const response = await request(context.app) .delete( - `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`, + `/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}` ) .set('xc-auth', context.token) .expect(200); const deleteRow = await getRow(context, { project, table, id: row['Id'] }); - if (deleteRow && Object.keys(deleteRow).length > 0) { - console.log(deleteRow); - throw new Error('Wrong delete'); + if (!deleteRow) { + throw new Error('Should not delete'); + } + + if ( + !(response.body.message[0] as string).includes( + 'is a LinkToAnotherRecord of' + ) + ) { + throw new Error('Should give ltar foreign key error'); } }; it('Delete view row with ltar foreign key constraint GALLERY', async function () { - await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.GALLERY); + await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.GALLERY); }); it('Delete view row with ltar foreign key constraint GRID', async function () { - await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.GRID); + await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.GRID); }); it('Delete view row with ltar foreign key constraint FORM', async function () { - await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.FORM); + await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.FORM); }); const testViewRowExists = async (viewType: ViewTypes) => { @@ -1409,7 +1415,7 @@ function viewRowTests() { const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/${row['CustomerId']}/exist`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/${row['CustomerId']}/exist` ) .set('xc-auth', context.token) .expect(200); @@ -1439,7 +1445,7 @@ function viewRowTests() { }); const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/999999/exist`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}/views/${view.id}/999999/exist` ) .set('xc-auth', context.token) .expect(200); @@ -1469,7 +1475,7 @@ function viewRowTests() { }); const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.title}/views/${view.id}/export/csv`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.title}/views/${view.id}/export/csv` ) .set('xc-auth', context.token) .expect(200); @@ -1493,7 +1499,7 @@ function viewRowTests() { }); const response = await request(context.app) .get( - `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.title}/views/${view.id}/export/excel`, + `/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.title}/views/${view.id}/export/excel` ) .set('xc-auth', context.token) .expect(200); diff --git a/tests/playwright/pages/Dashboard/Grid/index.ts b/tests/playwright/pages/Dashboard/Grid/index.ts index 585e22fa47..dedbdd956e 100644 --- a/tests/playwright/pages/Dashboard/Grid/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/index.ts @@ -184,12 +184,6 @@ 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(); @@ -206,7 +200,8 @@ export class GridPage extends BasePage { await this.rootPage.waitForTimeout(300); } - async deleteSelectedRows() { + async deleteAll() { + await this.selectAll(); await this.get().locator('[data-testid="nc-check-all"]').nth(0).click({ button: 'right', }); @@ -214,11 +209,6 @@ 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 deleted file mode 100644 index 7123e80b2c..0000000000 --- a/tests/playwright/setup/xcdbProject.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 7205c37c33..0000000000 --- a/tests/playwright/tests/db/metaLTAR.spec.ts +++ /dev/null @@ -1,324 +0,0 @@ -/* - * - * 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 }); - }); -});