Browse Source

Merge pull request #5873 from nocodb/revert-5816-feat/ltar-improvements

Revert "refactor: MetaDB LTAR revamp"
pull/5877/head
Pranav C 1 year ago committed by GitHub
parent
commit
0ea88d5308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 95
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  2. 5
      packages/nc-gui/composables/useColumnCreateStore.ts
  3. 6
      packages/nc-gui/composables/useTable.ts
  4. 240
      packages/nocodb/src/db/BaseModelSqlv2.ts
  5. 14
      packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts
  6. 23
      packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts
  7. 12
      packages/nocodb/src/db/sql-mgr/v2/SqlMgrv2.ts
  8. 1
      packages/nocodb/src/meta/meta.service.ts
  9. 1
      packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts
  10. 16
      packages/nocodb/src/models/Column.ts
  11. 6
      packages/nocodb/src/models/Model.ts
  12. 7
      packages/nocodb/src/models/Project.ts
  13. 10
      packages/nocodb/src/models/View.ts
  14. 2
      packages/nocodb/src/modules/global/init-meta-service.provider.ts
  15. 215
      packages/nocodb/src/services/columns.service.ts
  16. 12
      packages/nocodb/src/services/datas.service.ts
  17. 105
      packages/nocodb/src/services/tables.service.ts
  18. 2
      packages/nocodb/src/version-upgrader/NcUpgrader.ts
  19. 384
      packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts
  20. 164
      packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts
  21. 15
      packages/nocodb/tests/unit/rest/tests/tableRow.test.ts
  22. 148
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts
  23. 14
      tests/playwright/pages/Dashboard/Grid/index.ts
  24. 40
      tests/playwright/setup/xcdbProject.ts
  25. 324
      tests/playwright/tests/db/metaLTAR.spec.ts

95
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
</a-select>
</a-form-item>
</div>
<template v-if="!isXcdbBase">
<div
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
<div
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
v-model:value="vModel.onUpdate"
:disabled="vModel.virtual"
name="onUpdate"
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select
v-model:value="vModel.onDelete"
:disabled="vModel.virtual"
name="onDelete"
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
v-model:value="vModel.onUpdate"
:disabled="vModel.virtual"
name="onUpdate"
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select
v-model:value="vModel.onDelete"
:disabled="vModel.virtual"
name="onDelete"
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, i) of onUpdateDeleteOptions" :key="i" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="vModel.virtual" :disabled="appInfo.isCloud" name="virtual"
@change="onDataTypeChange"
>Virtual Relation
</a-checkbox
>
</a-form-item>
</div>
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="vModel.virtual" :disabled="appInfo.isCloud" name="virtual" @change="onDataTypeChange"
>Virtual Relation</a-checkbox
>
</a-form-item>
</div>
</template>
</div>
</div>
</template>

5
packages/nc-gui/composables/useColumnCreateStore.ts

@ -31,7 +31,7 @@ interface ValidationsObj {
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
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<ValidationsObj>({})
@ -291,7 +289,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isMssql,
isPg,
isMysql,
isXcdbBase
}
},
)

6
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(

240
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<any>)[] = [];
for (const column of this.model.columns) {
if (column.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOptions =
await column.getColOptions<LinkToAnotherRecordColumn>();
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<any>)[] =
[];
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<LinkToAnotherRecordColumn>();
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<any>)[] = [];
for (const column of this.model.columns) {
if (column.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOptions =
await column.getColOptions<LinkToAnotherRecordColumn>();
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;
}
}

14
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;');
}

23
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<SqlMgrv2Trans> {
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;
}
}

12
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);
}
}

1
packages/nocodb/src/meta/meta.service.ts

@ -532,6 +532,7 @@ export class MetaService {
} else {
query.where(idOrCondition);
}
return query.first();
}

1
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;
}

16
packages/nocodb/src/models/Column.ts

@ -65,13 +65,10 @@ export default class Column<T = any> implements ColumnType {
Object.assign(this, data);
}
public async getModel(ncMeta = Noco.ncMeta): Promise<Model> {
return Model.getByIdOrName(
{
id: this.fk_model_id,
},
ncMeta,
);
public async getModel(): Promise<Model> {
return Model.getByIdOrName({
id: this.fk_model_id,
});
}
public static async insert<T>(
@ -601,11 +598,6 @@ export default class Column<T = any> 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

6
packages/nocodb/src/models/Model.ts

@ -368,10 +368,10 @@ export default class Model implements TableType {
}
async delete(ncMeta = Noco.ncMeta, force = false): Promise<boolean> {
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)) {

7
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);
}

10
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) {

2
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();

215
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<LinkToAnotherRecordColumn>(ncMeta);
const childColumn = await relationColOpt.getChildColumn(ncMeta);
const childTable = await childColumn.getModel(ncMeta);
await column.getColOptions<LinkToAnotherRecordColumn>();
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<LinkToAnotherRecordColumn>(ncMeta);
await c.getColOptions<LinkToAnotherRecordColumn>();
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<LinkToAnotherRecordColumn>(ncMeta);
await c.getColOptions<LinkToAnotherRecordColumn>();
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<LinkToAnotherRecordColumn>(ncMeta);
await c.getColOptions<LinkToAnotherRecordColumn>();
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<LinkToAnotherRecordColumn>(ncMeta);
await c.getColOptions<LinkToAnotherRecordColumn>();
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<LinkToAnotherRecordColumn>(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<LinkToAnotherRecordColumn>(ncMeta);
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>();
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

12
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);
}

105
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 }) {

2
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;

384
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<string, boolean> = {};
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);
}

164
packages/nocodb/src/version-upgrader/ncXcdbLTARUpgrader.ts

@ -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<infer U>
? 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<LinkToAnotherRecordColumn>(
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),
});
}
}

15
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');
}
});

148
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);

14
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;

40
tests/playwright/setup/xcdbProject.ts

@ -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 };

324
tests/playwright/tests/db/metaLTAR.spec.ts

@ -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 <hm> TableB <hm> TableC
* TableA <mm> TableD <mm> TableE
* TableA <hm> TableA : self relation
* TableA <mm> 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<any>;
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 <hm> TableB <hm> 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 <mm> TableD <mm> 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 <hm> 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 <mm> 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 <hm> TableB <hm> 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 <mm> TableD <mm> 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 <hm> 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 <mm> 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 });
});
});
Loading…
Cancel
Save