From d4da9595d1b70d1795ded47a6810b4f4dc2a6dbe Mon Sep 17 00:00:00 2001 From: Muhammed Mustafa Date: Sat, 10 Sep 2022 12:03:13 +0530 Subject: [PATCH] refactor/Added unit test for BaseModelSqlV2, added creating audit for all the write action in BaseModelSqlV2, and some cleanups --- packages/nocodb-sdk/src/lib/globals.ts | 5 + .../sql-data-mapper/lib/sql/BaseModelSqlv2.ts | 132 ++++- .../nocodb/src/lib/meta/api/columnApis.ts | 6 +- .../meta/api/dataApis/bulkDataAliasApis.ts | 11 +- .../meta/api/dataApis/dataAliasNestedApis.ts | 8 +- .../src/lib/meta/api/dataApis/dataApis.ts | 2 + packages/nocodb/tests/unit/factory/row.ts | 10 +- packages/nocodb/tests/unit/factory/table.ts | 2 +- packages/nocodb/tests/unit/index.test.ts | 2 + .../nocodb/tests/unit/init/cleanupMeta.ts | 4 +- .../nocodb/tests/unit/init/cleanupSakila.ts | 5 +- .../nocodb/tests/unit/model/index.test.ts | 10 + .../unit/model/tests/baseModelSql.test.ts | 499 ++++++++++++++++++ .../tests/unit/rest/tests/tableRow.test.ts | 42 +- .../tests/unit/rest/tests/viewRow.test.ts | 4 +- 15 files changed, 704 insertions(+), 38 deletions(-) create mode 100644 packages/nocodb/tests/unit/model/index.test.ts diff --git a/packages/nocodb-sdk/src/lib/globals.ts b/packages/nocodb-sdk/src/lib/globals.ts index b63424b42a..3e30e8c11e 100644 --- a/packages/nocodb-sdk/src/lib/globals.ts +++ b/packages/nocodb-sdk/src/lib/globals.ts @@ -39,6 +39,11 @@ export enum AuditOperationTypes { export enum AuditOperationSubTypes { UPDATE = 'UPDATE', INSERT = 'INSERT', + BULK_INSERT = 'BULK_INSERT', + BULK_UPDATE = 'BULK_UPDATE', + BULK_DELETE = 'BULK_DELETE', + LINK_RECORD = 'LINK_RECORD', + UNLINK_RECORD = 'UNLINK_RECORD', DELETE = 'DELETE', CREATED = 'CREATED', DELETED = 'DELETED', diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts index 59c31e360d..fd9762b608 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts @@ -1666,8 +1666,10 @@ class BaseModelSqlv2 { datas: any[], { chunkSize: _chunkSize = 100, + cookie, }: { chunkSize?: number; + cookie?: any; } = {} ) { try { @@ -1698,7 +1700,7 @@ class BaseModelSqlv2 { .batchInsert(this.model.table_name, insertDatas, chunkSize) .returning(this.model.primaryKey?.column_name); - // await this.afterInsertb(insertDatas, null); + await this.afterBulkInsert(insertDatas, this.dbDriver, cookie); return response; } catch (e) { @@ -1707,7 +1709,7 @@ class BaseModelSqlv2 { } } - async bulkUpdate(datas: any[]) { + async bulkUpdate(datas: any[], { cookie }: { cookie?: any } = {}) { let transaction; try { const updateDatas = await Promise.all( @@ -1732,7 +1734,7 @@ class BaseModelSqlv2 { res.push(response); } - // await this.afterUpdateb(res, transaction); + await this.afterBulkUpdate(updateDatas.length, this.dbDriver, cookie); transaction.commit(); return res; @@ -1746,13 +1748,14 @@ class BaseModelSqlv2 { async bulkUpdateAll( args: { where?: string; filterArr?: Filter[] } = {}, - data + data, + { cookie }: { cookie?: any } = {} ) { + let queryResponse; try { const updateData = await this.model.mapAliasToColumn(data); await this.validate(updateData); const pkValues = await this._extractPksValues(updateData); - let res = null; if (pkValues) { // pk is specified - by pass } else { @@ -1774,21 +1777,25 @@ class BaseModelSqlv2 { is_group: true, logical_op: 'and', }), - ...(args.filterArr || []), ], qb, this.dbDriver ); + 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) { throw e; } } - async bulkDelete(ids: any[]) { + async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) { let transaction; try { transaction = await this.dbDriver.transaction(); @@ -1807,6 +1814,8 @@ class BaseModelSqlv2 { transaction.commit(); + await this.afterBulkDelete(ids.length, this.dbDriver, cookie); + return res; } catch (e) { 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 { await this.model.getColumns(); const { where } = this._getListArgs(args); @@ -1836,13 +1848,17 @@ class BaseModelSqlv2 { is_group: true, logical_op: 'and', }), - ...(args.filterArr || []), ], qb, this.dbDriver ); 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) { throw e; } @@ -1875,6 +1891,48 @@ class BaseModelSqlv2 { // } } + public async afterBulkUpdate(count: number, _trx: any, req): Promise { + 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 { + 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 { + 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 { const ignoreWebhook = req.query?.ignoreWebhook; if (ignoreWebhook) { @@ -1888,6 +1946,18 @@ class BaseModelSqlv2 { } public async afterUpdate(data: any, _trx: any, req): Promise { + 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; if (ignoreWebhook) { if (ignoreWebhook != 'true' && ignoreWebhook != 'false') { @@ -2069,10 +2139,12 @@ class BaseModelSqlv2 { colId, rowId, childId, + cookie, }: { colId: string; rowId: string; childId: string; + cookie?: any; }) { const columns = await this.model.getColumns(); const column = columns.find((c) => c.id === colId); @@ -2139,16 +2211,35 @@ class BaseModelSqlv2 { } break; } + + await this.afterAddChild(rowId, childId, cookie); + } + + public async afterAddChild(rowId, childId, req): Promise { + 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({ colId, rowId, childId, + cookie, }: { colId: string; rowId: string; childId: string; + cookie?: any; }) { const columns = await this.model.getColumns(); const column = columns.find((c) => c.id === colId); @@ -2213,6 +2304,23 @@ class BaseModelSqlv2 { } break; } + + await this.afterRemoveChild(rowId, childId, cookie); + } + + public async afterRemoveChild(rowId, childId, req): Promise { + 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) { diff --git a/packages/nocodb/src/lib/meta/api/columnApis.ts b/packages/nocodb/src/lib/meta/api/columnApis.ts index 1ff79397cb..e2513a8a21 100644 --- a/packages/nocodb/src/lib/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/meta/api/columnApis.ts @@ -832,7 +832,7 @@ export async function columnUpdate(req: Request, res: Response) { if (driverType === 'mssql') { await dbDriver.raw(`UPDATE ?? SET ?? = NULL WHERE ?? LIKE ?`, [table.table_name, column.column_name, column.column_name, option.title]); } 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) { if (driverType === 'mysql' || driverType === 'mysql2') { @@ -930,7 +930,7 @@ export async function columnUpdate(req: Request, res: Response) { if (driverType === 'mssql') { await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.column_name, newOp.title, column.column_name, option.title]); } 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) { if (driverType === 'mysql' || driverType === 'mysql2') { @@ -951,7 +951,7 @@ export async function columnUpdate(req: Request, res: Response) { if (driverType === 'mssql') { await dbDriver.raw(`UPDATE ?? SET ?? = ? WHERE ?? LIKE ?`, [table.table_name, column.column_name, newOp.title, column.column_name, ch.temp_title]); } 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) { if (driverType === 'mysql' || driverType === 'mysql2') { diff --git a/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts index bf357b4343..413ecad711 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts +++ b/packages/nocodb/src/lib/meta/api/dataApis/bulkDataAliasApis.ts @@ -17,7 +17,7 @@ async function bulkDataInsert(req: Request, res: Response) { 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) { @@ -30,9 +30,10 @@ async function bulkDataUpdate(req: Request, res: Response) { 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) { const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); const base = await Base.get(model.base_id); @@ -43,7 +44,7 @@ async function bulkDataUpdateAll(req: Request, res: Response) { 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) { @@ -55,10 +56,10 @@ async function bulkDataDelete(req: Request, res: Response) { 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) { const { model, view } = await getViewAndModelFromRequestByAliasOrId(req); const base = await Base.get(model.base_id); diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts index f084386d92..d260694fe5 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataAliasNestedApis.ts +++ b/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 { PagedResponseImpl } from '../../helpers/PagedResponse'; import ncMetaAclMw from '../../helpers/ncMetaAclMw'; -import { getColumnByIdOrName, getViewAndModelFromRequestByAliasOrId } from './helpers' +import { + getColumnByIdOrName, + getViewAndModelFromRequestByAliasOrId, +} from './helpers'; import { NcError } from '../../helpers/catchError'; import apiMetrics from '../../helpers/apiMetrics'; @@ -214,6 +217,7 @@ async function relationDataRemove(req, res) { colId: column.id, childId: req.params.refRowId, rowId: req.params.rowId, + cookie: req, }); res.json({ msg: 'success' }); @@ -238,12 +242,12 @@ async function relationDataAdd(req, res) { colId: column.id, childId: req.params.refRowId, rowId: req.params.rowId, + cookie: req, }); res.json({ msg: 'success' }); } - const router = Router({ mergeParams: true }); router.get( diff --git a/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts b/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts index 2635478eaa..a896ae2a99 100644 --- a/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts +++ b/packages/nocodb/src/lib/meta/api/dataApis/dataApis.ts @@ -494,6 +494,7 @@ async function relationDataDelete(req, res) { colId: req.params.colId, childId: req.params.childId, rowId: req.params.rowId, + cookie: req, }); res.json({ msg: 'success' }); @@ -521,6 +522,7 @@ async function relationDataAdd(req, res) { colId: req.params.colId, childId: req.params.childId, rowId: req.params.rowId, + cookie: req, }); res.json({ msg: 'success' }); diff --git a/packages/nocodb/tests/unit/factory/row.ts b/packages/nocodb/tests/unit/factory/row.ts index 29b4682402..8f6fbf1f62 100644 --- a/packages/nocodb/tests/unit/factory/row.ts +++ b/packages/nocodb/tests/unit/factory/row.ts @@ -70,10 +70,10 @@ const getOneRow = async ( const generateDefaultRowAttributes = ({ columns, - index, + index = 0, }: { columns: ColumnType[]; - index: number; + index?: number; }) => columns.reduce((acc, column) => { if ( @@ -83,7 +83,7 @@ const generateDefaultRowAttributes = ({ ) { return acc; } - acc[column.column_name] = rowValue(column, index); + acc[column.title!] = rowValue(column, index); return acc; }, {}); @@ -111,7 +111,7 @@ const createRow = async ( }; // Links 2 table rows together. Will create rows if ids are not provided -const createRelation = async ( +const createChildRow = async ( context, { project, @@ -155,7 +155,7 @@ const createRelation = async ( export { createRow, getRow, - createRelation, + createChildRow, getOneRow, listRow, generateDefaultRowAttributes, diff --git a/packages/nocodb/tests/unit/factory/table.ts b/packages/nocodb/tests/unit/factory/table.ts index 2b64e6cff4..8eb1724769 100644 --- a/packages/nocodb/tests/unit/factory/table.ts +++ b/packages/nocodb/tests/unit/factory/table.ts @@ -15,7 +15,7 @@ const createTable = async (context, project, args = {}) => { .set('xc-auth', context.token) .send({ ...defaultTableValue, ...args }); - const table = await Model.get(response.body.id); + const table: Model = await Model.get(response.body.id); return table; }; diff --git a/packages/nocodb/tests/unit/index.test.ts b/packages/nocodb/tests/unit/index.test.ts index 5147d7757d..fdafe524fc 100644 --- a/packages/nocodb/tests/unit/index.test.ts +++ b/packages/nocodb/tests/unit/index.test.ts @@ -3,6 +3,7 @@ import 'mocha'; import knex from 'knex'; import { dbName } from './dbConfig'; import restTests from './rest/index.test'; +import modelTests from './model/index.test'; process.env.NODE_ENV = 'test'; process.env.TEST = 'test'; @@ -29,6 +30,7 @@ const setupTestMetaDb = async () => { (async function() { await setupTestMetaDb(); + modelTests(); restTests(); run(); diff --git a/packages/nocodb/tests/unit/init/cleanupMeta.ts b/packages/nocodb/tests/unit/init/cleanupMeta.ts index ae7dd5f613..672dc0e4e1 100644 --- a/packages/nocodb/tests/unit/init/cleanupMeta.ts +++ b/packages/nocodb/tests/unit/init/cleanupMeta.ts @@ -5,7 +5,7 @@ import { orderedMetaTables } from "../../../src/lib/utils/globals"; const dropTablesAllNonExternalProjects = async (knexClient) => { const projects = await Project.list({}); - const userCreatedTableNames = []; + const userCreatedTableNames: string[] = []; await Promise.all( projects .filter((project) => project.is_meta) @@ -16,7 +16,7 @@ const dropTablesAllNonExternalProjects = async (knexClient) => { const models = await Model.list({ project_id: project.id, - base_id: base.id, + base_id: base.id!, }); models.forEach((model) => { userCreatedTableNames.push(model.table_name); diff --git a/packages/nocodb/tests/unit/init/cleanupSakila.ts b/packages/nocodb/tests/unit/init/cleanupSakila.ts index da796ee9f2..5c32eda9f9 100644 --- a/packages/nocodb/tests/unit/init/cleanupSakila.ts +++ b/packages/nocodb/tests/unit/init/cleanupSakila.ts @@ -45,10 +45,9 @@ const cleanUpSakila = async (sakilaKnexClient) => { try { 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) { - global.touchedSakilaDb = false; + if(audits?.length > 0) { return await resetAndSeedSakila(sakilaKnexClient); } diff --git a/packages/nocodb/tests/unit/model/index.test.ts b/packages/nocodb/tests/unit/model/index.test.ts new file mode 100644 index 0000000000..4c5649984d --- /dev/null +++ b/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); +} \ No newline at end of file diff --git a/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts b/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts index e69de29bb2..b6ef87d58c 100644 --- a/packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts +++ b/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(); + 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(); + 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(); + 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); +} diff --git a/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts b/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts index 9a4c69df04..621820e532 100644 --- a/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/tableRow.test.ts @@ -11,7 +11,7 @@ import { } from '../../factory/column'; import { createTable, getTable } from '../../factory/table'; import { - createRelation, + createChildRow, createRow, generateDefaultRowAttributes, getOneRow, @@ -1182,7 +1182,7 @@ function tableTest() { const row = await createRow(context, { project, table }); - await createRelation(context, { + await createChildRow(context, { project, table, 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 = 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. // it.only('Bulk update nested filtered table data list with a lookup column', async function () { // }); @@ -1800,7 +1836,7 @@ function tableTest() { 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 response = await request(context.app) diff --git a/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts b/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts index 7658fdc5ee..c125ec7428 100644 --- a/packages/nocodb/tests/unit/rest/tests/viewRow.test.ts +++ b/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 { createView } from '../../factory/view'; 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 responseColumnsListStr = Object.keys(row).sort().join(','); @@ -1081,7 +1081,7 @@ function viewRowTests() { const row = await createRow(context, { project, table }); - await createRelation(context, { + await createChildRow(context, { project, table, childTable: relatedTable,