diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 7f46a68f3c..eaac1c6e6a 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -136,15 +136,55 @@ export class DataTableController { @Query('viewId') viewId: string, @Param('columnId') columnId: string, @Param('rowId') rowId: string, - @Body() refRowIds: string | string[] | number | number[], ) { return await this.dataTableService.nestedDataList({ modelId, rowId: rowId, query: req.query, viewId, + columnId, + }); + } + + + @Get(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Post('nestedDataLink') + async nestedLink( + @Request() req, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + @Param('columnId') columnId: string, + @Param('rowId') rowId: string, + @Body() refRowIds: string | string[] | number | number[], + ) { + return await this.dataTableService.nestedLink({ + modelId, + rowId: rowId, + query: req.query, + viewId, + columnId, refRowIds, + }); + } + + + @Delete(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Acl('nestedDataUnlink') + async nestedUnlink( + @Request() req, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + @Param('columnId') columnId: string, + @Param('rowId') rowId: string, + @Body() refRowIds: string | string[] | number | number[], + ) { + return await this.dataTableService.nestedUnlink({ + modelId, + rowId: rowId, + query: req.query, + viewId, columnId, + refRowIds, }); } } diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index d67613ca4c..4f6157b6eb 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3483,6 +3483,197 @@ class BaseModelSqlv2 { } return data; } + + async addLinks({ + cookie, + childIds, + colId, + rowId, + }: { + cookie: any; + childIds: string | string[] | number | number[]; + colId: string; + rowId: string; + }) { + const columns = await this.model.getColumns(); + const column = columns.find((c) => c.id === colId); + + if (!column || column.uidt !== UITypes.LinkToAnotherRecord) + NcError.notFound('Column not found'); + + const colOptions = await column.getColOptions(); + + 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 RelationTypes.MANY_TO_MANY: + { + const vChildCol = await colOptions.getMMChildColumn(); + const vParentCol = await colOptions.getMMParentColumn(); + const vTable = await colOptions.getMMModel(); + + const vTn = this.getTnPath(vTable); + + if (this.isSnowflake) { + const parentPK = this.dbDriver(parentTn) + .select(parentColumn.column_name) + .where(_wherePk(parentTable.primaryKeys, childId)) + .first(); + + const childPK = this.dbDriver(childTn) + .select(childColumn.column_name) + .where(_wherePk(childTable.primaryKeys, rowId)) + .first(); + + await this.dbDriver.raw( + `INSERT INTO ?? (??, ??) SELECT (${parentPK.toQuery()}), (${childPK.toQuery()})`, + [vTn, vParentCol.column_name, vChildCol.column_name], + ); + } else { + await this.dbDriver(vTn).insert({ + [vParentCol.column_name]: this.dbDriver(parentTn) + .select(parentColumn.column_name) + .where(_wherePk(parentTable.primaryKeys, childId)) + .first(), + [vChildCol.column_name]: this.dbDriver(childTn) + .select(childColumn.column_name) + .where(_wherePk(childTable.primaryKeys, rowId)) + .first(), + }); + } + } + break; + case RelationTypes.HAS_MANY: + { + await this.dbDriver(childTn) + .update({ + [childColumn.column_name]: this.dbDriver.from( + this.dbDriver(parentTn) + .select(parentColumn.column_name) + .where(_wherePk(parentTable.primaryKeys, rowId)) + .first() + .as('___cn_alias'), + ), + }) + .where(_wherePk(childTable.primaryKeys, childId)); + } + break; + case RelationTypes.BELONGS_TO: + { + await this.dbDriver(childTn) + .update({ + [childColumn.column_name]: this.dbDriver.from( + this.dbDriver(parentTn) + .select(parentColumn.column_name) + .where(_wherePk(parentTable.primaryKeys, childId)) + .first() + .as('___cn_alias'), + ), + }) + .where(_wherePk(childTable.primaryKeys, rowId)); + } + break; + } + + const response = await this.readByPk(rowId); + await this.afterInsert(response, this.dbDriver, cookie); + await this.afterAddChild(rowId, childId, cookie); + } + + async removeLinks({ + cookie, + childIds, + colId, + rowId, + }: { + cookie: any; + childIds: string | string[] | number | number[]; + colId: string; + rowId: string; + }) { + const columns = await this.model.getColumns(); + const column = columns.find((c) => c.id === colId); + + if (!column || column.uidt !== UITypes.LinkToAnotherRecord) + NcError.notFound('Column not found'); + + const colOptions = await column.getColOptions(); + + 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); + + const prevData = await this.readByPk(rowId); + + switch (colOptions.type) { + case RelationTypes.MANY_TO_MANY: + { + const vChildCol = await colOptions.getMMChildColumn(); + const vParentCol = await colOptions.getMMParentColumn(); + const vTable = await colOptions.getMMModel(); + + const vTn = this.getTnPath(vTable); + + await this.dbDriver(vTn) + .where({ + [vParentCol.column_name]: this.dbDriver(parentTn) + .select(parentColumn.column_name) + .where(_wherePk(parentTable.primaryKeys, childId)) + .first(), + [vChildCol.column_name]: this.dbDriver(childTn) + .select(childColumn.column_name) + .where(_wherePk(childTable.primaryKeys, rowId)) + .first(), + }) + .delete(); + } + break; + case RelationTypes.HAS_MANY: + { + await this.dbDriver(childTn) + // .where({ + // [childColumn.cn]: this.dbDriver(parentTable.tn) + // .select(parentColumn.cn) + // .where(parentTable.primaryKey.cn, rowId) + // .first() + // }) + .where(_wherePk(childTable.primaryKeys, childId)) + .update({ [childColumn.column_name]: null }); + } + break; + case RelationTypes.BELONGS_TO: + { + await this.dbDriver(childTn) + // .where({ + // [childColumn.cn]: this.dbDriver(parentTable.tn) + // .select(parentColumn.cn) + // .where(parentTable.primaryKey.cn, childId) + // .first() + // }) + .where(_wherePk(childTable.primaryKeys, rowId)) + .update({ [childColumn.column_name]: null }); + } + break; + } + + const newData = await this.readByPk(rowId); + await this.afterUpdate(prevData, newData, this.dbDriver, cookie); + await this.afterRemoveChild(rowId, childId, cookie); + } } function extractSortsObject( diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 0042b0ae10..ddfb72efde 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -3,7 +3,7 @@ import { RelationTypes, UITypes } from 'nocodb-sdk' import { NcError } from '../helpers/catchError'; import { PagedResponseImpl } from '../helpers/PagedResponse' import { Base, Column, LinkToAnotherRecordColumn, Model, View } from '../models' -import { getColumnByIdOrName } from '../modules/datas/helpers' +import { getColumnByIdOrName, getViewAndModelByAliasOrId, PathParams } from '../modules/datas/helpers' import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { DatasService } from './datas.service'; @@ -250,16 +250,7 @@ export class DataTableService { viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); - - const column = await Column.get({colId: param.columnId}) - - if(!column) NcError.badRequest('Column not found' - - if(column.fk_model_id !== model.id) - NcError.badRequest('Column not belong to model') - - if (column.uidt !== UITypes.LinkToAnotherRecord) - NcError.badRequest('Column is not LTAR'); + const column = await this.getColumn(param) const colOptions = await column.getColOptions( ) @@ -296,4 +287,64 @@ export class DataTableService { ...param.query, }); } + + private async getColumn(param: {modelId: string; columnId: string }) { + const column = await Column.get({ colId: param.columnId }) + + if (!column) NcError.badRequest('Column not found' + + if (column.fk_model_id !== param.modelId) + NcError.badRequest('Column not belong to model') + + if (column.uidt !== UITypes.LinkToAnotherRecord) + NcError.badRequest('Column is not LTAR') + return column + } + + async nestedLink(param: { cookie:any;viewId: string; modelId: string; columnId: string; query: any; refRowIds: string | string[] | number | number[]; rowId: string }) { + const { model, view } = await this.getModelAndView(param); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + const column = await this.getColumn(param); + + await baseModel.addLinks({ + colId: column.id, + childIds: param.refRowIds, + rowId: param.rowId, + cookie: param.cookie, + }); + + return true; } + + async nestedUnlink(param: { cookie:any;viewId: string; modelId: string; columnId: string; query: any; refRowIds: string | string[] | number | number[]; rowId: string }) { + const { model, view } = await this.getModelAndView(param); + if (!model) NcError.notFound('Table not found'); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + const column = await this.getColumn(param); + + await baseModel.removeLinks({ + colId: column.id, + childIds: param.refRowIds, + rowId: param.rowId, + cookie: param.cookie, + }); + + return true; + } }