Browse Source

refactor/Added unit test for BaseModelSqlV2, added creating audit for all the write action in BaseModelSqlV2, and some cleanups

pull/3358/head
Muhammed Mustafa 2 years ago
parent
commit
d4da9595d1
  1. 5
      packages/nocodb-sdk/src/lib/globals.ts
  2. 132
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  3. 6
      packages/nocodb/src/lib/meta/api/columnApis.ts
  4. 11
      packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts
  5. 8
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts
  6. 2
      packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts
  7. 10
      packages/nocodb/tests/unit/factory/row.ts
  8. 2
      packages/nocodb/tests/unit/factory/table.ts
  9. 2
      packages/nocodb/tests/unit/index.test.ts
  10. 4
      packages/nocodb/tests/unit/init/cleanupMeta.ts
  11. 5
      packages/nocodb/tests/unit/init/cleanupSakila.ts
  12. 10
      packages/nocodb/tests/unit/model/index.test.ts
  13. 499
      packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
  14. 42
      packages/nocodb/tests/unit/rest/tests/tableRow.test.ts
  15. 4
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

5
packages/nocodb-sdk/src/lib/globals.ts

@ -39,6 +39,11 @@ export enum AuditOperationTypes {
export enum AuditOperationSubTypes { export enum AuditOperationSubTypes {
UPDATE = 'UPDATE', UPDATE = 'UPDATE',
INSERT = 'INSERT', INSERT = 'INSERT',
BULK_INSERT = 'BULK_INSERT',
BULK_UPDATE = 'BULK_UPDATE',
BULK_DELETE = 'BULK_DELETE',
LINK_RECORD = 'LINK_RECORD',
UNLINK_RECORD = 'UNLINK_RECORD',
DELETE = 'DELETE', DELETE = 'DELETE',
CREATED = 'CREATED', CREATED = 'CREATED',
DELETED = 'DELETED', DELETED = 'DELETED',

132
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -1666,8 +1666,10 @@ class BaseModelSqlv2 {
datas: any[], datas: any[],
{ {
chunkSize: _chunkSize = 100, chunkSize: _chunkSize = 100,
cookie,
}: { }: {
chunkSize?: number; chunkSize?: number;
cookie?: any;
} = {} } = {}
) { ) {
try { try {
@ -1698,7 +1700,7 @@ class BaseModelSqlv2 {
.batchInsert(this.model.table_name, insertDatas, chunkSize) .batchInsert(this.model.table_name, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name); .returning(this.model.primaryKey?.column_name);
// await this.afterInsertb(insertDatas, null); await this.afterBulkInsert(insertDatas, this.dbDriver, cookie);
return response; return response;
} catch (e) { } catch (e) {
@ -1707,7 +1709,7 @@ class BaseModelSqlv2 {
} }
} }
async bulkUpdate(datas: any[]) { async bulkUpdate(datas: any[], { cookie }: { cookie?: any } = {}) {
let transaction; let transaction;
try { try {
const updateDatas = await Promise.all( const updateDatas = await Promise.all(
@ -1732,7 +1734,7 @@ class BaseModelSqlv2 {
res.push(response); res.push(response);
} }
// await this.afterUpdateb(res, transaction); await this.afterBulkUpdate(updateDatas.length, this.dbDriver, cookie);
transaction.commit(); transaction.commit();
return res; return res;
@ -1746,13 +1748,14 @@ class BaseModelSqlv2 {
async bulkUpdateAll( async bulkUpdateAll(
args: { where?: string; filterArr?: Filter[] } = {}, args: { where?: string; filterArr?: Filter[] } = {},
data data,
{ cookie }: { cookie?: any } = {}
) { ) {
let queryResponse;
try { try {
const updateData = await this.model.mapAliasToColumn(data); const updateData = await this.model.mapAliasToColumn(data);
await this.validate(updateData); await this.validate(updateData);
const pkValues = await this._extractPksValues(updateData); const pkValues = await this._extractPksValues(updateData);
let res = null;
if (pkValues) { if (pkValues) {
// pk is specified - by pass // pk is specified - by pass
} else { } else {
@ -1774,21 +1777,25 @@ class BaseModelSqlv2 {
is_group: true, is_group: true,
logical_op: 'and', logical_op: 'and',
}), }),
...(args.filterArr || []),
], ],
qb, qb,
this.dbDriver this.dbDriver
); );
qb.update(updateData); qb.update(updateData);
res = ((await qb) as any).count; queryResponse = (await qb) as any;
} }
return res;
const count = queryResponse.count || queryResponse;
await this.afterBulkUpdate(count, this.dbDriver, cookie);
return count;
} catch (e) { } catch (e) {
throw e; throw e;
} }
} }
async bulkDelete(ids: any[]) { async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) {
let transaction; let transaction;
try { try {
transaction = await this.dbDriver.transaction(); transaction = await this.dbDriver.transaction();
@ -1807,6 +1814,8 @@ class BaseModelSqlv2 {
transaction.commit(); transaction.commit();
await this.afterBulkDelete(ids.length, this.dbDriver, cookie);
return res; return res;
} catch (e) { } catch (e) {
if (transaction) transaction.rollback(); if (transaction) transaction.rollback();
@ -1816,7 +1825,10 @@ class BaseModelSqlv2 {
} }
} }
async bulkDeleteAll(args: { where?: string; filterArr?: Filter[] } = {}) { async bulkDeleteAll(
args: { where?: string; filterArr?: Filter[] } = {},
{ cookie }: { cookie?: any } = {}
) {
try { try {
await this.model.getColumns(); await this.model.getColumns();
const { where } = this._getListArgs(args); const { where } = this._getListArgs(args);
@ -1836,13 +1848,17 @@ class BaseModelSqlv2 {
is_group: true, is_group: true,
logical_op: 'and', logical_op: 'and',
}), }),
...(args.filterArr || []),
], ],
qb, qb,
this.dbDriver this.dbDriver
); );
qb.del(); qb.del();
return ((await qb) as any).count; const queryResponse = (await qb) as any;
const count = queryResponse.count || queryResponse;
await this.afterBulkDelete(count, this.dbDriver, cookie);
return count;
} catch (e) { } catch (e) {
throw e; throw e;
} }
@ -1875,6 +1891,48 @@ class BaseModelSqlv2 {
// } // }
} }
public async afterBulkUpdate(count: number, _trx: any, req): Promise<void> {
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.BULK_UPDATE,
description: DOMPurify.sanitize(
`${count} records bulk updated in ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email,
});
}
public async afterBulkDelete(count: number, _trx: any, req): Promise<void> {
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.BULK_DELETE,
description: DOMPurify.sanitize(
`${count} records bulk deleted in ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email,
});
}
public async afterBulkInsert(data: any[], _trx: any, req): Promise<void> {
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.BULK_INSERT,
description: DOMPurify.sanitize(
`${data.length} records bulk inserted into ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email,
});
}
public async beforeUpdate(data: any, _trx: any, req): Promise<void> { public async beforeUpdate(data: any, _trx: any, req): Promise<void> {
const ignoreWebhook = req.query?.ignoreWebhook; const ignoreWebhook = req.query?.ignoreWebhook;
if (ignoreWebhook) { if (ignoreWebhook) {
@ -1888,6 +1946,18 @@ class BaseModelSqlv2 {
} }
public async afterUpdate(data: any, _trx: any, req): Promise<void> { public async afterUpdate(data: any, _trx: any, req): Promise<void> {
const id = this._extractPksValues(data);
Audit.insert({
fk_model_id: this.model.id,
row_id: id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.UPDATE,
description: DOMPurify.sanitize(`${id} updated in ${this.model.title}`),
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email,
});
const ignoreWebhook = req.query?.ignoreWebhook; const ignoreWebhook = req.query?.ignoreWebhook;
if (ignoreWebhook) { if (ignoreWebhook) {
if (ignoreWebhook != 'true' && ignoreWebhook != 'false') { if (ignoreWebhook != 'true' && ignoreWebhook != 'false') {
@ -2069,10 +2139,12 @@ class BaseModelSqlv2 {
colId, colId,
rowId, rowId,
childId, childId,
cookie,
}: { }: {
colId: string; colId: string;
rowId: string; rowId: string;
childId: string; childId: string;
cookie?: any;
}) { }) {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId); const column = columns.find((c) => c.id === colId);
@ -2139,16 +2211,35 @@ class BaseModelSqlv2 {
} }
break; break;
} }
await this.afterAddChild(rowId, childId, cookie);
}
public async afterAddChild(rowId, childId, req): Promise<void> {
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.LINK_RECORD,
row_id: rowId,
description: DOMPurify.sanitize(
`Record [id:${childId}] record linked with record [id:${rowId}] record in ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email,
});
} }
async removeChild({ async removeChild({
colId, colId,
rowId, rowId,
childId, childId,
cookie,
}: { }: {
colId: string; colId: string;
rowId: string; rowId: string;
childId: string; childId: string;
cookie?: any;
}) { }) {
const columns = await this.model.getColumns(); const columns = await this.model.getColumns();
const column = columns.find((c) => c.id === colId); const column = columns.find((c) => c.id === colId);
@ -2213,6 +2304,23 @@ class BaseModelSqlv2 {
} }
break; break;
} }
await this.afterRemoveChild(rowId, childId, cookie);
}
public async afterRemoveChild(rowId, childId, req): Promise<void> {
await Audit.insert({
fk_model_id: this.model.id,
op_type: AuditOperationTypes.DATA,
op_sub_type: AuditOperationSubTypes.UNLINK_RECORD,
row_id: rowId,
description: DOMPurify.sanitize(
`Record [id:${childId}] record unlinked with record [id:${rowId}] record in ${this.model.title}`
),
// details: JSON.stringify(data),
ip: req?.clientIp,
user: req?.user?.email,
});
} }
private async extractRawQueryAndExec(qb: QueryBuilder) { private async extractRawQueryAndExec(qb: QueryBuilder) {

6
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -832,7 +832,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
if (driverType === 'mssql') { if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = NULL WHERE ?? LIKE ?`, [table.table_name, column.column_name, column.column_name, option.title]); await dbDriver.raw(`UPDATE ?? SET ?? = NULL WHERE ?? LIKE ?`, [table.table_name, column.column_name, column.column_name, option.title]);
} else { } else {
await baseModel.bulkUpdateAll({ where: `(${column.column_name},eq,${option.title})` }, { [column.column_name]: null }); await baseModel.bulkUpdateAll({ where: `(${column.column_name},eq,${option.title})` }, { [column.column_name]: null }, { cookie: req});
} }
} else if (column.uidt === UITypes.MultiSelect) { } else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') { if (driverType === 'mysql' || driverType === 'mysql2') {
@ -930,7 +930,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
if (driverType === 'mssql') { if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.column_name, newOp.title, column.column_name, option.title]); await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.column_name, newOp.title, column.column_name, option.title]);
} else { } else {
await baseModel.bulkUpdateAll({ where: `(${column.column_name},eq,${option.title})` }, { [column.column_name]: newOp.title }); await baseModel.bulkUpdateAll({ where: `(${column.column_name},eq,${option.title})` }, { [column.column_name]: newOp.title }, { cookie: req});
} }
} else if (column.uidt === UITypes.MultiSelect) { } else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') { if (driverType === 'mysql' || driverType === 'mysql2') {
@ -951,7 +951,7 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
if (driverType === 'mssql') { if (driverType === 'mssql') {
await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.column_name, newOp.title, column.column_name, ch.temp_title]); await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.column_name, newOp.title, column.column_name, ch.temp_title]);
} else { } else {
await baseModel.bulkUpdateAll({ where: `(${column.column_name},eq,${ch.temp_title})` }, { [column.column_name]: newOp.title }); await baseModel.bulkUpdateAll({ where: `(${column.column_name},eq,${ch.temp_title})` }, { [column.column_name]: newOp.title }, { cookie: req});
} }
} else if (column.uidt === UITypes.MultiSelect) { } else if (column.uidt === UITypes.MultiSelect) {
if (driverType === 'mysql' || driverType === 'mysql2') { if (driverType === 'mysql' || driverType === 'mysql2') {

11
packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts

@ -17,7 +17,7 @@ async function bulkDataInsert(req: Request, res: Response) {
dbDriver: NcConnectionMgrv2.get(base), dbDriver: NcConnectionMgrv2.get(base),
}); });
res.json(await baseModel.bulkInsert(req.body)); res.json(await baseModel.bulkInsert(req.body, { cookie: req }));
} }
async function bulkDataUpdate(req: Request, res: Response) { async function bulkDataUpdate(req: Request, res: Response) {
@ -30,9 +30,10 @@ async function bulkDataUpdate(req: Request, res: Response) {
dbDriver: NcConnectionMgrv2.get(base), dbDriver: NcConnectionMgrv2.get(base),
}); });
res.json(await baseModel.bulkUpdate(req.body)); res.json(await baseModel.bulkUpdate(req.body, { cookie: req }));
} }
// todo: Integrate with filterArrJson bulkDataUpdateAll
async function bulkDataUpdateAll(req: Request, res: Response) { async function bulkDataUpdateAll(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
const base = await Base.get(model.base_id); const base = await Base.get(model.base_id);
@ -43,7 +44,7 @@ async function bulkDataUpdateAll(req: Request, res: Response) {
dbDriver: NcConnectionMgrv2.get(base), dbDriver: NcConnectionMgrv2.get(base),
}); });
res.json(await baseModel.bulkUpdateAll(req.query, req.body)); res.json(await baseModel.bulkUpdateAll(req.query, req.body, { cookie: req }));
} }
async function bulkDataDelete(req: Request, res: Response) { async function bulkDataDelete(req: Request, res: Response) {
@ -55,10 +56,10 @@ async function bulkDataDelete(req: Request, res: Response) {
dbDriver: NcConnectionMgrv2.get(base), dbDriver: NcConnectionMgrv2.get(base),
}); });
res.json(await baseModel.bulkDelete(req.body)); res.json(await baseModel.bulkDelete(req.body, { cookie: req }));
} }
// todo: Integrate with filterArrJson bulkDataDelete // todo: Integrate with filterArrJson bulkDataDeleteAll
async function bulkDataDeleteAll(req: Request, res: Response) { async function bulkDataDeleteAll(req: Request, res: Response) {
const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); const { model, view } = await getViewAndModelFromRequestByAliasOrId(req);
const base = await Base.get(model.base_id); const base = await Base.get(model.base_id);

8
packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts

@ -4,7 +4,10 @@ import Base from '../../../models/Base';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import { PagedResponseImpl } from '../../helpers/PagedResponse'; import { PagedResponseImpl } from '../../helpers/PagedResponse';
import ncMetaAclMw from '../../helpers/ncMetaAclMw'; import ncMetaAclMw from '../../helpers/ncMetaAclMw';
import { getColumnByIdOrName, getViewAndModelFromRequestByAliasOrId } from './helpers' import {
getColumnByIdOrName,
getViewAndModelFromRequestByAliasOrId,
} from './helpers';
import { NcError } from '../../helpers/catchError'; import { NcError } from '../../helpers/catchError';
import apiMetrics from '../../helpers/apiMetrics'; import apiMetrics from '../../helpers/apiMetrics';
@ -214,6 +217,7 @@ async function relationDataRemove(req, res) {
colId: column.id, colId: column.id,
childId: req.params.refRowId, childId: req.params.refRowId,
rowId: req.params.rowId, rowId: req.params.rowId,
cookie: req,
}); });
res.json({ msg: 'success' }); res.json({ msg: 'success' });
@ -238,12 +242,12 @@ async function relationDataAdd(req, res) {
colId: column.id, colId: column.id,
childId: req.params.refRowId, childId: req.params.refRowId,
rowId: req.params.rowId, rowId: req.params.rowId,
cookie: req,
}); });
res.json({ msg: 'success' }); res.json({ msg: 'success' });
} }
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
router.get( router.get(

2
packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts

@ -494,6 +494,7 @@ async function relationDataDelete(req, res) {
colId: req.params.colId, colId: req.params.colId,
childId: req.params.childId, childId: req.params.childId,
rowId: req.params.rowId, rowId: req.params.rowId,
cookie: req,
}); });
res.json({ msg: 'success' }); res.json({ msg: 'success' });
@ -521,6 +522,7 @@ async function relationDataAdd(req, res) {
colId: req.params.colId, colId: req.params.colId,
childId: req.params.childId, childId: req.params.childId,
rowId: req.params.rowId, rowId: req.params.rowId,
cookie: req,
}); });
res.json({ msg: 'success' }); res.json({ msg: 'success' });

10
packages/nocodb/tests/unit/factory/row.ts

@ -70,10 +70,10 @@ const getOneRow = async (
const generateDefaultRowAttributes = ({ const generateDefaultRowAttributes = ({
columns, columns,
index, index = 0,
}: { }: {
columns: ColumnType[]; columns: ColumnType[];
index: number; index?: number;
}) => }) =>
columns.reduce((acc, column) => { columns.reduce((acc, column) => {
if ( if (
@ -83,7 +83,7 @@ const generateDefaultRowAttributes = ({
) { ) {
return acc; return acc;
} }
acc[column.column_name] = rowValue(column, index); acc[column.title!] = rowValue(column, index);
return acc; return acc;
}, {}); }, {});
@ -111,7 +111,7 @@ const createRow = async (
}; };
// Links 2 table rows together. Will create rows if ids are not provided // Links 2 table rows together. Will create rows if ids are not provided
const createRelation = async ( const createChildRow = async (
context, context,
{ {
project, project,
@ -155,7 +155,7 @@ const createRelation = async (
export { export {
createRow, createRow,
getRow, getRow,
createRelation, createChildRow,
getOneRow, getOneRow,
listRow, listRow,
generateDefaultRowAttributes, generateDefaultRowAttributes,

2
packages/nocodb/tests/unit/factory/table.ts

@ -15,7 +15,7 @@ const createTable = async (context, project, args = {}) => {
.set('xc-auth', context.token) .set('xc-auth', context.token)
.send({ ...defaultTableValue, ...args }); .send({ ...defaultTableValue, ...args });
const table = await Model.get(response.body.id); const table: Model = await Model.get(response.body.id);
return table; return table;
}; };

2
packages/nocodb/tests/unit/index.test.ts

@ -3,6 +3,7 @@ import 'mocha';
import knex from 'knex'; import knex from 'knex';
import { dbName } from './dbConfig'; import { dbName } from './dbConfig';
import restTests from './rest/index.test'; import restTests from './rest/index.test';
import modelTests from './model/index.test';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
process.env.TEST = 'test'; process.env.TEST = 'test';
@ -29,6 +30,7 @@ const setupTestMetaDb = async () => {
(async function() { (async function() {
await setupTestMetaDb(); await setupTestMetaDb();
modelTests();
restTests(); restTests();
run(); run();

4
packages/nocodb/tests/unit/init/cleanupMeta.ts

@ -5,7 +5,7 @@ import { orderedMetaTables } from "../../../src/lib/utils/globals";
const dropTablesAllNonExternalProjects = async (knexClient) => { const dropTablesAllNonExternalProjects = async (knexClient) => {
const projects = await Project.list({}); const projects = await Project.list({});
const userCreatedTableNames = []; const userCreatedTableNames: string[] = [];
await Promise.all( await Promise.all(
projects projects
.filter((project) => project.is_meta) .filter((project) => project.is_meta)
@ -16,7 +16,7 @@ const dropTablesAllNonExternalProjects = async (knexClient) => {
const models = await Model.list({ const models = await Model.list({
project_id: project.id, project_id: project.id,
base_id: base.id, base_id: base.id!,
}); });
models.forEach((model) => { models.forEach((model) => {
userCreatedTableNames.push(model.table_name); userCreatedTableNames.push(model.table_name);

5
packages/nocodb/tests/unit/init/cleanupSakila.ts

@ -45,10 +45,9 @@ const cleanUpSakila = async (sakilaKnexClient) => {
try { try {
const sakilaProject = await Project.getByTitle('sakila'); const sakilaProject = await Project.getByTitle('sakila');
const audits = sakilaProject && await Audit.projectAuditList(sakilaProject.id, {limit: 10}); const audits = sakilaProject && await Audit.projectAuditList(sakilaProject.id, {});
if(audits?.length > 0 || global.touchedSakilaDb) { if(audits?.length > 0) {
global.touchedSakilaDb = false;
return await resetAndSeedSakila(sakilaKnexClient); return await resetAndSeedSakila(sakilaKnexClient);
} }

10
packages/nocodb/tests/unit/model/index.test.ts

@ -0,0 +1,10 @@
import 'mocha';
import baseModelSqlTest from './tests/baseModelSql.test';
function modelTests() {
baseModelSqlTest();
}
export default function () {
describe('Model', modelTests);
}

499
packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts

@ -0,0 +1,499 @@
import 'mocha';
import init from '../../init';
import { BaseModelSqlv2 } from '../../../../src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2';
import { createProject } from '../../factory/project';
import { createTable } from '../../factory/table';
import NcConnectionMgrv2 from '../../../../src/lib/utils/common/NcConnectionMgrv2';
import Base from '../../../../src/lib/models/Base';
import Model from '../../../../src/lib/models/Model';
import Project from '../../../../src/lib/models/Project';
import View from '../../../../src/lib/models/View';
import { createRow, generateDefaultRowAttributes } from '../../factory/row';
import Audit from '../../../../src/lib/models/Audit';
import { expect } from 'chai';
import Filter from '../../../../src/lib/models/Filter';
import { createLtarColumn } from '../../factory/column';
import LinkToAnotherRecordColumn from '../../../../src/lib/models/LinkToAnotherRecordColumn';
function baseModelSqlTests() {
let context;
let project: Project;
let table: Model;
let view: View;
let baseModelSql: BaseModelSqlv2;
beforeEach(async function () {
context = await init();
project = await createProject(context);
table = await createTable(context, project);
view = table.getViews()[0];
const base = await Base.get(table.base_id);
baseModelSql = new BaseModelSqlv2({
dbDriver: NcConnectionMgrv2.get(base),
model: table,
view
})
});
it('Insert record', async () => {
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const columns = await table.getColumns();
const inputData = generateDefaultRowAttributes({columns})
const response = await baseModelSql.insert(generateDefaultRowAttributes({columns}), undefined, request);
const insertedRow = (await baseModelSql.list())[0];
expect(insertedRow).to.include(inputData);
expect(insertedRow).to.include(response);
const rowInsertedAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'INSERT');
expect(rowInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: '1',
op_type: 'DATA',
op_sub_type: 'INSERT',
description: '1 inserted into Table1_Title',
});
});
it('Bulk insert record', async () => {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const bulkData = Array(10).fill(0).map((_, index) => generateDefaultRowAttributes({columns, index}))
await baseModelSql.bulkInsert(bulkData, {cookie:request});
const insertedRows = await baseModelSql.list();
bulkData.forEach((inputData, index) => {
expect(insertedRows[index]).to.include(inputData);
});
const rowBulkInsertedAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'BULK_INSERT');;
expect(rowBulkInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: null,
op_type: 'DATA',
op_sub_type: 'BULK_INSERT',
status: null,
description: '10 records bulk inserted into Table1_Title',
details: null,
});
});
it('Update record', async () => {
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const columns = await table.getColumns();
await baseModelSql.insert(generateDefaultRowAttributes({columns}));
const rowId = 1;
await baseModelSql.updateByPk(rowId, {Title: 'test'},undefined, request);
const updatedRow = await baseModelSql.readByPk(1);
expect(updatedRow).to.include({Id: rowId, Title: 'test'});
const rowUpdatedAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'UPDATE');
expect(rowUpdatedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: '1',
op_type: 'DATA',
op_sub_type: 'UPDATE',
description: '1 updated in Table1_Title',
});
});
it('Bulk update record', async () => {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const bulkData = Array(10).fill(0).map((_, index) => generateDefaultRowAttributes({columns, index}))
await baseModelSql.bulkInsert(bulkData, {cookie:request});
const insertedRows: any[] = await baseModelSql.list();
await baseModelSql.bulkUpdate(insertedRows.map((row)=> ({...row, Title: `new-${row['Title']}`})), { cookie: request });
const updatedRows = await baseModelSql.list();
updatedRows.forEach((row, index) => {
expect(row['Title']).to.equal(`new-test-${index}`);
})
const rowBulkUpdateAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'BULK_UPDATE');
expect(rowBulkUpdateAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
fk_model_id: table.id,
project_id: project.id,
row_id: null,
op_type: 'DATA',
op_sub_type: 'BULK_UPDATE',
status: null,
description: '10 records bulk updated in Table1_Title',
details: null,
});
});
it('Bulk update all record', async () => {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const bulkData = Array(10).fill(0).map((_, index) => generateDefaultRowAttributes({columns, index}))
await baseModelSql.bulkInsert(bulkData, {cookie:request});
const idColumn = columns.find((column) => column.title === 'Id')!;
await baseModelSql.bulkUpdateAll({filterArr: [
new Filter({
logical_op: 'and',
fk_column_id: idColumn.id,
comparison_op: 'lt',
value: 5,
})
]}, ({Title: 'new-1'}), { cookie: request });
const updatedRows = await baseModelSql.list();
updatedRows.forEach((row) => {
if(row.id < 5) expect(row['Title']).to.equal('new-1');
})
const rowBulkUpdateAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'BULK_UPDATE');
expect(rowBulkUpdateAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
fk_model_id: table.id,
project_id: project.id,
row_id: null,
op_type: 'DATA',
op_sub_type: 'BULK_UPDATE',
status: null,
description: '4 records bulk updated in Table1_Title',
details: null,
});
});
it('Delete record', async () => {
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'},
params: {id: 1}
}
const columns = await table.getColumns();
const bulkData = Array(10).fill(0).map((_, index) => generateDefaultRowAttributes({columns, index}))
await baseModelSql.bulkInsert(bulkData, {cookie:request});
const rowIdToDeleted = 1;
await baseModelSql.delByPk(rowIdToDeleted,undefined ,request);
const deletedRow = await baseModelSql.readByPk(rowIdToDeleted);
expect(deletedRow).to.be.undefined;
const rowDeletedAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'DELETE');
expect(rowDeletedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: '1',
op_type: 'DATA',
op_sub_type: 'DELETE',
description: '1 deleted from Table1_Title',
});
});
it('Bulk delete records', async () => {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const bulkData = Array(10).fill(0).map((_, index) => generateDefaultRowAttributes({columns, index}))
await baseModelSql.bulkInsert(bulkData, {cookie:request});
const insertedRows: any[] = await baseModelSql.list();
await baseModelSql.bulkDelete(
insertedRows
.filter((row) => row['Id'] < 5)
.map((row)=> ({'id': row['Id']})),
{ cookie: request }
);
const remainingRows = await baseModelSql.list();
expect(remainingRows).to.length(6);
const rowBulkDeleteAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'BULK_DELETE');
expect(rowBulkDeleteAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
fk_model_id: table.id,
project_id: project.id,
row_id: null,
op_type: 'DATA',
op_sub_type: 'BULK_DELETE',
status: null,
description: '4 records bulk deleted in Table1_Title',
details: null,
});
});
it('Bulk delete all record', async () => {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
const bulkData = Array(10).fill(0).map((_, index) => generateDefaultRowAttributes({columns, index}))
await baseModelSql.bulkInsert(bulkData, {cookie:request});
const idColumn = columns.find((column) => column.title === 'Id')!;
await baseModelSql.bulkDeleteAll({filterArr: [
new Filter({
logical_op: 'and',
fk_column_id: idColumn.id,
comparison_op: 'lt',
value: 5,
})
]}, { cookie: request });
const remainingRows = await baseModelSql.list();
expect(remainingRows).to.length(6);
const rowBulkDeleteAudit = (await Audit.projectAuditList(project.id, {})).find((audit) => audit.op_sub_type === 'BULK_DELETE');
expect(rowBulkDeleteAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
fk_model_id: table.id,
project_id: project.id,
row_id: null,
op_type: 'DATA',
op_sub_type: 'BULK_DELETE',
status: null,
description: '4 records bulk deleted in Table1_Title',
details: null,
});
});
it('Nested insert', async () => {
const childTable = await createTable(context, project, {
title: 'Child Table',
table_name: 'child_table',
})
const ltarColumn = await createLtarColumn(context, {
title: 'Ltar Column',
parentTable: table,
childTable,
type: "hm"
})
const childRow = await createRow(context, {
project,
table: childTable,
})
const ltarColOptions = await ltarColumn.getColOptions<LinkToAnotherRecordColumn>();
const childCol = await ltarColOptions.getChildColumn();
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
await baseModelSql.nestedInsert(
{...generateDefaultRowAttributes({columns}), [ltarColumn.title]: [{'Id': childRow['Id']}]},
undefined,
request
);
const childBaseModel = new BaseModelSqlv2({
dbDriver: NcConnectionMgrv2.get(await Base.get(table.base_id)),
model: childTable,
view
})
const insertedChildRow = await childBaseModel.readByPk(childRow['Id']);
expect(insertedChildRow[childCol.column_name]).to.equal(childRow['Id']);
const rowInsertedAudit = (await Audit.projectAuditList(project.id, {}))
.filter((audit) => audit.fk_model_id === table.id)
.find((audit) => audit.op_sub_type === 'INSERT');
expect(rowInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: '1',
op_type: 'DATA',
op_sub_type: 'INSERT',
description: '1 inserted into Table1_Title',
});
})
it('Link child', async () => {
const childTable = await createTable(context, project, {
title: 'Child Table',
table_name: 'child_table',
})
const ltarColumn = await createLtarColumn(context, {
title: 'Ltar Column',
parentTable: table,
childTable,
type: "hm"
})
const insertedChildRow = await createRow(context, {
project,
table: childTable,
})
const ltarColOptions = await ltarColumn.getColOptions<LinkToAnotherRecordColumn>();
const childCol = await ltarColOptions.getChildColumn();
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
await baseModelSql.insert(generateDefaultRowAttributes({columns}), undefined, request);
const insertedRow = await baseModelSql.readByPk(1);
await baseModelSql.addChild({
colId: ltarColumn.id,
rowId: insertedRow['Id'],
childId: insertedChildRow['Id'],
cookie: request
});
const childBaseModel = new BaseModelSqlv2({
dbDriver: NcConnectionMgrv2.get(await Base.get(table.base_id)),
model: childTable,
view
})
const updatedChildRow = await childBaseModel.readByPk(insertedChildRow['Id']);
expect(updatedChildRow[childCol.column_name]).to.equal(insertedRow['Id']);
const rowInsertedAudit = (await Audit.projectAuditList(project.id, {}))
.filter((audit) => audit.fk_model_id === table.id)
.find((audit) => audit.op_sub_type === 'LINK_RECORD');
expect(rowInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: '1',
op_type: 'DATA',
op_sub_type: 'LINK_RECORD',
description: 'Record [id:1] record linked with record [id:1] record in Table1_Title',
});
})
it('Unlink child', async () => {
const childTable = await createTable(context, project, {
title: 'Child Table',
table_name: 'child_table',
})
const ltarColumn = await createLtarColumn(context, {
title: 'Ltar Column',
parentTable: table,
childTable,
type: "hm"
})
const insertedChildRow = await createRow(context, {
project,
table: childTable,
})
const ltarColOptions = await ltarColumn.getColOptions<LinkToAnotherRecordColumn>();
const childCol = await ltarColOptions.getChildColumn();
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: {email: 'test@example.com'}
}
await baseModelSql.insert(generateDefaultRowAttributes({columns}), undefined, request);
const insertedRow = await baseModelSql.readByPk(1);
await baseModelSql.addChild({
colId: ltarColumn.id,
rowId: insertedRow['Id'],
childId: insertedChildRow['Id'],
cookie: request
});
await baseModelSql.removeChild({
colId: ltarColumn.id,
rowId: insertedRow['Id'],
childId: insertedChildRow['Id'],
cookie: request
});
const childBaseModel = new BaseModelSqlv2({
dbDriver: NcConnectionMgrv2.get(await Base.get(table.base_id)),
model: childTable,
view
})
const updatedChildRow = await childBaseModel.readByPk(insertedChildRow['Id']);
expect(updatedChildRow[childCol.column_name]).to.be.null;
const rowInsertedAudit = (await Audit.projectAuditList(project.id, {}))
.filter((audit) => audit.fk_model_id === table.id)
.find((audit) => audit.op_sub_type === 'UNLINK_RECORD');
expect(rowInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
base_id: null,
project_id: project.id,
fk_model_id: table.id,
row_id: '1',
op_type: 'DATA',
op_sub_type: 'UNLINK_RECORD',
description: 'Record [id:1] record unlinked with record [id:1] record in Table1_Title',
});
})
}
export default function () {
describe('BaseModelSql', baseModelSqlTests);
}

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

@ -11,7 +11,7 @@ import {
} from '../../factory/column'; } from '../../factory/column';
import { createTable, getTable } from '../../factory/table'; import { createTable, getTable } from '../../factory/table';
import { import {
createRelation, createChildRow,
createRow, createRow,
generateDefaultRowAttributes, generateDefaultRowAttributes,
getOneRow, getOneRow,
@ -1182,7 +1182,7 @@ function tableTest() {
const row = await createRow(context, { project, table }); const row = await createRow(context, { project, table });
await createRelation(context, { await createChildRow(context, {
project, project,
table, table,
childTable: relatedTable, childTable: relatedTable,
@ -1365,6 +1365,42 @@ function tableTest() {
} }
}); });
// todo: Integrate filterArrJson with bulk delete all and update all
// it.only('Bulk delete all with condition', async function () {
// const table = await createTable(context, project);
// const columns = await table.getColumns();
// const idColumn = columns.find((column) => column.title === 'Id')!;
// const arr = Array(120)
// .fill(0)
// .map((_, index) => index);
// for (const index of arr) {
// await createRow(context, { project, table, index });
// }
// const rows = await listRow({ project, table });
// await request(context.app)
// .delete(`/api/v1/db/data/bulk/noco/${project.id}/${table.id}/all`)
// .set('xc-auth', context.token)
// .query({ filterArr: [
// {
// logical_op: 'and',
// fk_column_id: idColumn.id,
// comparison_op: 'lt',
// value: 20,
// }
// ]})
// .send(rows.map((row) => ({ id: row['Id'] })))
// .expect(200);
// const updatedRows: Array<any> = await listRow({ project, table });
// if (updatedRows.length !== 0) {
// console.log(updatedRows.length)
// throw new Error('Wrong number of rows delete');
// }
// });
// todo: add test for bulk delete with ltar but need filterArrJson. filterArrJson not now supported with this api. // todo: add test for bulk delete with ltar but need filterArrJson. filterArrJson not now supported with this api.
// it.only('Bulk update nested filtered table data list with a lookup column', async function () { // it.only('Bulk update nested filtered table data list with a lookup column', async function () {
// }); // });
@ -1800,7 +1836,7 @@ function tableTest() {
type: 'hm', type: 'hm',
}); });
const row = await createRelation(context, { project, table,childTable: relatedTable, column:ltarColumn, type: 'hm' }); const row = await createChildRow(context, { project, table,childTable: relatedTable, column:ltarColumn, type: 'hm' });
const childRow = row['Ltar'][0] const childRow = row['Ltar'][0]
const response = await request(context.app) const response = await request(context.app)

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

@ -9,7 +9,7 @@ import View from '../../../../src/lib/models/View';
import { ColumnType, UITypes, ViewTypes } from 'nocodb-sdk'; import { ColumnType, UITypes, ViewTypes } from 'nocodb-sdk';
import { createView } from '../../factory/view'; import { createView } from '../../factory/view';
import { createColumn, createLookupColumn, createLtarColumn, createRollupColumn, updateViewColumn } from '../../factory/column'; import { createColumn, createLookupColumn, createLtarColumn, createRollupColumn, updateViewColumn } from '../../factory/column';
import { createRelation, createRow, getOneRow, getRow } from '../../factory/row'; import { createChildRow, createRow, getOneRow, getRow } from '../../factory/row';
const isColumnsCorrectInResponse = (row, columns: ColumnType[]) => { const isColumnsCorrectInResponse = (row, columns: ColumnType[]) => {
const responseColumnsListStr = Object.keys(row).sort().join(','); const responseColumnsListStr = Object.keys(row).sort().join(',');
@ -1081,7 +1081,7 @@ function viewRowTests() {
const row = await createRow(context, { project, table }); const row = await createRow(context, { project, table });
await createRelation(context, { await createChildRow(context, {
project, project,
table, table,
childTable: relatedTable, childTable: relatedTable,

Loading…
Cancel
Save