From 16da34f7fc326abfb5009d137fe9d606ec93e9c9 Mon Sep 17 00:00:00 2001 From: Ramesh Mane <101566080+rameshmane7218@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:41:21 +0000 Subject: [PATCH] feat(nocodb): copy paste link cell api --- .../src/controllers/data-table.controller.ts | 4 +- packages/nocodb/src/db/BaseModelSqlv2.ts | 5 +- packages/nocodb/src/schema/swagger.json | 7 +- .../nocodb/src/services/data-table.service.ts | 146 +++++++++++++++++- 4 files changed, 157 insertions(+), 5 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 9a3c5052c1..1d3e424fee 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -194,6 +194,7 @@ export class DataTableController { }); } + // todo: naming @Post(['/api/v2/tables/:modelId/links/:columnId/records']) @Acl('nestedDataLinkUnlink') async nestedLinkUnlink( @@ -205,10 +206,9 @@ export class DataTableController { data: { operation: 'copy' | 'paste'; rowId: string; + fk_related_model_id: string; }[], ) { - console.log('data', data); - return await this.dataTableService.nestedLinkUnlink({ modelId, query: req.query, diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 14d822b269..cc6e0a88dd 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -1330,6 +1330,7 @@ class BaseModelSqlv2 { public async mmList( { colId, parentId }, args: { limit?; offset?; fieldsSet?: Set } = {}, + selectAllRecords = false ) { const { where, sort, ...rest } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( @@ -1375,7 +1376,9 @@ class BaseModelSqlv2 { await this.applySortAndFilter({ table: childTable, where, qb, sort }); // todo: sanitize - qb.limit(+rest?.limit || 25); + if (!selectAllRecords) { + qb.limit(+rest?.limit || 25); + } qb.offset(+rest?.offset || 0); const children = await this.execAndParse(qb, await childTable.getColumns()); diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index f9ab81d13f..d71179ea71 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -23237,6 +23237,8 @@ }, "NestedLinkUnlinkReq": { "type": "array", + "minItems": 2, + "maxItems": 2, "items": { "type": "object", "properties": { @@ -23246,9 +23248,12 @@ }, "rowId": { "type": "string" + }, + "fk_related_model_id":{ + "type": "string" } }, - "required": ["operation", "rowId"] + "required": ["operation", "rowId", "fk_related_model_id"] } } }, diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index a97dd00686..71a07bcfec 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -444,6 +444,7 @@ export class DataTableService { return true; } + // todo: naming & optimizing async nestedLinkUnlink(param: { cookie: any; viewId: string; @@ -453,6 +454,7 @@ export class DataTableService { data: { operation: 'copy' | 'paste'; rowId: string; + fk_related_model_id: string; }[]; }) { validatePayload( @@ -460,7 +462,124 @@ export class DataTableService { param.data, ); - return true; + if ( + param.data[0]?.fk_related_model_id !== param.data[1]?.fk_related_model_id + ) { + throw new Error( + 'The operation is not supported on different fk_related_model_id', + ); + } + + const operationMap = param.data.reduce( + (map, p) => { + map[p.operation] = p; + return map; + }, + {} as Record< + 'copy' | 'paste', + { + operation: 'copy' | 'paste'; + rowId: string; + fk_related_model_id: string; + } + >, + ); + + const { model, view } = await this.getModelAndView(param); + + const source = await Source.get(model.source_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + viewId: view?.id, + dbDriver: await NcConnectionMgrv2.get(source), + }); + + const existsCopyRow = await baseModel.exist(operationMap.copy.rowId); + const existsPasteRow = await baseModel.exist(operationMap.paste.rowId); + + if (!existsCopyRow && !existsPasteRow) { + NcError.notFound( + `Record with id '${operationMap.copy.rowId}' and '${operationMap.paste.rowId}' not found`, + ); + } else if (!existsCopyRow) { + NcError.notFound(`Record with id '${operationMap.copy.rowId}' not found`); + } else if (!existsPasteRow) { + NcError.notFound( + `Record with id '${operationMap.paste.rowId}' not found`, + ); + } + + const column = await this.getColumn(param); + const colOptions = await column.getColOptions(); + const relatedModel = await colOptions.getRelatedTable(); + await relatedModel.getColumns(); + + if (colOptions.type !== RelationTypes.MANY_TO_MANY) return; + + const { ast, dependencyFields } = await getAst({ + model: relatedModel, + query: param.query, + extractOnlyPrimaries: !(param.query?.f || param.query?.fields), + }); + + const listArgs: any = dependencyFields; + + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + + const [copiedCellNestedList, pasteCellNestedList] = await Promise.all([ + baseModel.mmList( + { + colId: column.id, + parentId: operationMap.copy.rowId, + }, + listArgs as any, + true, + ), + baseModel.mmList( + { + colId: column.id, + parentId: operationMap.paste.rowId, + }, + listArgs as any, + true, + ), + ]); + + const filteredRowsToLink = this.filterAndMapRows( + copiedCellNestedList, + pasteCellNestedList, + relatedModel.primaryKeys, + ); + + const filteredRowsToUnlink = this.filterAndMapRows( + pasteCellNestedList, + copiedCellNestedList, + relatedModel.primaryKeys, + ); + + await Promise.all([ + baseModel.addLinks({ + colId: column.id, + childIds: filteredRowsToLink, + rowId: operationMap.paste.rowId, + cookie: param.cookie, + }), + baseModel.removeLinks({ + colId: column.id, + childIds: filteredRowsToUnlink, + rowId: operationMap.paste.rowId, + cookie: param.cookie, + }), + ]); + + return { link: filteredRowsToLink, unlink: filteredRowsToUnlink }; } private validateIds(rowIds: any[] | any) { @@ -485,4 +604,29 @@ export class DataTableService { NcError.unprocessableEntity('Invalid row id ' + rowIds); } } + + private filterAndMapRows( + sourceList: Record[], + targetList: Record[], + primaryKeys: Column[], + ): Record[] { + return sourceList + .filter( + (sourceRow: Record) => + !targetList.some((targetRow: Record) => + primaryKeys.every( + (key) => + sourceRow[key.title || key.column_name] === + targetRow[key.title || key.column_name], + ), + ), + ) + .map((item: Record) => + primaryKeys.reduce((acc, key) => { + acc[key.title || key.column_name] = + item[key.title || key.column_name]; + return acc; + }, {} as Record), + ); + } }