Browse Source

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

refactor: MetaDB LTAR revamp
pull/5161/merge
Raju Udava 2 years ago committed by GitHub
parent
commit
474ba0b427
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. 14
      packages/nocodb/tests/unit/rest/tests/tableRow.test.ts
  22. 24
      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
  26. 25
      tests/playwright/tests/db/timezone.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 } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos, onDataTypeChange, sqlUi, isXcdbBase } = useColumnCreateStoreOrThrow()
const { tables } = $(storeToRefs(useProject()))
@ -86,54 +86,57 @@ const filterOption = (value: string, option: { key: string }) => option.key.toLo
</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') }}
<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>
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</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 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>
</div>
</template>
</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 } = projectStore
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc } = projectStore
const { project, sqlUis } = storeToRefs(projectStore)
const { $api } = useNuxtApp()
@ -52,6 +52,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const isMssql = computed(() => isMssqlFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const isXcdbBase = computed(() => isXcdbBaseFunc(meta.value?.base_id ? meta.value?.base_id : Object.keys(sqlUis.value)[0]))
const idType = null
const additionalValidations = ref<ValidationsObj>({})
@ -289,6 +291,7 @@ 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 } = useProject()
const { loadTables, isXcdbBase } = useProject()
const { closeTab } = useTabs()
const projectStore = useProject()
@ -88,7 +88,9 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?
const meta = (await getMeta(table.id as string, true)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c))
if (relationColumns?.length) {
// Check if table has any relation columns and show notification
// skip for xcdb base
if (relationColumns?.length && !isXcdbBase(table.base_id)) {
const refColMsgs = await Promise.all(
relationColumns.map(async (c, i) => {
const refMeta = (await getMeta(

240
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -17,9 +17,19 @@ import Validator from 'validator';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import { Knex } from 'knex';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import { Audit, Column, Filter, Model, Project, Sort, View } from '../models';
import {
Audit,
Base,
Column,
Filter,
Model,
Project,
Sort,
View,
} from '../models';
import { sanitize, unsanitize } from '../helpers/sqlSanitize';
import {
COMPARISON_OPS,
@ -47,8 +57,8 @@ import type {
RollupColumn,
SelectOption,
} from '../models';
import type { Knex } from 'knex';
import type { SortType } from 'nocodb-sdk';
import Transaction = Knex.Transaction;
dayjs.extend(utc);
dayjs.extend(timezone);
@ -1875,18 +1885,80 @@ class BaseModelSqlv2 {
}
}
async delByPk(id, trx?, cookie?) {
async delByPk(id, _trx?, cookie?) {
let trx: Transaction = _trx;
try {
// retrieve data for handling params in hook
const data = await this.readByPk(id);
await this.beforeDelete(id, trx, cookie);
const response = await this.dbDriver(this.tnPath)
.del()
.where(await this._wherePk(id));
const execQueries: ((trx: Transaction) => Promise<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();
await this.afterDelete(data, trx, cookie);
return response;
} catch (e) {
console.log(e);
if (!_trx) await trx.rollback();
await this.errorDelete(e, id, trx, cookie);
throw e;
}
@ -2379,8 +2451,71 @@ class BaseModelSqlv2 {
res.push(d);
}
const execQueries: ((trx: Transaction, ids: any[]) => Promise<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);
}
@ -2401,6 +2536,7 @@ class BaseModelSqlv2 {
args: { where?: string; filterArr?: Filter[] } = {},
{ cookie }: { cookie?: any } = {},
) {
let trx: Transaction;
try {
await this.model.getColumns();
const { where } = this._getListArgs(args);
@ -2425,14 +2561,102 @@ class BaseModelSqlv2 {
this.dbDriver,
);
qb.del();
const execQueries: ((trx: Transaction, qb: any) => Promise<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;
}
const count = (await qb) as any;
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const parentTable = await parentColumn.getModel();
const childTable = await childColumn.getModel();
await childTable.getColumns();
await parentTable.getColumns();
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
switch (colOptions.type) {
case 'mm':
{
const vChildCol = await colOptions.getMMChildColumn();
const vParentCol = await colOptions.getMMParentColumn();
const vTable = await colOptions.getMMModel();
const vTn = this.getTnPath(vTable);
execQueries.push((trx, qb) =>
this.dbDriver(vTn)
.where({
[vChildCol.column_name]: this.dbDriver(childTn)
.select(childColumn.column_name)
.first(),
})
.delete(),
);
}
break;
case 'hm':
{
// skip if it's an mm table column
const relatedTable = await colOptions.getRelatedTable();
if (relatedTable.mm) {
break;
}
const childColumn = await Column.get({
colId: colOptions.fk_child_column_id,
});
execQueries.push((trx, qb) =>
trx(childTn)
.where({
[childColumn.column_name]: this.dbDriver.from(
qb
.select(parentColumn.column_name)
// .where(_wherePk(parentTable.primaryKeys, rowId))
.first()
.as('___cn_alias'),
),
})
.update({
[childColumn.column_name]: null,
}),
);
}
break;
}
}
const base = await Base.get(this.model.base_id);
trx = await this.dbDriver.transaction();
// unlink LTAR data
if (base.is_meta) {
for (const execQuery of execQueries) {
await execQuery(trx, qb.clone());
}
}
const deleteQb = qb.clone().transacting(trx).del();
const count = (await deleteQb) as any;
await trx.commit();
await this.afterBulkDelete(count, this.dbDriver, cookie, true);
return count;
} catch (e) {
if (trx) await trx.rollback();
throw e;
}
}

14
packages/nocodb/src/db/sql-client/lib/sqlite/SqliteClient.ts

@ -22,7 +22,8 @@ class SqliteClient extends KnexClient {
// sqlite does not support inserting default values and knex fires a warning without this flag
connectionConfig.connection.useNullAsDefault = true;
super(connectionConfig);
this.sqlClient = knex(connectionConfig.connection);
this.sqlClient =
connectionConfig?.knex || knex(connectionConfig.connection);
this.queries = queries;
this._version = {};
}
@ -1557,7 +1558,13 @@ class SqliteClient extends KnexClient {
upQuery,
);
await this.sqlClient.raw('PRAGMA foreign_keys = OFF;');
const fkCheckEnabled = (
await this.sqlClient.raw('PRAGMA foreign_keys;')
)?.[0]?.foreign_keys;
if (fkCheckEnabled)
await this.sqlClient.raw('PRAGMA foreign_keys = OFF;');
await this.sqlClient.raw('PRAGMA legacy_alter_table = ON;');
const trx = await this.sqlClient.transaction();
@ -1594,7 +1601,8 @@ class SqliteClient extends KnexClient {
log.ppe(e, _func);
throw e;
} finally {
await this.sqlClient.raw('PRAGMA foreign_keys = ON;');
if (fkCheckEnabled)
await this.sqlClient.raw('PRAGMA foreign_keys = ON;');
await this.sqlClient.raw('PRAGMA legacy_alter_table = OFF;');
}

23
packages/nocodb/src/db/sql-mgr/v2/ProjectMgrv2.ts

@ -1,18 +1,21 @@
import SqlMgrv2 from './SqlMgrv2';
import SqlMgrv2Trans from './SqlMgrv2Trans';
import { MetaService } from '../../../meta/meta.service'
import SqlMgrv2 from './SqlMgrv2'
import SqlMgrv2Trans from './SqlMgrv2Trans'
// import type NcMetaIO from '../../../meta/NcMetaIO';
import type Base from '../../../models/Base';
import type Base from '../../../models/Base'
export default class ProjectMgrv2 {
private static sqlMgrMap: {
[key: string]: SqlMgrv2;
} = {};
} = {}
public static getSqlMgr(project: { id: string }, ncMeta: MetaService = null): SqlMgrv2 {
if (ncMeta) return new SqlMgrv2(project, ncMeta)
public static getSqlMgr(project: { id: string }): SqlMgrv2 {
if (!this.sqlMgrMap[project.id]) {
this.sqlMgrMap[project.id] = new SqlMgrv2(project);
this.sqlMgrMap[project.id] = new SqlMgrv2(project)
}
return this.sqlMgrMap[project.id];
return this.sqlMgrMap[project.id]
}
public static async getSqlMgrTrans(
@ -21,8 +24,8 @@ export default class ProjectMgrv2 {
ncMeta: any,
base: Base,
): Promise<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,12 +5,14 @@ import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import SqlClientFactory from '../../sql-client/lib/SqlClientFactory';
import KnexMigratorv2 from '../../sql-migrator/lib/KnexMigratorv2';
import Debug from '../../util/Debug';
import type { MetaService } from '../../../meta/meta.service';
import type Base from '../../../models/Base';
const log = new Debug('SqlMgr');
export default class SqlMgrv2 {
protected _migrator: KnexMigratorv2;
protected ncMeta?: MetaService;
// @ts-ignore
private currentProjectFolder: any;
@ -20,18 +22,18 @@ export default class SqlMgrv2 {
* @param {String} args.toolDbPath - path to sqlite file that sql mgr will use
* @memberof SqlMgr
*/
constructor(args: { id: string }) {
constructor(args: { id: string }, ncMeta = null) {
const func = 'constructor';
log.api(`${func}:args:`, args);
// this.metaDb = args.metaDb;
this._migrator = new KnexMigratorv2(args);
return this;
this.ncMeta = ncMeta;
}
public async migrator(_base: Base) {
return this._migrator;
}
public static async testConnection(args = {}) {
const client = await SqlClientFactory.create(args);
return client.testConnection();
@ -119,6 +121,10 @@ export default class SqlMgrv2 {
}
protected async getSqlClient(base: Base) {
if (base.is_meta && this.ncMeta) {
return NcConnectionMgrv2.getSqlClient(base, this.ncMeta.knex);
}
return NcConnectionMgrv2.getSqlClient(base);
}
}

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

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

1
packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts

@ -37,6 +37,7 @@ export class ExtractProjectIdMiddleware implements NestMiddleware, CanActivate {
// extract project id based on request path params
if (params.projectName) {
const project = await Project.getByTitleOrId(params.projectName);
if (!project) NcError.notFound('Project not found');
req.ncProjectId = project.id;
res.locals.project = project;
}

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

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

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,6 +309,11 @@ export default class Project implements ProjectType {
`${CacheScope.PROJECT}:${projectId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
await ncMeta.metaDelete(null, null, MetaTable.AUDIT, {
project_id: projectId,
});
return await ncMeta.metaDelete(null, null, MetaTable.PROJECT, projectId);
}

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);
await Sort.deleteAll(viewId);
await Filter.deleteAll(viewId);
const view = await this.get(viewId, ncMeta);
await Sort.deleteAll(viewId, ncMeta);
await Filter.deleteAll(viewId, ncMeta);
const table = this.extractViewTableName(view);
const tableScope = this.extractViewTableNameScope(view);
const columnTable = this.extractViewColumnsTableName(view);
@ -1273,8 +1273,8 @@ export default class View implements ViewType {
);
}
async delete() {
await View.delete(this.id);
async delete(ncMeta = Noco.ncMeta){
await View.delete(this.id, ncMeta);
}
static async shareViewList(tableId, ncMeta = Noco.ncMeta) {

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 = '0107004';
process.env.NC_VERSION = '0108002';
// 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,6 +57,8 @@ export enum Altered {
@Injectable()
export class ColumnsService {
constructor(private readonly metaService: MetaService) {}
async columnUpdate(param: {
req?: any;
columnId: string;
@ -1142,21 +1144,23 @@ export class ColumnsService {
return table;
}
async columnDelete(param: { req?: any; columnId: string }) {
const column = await Column.get({ colId: param.columnId });
const table = await Model.getWithInfo({
id: column.fk_model_id,
});
const base = await Base.get(table.base_id);
// const ncMeta = await Noco.ncMeta.startTransaction();
// const sql-mgr = await ProjectMgrv2.getSqlMgrTrans(
// { id: base.project_id },
// ncMeta,
// base
// );
async columnDelete(
param: { req?: any; columnId: string },
ncMeta = this.metaService,
) {
const column = await Column.get({ colId: param.columnId }, ncMeta);
const table = await Model.getWithInfo(
{
id: column.fk_model_id,
},
ncMeta,
);
const base = await Base.get(table.base_id, ncMeta);
const sqlMgr = await ProjectMgrv2.getSqlMgr({ id: base.project_id });
const sqlMgr = await ProjectMgrv2.getSqlMgr(
{ id: base.project_id },
ncMeta,
);
switch (column.uidt) {
case UITypes.Lookup:
@ -1164,17 +1168,17 @@ export class ColumnsService {
case UITypes.QrCode:
case UITypes.Barcode:
case UITypes.Formula:
await Column.delete(param.columnId);
await Column.delete(param.columnId, ncMeta);
break;
case UITypes.LinkToAnotherRecord:
{
const relationColOpt =
await column.getColOptions<LinkToAnotherRecordColumn>();
const childColumn = await relationColOpt.getChildColumn();
const childTable = await childColumn.getModel();
await column.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
const childColumn = await relationColOpt.getChildColumn(ncMeta);
const childTable = await childColumn.getModel(ncMeta);
const parentColumn = await relationColOpt.getParentColumn();
const parentTable = await parentColumn.getModel();
const parentColumn = await relationColOpt.getParentColumn(ncMeta);
const parentTable = await parentColumn.getModel(ncMeta);
switch (relationColOpt.type) {
case 'bt':
@ -1188,15 +1192,19 @@ export class ColumnsService {
parentColumn,
parentTable,
sqlMgr,
// ncMeta
ncMeta,
});
}
break;
case 'mm':
{
const mmTable = await relationColOpt.getMMModel();
const mmParentCol = await relationColOpt.getMMParentColumn();
const mmChildCol = await relationColOpt.getMMChildColumn();
const mmTable = await relationColOpt.getMMModel(ncMeta);
const mmParentCol = await relationColOpt.getMMParentColumn(
ncMeta,
);
const mmChildCol = await relationColOpt.getMMChildColumn(
ncMeta,
);
await this.deleteHmOrBtRelation(
{
@ -1207,7 +1215,7 @@ export class ColumnsService {
parentTable: parentTable,
childColumn: mmParentCol,
base,
// ncMeta
ncMeta,
},
true,
);
@ -1221,18 +1229,18 @@ export class ColumnsService {
parentTable: childTable,
childColumn: mmChildCol,
base,
// ncMeta
ncMeta,
},
true,
);
const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable()
.then((m) => m.getColumns());
.getRelatedTable(ncMeta)
.then((m) => m.getColumns(ncMeta));
for (const c of columnsInRelatedTable) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>();
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (
colOpt.type === 'mm' &&
colOpt.fk_parent_column_id === childColumn.id &&
@ -1241,55 +1249,55 @@ export class ColumnsService {
colOpt.fk_mm_parent_column_id === mmChildCol.id &&
colOpt.fk_mm_child_column_id === mmParentCol.id
) {
await Column.delete(c.id);
await Column.delete(c.id, ncMeta);
break;
}
}
await Column.delete(relationColOpt.fk_column_id);
await Column.delete(relationColOpt.fk_column_id, ncMeta);
// delete bt columns in m2m table
await mmTable.getColumns();
await mmTable.getColumns(ncMeta);
for (const c of mmTable.columns) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>();
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOpt.type === 'bt') {
await Column.delete(c.id);
await Column.delete(c.id, ncMeta);
}
}
// delete hm columns in parent table
await parentTable.getColumns();
await parentTable.getColumns(ncMeta);
for (const c of parentTable.columns) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>();
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOpt.fk_related_model_id === mmTable.id) {
await Column.delete(c.id);
await Column.delete(c.id, ncMeta);
}
}
// delete hm columns in child table
await childTable.getColumns();
await childTable.getColumns(ncMeta);
for (const c of childTable.columns) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt =
await c.getColOptions<LinkToAnotherRecordColumn>();
await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (colOpt.fk_related_model_id === mmTable.id) {
await Column.delete(c.id);
await Column.delete(c.id, ncMeta);
}
}
// retrieve columns in m2m table again
await mmTable.getColumns();
await mmTable.getColumns(ncMeta);
// ignore deleting table if it has more than 2 columns
// the expected 2 columns would be table1_id & table2_id
if (mmTable.columns.length === 2) {
(mmTable as any).tn = mmTable.table_name;
await sqlMgr.sqlOpPlus(base, 'tableDelete', mmTable);
await mmTable.delete();
await mmTable.delete(ncMeta);
}
}
break;
@ -1337,26 +1345,30 @@ export class ColumnsService {
await sqlMgr.sqlOpPlus(base, 'tableUpdate', tableUpdateBody);
await Column.delete(param.columnId);
await Column.delete(param.columnId, ncMeta);
}
}
await Audit.insert({
project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN,
op_sub_type: AuditOperationSubTypes.DELETE,
user: param?.req?.user?.email,
description: `The column ${column.column_name} with alias ${column.title} from table ${table.table_name} has been deleted`,
ip: param?.req.clientIp,
});
await table.getColumns();
await Audit.insert(
{
project_id: base.project_id,
op_type: AuditOperationTypes.TABLE_COLUMN,
op_sub_type: AuditOperationSubTypes.DELETE,
user: param?.req?.user?.email,
description: `The column ${column.column_name} with alias ${column.title} from table ${table.table_name} has been deleted`,
ip: param?.req.clientIp,
},
ncMeta,
);
await table.getColumns(ncMeta);
const displayValueColumn = mapDefaultDisplayValue(table.columns);
if (displayValueColumn) {
await Model.updatePrimaryColumn(
displayValueColumn.fk_model_id,
displayValueColumn.id,
ncMeta,
);
}
@ -1394,45 +1406,47 @@ export class ColumnsService {
if (!relationColOpt) {
foreignKeyName = (
(
await childTable.getColumns().then((cols) => {
return cols?.find((c) => {
return (
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.fk_related_model_id === parentTable.id &&
(c.colOptions as LinkToAnotherRecordType).fk_child_column_id ===
childColumn.id &&
(c.colOptions as LinkToAnotherRecordType)
.fk_parent_column_id === parentColumn.id
);
});
await childTable.getColumns(ncMeta).then(async (cols) => {
for (const col of cols) {
if (col.uidt === UITypes.LinkToAnotherRecord) {
const colOptions =
await col.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
console.log(colOptions);
if (colOptions.fk_related_model_id === parentTable.id) {
return { colOptions };
}
}
}
})
).colOptions as LinkToAnotherRecordType
)?.colOptions as LinkToAnotherRecordType
).fk_index_name;
} else {
foreignKeyName = relationColOpt.fk_index_name;
}
// todo: handle relation delete exception
try {
await sqlMgr.sqlOpPlus(base, 'relationDelete', {
childColumn: childColumn.column_name,
childTable: childTable.table_name,
parentTable: parentTable.table_name,
parentColumn: parentColumn.column_name,
foreignKeyName,
});
} catch (e) {
console.log(e);
if (!relationColOpt?.virtual) {
// todo: handle relation delete exception
try {
await sqlMgr.sqlOpPlus(base, 'relationDelete', {
childColumn: childColumn.column_name,
childTable: childTable.table_name,
parentTable: parentTable.table_name,
parentColumn: parentColumn.column_name,
foreignKeyName,
});
} catch (e) {
console.log(e);
}
}
if (!relationColOpt) return;
const columnsInRelatedTable: Column[] = await relationColOpt
.getRelatedTable()
.then((m) => m.getColumns());
.getRelatedTable(ncMeta)
.then((m) => m.getColumns(ncMeta));
const relType = relationColOpt.type === 'bt' ? 'hm' : 'bt';
for (const c of columnsInRelatedTable) {
if (c.uidt !== UITypes.LinkToAnotherRecord) continue;
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>();
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>(ncMeta);
if (
colOpt.fk_parent_column_id === parentColumn.id &&
colOpt.fk_child_column_id === childColumn.id &&
@ -1447,9 +1461,34 @@ export class ColumnsService {
await Column.delete(relationColOpt.fk_column_id, ncMeta);
if (!ignoreFkDelete) {
const cTable = await Model.getWithInfo({
id: childTable.id,
});
const cTable = await Model.getWithInfo(
{
id: childTable.id,
},
ncMeta,
);
// if virtual column delete all index before deleting the column
if (relationColOpt?.virtual) {
const indexes =
(
await sqlMgr.sqlOp(base, 'indexList', {
tn: cTable.table_name,
})
)?.data?.list ?? [];
for (const index of indexes) {
if (index.cn !== childColumn.column_name) continue;
await sqlMgr.sqlOpPlus(base, 'indexDelete', {
...index,
tn: cTable.table_name,
columns: [childColumn.column_name],
indexName: index.index_name,
});
}
}
const tableUpdateBody = {
...cTable,
tn: cTable.table_name,
@ -1499,6 +1538,12 @@ export class ColumnsService {
const sqlMgr = await ProjectMgrv2.getSqlMgr({
id: param.base.project_id,
});
// if xcdb base then treat as virtual relation to avoid creating foreign key
if (param.base.is_meta) {
(param.column as LinkToAnotherColumnReqType).virtual = true;
}
if (
(param.column as LinkToAnotherColumnReqType).type === 'hm' ||
(param.column as LinkToAnotherColumnReqType).type === 'bt'
@ -1570,7 +1615,7 @@ export class ColumnsService {
}
// todo: create index for virtual relations as well
// create index for foreign key in pg
// create index for foreign key in pg
if (
param.base.type === 'pg' ||
(param.column as LinkToAnotherColumnReqType).virtual
@ -1729,6 +1774,7 @@ export class ColumnsService {
fk_mm_child_column_id: childCol.id,
fk_mm_parent_column_id: parentCol.id,
fk_related_model_id: parent.id,
virtual: (param.column as LinkToAnotherColumnReqType).virtual,
});
await Column.insert({
title: getUniqueColumnAliasName(
@ -1748,6 +1794,7 @@ export class ColumnsService {
fk_mm_child_column_id: parentCol.id,
fk_mm_parent_column_id: childCol.id,
fk_related_model_id: child.id,
virtual: (param.column as LinkToAnotherColumnReqType).virtual,
});
// todo: create index for virtual relations as well

12
packages/nocodb/src/services/datas.service.ts

@ -103,11 +103,15 @@ export class DatasService {
dbDriver: await NcConnectionMgrv2.get(base),
});
// todo: Should have error http status code
const message = await baseModel.hasLTARData(param.rowId, model);
if (message.length) {
NcError.badRequest(message);
// if xcdb project skip checking for LTAR
if (!base.is_meta) {
// todo: Should have error http status code
const message = await baseModel.hasLTARData(param.rowId, model);
if (message.length) {
NcError.badRequest(message);
}
}
return await baseModel.delByPk(param.rowId, null, param.cookie);
}

105
packages/nocodb/src/services/tables.service.ts

@ -14,9 +14,19 @@ import getColumnPropsFromUIDT from '../helpers/getColumnPropsFromUIDT';
import getColumnUiType from '../helpers/getColumnUiType';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import mapDefaultDisplayValue from '../helpers/mapDefaultDisplayValue';
import { Audit, Column, Model, ModelRoleVisibility, Project } from '../models';
import {
Audit,
Base,
Column,
Model,
ModelRoleVisibility,
Project,
} from '../models';
import Noco from '../Noco';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { validatePayload } from '../helpers';
import { ColumnsService } from './columns.service';
import type { MetaService } from '../meta/meta.service';
import type { LinkToAnotherRecordColumn, User, View } from '../models';
import type {
ColumnType,
@ -26,6 +36,8 @@ import type {
@Injectable()
export class TablesService {
constructor(private readonly columnsService: ColumnsService) {}
async tableUpdate(param: {
tableId: any;
table: TableReqType & { project_id?: string };
@ -142,11 +154,14 @@ export class TablesService {
const table = await Model.getByIdOrName({ id: param.tableId });
await table.getColumns();
const project = await Project.getWithInfo(table.project_id);
const base = project.bases.find((b) => b.id === table.base_id);
const relationColumns = table.columns.filter(
(c) => c.uidt === UITypes.LinkToAnotherRecord,
);
if (relationColumns?.length) {
if (relationColumns?.length && !base.is_meta) {
const referredTables = await Promise.all(
relationColumns.map(async (c) =>
c
@ -162,37 +177,69 @@ export class TablesService {
);
}
const project = await Project.getWithInfo(table.project_id);
const base = project.bases.find((b) => b.id === table.base_id);
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
(table as any).tn = table.table_name;
table.columns = table.columns.filter((c) => !isVirtualCol(c));
table.columns.forEach((c) => {
(c as any).cn = c.column_name;
});
// start a transaction
const ncMeta = await (Noco.ncMeta as MetaService).startTransaction();
let result;
try {
// delete all relations
for (const c of relationColumns) {
// skip if column is hasmany relation to mm table
if (c.system) {
continue;
}
// verify column exist or not and based on that delete the column
if (!(await Column.get({ colId: c.id }, ncMeta))) {
continue;
}
await this.columnsService.columnDelete(
{
req: param.req,
columnId: c.id,
},
ncMeta,
);
}
if (table.type === ModelTypes.TABLE) {
await sqlMgr.sqlOpPlus(base, 'tableDelete', table);
} else if (table.type === ModelTypes.VIEW) {
await sqlMgr.sqlOpPlus(base, 'viewDelete', {
...table,
view_name: table.table_name,
const sqlMgr = await ProjectMgrv2.getSqlMgr(project, ncMeta);
(table as any).tn = table.table_name;
table.columns = table.columns.filter((c) => !isVirtualCol(c));
table.columns.forEach((c) => {
(c as any).cn = c.column_name;
});
}
await Audit.insert({
project_id: project.id,
base_id: base.id,
op_type: AuditOperationTypes.TABLE,
op_sub_type: AuditOperationSubTypes.DELETE,
user: param.user?.email,
description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `,
ip: param.req?.clientIp,
}).then(() => {});
T.emit('evt', { evt_type: 'table:deleted' });
if (table.type === ModelTypes.TABLE) {
await sqlMgr.sqlOpPlus(base, 'tableDelete', table);
} else if (table.type === ModelTypes.VIEW) {
await sqlMgr.sqlOpPlus(base, 'viewDelete', {
...table,
view_name: table.table_name,
});
}
return table.delete();
await Audit.insert(
{
project_id: project.id,
base_id: base.id,
op_type: AuditOperationTypes.TABLE,
op_sub_type: AuditOperationSubTypes.DELETE,
user: param.user?.email,
description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `,
ip: param.req?.clientIp,
},
ncMeta,
).then(() => {});
T.emit('evt', { evt_type: 'table:deleted' });
result = await table.delete(ncMeta);
await ncMeta.commit();
} catch (e) {
await ncMeta.rollback();
throw e;
}
return result;
}
async getTableWithAccessibleViews(param: { tableId: string; user: User }) {

2
packages/nocodb/src/version-upgrader/NcUpgrader.ts

@ -14,6 +14,7 @@ import ncProjectEnvUpgrader0011045 from './ncProjectEnvUpgrader0011045';
import ncProjectEnvUpgrader from './ncProjectEnvUpgrader';
import ncHookUpgrader from './ncHookUpgrader';
import ncProjectConfigUpgrader from './ncProjectConfigUpgrader';
import ncXcdbLTARUpgrader from './ncXcdbLTARUpgrader';
import type { MetaService } from '../meta/meta.service';
import type { NcConfig } from '../interface/config';
@ -50,6 +51,7 @@ export default class NcUpgrader {
{ name: '0105003', handler: ncFilterUpgrader_0105003 },
{ name: '0105004', handler: ncHookUpgrader },
{ name: '0107004', handler: ncProjectConfigUpgrader },
{ name: '0108002', handler: ncXcdbLTARUpgrader },
];
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) {
return;

384
packages/nocodb/src/version-upgrader/ncFilterUpgrader_0104004.ts

@ -1,359 +1,43 @@
import { UITypes } from 'nocodb-sdk';
import { OrgUserRoles } from 'nocodb-sdk';
import { NC_APP_SETTINGS } from '../constants';
import Store from '../models/Store';
import { MetaTable } from '../utils/globals';
import Column from '../models/Column';
import Filter from '../models/Filter';
import Project from '../models/Project';
import type { MetaService } from '../meta/meta.service';
import type { NcUpgraderCtx } from './NcUpgrader';
import type { SelectOptionsType } from 'nocodb-sdk';
// as of 0.104.3, almost all filter operators are available to all column types
// while some of them aren't supposed to be shown
// this upgrader is to remove those unsupported filters / migrate to the correct filter
// Change Summary:
// - Text-based columns:
// - remove `>`, `<`, `>=`, `<=`
// - Numeric-based / SingleSelect columns:
// - remove `like`
// - migrate `null`, and `empty` to `blank`
// - Checkbox columns:
// - remove `equal`
// - migrate `empty` and `null` to `notchecked`
// - MultiSelect columns:
// - remove `like`
// - migrate `equal`, `null`, `empty`
// - Attachment columns:
// - remove `>`, `<`, `>=`, `<=`, `equal`
// - migrate `empty`, `null` to `blank`
// - LTAR columns:
// - remove `>`, `<`, `>=`, `<=`
// - migrate `empty`, `null` to `blank`
// - Lookup columns:
// - migrate `empty`, `null` to `blank`
// - Duration columns:
// - remove `like`
// - migrate `empty`, `null` to `blank`
const removeEqualFilters = (filter, ncMeta) => {
const actions = [];
// remove `is equal`, `is not equal`
if (['eq', 'neq'].includes(filter.comparison_op)) {
actions.push(Filter.delete(filter.id, ncMeta));
}
return actions;
};
const removeArithmeticFilters = (filter, ncMeta) => {
const actions = [];
// remove `>`, `<`, `>=`, `<=`
if (['gt', 'lt', 'gte', 'lte'].includes(filter.comparison_op)) {
actions.push(Filter.delete(filter.id, ncMeta));
}
return actions;
};
const removeLikeFilters = (filter, ncMeta) => {
const actions = [];
// remove `is like`, `is not like`
if (['like', 'nlike'].includes(filter.comparison_op)) {
actions.push(Filter.delete(filter.id, ncMeta));
}
return actions;
};
const migrateNullAndEmptyToBlankFilters = (filter, ncMeta) => {
const actions = [];
if (['empty', 'null'].includes(filter.comparison_op)) {
// migrate to blank
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'blank',
},
ncMeta,
),
);
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) {
// migrate to not blank
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'notblank',
},
ncMeta,
),
);
}
return actions;
};
const migrateMultiSelectEq = async (filter, col: Column, ncMeta) => {
// only allow eq / neq
if (!['eq', 'neq'].includes(filter.comparison_op)) return;
// if there is no value -> delete this filter
if (!filter.value) {
return await Filter.delete(filter.id, ncMeta);
}
// options inputted from users
const options = filter.value.split(',');
// retrieve the possible col options
const colOptions = (await col.getColOptions()) as SelectOptionsType;
// only include valid options as the input value becomes dropdown type now
const validOptions = [];
for (const option of options) {
if (colOptions.options.includes(option)) {
validOptions.push(option);
}
}
const newFilterValue = validOptions.join(',');
// if all inputted options are invalid -> delete this filter
if (!newFilterValue) {
return await Filter.delete(filter.id, ncMeta);
}
const actions = [];
if (filter.comparison_op === 'eq') {
// migrate to `contains all of`
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'anyof',
value: newFilterValue,
},
ncMeta,
),
);
} else if (filter.comparison_op === 'neq') {
// migrate to `doesn't contain all of`
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'nanyof',
value: newFilterValue,
},
ncMeta,
),
);
}
return await Promise.all(actions);
};
const migrateToCheckboxFilter = (filter, ncMeta) => {
const actions = [];
const possibleTrueValues = ['true', 'True', '1', 'T', 'Y'];
const possibleFalseValues = ['false', 'False', '0', 'F', 'N'];
if (['empty', 'null'].includes(filter.comparison_op)) {
// migrate to not checked
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'notchecked',
},
ncMeta,
),
);
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) {
// migrate to checked
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'checked',
},
ncMeta,
),
/** Upgrader for upgrading roles */
export default async function ({ ncMeta }: NcUpgraderCtx) {
const users = await ncMeta.metaList2(null, null, MetaTable.USERS);
for (const user of users) {
user.roles = user.roles
.split(',')
.map((r) => {
// update old role names with new roles
if (r === 'user') {
return OrgUserRoles.CREATOR;
} else if (r === 'user-new') {
return OrgUserRoles.VIEWER;
}
return r;
})
.join(',');
await ncMeta.metaUpdate(
null,
null,
MetaTable.USERS,
{ roles: user.roles },
user.id,
);
} else if (filter.comparison_op === 'eq') {
if (possibleTrueValues.includes(filter.value)) {
// migrate to checked
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'checked',
value: '',
},
ncMeta,
),
);
} else if (possibleFalseValues.includes(filter.value)) {
// migrate to notchecked
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'notchecked',
value: '',
},
ncMeta,
),
);
} else {
// invalid value - good to delete
actions.push(Filter.delete(filter.id, ncMeta));
}
} else if (filter.comparison_op === 'neq') {
if (possibleFalseValues.includes(filter.value)) {
// migrate to checked
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'checked',
value: '',
},
ncMeta,
),
);
} else if (possibleTrueValues.includes(filter.value)) {
// migrate to not checked
actions.push(
Filter.update(
filter.id,
{
comparison_op: 'notchecked',
value: '',
},
ncMeta,
),
);
} else {
// invalid value - good to delete
actions.push(Filter.delete(filter.id, ncMeta));
}
}
return actions;
};
async function migrateFilters(ncMeta: MetaService) {
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP);
for (const filter of filters) {
if (!filter.fk_column_id || filter.is_group) {
continue;
}
const col = await Column.get({ colId: filter.fk_column_id }, ncMeta);
if (
[
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
].includes(col.uidt)
) {
await Promise.all(removeArithmeticFilters(filter, ncMeta));
} else if (
[
// numeric fields
UITypes.Duration,
UITypes.Currency,
UITypes.Percent,
UITypes.Number,
UITypes.Decimal,
UITypes.Rating,
UITypes.Rollup,
// select fields
UITypes.SingleSelect,
].includes(col.uidt)
) {
await Promise.all([
...removeLikeFilters(filter, ncMeta),
...migrateNullAndEmptyToBlankFilters(filter, ncMeta),
]);
} else if (col.uidt === UITypes.Checkbox) {
await Promise.all(migrateToCheckboxFilter(filter, ncMeta));
} else if (col.uidt === UITypes.MultiSelect) {
await Promise.all([
...removeLikeFilters(filter, ncMeta),
...migrateNullAndEmptyToBlankFilters(filter, ncMeta),
]);
await migrateMultiSelectEq(filter, col, ncMeta);
} else if (col.uidt === UITypes.Attachment) {
await Promise.all([
...removeArithmeticFilters(filter, ncMeta),
...removeEqualFilters(filter, ncMeta),
...migrateNullAndEmptyToBlankFilters(filter, ncMeta),
]);
} else if (col.uidt === UITypes.LinkToAnotherRecord) {
await Promise.all([
...removeArithmeticFilters(filter, ncMeta),
...migrateNullAndEmptyToBlankFilters(filter, ncMeta),
]);
} else if (col.uidt === UITypes.Lookup) {
await Promise.all([
...removeArithmeticFilters(filter, ncMeta),
...migrateNullAndEmptyToBlankFilters(filter, ncMeta),
]);
} else if (col.uidt === UITypes.Duration) {
await Promise.all([
...removeLikeFilters(filter, ncMeta),
...migrateNullAndEmptyToBlankFilters(filter, ncMeta),
]);
}
}
}
async function updateProjectMeta(ncMeta: MetaService) {
const projectHasEmptyOrFilters: Record<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;
}
}
const projects = await ncMeta.metaList2(null, null, MetaTable.PROJECT);
const defaultProjectMeta = {
showNullAndEmptyInFilter: false,
};
for (const project of projects) {
const oldProjectMeta = project.meta;
let newProjectMeta = defaultProjectMeta;
try {
newProjectMeta =
(typeof oldProjectMeta === 'string'
? JSON.parse(oldProjectMeta)
: oldProjectMeta) ?? defaultProjectMeta;
} catch {}
newProjectMeta = {
...newProjectMeta,
showNullAndEmptyInFilter: projectHasEmptyOrFilters[project.id] ?? false,
};
actions.push(
Project.update(
project.id,
{
meta: JSON.stringify(newProjectMeta),
},
ncMeta,
),
// set invite only signup if user have environment variable set
if (process.env.NC_INVITE_ONLY_SIGNUP) {
await Store.saveOrUpdate(
{
value: '{ "invite_only_signup": true }',
key: NC_APP_SETTINGS,
},
ncMeta,
);
}
await Promise.all(actions);
}
export default async function ({ ncMeta }: NcUpgraderCtx) {
// fix the existing filter behaviours or
// migrate `null` or `empty` filters to `blank`
await migrateFilters(ncMeta);
// enrich `showNullAndEmptyInFilter` in project meta
// if there is empty / null filters in existing projects,
// then set `showNullAndEmptyInFilter` to true
// else set to false
await updateProjectMeta(ncMeta);
}

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

@ -0,0 +1,164 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import NocoCache from '../cache/NocoCache';
import { MetaTable } from '../meta/meta.service';
import { Base } from '../models';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { CacheGetType, CacheScope } from '../utils/globals';
import { Model } from '../models';
import type { LinkToAnotherRecordColumn } from '../models';
import type { MetaService } from '../meta/meta.service';
import type { NcUpgraderCtx } from './NcUpgrader';
// An upgrader for upgrading LTAR relations in XCDB bases
// it will delete all the foreign keys and create a new index
// and treat all the LTAR as virtual
async function upgradeModelRelations({
model,
relations,
ncMeta,
sqlClient,
}: {
ncMeta: MetaService;
model: Model;
sqlClient: ReturnType<
(typeof NcConnectionMgrv2)['getSqlClient']
> extends Promise<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),
});
}
}

14
packages/nocodb/tests/unit/rest/tests/tableRow.test.ts

@ -1422,20 +1422,14 @@ function tableTest() {
rowId: row['Id'],
});
const response = await request(context.app)
await request(context.app)
.delete(`/api/v1/db/data/noco/${project.id}/${table.id}/${row['Id']}`)
.set('xc-auth', context.token)
.expect(400);
.expect(200);
const deleteRow = await getRow(context, { project, table, id: row['Id'] });
if (!deleteRow) {
throw new Error('Should not delete');
}
if (
!(response.body.msg as string).includes('is a LinkToAnotherRecord of')
) {
throw new Error('Should give LTAR foreign key error');
if (deleteRow !== undefined) {
throw new Error('Record should have been deleted!');
}
});

24
packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

@ -1,5 +1,4 @@
import 'mocha';
import { isString } from 'util';
import request from 'supertest';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { expect } from 'chai';
@ -20,7 +19,6 @@ import {
getOneRow,
getRow,
} from '../../factory/row';
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';
@ -1339,7 +1337,7 @@ function viewRowTests() {
await testDeleteViewRow(ViewTypes.FORM);
});
const testDeleteViewRowWithForiegnKeyConstraint = async (
const testDeleteViewRowWithForeignKeyConstraint = async (
viewType: ViewTypes,
) => {
const table = await createTable(context, project);
@ -1370,35 +1368,29 @@ function viewRowTests() {
rowId: row['Id'],
});
const response = await request(context.app)
await request(context.app)
.delete(
`/api/v1/db/data/noco/${project.id}/${table.id}/views/${view.id}/${row['Id']}`,
)
.set('xc-auth', context.token)
.expect(400);
.expect(200);
const deleteRow = await getRow(context, { project, table, id: row['Id'] });
if (!deleteRow) {
throw new Error('Should not delete');
}
if (
!(response.body.msg as string).includes('is a LinkToAnotherRecord of')
) {
throw new Error('Should give ltar foreign key error');
if (deleteRow !== undefined) {
throw new Error('Record should have been deleted!');
}
};
it('Delete view row with ltar foreign key constraint GALLERY', async function () {
await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.GALLERY);
await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.GALLERY);
});
it('Delete view row with ltar foreign key constraint GRID', async function () {
await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.GRID);
await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.GRID);
});
it('Delete view row with ltar foreign key constraint FORM', async function () {
await testDeleteViewRowWithForiegnKeyConstraint(ViewTypes.FORM);
await testDeleteViewRowWithForeignKeyConstraint(ViewTypes.FORM);
});
const testViewRowExists = async (viewType: ViewTypes) => {

14
tests/playwright/pages/Dashboard/Grid/index.ts

@ -185,6 +185,12 @@ export class GridPage extends BasePage {
await (await this.rootPage.locator('.ant-drawer-body').elementHandle())?.waitForElementState('stable');
}
async selectRow(index: number) {
const cell: Locator = await this.get().locator(`td[data-testid="cell-Id-${index}"]`);
await cell.hover();
await cell.locator('input[type="checkbox"]').check({ force: true });
}
async selectAll() {
await this.get().locator('[data-testid="nc-check-all"]').hover();
@ -201,8 +207,7 @@ export class GridPage extends BasePage {
await this.rootPage.waitForTimeout(300);
}
async deleteAll() {
await this.selectAll();
async deleteSelectedRows() {
await this.get().locator('[data-testid="nc-check-all"]').nth(0).click({
button: 'right',
});
@ -210,6 +215,11 @@ export class GridPage extends BasePage {
await this.dashboard.waitForLoaderToDisappear();
}
async deleteAll() {
await this.selectAll();
await this.deleteSelectedRows();
}
async verifyTotalRowCount({ count }: { count: number }) {
// wait for 100 ms and try again : 5 times
let i = 0;

40
tests/playwright/setup/xcdbProject.ts

@ -0,0 +1,40 @@
import { Api } from 'nocodb-sdk';
let api: Api;
async function createXcdb(token?: string) {
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': token,
},
});
const projectList = await api.project.list();
for (const project of projectList.list) {
// delete project with title 'xcdb' if it exists
if (project.title === 'xcdb') {
await api.project.delete(project.id);
}
}
const project = await api.project.create({ title: 'xcdb' });
return project;
}
async function deleteXcdb(token?: string) {
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': token,
},
});
const projectList = await api.project.list();
for (const project of projectList.list) {
// delete project with title 'xcdb' if it exists
if (project.title === 'xcdb') {
await api.project.delete(project.id);
}
}
}
export { createXcdb, deleteXcdb };

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

@ -0,0 +1,324 @@
/*
*
* Meta projects, additional provision for deleting of rows, columns and tables with Link to another record field type
*
* Pre-requisite:
* TableA <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 });
});
});

25
tests/playwright/tests/db/timezone.spec.ts

@ -30,7 +30,7 @@ const rowAttributes = [
{ Id: 3, DateTime: '2020-12-31 20:00:00-04:00' },
];
async function timezoneSuite(token?: string, skipTableCreate?: boolean) {
async function timezoneSuite(token: string, projectTitle: string, skipTableCreate?: boolean) {
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
@ -41,12 +41,12 @@ async function timezoneSuite(token?: string, skipTableCreate?: boolean) {
const projectList = await api.project.list();
for (const project of projectList.list) {
// delete project with title 'xcdb' if it exists
if (project.title === 'xcdb') {
if (project.title === projectTitle) {
await api.project.delete(project.id);
}
}
const project = await api.project.create({ title: 'xcdb' });
const project = await api.project.create({ title: projectTitle });
if (skipTableCreate) return { project };
const table = await api.base.tableCreate(project.id, project.bases?.[0].id, {
table_name: 'dateTimeTable',
@ -99,7 +99,8 @@ async function connectToExtDb(context: any, dbName: string) {
////////////////////////////////////////////////////////////////////////////////////////////////////////////
test.describe('Timezone-XCDB : Japan/Tokyo', () => {
// serial : as we are creating an external db, we need to run the tests sequentially
test.describe.serial('Timezone-XCDB : Japan/Tokyo', () => {
let dashboard: DashboardPage;
let context: any;
@ -109,7 +110,7 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
if (!isSqlite(context)) return;
try {
const { project, table } = await timezoneSuite(context.token);
const { project, table } = await timezoneSuite(context.token, 'xcdb0');
await api.dbTableRow.bulkCreate('noco', project.id, table.id, rowAttributes);
records = await api.dbTableRow.list('noco', project.id, table.id, { limit: 10 });
@ -143,7 +144,7 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
await dashboard.clickHome();
const projectsPage = new ProjectsPage(dashboard.rootPage);
await projectsPage.openProject({ title: 'xcdb', withoutPrefix: true });
await projectsPage.openProject({ title: 'xcdb0', withoutPrefix: true });
await dashboard.treeView.openTable({ title: 'dateTimeTable' });
@ -202,7 +203,7 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
// Change browser timezone & locale to Asia/Hong-Kong
//
test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
test.describe.serial('Timezone-XCDB : Asia/Hong-kong', () => {
let dashboard: DashboardPage;
let context: any;
@ -211,7 +212,7 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
dashboard = new DashboardPage(page, context.project);
try {
const { project, table } = await timezoneSuite(context.token);
const { project, table } = await timezoneSuite(context.token, 'xcdb1');
await api.dbTableRow.bulkCreate('noco', project.id, table.id, rowAttributes);
} catch (e) {
console.error(e);
@ -240,7 +241,7 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
test('API insert, verify display value', async () => {
await dashboard.clickHome();
const projectsPage = new ProjectsPage(dashboard.rootPage);
await projectsPage.openProject({ title: 'xcdb', withoutPrefix: true });
await projectsPage.openProject({ title: 'xcdb1', withoutPrefix: true });
await dashboard.treeView.openTable({ title: 'dateTimeTable' });
@ -268,7 +269,7 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
////////////////////////////////////////////////////////////////////////////////////////////////////////////
test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
test.describe.serial('Timezone-XCDB : Asia/Hong-kong', () => {
let dashboard: DashboardPage;
let context: any;
@ -284,7 +285,7 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
// Apply only for sqlite, as currently- root DB for all instances is SQLite
if (!isSqlite(context)) return;
const { project } = await timezoneSuite(context.token, true);
const { project } = await timezoneSuite(context.token, 'xcdb2', true);
context.project = project;
// Kludge: Using API for test preparation was not working
@ -292,7 +293,7 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
await dashboard.clickHome();
const projectsPage = new ProjectsPage(dashboard.rootPage);
await projectsPage.openProject({ title: 'xcdb', withoutPrefix: true });
await projectsPage.openProject({ title: 'xcdb2', withoutPrefix: true });
await dashboard.treeView.createTable({ title: 'dateTimeTable', mode: 'Xcdb' });
await dashboard.grid.column.create({

Loading…
Cancel
Save