From b722d039bbdf0b9a27d445d20f47bffbd813d415 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Fri, 26 May 2023 19:20:10 +0530 Subject: [PATCH 01/97] feat: add new data apis Signed-off-by: Pranav C --- packages/nocodb/src/app.module.ts | 6 +- .../data-alias-nested.controller.ts | 1 - .../controllers/data-table.controller.spec.ts | 18 +++ .../src/controllers/data-table.controller.ts | 137 +++++++++++++++++ .../src/services/data-table.service.spec.ts | 18 +++ .../nocodb/src/services/data-table.service.ts | 140 ++++++++++++++++++ packages/nocodb/src/services/datas.service.ts | 2 +- 7 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 packages/nocodb/src/controllers/data-table.controller.spec.ts create mode 100644 packages/nocodb/src/controllers/data-table.controller.ts create mode 100644 packages/nocodb/src/services/data-table.service.spec.ts create mode 100644 packages/nocodb/src/services/data-table.service.ts diff --git a/packages/nocodb/src/app.module.ts b/packages/nocodb/src/app.module.ts index f5a4e8f1b7..ad8a162866 100644 --- a/packages/nocodb/src/app.module.ts +++ b/packages/nocodb/src/app.module.ts @@ -19,6 +19,9 @@ import { MetasModule } from './modules/metas/metas.module'; import { JobsModule } from './modules/jobs/jobs.module'; import { AppInitService } from './services/app-init.service'; import type { MiddlewareConsumer } from '@nestjs/common'; +import { DataTableController } from './controllers/data-table.controller'; +import { DataTableController } from './servicess/data-table.controller'; +import { DataTableService } from './services/data-table.service'; @Module({ imports: [ @@ -38,7 +41,7 @@ import type { MiddlewareConsumer } from '@nestjs/common'; ] : []), ], - controllers: [], + controllers: [DataTableController], providers: [ AuthService, { @@ -50,6 +53,7 @@ import type { MiddlewareConsumer } from '@nestjs/common'; BaseViewStrategy, HookHandlerService, AppInitService, + DataTableService, ], }) export class AppModule { diff --git a/packages/nocodb/src/controllers/data-alias-nested.controller.ts b/packages/nocodb/src/controllers/data-alias-nested.controller.ts index 53c11f5f42..c10bdb8088 100644 --- a/packages/nocodb/src/controllers/data-alias-nested.controller.ts +++ b/packages/nocodb/src/controllers/data-alias-nested.controller.ts @@ -8,7 +8,6 @@ import { Request, UseGuards, } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; import { GlobalGuard } from '../guards/global/global.guard'; import { Acl, diff --git a/packages/nocodb/src/controllers/data-table.controller.spec.ts b/packages/nocodb/src/controllers/data-table.controller.spec.ts new file mode 100644 index 0000000000..cb22816371 --- /dev/null +++ b/packages/nocodb/src/controllers/data-table.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DataTableController } from './data-table.controller'; + +describe('DataTableController', () => { + let controller: DataTableController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [DataTableController], + }).compile(); + + controller = module.get(DataTableController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts new file mode 100644 index 0000000000..da18ac5b30 --- /dev/null +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -0,0 +1,137 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + Param, + Patch, + Post, + Query, + Request, + Response, + UseGuards, +} from '@nestjs/common'; +import { GlobalGuard } from '../guards/global/global.guard'; +import { parseHrtimeToSeconds } from '../helpers'; +import { + Acl, + ExtractProjectIdMiddleware, +} from '../middlewares/extract-project-id/extract-project-id.middleware'; +import { DataTableService } from '../services/data-table.service'; +import { DatasService } from '../services/datas.service'; + +@Controller() +@UseGuards(ExtractProjectIdMiddleware, GlobalGuard) +export class DataTableController { + constructor(private readonly dataTableService: DataTableService) {} + + // todo: Handle the error case where view doesnt belong to model + @Get('/api/v1/db/:projectId/tables/:modelId') + @Acl('dataList') + async dataList( + @Request() req, + @Response() res, + @Param('projectId') projectId: string, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + ) { + const startTime = process.hrtime(); + const responseData = await this.dataTableService.dataList({ + query: req.query, + projectId: projectId, + modelId: modelId, + }); + const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); + res.setHeader('xc-db-response', elapsedSeconds); + res.json(responseData); + } + + @Get(['/api/v1/db/:projectId/tables/:modelId/count']) + @Acl('dataCount') + async dataCount( + @Request() req, + @Response() res, + @Param('projectId') projectId: string, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + ) { + const countResult = await this.dataTableService.dataCount({ + query: req.query, + modelId, + projectId, + }); + + res.json(countResult); + } + + @Post(['/api/v1/db/:projectId/tables/:modelId']) + @HttpCode(200) + @Acl('dataInsert') + async dataInsert( + @Request() req, + @Param('projectId') projectId: string, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + @Body() body: any, + ) { + return await this.dataTableService.dataInsert({ + projectId: projectId, + modelId: modelId, + body: body, + cookie: req, + }); + } + + @Patch(['/api/v1/db/:projectId/tables/:modelId/:rowId']) + @Acl('dataUpdate') + async dataUpdate( + @Request() req, + @Param('projectId') projectId: string, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + @Param('rowId') rowId: string, + ) { + return await this.dataTableService.dataUpdate({ + projectId: projectId, + modelId: modelId, + body: req.body, + cookie: req, + rowId: rowId, + }); + } + + @Delete(['/api/v1/db/:projectId/tables/:modelId/:rowId']) + @Acl('dataDelete') + async dataDelete( + @Request() req, + @Param('projectId') projectId: string, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + @Param('rowId') rowId: string, + ) { + return await this.dataTableService.dataDelete({ + projectId: projectId, + modelId: modelId, + cookie: req, + rowId: rowId, + }); + } + + @Get(['/api/v1/db/:projectId/tables/:modelId/:rowId']) + @Acl('dataRead') + async dataRead( + @Request() req, + @Param('projectId') projectId: string, + @Param('modelId') modelId: string, + @Query('viewId') viewId: string, + @Param('rowId') rowId: string, + ) { + return await this.dataTableService.dataRead({ + modelId, + projectId, + rowId: rowId, + query: req.query, + }); + } +} diff --git a/packages/nocodb/src/services/data-table.service.spec.ts b/packages/nocodb/src/services/data-table.service.spec.ts new file mode 100644 index 0000000000..cf61df154f --- /dev/null +++ b/packages/nocodb/src/services/data-table.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DataTableService } from './data-table.service'; + +describe('DataTableService', () => { + let service: DataTableService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DataTableService], + }).compile(); + + service = module.get(DataTableService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts new file mode 100644 index 0000000000..5fad03a26d --- /dev/null +++ b/packages/nocodb/src/services/data-table.service.ts @@ -0,0 +1,140 @@ +import { Injectable } from '@nestjs/common'; +import { NcError } from '../helpers/catchError'; +import { Base, Model } from '../models'; +import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import { DatasService } from './datas.service'; + +@Injectable() +export class DataTableService { + constructor(private datasService: DatasService) {} + + async dataList(param: { projectId?: string; modelId: string; query: any }) { + const model = await this.getModelAndValidate(param); + + return await this.datasService.getDataList({ + model, + query: param.query, + }); + } + + async dataRead(param: { + projectId?: string; + modelId: string; + rowId: string; + query: any; + }) { + const model = await this.getModelAndValidate(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + const row = await baseModel.readByPk(param.rowId, false, param.query); + + if (!row) { + NcError.notFound('Row not found'); + } + + return row; + } + + async dataInsert(param: { + projectId?: string; + modelId: string; + body: any; + cookie: any; + }) { + const model = await this.getModelAndValidate(param); + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + return await baseModel.insert(param.body, null, param.cookie); + } + + async dataUpdate(param: { + projectId?: string; + modelId: string; + rowId: string; + body: any; + cookie: any; + }) { + const model = await this.getModelAndValidate(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + return await baseModel.updateByPk( + param.rowId, + param.body, + null, + param.cookie, + ); + } + + async dataDelete(param: { + projectId?: string; + modelId: string; + rowId: string; + cookie: any; + }) { + const model = await this.getModelAndValidate(param); + const base = await Base.get(model.base_id); + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + // todo: Should have error http status code + const message = await baseModel.hasLTARData(param.rowId, model); + if (message.length) { + return { message }; + } + return await baseModel.delByPk(param.rowId, null, param.cookie); + } + + private async getModelAndValidate(param: { + projectId?: string; + modelId: string; + query: any; + }) { + const model = await Model.get(param.modelId); + + if (model.project_id && model.project_id !== param.projectId) { + throw new Error('Model not found in project'); + } + return model; + } + + async dataCount(param: { projectId?: string; modelId: string; query: any }) { + + const model = await this.getModelAndValidate(param); + + const base = await Base.get(model.base_id); + + const baseModel = await Model.getBaseModelSQL({ + id: model.id, + dbDriver: await NcConnectionMgrv2.get(base), + }); + + const countArgs: any = { ...param.query }; + try { + countArgs.filterArr = JSON.parse(countArgs.filterArrJson); + } catch (e) {} + + const count: number = await baseModel.count(countArgs); + + return { count }; + + } +} diff --git a/packages/nocodb/src/services/datas.service.ts b/packages/nocodb/src/services/datas.service.ts index c41609f6c0..68e45dfc49 100644 --- a/packages/nocodb/src/services/datas.service.ts +++ b/packages/nocodb/src/services/datas.service.ts @@ -113,7 +113,7 @@ export class DatasService { async getDataList(param: { model: Model; - view: View; + view?: View; query: any; baseModel?: BaseModelSqlv2; }) { From a9e49ca90981dfde49562c5ea3f9cb03e08a02cc Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 27 May 2023 00:06:24 +0530 Subject: [PATCH 02/97] feat: corrections and include viewId Signed-off-by: Pranav C --- packages/nocodb/src/app.module.ts | 5 -- .../src/controllers/data-table.controller.ts | 6 ++ .../nocodb/src/modules/datas/datas.module.ts | 4 ++ .../nocodb/src/services/data-table.service.ts | 58 ++++++++++++++----- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/nocodb/src/app.module.ts b/packages/nocodb/src/app.module.ts index ad8a162866..fc79033b15 100644 --- a/packages/nocodb/src/app.module.ts +++ b/packages/nocodb/src/app.module.ts @@ -19,9 +19,6 @@ import { MetasModule } from './modules/metas/metas.module'; import { JobsModule } from './modules/jobs/jobs.module'; import { AppInitService } from './services/app-init.service'; import type { MiddlewareConsumer } from '@nestjs/common'; -import { DataTableController } from './controllers/data-table.controller'; -import { DataTableController } from './servicess/data-table.controller'; -import { DataTableService } from './services/data-table.service'; @Module({ imports: [ @@ -41,7 +38,6 @@ import { DataTableService } from './services/data-table.service'; ] : []), ], - controllers: [DataTableController], providers: [ AuthService, { @@ -53,7 +49,6 @@ import { DataTableService } from './services/data-table.service'; BaseViewStrategy, HookHandlerService, AppInitService, - DataTableService, ], }) export class AppModule { diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index da18ac5b30..3f772cd35b 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -41,6 +41,7 @@ export class DataTableController { query: req.query, projectId: projectId, modelId: modelId, + viewId: viewId, }); const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); res.setHeader('xc-db-response', elapsedSeconds); @@ -59,6 +60,7 @@ export class DataTableController { const countResult = await this.dataTableService.dataCount({ query: req.query, modelId, + viewId, projectId, }); @@ -79,6 +81,7 @@ export class DataTableController { projectId: projectId, modelId: modelId, body: body, + viewId, cookie: req, }); } @@ -97,6 +100,7 @@ export class DataTableController { modelId: modelId, body: req.body, cookie: req, + viewId, rowId: rowId, }); } @@ -114,6 +118,7 @@ export class DataTableController { projectId: projectId, modelId: modelId, cookie: req, + viewId, rowId: rowId, }); } @@ -132,6 +137,7 @@ export class DataTableController { projectId, rowId: rowId, query: req.query, + viewId }); } } diff --git a/packages/nocodb/src/modules/datas/datas.module.ts b/packages/nocodb/src/modules/datas/datas.module.ts index 6c9f6f713b..054e2769df 100644 --- a/packages/nocodb/src/modules/datas/datas.module.ts +++ b/packages/nocodb/src/modules/datas/datas.module.ts @@ -3,8 +3,10 @@ import { MulterModule } from '@nestjs/platform-express'; import multer from 'multer'; import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; import { DataAliasController } from '../../controllers/data-alias.controller'; +import { DataTableController } from '../../controllers/data-table.controller'; import { PublicDatasExportController } from '../../controllers/public-datas-export.controller'; import { PublicDatasController } from '../../controllers/public-datas.controller'; +import { DataTableService } from '../../services/data-table.service'; import { DatasService } from '../../services/datas.service'; import { DatasController } from '../../controllers/datas.controller'; import { BulkDataAliasController } from '../../controllers/bulk-data-alias.controller'; @@ -27,6 +29,7 @@ import { PublicDatasService } from '../../services/public-datas.service'; }), ], controllers: [ + DataTableController, DatasController, BulkDataAliasController, DataAliasController, @@ -37,6 +40,7 @@ import { PublicDatasService } from '../../services/public-datas.service'; PublicDatasExportController, ], providers: [ + DataTableService, DatasService, BulkDataAliasService, DataAliasNestedService, diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 5fad03a26d..f29d6c79f0 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { NcError } from '../helpers/catchError'; -import { Base, Model } from '../models'; +import { Base, Model, View } from '../models'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { DatasService } from './datas.service'; @@ -8,11 +8,17 @@ import { DatasService } from './datas.service'; export class DataTableService { constructor(private datasService: DatasService) {} - async dataList(param: { projectId?: string; modelId: string; query: any }) { - const model = await this.getModelAndValidate(param); + async dataList(param: { + projectId?: string; + modelId: string; + query: any; + viewId?: string; + }) { + const { model, view } = await this.getModelAndView(param); return await this.datasService.getDataList({ model, + view, query: param.query, }); } @@ -21,14 +27,16 @@ export class DataTableService { projectId?: string; modelId: string; rowId: string; + viewId?: string; query: any; }) { - const model = await this.getModelAndValidate(param); + const { model, view } = await this.getModelAndView(param); const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, + viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); @@ -43,15 +51,17 @@ export class DataTableService { async dataInsert(param: { projectId?: string; + viewId?: string; modelId: string; body: any; cookie: any; }) { - const model = await this.getModelAndValidate(param); + const { model, view } = await this.getModelAndView(param); const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, + viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); @@ -61,16 +71,18 @@ export class DataTableService { async dataUpdate(param: { projectId?: string; modelId: string; + viewId?: string; rowId: string; body: any; cookie: any; }) { - const model = await this.getModelAndValidate(param); + const { model, view } = await this.getModelAndView(param); const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, + viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); @@ -85,13 +97,15 @@ export class DataTableService { async dataDelete(param: { projectId?: string; modelId: string; + viewId?: string; rowId: string; cookie: any; }) { - const model = await this.getModelAndValidate(param); + const { model, view } = await this.getModelAndView(param); const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, + viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); @@ -103,27 +117,42 @@ export class DataTableService { return await baseModel.delByPk(param.rowId, null, param.cookie); } - private async getModelAndValidate(param: { + private async getModelAndView(param: { projectId?: string; + viewId?: string; modelId: string; - query: any; }) { const model = await Model.get(param.modelId); if (model.project_id && model.project_id !== param.projectId) { - throw new Error('Model not found in project'); + throw new Error('Model not belong to project'); } - return model; - } - async dataCount(param: { projectId?: string; modelId: string; query: any }) { + let view: View; - const model = await this.getModelAndValidate(param); + if (param.viewId) { + view = await View.get(param.viewId); + if (view.fk_model_id && view.fk_model_id !== param.modelId) { + throw new Error('View not belong to model'); + } + } + + return { model, view }; + } + + async dataCount(param: { + projectId?: string; + viewId?: string; + modelId: string; + query: any; + }) { + const { model, view } = await this.getModelAndView(param); const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, + viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); @@ -135,6 +164,5 @@ export class DataTableService { const count: number = await baseModel.count(countArgs); return { count }; - } } From a1f0884eff8460a9060405f5ede4dd647f18827c Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 27 May 2023 00:34:49 +0530 Subject: [PATCH 03/97] feat: path correction and swagger Signed-off-by: Pranav C --- packages/nocodb-sdk/src/lib/Api.ts | 239 ++++++++++ .../src/controllers/data-table.controller.ts | 14 +- packages/nocodb/src/schema/swagger.json | 449 ++++++++++++++++++ 3 files changed, 694 insertions(+), 8 deletions(-) diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts index 3d0a0e92f3..b479791731 100644 --- a/packages/nocodb-sdk/src/lib/Api.ts +++ b/packages/nocodb-sdk/src/lib/Api.ts @@ -7465,6 +7465,245 @@ export class Api< format: 'json', ...params, }), + + /** + * @description List all table view rows + * + * @tags DB Table Row + * @name TableRowList + * @summary List Table View Rows + * @request GET:/api/v1/base/{projectId}/tables/{tableId} + * @response `200` `{ + \** List of table view rows *\ + list: (object)[], + \** Paginated Info *\ + pageInfo: PaginatedType, + +}` OK + * @response `400` `{ + \** @example BadRequest [Error]: *\ + msg: string, + +}` + */ + tableRowList: ( + projectId: string, + tableId: string, + query: { + viewId: string; + fields?: any[]; + sort?: any[]; + where?: string; + /** Query params for nested data */ + nested?: any; + offset?: number; + }, + params: RequestParams = {} + ) => + this.request< + { + /** List of table view rows */ + list: object[]; + /** Paginated Info */ + pageInfo: PaginatedType; + }, + { + /** @example BadRequest [Error]: */ + msg: string; + } + >({ + path: `/api/v1/base/${projectId}/tables/${tableId}`, + method: 'GET', + query: query, + format: 'json', + ...params, + }), + + /** + * @description Create a new row in the given Table View + * + * @tags DB Table Row + * @name TableRowCreate + * @summary Create Table View Row + * @request POST:/api/v1/base/{projectId}/tables/{tableId} + * @response `200` `object` OK + * @response `400` `{ + \** @example BadRequest [Error]: *\ + msg: string, + +}` + */ + tableRowCreate: ( + projectId: string, + tableId: string, + query: { + viewId: string; + }, + data: object, + params: RequestParams = {} + ) => + this.request< + object, + { + /** @example BadRequest [Error]: */ + msg: string; + } + >({ + path: `/api/v1/base/${projectId}/tables/${tableId}`, + method: 'POST', + query: query, + body: data, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Count how many rows in the given Table View + * + * @tags DB Table Row + * @name TableRowCount + * @summary Count Table View Rows + * @request GET:/api/v1/base/{projectId}/tables/{tableId}/count + * @response `200` `{ + count?: number, + +}` OK + */ + tableRowCount: ( + projectId: string, + tableId: string, + query: { + viewId: string; + where?: string; + /** Query params for nested data */ + nested?: any; + }, + params: RequestParams = {} + ) => + this.request< + { + count?: number; + }, + any + >({ + path: `/api/v1/base/${projectId}/tables/${tableId}/count`, + method: 'GET', + query: query, + format: 'json', + ...params, + }), + + /** + * @description Get the target Table View Row + * + * @tags DB Table Row + * @name DbViewRowRead + * @summary Get Table View Row + * @request GET:/api/v1/db/tables/{tableId}/rows/{rowId} + * @response `200` `object` OK + * @response `400` `{ + \** @example BadRequest [Error]: *\ + msg: string, + +}` + */ + dbViewRowRead: ( + tableId: string, + rowId: any, + query: { + viewId: string; + }, + params: RequestParams = {} + ) => + this.request< + object, + { + /** @example BadRequest [Error]: */ + msg: string; + } + >({ + path: `/api/v1/db/tables/${tableId}/rows/${rowId}`, + method: 'GET', + query: query, + format: 'json', + ...params, + }), + + /** + * @description Update the target Table View Row + * + * @tags DB Table Row + * @name TableRowUpdate + * @summary Update Table View Row + * @request PATCH:/api/v1/db/tables/{tableId}/rows/{rowId} + * @response `200` `object` OK + * @response `400` `{ + \** @example BadRequest [Error]: *\ + msg: string, + +}` + */ + tableRowUpdate: ( + tableId: string, + rowId: any, + query: { + viewId: string; + }, + data: object, + params: RequestParams = {} + ) => + this.request< + object, + { + /** @example BadRequest [Error]: */ + msg: string; + } + >({ + path: `/api/v1/db/tables/${tableId}/rows/${rowId}`, + method: 'PATCH', + query: query, + body: data, + type: ContentType.Json, + format: 'json', + ...params, + }), + + /** + * @description Delete the target Table View Row + * + * @tags DB Table Row + * @name TableRowDelete + * @summary Delete Table View Row + * @request DELETE:/api/v1/db/tables/{tableId}/rows/{rowId} + * @response `200` `number` OK + * @response `400` `{ + \** @example BadRequest [Error]: *\ + msg: string, + +}` + */ + tableRowDelete: ( + tableId: string, + rowId: any, + query: { + viewId: string; + }, + params: RequestParams = {} + ) => + this.request< + number, + { + /** @example BadRequest [Error]: */ + msg: string; + } + >({ + path: `/api/v1/db/tables/${tableId}/rows/${rowId}`, + method: 'DELETE', + query: query, + format: 'json', + ...params, + }), }; dbViewRow = { /** diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 3f772cd35b..83f414179f 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -27,7 +27,7 @@ export class DataTableController { constructor(private readonly dataTableService: DataTableService) {} // todo: Handle the error case where view doesnt belong to model - @Get('/api/v1/db/:projectId/tables/:modelId') + @Get('/api/v1/base/:projectId/tables/:modelId') @Acl('dataList') async dataList( @Request() req, @@ -48,7 +48,7 @@ export class DataTableController { res.json(responseData); } - @Get(['/api/v1/db/:projectId/tables/:modelId/count']) + @Get(['/api/v1/base/:projectId/tables/:modelId/count']) @Acl('dataCount') async dataCount( @Request() req, @@ -67,7 +67,7 @@ export class DataTableController { res.json(countResult); } - @Post(['/api/v1/db/:projectId/tables/:modelId']) + @Post(['/api/v1/base/:projectId/tables/:modelId']) @HttpCode(200) @Acl('dataInsert') async dataInsert( @@ -86,17 +86,15 @@ export class DataTableController { }); } - @Patch(['/api/v1/db/:projectId/tables/:modelId/:rowId']) + @Patch(['/api/v1/db/tables/:modelId/rows/:rowId']) @Acl('dataUpdate') async dataUpdate( @Request() req, - @Param('projectId') projectId: string, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, ) { return await this.dataTableService.dataUpdate({ - projectId: projectId, modelId: modelId, body: req.body, cookie: req, @@ -105,7 +103,7 @@ export class DataTableController { }); } - @Delete(['/api/v1/db/:projectId/tables/:modelId/:rowId']) + @Delete(['/api/v1/db/tables/:modelId/rows/:rowId']) @Acl('dataDelete') async dataDelete( @Request() req, @@ -123,7 +121,7 @@ export class DataTableController { }); } - @Get(['/api/v1/db/:projectId/tables/:modelId/:rowId']) + @Get(['/api/v1/db/tables/:modelId/rows/:rowId']) @Acl('dataRead') async dataRead( @Request() req, diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 2912be1cb4..5fd19b9012 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -13963,6 +13963,455 @@ } ] } + }, + "/api/v1/base/{projectId}/tables/{tableId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "projectId", + "in": "path", + "required": true, + "description": "Project Id" + }, + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "Table Id" + }, + { + "schema": { + "type": "string" + }, + "name": "viewId", + "in": "query", + "required": true + } + ], + "get": { + "summary": "List Table View Rows", + "operationId": "table-row-list", + "description": "List all table view rows", + "tags": ["DB Table Row"], + "parameters": [ + { + "schema": { + "type": "array" + }, + "in": "query", + "name": "fields" + }, + { + "schema": { + "type": "array" + }, + "in": "query", + "name": "sort" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "where" + }, + { + "schema": {}, + "in": "query", + "name": "nested", + "description": "Query params for nested data" + }, + { + "schema": { + "type": "number" + }, + "in": "query", + "name": "offset" + }, + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list": { + "type": "array", + "x-stoplight": { + "id": "okd8utzm9xqet" + }, + "description": "List of table view rows", + "items": { + "x-stoplight": { + "id": "j758lsjv53o4q" + }, + "type": "object" + } + }, + "pageInfo": { + "$ref": "#/components/schemas/Paginated", + "x-stoplight": { + "id": "hylgqzgm8yhye" + }, + "description": "Paginated Info" + } + }, + "required": ["list", "pageInfo"] + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "Id": 1, + "Title": "baz", + "SingleSelect": null, + "Sheet-1 List": [ + { + "Id": 1, + "Title": "baz" + } + ], + "LTAR": [ + { + "Id": 1, + "Title": "baz" + } + ] + }, + { + "Id": 2, + "Title": "foo", + "SingleSelect": "a", + "Sheet-1 List": [ + { + "Id": 2, + "Title": "foo" + } + ], + "LTAR": [ + { + "Id": 2, + "Title": "foo" + } + ] + }, + { + "Id": 3, + "Title": "bar", + "SingleSelect": "b", + "Sheet-1 List": [], + "LTAR": [] + } + ], + "pageInfo": { + "totalRows": 3, + "page": 1, + "pageSize": 25, + "isFirstPage": true, + "isLastPage": true + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + }, + "post": { + "summary": "Create Table View Row", + "operationId": "table-row-create", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "Example 1": { + "value": { + "Id": 1, + "col1": "foo", + "col2": "bar", + "CreatedAt": "2023-03-11T08:48:25.598Z", + "UpdatedAt": "2023-03-11T08:48:25.598Z" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": ["DB Table Row"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "Example 1": { + "value": { + "col1": "foo", + "col2": "bar" + } + } + } + } + } + }, + "description": "Create a new row in the given Table View" + } + }, + "/api/v1/base/{projectId}/tables/{tableId}/count": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "projectId", + "in": "path", + "required": true, + "description": "Project Id" + }, + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "Table Id" + }, + { + "schema": { + "type": "string" + }, + "name": "viewId", + "in": "query", + "required": true + } + ], + "get": { + "summary": "Count Table View Rows", + "operationId": "table-row-count", + "description": "Count how many rows in the given Table View", + "tags": ["DB Table Row"], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "where" + }, + { + "schema": {}, + "in": "query", + "name": "nested", + "description": "Query params for nested data" + }, + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "number", + "x-stoplight": { + "id": "hwq29x70rcipi" + } + } + } + }, + "examples": { + "Example 1": { + "value": { + "count": 25 + } + } + } + } + } + } + } + } + }, + "/api/v1/db/tables/{tableId}/rows/{rowId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "Table Id" + }, + { + "schema": { + "type": "string" + }, + "name": "viewId", + "in": "query", + "required": true + }, + { + "schema": { + "example": "1" + }, + "name": "rowId", + "in": "path", + "required": true, + "description": "Unique Row ID" + } + ], + "get": { + "summary": "Get Table View Row", + "operationId": "db-view-row-read", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + }, + "examples": { + "Example 1": { + "value": { + "Id": 1, + "Title": "foo", + "CreatedAt": "2023-03-11T09:11:47.437Z", + "UpdatedAt": "2023-03-11T09:11:47.784Z" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "description": "Get the target Table View Row", + "tags": ["DB Table Row"], + "parameters": [ + { + "$ref": "#/components/parameters/xc-auth" + } + ] + }, + "patch": { + "summary": "Update Table View Row", + "operationId": "table-row-update", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "Example 1": { + "value": { + "Id": 1, + "Title": "bar", + "CreatedAt": "2023-03-11T09:11:47.437Z", + "UpdatedAt": "2023-03-11T09:20:21.133Z" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": ["DB Table Row"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "Example 1": { + "value": { + "Title": "bar" + } + } + } + } + } + }, + "description": "Update the target Table View Row", + "parameters": [ + { + "$ref": "#/components/parameters/xc-auth" + } + ] + }, + "delete": { + "summary": "Delete Table View Row", + "operationId": "table-row-delete", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "number" + }, + "examples": { + "Example 1": { + "value": 1 + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": ["DB Table Row"], + "description": "Delete the target Table View Row", + "parameters": [ + { + "$ref": "#/components/parameters/xc-auth" + } + ] + } } }, "components": { From 47acb30ff8d7889fe2c1aa4cfcd957686269cd56 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 27 May 2023 11:23:59 +0530 Subject: [PATCH 04/97] docs: add apis in docs Signed-off-by: Pranav C --- .../noco-docs/content/en/developer-resources/rest-apis.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/noco-docs/content/en/developer-resources/rest-apis.md b/packages/noco-docs/content/en/developer-resources/rest-apis.md index b2e8aa693b..f18a57d348 100644 --- a/packages/noco-docs/content/en/developer-resources/rest-apis.md +++ b/packages/noco-docs/content/en/developer-resources/rest-apis.md @@ -74,6 +74,12 @@ Currently, the default value for {orgs} is noco. Users will be able to ch | Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} | | Data | Get | dbViewRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count | | Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/group/{columnId} | +| Data | Get | dbTableRow | tableRowList | /api/v1/base/{baseId}/tables/{tableId} | +| Data | Post | dbTableRow | tableRowCreate | /api/v1/base/{baseId}/tables/{tableId} | +| Data | Get | dbTableRow | tableRowRead | /api/v1/db/tables/{tableId}/rows/{rowId} | +| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/db/tables/{tableId}/rows/{rowId} | +| Data | Delete| dbTableRow | tableRowDelete | /api/v1/db/tables/{tableId}/rows/{rowId} | +| Data | Get | dbTableRow | tableRowCount | /api/v1/base/{baseId}/tables/{tableId}/count | ### Meta APIs From 07b90c3c6ef410a866f2dde9aaf683a0fc844857 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 27 May 2023 11:25:29 +0530 Subject: [PATCH 05/97] chore: cleanup Signed-off-by: Pranav C --- packages/nocodb/src/controllers/data-table.controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 83f414179f..57f13b55a4 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -19,7 +19,6 @@ import { ExtractProjectIdMiddleware, } from '../middlewares/extract-project-id/extract-project-id.middleware'; import { DataTableService } from '../services/data-table.service'; -import { DatasService } from '../services/datas.service'; @Controller() @UseGuards(ExtractProjectIdMiddleware, GlobalGuard) @@ -135,7 +134,7 @@ export class DataTableController { projectId, rowId: rowId, query: req.query, - viewId + viewId, }); } } From b9578ba359aa6f936d4cf7dd86e2690679541e15 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 27 May 2023 11:34:00 +0530 Subject: [PATCH 06/97] feat: verify modelId exists or not Signed-off-by: Pranav C --- .../nocodb/src/services/data-table.service.ts | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index f29d6c79f0..d89fe3b722 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -117,29 +117,6 @@ export class DataTableService { return await baseModel.delByPk(param.rowId, null, param.cookie); } - private async getModelAndView(param: { - projectId?: string; - viewId?: string; - modelId: string; - }) { - const model = await Model.get(param.modelId); - - if (model.project_id && model.project_id !== param.projectId) { - throw new Error('Model not belong to project'); - } - - let view: View; - - if (param.viewId) { - view = await View.get(param.viewId); - if (view.fk_model_id && view.fk_model_id !== param.modelId) { - throw new Error('View not belong to model'); - } - } - - return { model, view }; - } - async dataCount(param: { projectId?: string; viewId?: string; @@ -165,4 +142,34 @@ export class DataTableService { return { count }; } + + + + private async getModelAndView(param: { + projectId?: string; + viewId?: string; + modelId: string; + }) { + const model = await Model.get(param.modelId); + + if(!model) { + throw new Error('Model not found'); + } + + if (model.project_id && model.project_id !== param.projectId) { + throw new Error('Model not belong to project'); + } + + let view: View; + + if (param.viewId) { + view = await View.get(param.viewId); + if (view.fk_model_id && view.fk_model_id !== param.modelId) { + throw new Error('View not belong to model'); + } + } + + return { model, view }; + } + } From 8be59747e5fd021f096d3b3d91f9be35d720f265 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 27 May 2023 06:04:58 +0530 Subject: [PATCH 07/97] test: draft (WIP) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts new file mode 100644 index 0000000000..552fa916fb --- /dev/null +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -0,0 +1,82 @@ +/** + * Records List + * - default + * - pageSize + * - limit + * - offset + * - fields, single + * - fields, multiple + * - sort, ascending + * - sort, descending + * - sort, multiple + * - filter, single + * - filter, multiple + * - field type : number based (number, currency, percent, decimal, rating, duration) + * - field type : text based (text, longtext, email, phone, url) + * - field type : select based (single select, multi select) + * - field type : date based (date, datetime, time) + * - field type : virtual (link, lookup, rollup, formula) + * - field type : misc (checkbox, attachment, barcode, qrcode, json) + * - viewID + * - viewID, explicit fields + * - viewID, explicit sort + * - viewID, explicit filter + * # Error handling + * - invalid table ID + * - invalid view ID + * - invalid field name + * - invalid sort condition + * - invalid filter condition + * - invalid pageSize + * - invalid limit + * - invalid offset + * + * + * Records Create + * - field type : number based (number, currency, percent, decimal, rating, duration) + * - field type : text based (text, longtext, email, phone, url) + * - field type : select based (single select, multi select) + * - field type : date based (date, datetime, time) + * - field type : virtual (link) + * - field type : misc (checkbox, attachment, barcode, qrcode, json) + * - bulk insert + * - bulk insert-all + * # Error handling + * - invalid table ID + * - invalid field type + * - invalid field value (when validation enabled : email, url, phone number, rating, select fields, text fields, number fields, date fields) + * - invalid payload size + * + * + * Record read + * - field type : number based (number, currency, percent, decimal, rating, duration) + * - field type : text based (text, longtext, email, phone, url) + * - field type : select based (single select, multi select) + * - field type : date based (date, datetime, time) + * - field type : virtual (link, lookup, rollup, formula) + * - field type : misc (checkbox, attachment, barcode, qrcode, json) + * # Error handling + * - invalid table ID + * - invalid record ID + * + * + * Record update + * - field type : number based (number, currency, percent, decimal, rating, duration) + * - field type : text based (text, longtext, email, phone, url) + * - field type : select based (single select, multi select) + * - field type : date based (date, datetime, time) + * - field type : virtual (link) + * - field type : misc (checkbox, attachment, barcode, qrcode, json) + * - bulk update + * - bulk update-all + * # Error handling + * - invalid table ID + * - invalid record ID + * + * Record delete + * - default + * - bulk delete + * # Error handling + * - invalid table ID + * - invalid record ID + */ From a9f4b21f98e34c78f9bcf1697380b2dd31a7b126 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sun, 28 May 2023 19:20:17 +0530 Subject: [PATCH 08/97] test: text based list api Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/tests/unit/factory/column.ts | 161 ++++- packages/nocodb/tests/unit/factory/row.ts | 33 +- packages/nocodb/tests/unit/factory/view.ts | 84 ++- packages/nocodb/tests/unit/init/index.ts | 7 +- packages/nocodb/tests/unit/rest/index.test.ts | 2 + .../tests/unit/rest/tests/newDataApis.test.ts | 653 ++++++++++++++++++ 6 files changed, 894 insertions(+), 46 deletions(-) diff --git a/packages/nocodb/tests/unit/factory/column.ts b/packages/nocodb/tests/unit/factory/column.ts index 1b9617f7b9..27818e0db2 100644 --- a/packages/nocodb/tests/unit/factory/column.ts +++ b/packages/nocodb/tests/unit/factory/column.ts @@ -1,13 +1,13 @@ import { UITypes } from 'nocodb-sdk'; import request from 'supertest'; -import Column from '../../../src/models/Column'; -import FormViewColumn from '../../../src/models/FormViewColumn'; -import GalleryViewColumn from '../../../src/models/GalleryViewColumn'; -import GridViewColumn from '../../../src/models/GridViewColumn'; import Model from '../../../src/models/Model'; -import Project from '../../../src/models/Project'; -import View from '../../../src/models/View'; -import { isSqlite, isPg } from '../init/db'; +import { isPg, isSqlite } from '../init/db'; +import type Column from '../../../src/models/Column'; +import type FormViewColumn from '../../../src/models/FormViewColumn'; +import type GalleryViewColumn from '../../../src/models/GalleryViewColumn'; +import type GridViewColumn from '../../../src/models/GridViewColumn'; +import type Project from '../../../src/models/Project'; +import type View from '../../../src/models/View'; const defaultColumns = function (context) { return [ @@ -46,6 +46,120 @@ const defaultColumns = function (context) { ]; }; +const customColumns = function (type: string) { + switch (type) { + case 'textBased': + return [ + { + column_name: 'Id', + title: 'Id', + uidt: UITypes.ID, + }, + { + column_name: 'SingleLineText', + title: 'SingleLineText', + uidt: UITypes.SingleLineText, + }, + { + column_name: 'MultiLineText', + title: 'MultiLineText', + uidt: UITypes.LongText, + }, + { + column_name: 'Email', + title: 'Email', + uidt: UITypes.Email, + }, + { + column_name: 'Phone', + title: 'Phone', + uidt: UITypes.PhoneNumber, + }, + { + column_name: 'Url', + title: 'Url', + uidt: UITypes.URL, + }, + ]; + case 'numberBased': + return [ + { + column_name: 'Id', + title: 'Id', + uidt: UITypes.ID, + }, + { + column_name: 'Number', + title: 'Number', + uidt: UITypes.Number, + }, + { + column_name: 'Decimal', + title: 'Decimal', + uidt: UITypes.Decimal, + }, + { + column_name: 'Currency', + title: 'Currency', + uidt: UITypes.Currency, + }, + { + column_name: 'Percent', + title: 'Percent', + uidt: UITypes.Percent, + }, + { + column_name: 'Duration', + title: 'Duration', + uidt: UITypes.Duration, + }, + { + column_name: 'Rating', + title: 'Rating', + uidt: UITypes.Rating, + }, + ]; + case 'dateBased': + return [ + { + column_name: 'Id', + title: 'Id', + uidt: UITypes.ID, + }, + { + column_name: 'Date', + title: 'Date', + uidt: UITypes.Date, + }, + { + column_name: 'DateTime', + title: 'DateTime', + uidt: UITypes.DateTime, + }, + ]; + case 'selectBased': + return [ + { + column_name: 'Id', + title: 'Id', + uidt: UITypes.ID, + }, + { + column_name: 'SingleSelect', + title: 'SingleSelect', + uidt: UITypes.SingleSelect, + dtxp: "'jan','feb','mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'", + }, + { + column_name: 'MultiSelect', + title: 'MultiSelect', + uidt: UITypes.MultiSelect, + dtxp: "'jan','feb','mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'", + }, + ]; + } +}; + const createColumn = async (context, table, columnAttr) => { await request(context.app) .post(`/api/v1/db/meta/tables/${table.id}/columns`) @@ -55,7 +169,7 @@ const createColumn = async (context, table, columnAttr) => { }); const column: Column = (await table.getColumns()).find( - (column) => column.title === columnAttr.title + (column) => column.title === columnAttr.title, ); return column; }; @@ -76,7 +190,7 @@ const createRollupColumn = async ( table: Model; relatedTableName: string; relatedTableColumnTitle: string; - } + }, ) => { const childBases = await project.getBases(); const childTable = await Model.getByIdOrName({ @@ -86,13 +200,13 @@ const createRollupColumn = async ( }); const childTableColumns = await childTable.getColumns(); const childTableColumn = await childTableColumns.find( - (column) => column.title === relatedTableColumnTitle + (column) => column.title === relatedTableColumnTitle, ); const ltarColumn = (await table.getColumns()).find( (column) => column.uidt === UITypes.LinkToAnotherRecord && - column.colOptions?.fk_related_model_id === childTable.id + column.colOptions?.fk_related_model_id === childTable.id, ); const rollupColumn = await createColumn(context, table, { @@ -122,7 +236,7 @@ const createLookupColumn = async ( table: Model; relatedTableName: string; relatedTableColumnTitle: string; - } + }, ) => { const childBases = await project.getBases(); const childTable = await Model.getByIdOrName({ @@ -132,19 +246,19 @@ const createLookupColumn = async ( }); const childTableColumns = await childTable.getColumns(); const childTableColumn = await childTableColumns.find( - (column) => column.title === relatedTableColumnTitle + (column) => column.title === relatedTableColumnTitle, ); if (!childTableColumn) { throw new Error( - `Could not find column ${relatedTableColumnTitle} in ${relatedTableName}` + `Could not find column ${relatedTableColumnTitle} in ${relatedTableName}`, ); } const ltarColumn = (await table.getColumns()).find( (column) => column.uidt === UITypes.LinkToAnotherRecord && - column.colOptions?.fk_related_model_id === childTable.id + column.colOptions?.fk_related_model_id === childTable.id, ); const lookupColumn = await createColumn(context, table, { title: title, @@ -168,15 +282,15 @@ const createQrCodeColumn = async ( title: string; table: Model; referencedQrValueTableColumnTitle: string; - } + }, ) => { const referencedQrValueTableColumnId = await table .getColumns() .then( (cols) => cols.find( - (column) => column.title == referencedQrValueTableColumnTitle - )['id'] + (column) => column.title == referencedQrValueTableColumnTitle, + )['id'], ); const qrCodeColumn = await createColumn(context, table, { @@ -198,15 +312,15 @@ const createBarcodeColumn = async ( title: string; table: Model; referencedBarcodeValueTableColumnTitle: string; - } + }, ) => { const referencedBarcodeValueTableColumnId = await table .getColumns() .then( (cols) => cols.find( - (column) => column.title == referencedBarcodeValueTableColumnTitle - )['id'] + (column) => column.title == referencedBarcodeValueTableColumnTitle, + )['id'], ); const barcodeColumn = await createColumn(context, table, { @@ -230,7 +344,7 @@ const createLtarColumn = async ( parentTable: Model; childTable: Model; type: string; - } + }, ) => { const ltarColumn = await createColumn(context, parentTable, { title: title, @@ -246,7 +360,7 @@ const createLtarColumn = async ( const updateViewColumn = async ( context, - { view, column, attr }: { column: Column; view: View; attr: any } + { view, column, attr }: { column: Column; view: View; attr: any }, ) => { const res = await request(context.app) .patch(`/api/v1/db/meta/views/${view.id}/columns/${column.id}`) @@ -263,6 +377,7 @@ const updateViewColumn = async ( }; export { + customColumns, defaultColumns, createColumn, createQrCodeColumn, diff --git a/packages/nocodb/tests/unit/factory/row.ts b/packages/nocodb/tests/unit/factory/row.ts index 1883f185fe..4f4f42abbe 100644 --- a/packages/nocodb/tests/unit/factory/row.ts +++ b/packages/nocodb/tests/unit/factory/row.ts @@ -1,11 +1,12 @@ -import { ColumnType, UITypes } from 'nocodb-sdk'; +import { UITypes } from 'nocodb-sdk'; import request from 'supertest'; -import Column from '../../../src/models/Column'; -import Filter from '../../../src/models/Filter'; import Model from '../../../src/models/Model'; -import Project from '../../../src/models/Project'; -import Sort from '../../../src/models/Sort'; import NcConnectionMgrv2 from '../../../src/utils/common/NcConnectionMgrv2'; +import type { ColumnType } from 'nocodb-sdk'; +import type Column from '../../../src/models/Column'; +import type Filter from '../../../src/models/Filter'; +import type Project from '../../../src/models/Project'; +import type Sort from '../../../src/models/Sort'; const rowValue = (column: ColumnType, index: number) => { switch (column.uidt) { @@ -175,9 +176,15 @@ const rowMixedValue = (column: ColumnType, index: number) => { case UITypes.Date: // set startDate as 400 days before today // eslint-disable-next-line no-case-declarations - const result = new Date(); - result.setDate(result.getDate() - 400 + index); - return result.toISOString().slice(0, 10); + const d1 = new Date(); + d1.setDate(d1.getDate() - 400 + index); + return d1.toISOString().slice(0, 10); + case UITypes.DateTime: + // set startDate as 400 days before today + // eslint-disable-next-line no-case-declarations + const d2 = new Date(); + d2.setDate(d2.getDate() - 400 + index); + return d2.toISOString(); case UITypes.URL: return urls[index % urls.length]; case UITypes.SingleSelect: @@ -228,7 +235,7 @@ const listRow = async ({ const getOneRow = async ( context, - { project, table }: { project: Project; table: Model } + { project, table }: { project: Project; table: Model }, ) => { const response = await request(context.app) .get(`/api/v1/db/data/noco/${project.id}/${table.id}/find-one`) @@ -266,7 +273,7 @@ const createRow = async ( project: Project; table: Model; index?: number; - } + }, ) => { const columns = await table.getColumns(); const rowData = generateDefaultRowAttributes({ columns, index }); @@ -289,7 +296,7 @@ const createBulkRows = async ( project: Project; table: Model; values: any[]; - } + }, ) => { await request(context.app) .post(`/api/v1/db/data/bulk/noco/${project.id}/${table.id}`) @@ -317,7 +324,7 @@ const createChildRow = async ( rowId?: string; childRowId?: string; type: string; - } + }, ) => { if (!rowId) { const row = await createRow(context, { project, table }); @@ -331,7 +338,7 @@ const createChildRow = async ( await request(context.app) .post( - `/api/v1/db/data/noco/${project.id}/${table.id}/${rowId}/${type}/${column.title}/${childRowId}` + `/api/v1/db/data/noco/${project.id}/${table.id}/${rowId}/${type}/${column.title}/${childRowId}`, ) .set('xc-auth', context.token); diff --git a/packages/nocodb/tests/unit/factory/view.ts b/packages/nocodb/tests/unit/factory/view.ts index 578b156423..a6193f9df8 100644 --- a/packages/nocodb/tests/unit/factory/view.ts +++ b/packages/nocodb/tests/unit/factory/view.ts @@ -1,9 +1,20 @@ import { ViewTypes } from 'nocodb-sdk'; import request from 'supertest'; -import Model from '../../../src/models/Model'; import View from '../../../src/models/View'; +import type Model from '../../../src/models/Model'; -const createView = async (context, {title, table, type}: {title: string, table: Model, type: ViewTypes}) => { +const createView = async ( + context, + { + title, + table, + type, + }: { + title: string; + table: Model; + type: ViewTypes; + }, +) => { const viewTypeStr = (type) => { switch (type) { case ViewTypes.GALLERY: @@ -26,13 +37,70 @@ const createView = async (context, {title, table, type}: {title: string, table: title, type, }); - if(response.status !== 200) { - throw new Error('createView',response.body.message); + if (response.status !== 200) { + throw new Error('createView', response.body.message); } - const view = await View.getByTitleOrId({fk_model_id: table.id, titleOrId:title}) as View; + const view = (await View.getByTitleOrId({ + fk_model_id: table.id, + titleOrId: title, + })) as View; + return view; +}; - return view -} +const updateView = async ( + context, + { + table, + view, + filter = [], + sort = [], + field = [], + }: { + table: Model; + view: View; + filter?: any[]; + sort?: any[]; + field?: any[]; + }, +) => { + if (filter.length) { + for (let i = 0; i < filter.length; i++) { + await request(context.app) + .post(`/api/v1/db/meta/views/${view.id}/filters`) + .set('xc-auth', context.token) + .send(filter[i]) + .expect(200); + } + } + + if (sort.length) { + for (let i = 0; i < sort.length; i++) { + await request(context.app) + .post(`/api/v1/db/meta/views/${view.id}/sorts`) + .set('xc-auth', context.token) + .send(sort[i]) + .expect(200); + } + } + + if (field.length) { + for (let i = 0; i < field.length; i++) { + const columns = await table.getColumns(); + const viewColumns = await view.getColumns(); + + const columnId = columns.find((c) => c.title === field[i]).id; + const viewColumnId = viewColumns.find( + (c) => c.fk_column_id === columnId, + ).id; + // configure view to hide selected fields + await request(context.app) + .patch(`/api/v1/db/meta/views/${view.id}/columns/${viewColumnId}`) + .set('xc-auth', context.token) + .send({ show: false }) + .expect(200); + } + } +}; -export {createView} +export { createView, updateView }; diff --git a/packages/nocodb/tests/unit/init/index.ts b/packages/nocodb/tests/unit/init/index.ts index 5081543ed7..f456e25c56 100644 --- a/packages/nocodb/tests/unit/init/index.ts +++ b/packages/nocodb/tests/unit/init/index.ts @@ -25,7 +25,7 @@ const serverInit = async () => { const isFirstTimeRun = () => !server; -export default async function () { +export default async function (isSakila = true) { const { default: TestDbMngr } = await import('../TestDbMngr'); if (isFirstTimeRun()) { @@ -33,7 +33,10 @@ export default async function () { server = await serverInit(); } - await cleanUpSakila(); + if (isSakila) { + await cleanUpSakila(); + } + await cleanupMeta(); const { token } = await createUser({ app: server }, { roles: 'editor' }); diff --git a/packages/nocodb/tests/unit/rest/index.test.ts b/packages/nocodb/tests/unit/rest/index.test.ts index e7954e4a35..469add0a82 100644 --- a/packages/nocodb/tests/unit/rest/index.test.ts +++ b/packages/nocodb/tests/unit/rest/index.test.ts @@ -8,6 +8,7 @@ import tableRowTests from './tests/tableRow.test'; import viewRowTests from './tests/viewRow.test'; import attachmentTests from './tests/attachment.test'; import filterTest from './tests/filter.test'; +import newDataApisTest from './tests/newDataApis.test'; function restTests() { authTests(); @@ -19,6 +20,7 @@ function restTests() { columnTypeSpecificTests(); attachmentTests(); filterTest(); + newDataApisTest(); } export default function () { diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 552fa916fb..208618307e 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -80,3 +80,656 @@ * - invalid table ID * - invalid record ID */ + +import 'mocha'; +import { UITypes, ViewTypes } from 'nocodb-sdk'; +import { expect } from 'chai'; +import request from 'supertest'; +import init from '../../init'; +import { createProject, createSakilaProject } from '../../factory/project'; +import { createTable, getTable } from '../../factory/table'; +import { createBulkRows, listRow, rowMixedValue } from '../../factory/row'; +import { customColumns } from '../../factory/column'; +import { createView, updateView } from '../../factory/view'; +import type { Api } from 'nocodb-sdk'; + +import type { ColumnType } from 'nocodb-sdk'; +import type Project from '../../../../src/models/Project'; +import type Model from '../../../../src/models/Model'; + +let api: Api; + +const debugMode = true; + +let context; +let project: Project; +let table: Model; +let columns: any[]; +let insertedRecords: any[] = []; + +let sakilaProject: Project; +let customerTable: Model; +let customerColumns; + +// Optimisation scope for time reduction +// 1. BeforeEach can be changed to BeforeAll for List and Read APIs + +/////////////////////////////////////////////////////////////////////////////// +// Utility routines + +const verifyColumnsInRsp = (row, columns: ColumnType[]) => { + const responseColumnsListStr = Object.keys(row).sort().join(','); + const expectedColumnsListStr = columns + .map((c) => c.title) + .sort() + .join(','); + + return responseColumnsListStr === expectedColumnsListStr; +}; + +async function ncAxiosGet(url: string, query = {}, status = 200) { + const response = await request(context.app) + .get(url) + .set('xc-auth', context.token) + .query(query) + .send({}) + .expect(status); + return response; +} +async function ncAxiosPost(url: string, body = {}, status = 200) { + const response = await request(context.app) + .post(url) + .set('xc-auth', context.token) + .send(body) + .expect(status); + return response; +} + +/////////////////////////////////////////////////////////////////////////////// + +// generic table, sakila based +function generalDb() { + beforeEach(async function () { + context = await init(); + + sakilaProject = await createSakilaProject(context); + project = await createProject(context); + + customerTable = await getTable({ + project: sakilaProject, + name: 'customer', + }); + customerColumns = await customerTable.getColumns(); + }); +} + +function textBased() { + // prepare data for test cases + beforeEach(async function () { + context = await init(false); + project = await createProject(context); + table = await createTable(context, project, { + table_name: 'textBased', + title: 'TextBased', + columns: customColumns('textBased'), + }); + + // retrieve column meta + columns = await table.getColumns(); + + // build records + const rowAttributes = []; + for (let i = 0; i < 400; i++) { + const row = { + SingleLineText: rowMixedValue(columns[1], i), + MultiLineText: rowMixedValue(columns[2], i), + Email: rowMixedValue(columns[3], i), + Phone: rowMixedValue(columns[4], i), + Url: rowMixedValue(columns[5], i), + }; + rowAttributes.push(row); + } + + // insert records + // creating bulk records using older set of APIs + await createBulkRows(context, { + project, + table, + values: rowAttributes, + }); + + // retrieve inserted records + insertedRecords = await listRow({ project, table }); + + // verify length of unfiltered records to be 400 + expect(insertedRecords.length).to.equal(400); + }); + + ///////////////////////////////////////////////////////////////////////////// + + // LIST + // + + ///////////////////////////////////////////////////////////////////////////// + + it('List: default', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + ); + + const expectedPageInfo = { + totalRows: 400, + page: 1, + pageSize: 25, + isFirstPage: true, + isLastPage: false, + }; + expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + }); + + it('List: offset, limit', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { offset: 200, limit: 100 }, + ); + + const expectedPageInfo = { + totalRows: 400, + page: 3, + pageSize: 100, + isFirstPage: false, + isLastPage: false, + }; + expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); + }); + + it('List: fields, single', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { fields: 'SingleLineText' }, + ); + + expect( + verifyColumnsInRsp(rsp.body.list[0], [{ title: 'SingleLineText' }]), + ).to.equal(true); + }); + + it('List: fields, multiple', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { fields: ['SingleLineText', 'MultiLineText'] }, + ); + + expect( + verifyColumnsInRsp(rsp.body.list[0], [ + { title: 'SingleLineText' }, + { title: 'MultiLineText' }, + ]), + ).to.equal(true); + }); + + it('List: sort, ascending', async function () { + const sortColumn = columns.find((c) => c.title === 'SingleLineText'); + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { sort: 'SingleLineText', limit: 400 }, + ); + + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + const sortedArray = rsp.body.list.map((r) => r[sortColumn.title]); + expect(sortedArray).to.deep.equal(sortedArray.sort()); + }); + + it('List: sort, descending', async function () { + const sortColumn = columns.find((c) => c.title === 'SingleLineText'); + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { sort: '-SingleLineText', limit: 400 }, + ); + + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + const descSortedArray = rsp.body.list.map((r) => r[sortColumn.title]); + expect(descSortedArray).to.deep.equal(descSortedArray.sort().reverse()); + }); + + it('List: sort, multiple', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { sort: ['-SingleLineText', '-MultiLineText'], limit: 400 }, + ); + + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + // Combination of SingleLineText & MultiLineText should be in descending order + const sortedArray = rsp.body.list.map( + (r) => r.SingleLineText + r.MultiLineText, + ); + expect(sortedArray).to.deep.equal(sortedArray.sort().reverse()); + }); + + it('List: filter, single', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { where: '(SingleLineText,eq,Afghanistan)', limit: 400 }, + ); + + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + const filteredArray = rsp.body.list.map((r) => r.SingleLineText); + expect(filteredArray).to.deep.equal(filteredArray.fill('Afghanistan')); + }); + + it('List: filter, multiple', async function () { + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + where: + '(SingleLineText,eq,Afghanistan)~and(MultiLineText,eq,Allahabad, India)', + limit: 400, + }, + ); + + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + const filteredArray = rsp.body.list.map( + (r) => r.SingleLineText + ' ' + r.MultiLineText, + ); + expect(filteredArray).to.deep.equal( + filteredArray.fill('Afghanistan Allahabad, India'), + ); + }); + + it('List: view ID', async function () { + const gridView = await createView(context, { + title: 'grid0', + table, + type: ViewTypes.GRID, + }); + + const fk_column_id = columns.find((c) => c.title === 'SingleLineText').id; + await updateView(context, { + table, + view: gridView, + filter: [ + { + comparison_op: 'eq', + fk_column_id, + logical_op: 'or', + value: 'Afghanistan', + }, + ], + }); + + // fetch records from view + let rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { viewId: gridView.id }, + ); + expect(rsp.body.pageInfo.totalRows).to.equal(31); + + await updateView(context, { + table, + view: gridView, + filter: [ + { + comparison_op: 'eq', + fk_column_id, + logical_op: 'or', + value: 'Austria', + }, + ], + }); + + // fetch records from view + rsp = await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { + viewId: gridView.id, + }); + expect(rsp.body.pageInfo.totalRows).to.equal(61); + + // Sort by SingleLineText + await updateView(context, { + table, + view: gridView, + sort: [ + { + direction: 'asc', + fk_column_id, + push_to_top: true, + }, + ], + }); + + // fetch records from view + rsp = await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { + viewId: gridView.id, + }); + expect(rsp.body.pageInfo.totalRows).to.equal(61); + + // verify sorted order + // Would contain all 'Afghanistan' as we have 31 records for it + expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + const filteredArray = rsp.body.list.map((r) => r.SingleLineText); + expect(filteredArray).to.deep.equal(filteredArray.fill('Afghanistan')); + + await updateView(context, { + table, + view: gridView, + field: ['SingleLineText'], + }); + + // fetch records from view + rsp = await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { + viewId: gridView.id, + }); + const displayColumns = columns.filter((c) => c.title !== 'SingleLineText'); + expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true); + }); + + async function prepareViewForTests() { + const gridView = await createView(context, { + title: 'grid0', + table, + type: ViewTypes.GRID, + }); + + const fk_column_id = columns.find((c) => c.title === 'SingleLineText').id; + await updateView(context, { + table, + view: gridView, + filter: [ + { + comparison_op: 'eq', + fk_column_id, + logical_op: 'or', + value: 'Afghanistan', + }, + { + comparison_op: 'eq', + fk_column_id, + logical_op: 'or', + value: 'Austria', + }, + ], + sort: [ + { + direction: 'asc', + fk_column_id, + push_to_top: true, + }, + ], + field: ['MultiLineText', 'Email'], + }); + + // fetch records from view + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { viewId: gridView.id }, + ); + expect(rsp.body.pageInfo.totalRows).to.equal(61); + const displayColumns = columns.filter( + (c) => c.title !== 'MultiLineText' && c.title !== 'Email', + ); + expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true); + return gridView; + } + + it('List: view ID + sort', async function () { + const gridView = await prepareViewForTests(); + + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + viewId: gridView.id, + sort: 'Url', + limit: 100, + }, + ); + const displayColumns = columns.filter( + (c) => c.title !== 'MultiLineText' && c.title !== 'Email', + ); + expect(rsp.body.pageInfo.totalRows).to.equal(61); + expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true); + const sortedArray = rsp.body.list.map((r) => r['Url']); + expect(sortedArray).to.deep.equal(sortedArray.sort()); + }); + + it('List: view ID + filter', async function () { + const gridView = await prepareViewForTests(); + + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + viewId: gridView.id, + where: '(Phone,eq,1-541-754-3010)', + limit: 100, + }, + ); + const displayColumns = columns.filter( + (c) => c.title !== 'MultiLineText' && c.title !== 'Email', + ); + expect(rsp.body.pageInfo.totalRows).to.equal(7); + expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true); + const filteredArray = rsp.body.list.map((r) => r['Phone']); + expect(filteredArray).to.deep.equal(filteredArray.fill('1-541-754-3010')); + }); + + it('List: view ID + fields', async function () { + const gridView = await prepareViewForTests(); + + const rsp = await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + viewId: gridView.id, + fields: ['Phone', 'MultiLineText', 'SingleLineText', 'Email'], + limit: 100, + }, + ); + expect(rsp.body.pageInfo.totalRows).to.equal(61); + expect( + verifyColumnsInRsp(rsp.body.list[0], [ + { title: 'Phone' }, + { title: 'SingleLineText' }, + ]), + ).to.equal(true); + }); + + // Error handling + it('List: invalid ID', async function () { + // Invalid table ID + await ncAxiosGet(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + // Invalid project ID + await ncAxiosGet(`/api/v1/base/123456789/tables/123456789`, {}, 400); + // Invalid view ID + await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { viewId: '123456789' }, + 400, + ); + }); + + it('List: invalid limit & offset', async function () { + // Invalid limit + await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + limit: -100, + }, + 200, + ); + await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + limit: 'abc', + }, + 200, + ); + + // Invalid offset + await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + offset: -100, + }, + 200, + ); + await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + offset: 'abc', + }, + 200, + ); + await ncAxiosGet( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + offset: 10000, + }, + 200, + ); + }); + + it('List: invalid sort, filter, fields', async function () { + await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { + sort: 'abc', + }); + await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { + where: 'abc', + }); + await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { + fields: 'abc', + }); + }); +} + +function numberBased() { + // prepare data for test cases + beforeEach(async function () { + context = await init(); + project = await createProject(context); + table = await createTable(context, project, { + table_name: 'numberBased', + title: 'numberBased', + columns: customColumns('numberBased'), + }); + + // retrieve column meta + columns = await table.getColumns(); + + // build records + const rowAttributes = []; + for (let i = 0; i < 400; i++) { + const row = { + Number: rowMixedValue(columns[1], i), + Decimal: rowMixedValue(columns[2], i), + Currency: rowMixedValue(columns[3], i), + Percent: rowMixedValue(columns[4], i), + Duration: rowMixedValue(columns[5], i), + Rating: rowMixedValue(columns[6], i), + }; + rowAttributes.push(row); + } + + // insert records + await createBulkRows(context, { + project, + table, + values: rowAttributes, + }); + + // retrieve inserted records + insertedRecords = await listRow({ project, table }); + + // verify length of unfiltered records to be 400 + expect(insertedRecords.length).to.equal(400); + }); +} + +function selectBased() { + // prepare data for test cases + beforeEach(async function () { + context = await init(); + project = await createProject(context); + table = await createTable(context, project, { + table_name: 'selectBased', + title: 'selectBased', + columns: customColumns('selectBased'), + }); + + // retrieve column meta + columns = await table.getColumns(); + + // build records + const rowAttributes = []; + for (let i = 0; i < 400; i++) { + const row = { + SingleSelect: rowMixedValue(columns[1], i), + MultiSelect: rowMixedValue(columns[2], i), + }; + rowAttributes.push(row); + } + + // insert records + await createBulkRows(context, { + project, + table, + values: rowAttributes, + }); + + // retrieve inserted records + insertedRecords = await listRow({ project, table }); + + // verify length of unfiltered records to be 400 + expect(insertedRecords.length).to.equal(400); + }); +} + +function dateBased() { + // prepare data for test cases + beforeEach(async function () { + context = await init(); + project = await createProject(context); + table = await createTable(context, project, { + table_name: 'dateBased', + title: 'dateBased', + columns: customColumns('dateBased'), + }); + + // retrieve column meta + columns = await table.getColumns(); + + // build records + // 800: one year before to one year after + const rowAttributes = []; + for (let i = 0; i < 800; i++) { + const row = { + Date: rowMixedValue(columns[1], i), + }; + rowAttributes.push(row); + } + + // insert records + await createBulkRows(context, { + project, + table, + values: rowAttributes, + }); + + // retrieve inserted records + insertedRecords = await listRow({ project, table }); + + // verify length of unfiltered records to be 800 + expect(insertedRecords.length).to.equal(800); + }); +} + +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// + +export default function () { + // describe('General', generalDb); + describe('Text based', textBased); + // describe('Numerical', numberBased); + // describe('Select based', selectBased); + // describe('Date based', dateBased); +} + +/////////////////////////////////////////////////////////////////////////////// From 51ea8f26a1fa4ccd567be73322158f44cccc8f32 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Mon, 29 May 2023 07:38:01 +0530 Subject: [PATCH 09/97] test: CRUD Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 208618307e..f79abb26a8 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -140,6 +140,22 @@ async function ncAxiosPost(url: string, body = {}, status = 200) { const response = await request(context.app) .post(url) .set('xc-auth', context.token) + .send(body); + // .expect(status); + return response; +} +async function ncAxiosPatch(url: string, body = {}, status = 200) { + const response = await request(context.app) + .patch(url) + .set('xc-auth', context.token) + .send(body) + .expect(status); + return response; +} +async function ncAxiosDelete(url: string, body = {}, status = 200) { + const response = await request(context.app) + .delete(url) + .set('xc-auth', context.token) .send(body) .expect(status); return response; @@ -597,6 +613,228 @@ function textBased() { fields: 'abc', }); }); + + ///////////////////////////////////////////////////////////////////////////// + + // CREATE + // + + ///////////////////////////////////////////////////////////////////////////// + const newRecord = { + SingleLineText: 'abc', + MultiLineText: 'abc abc \n abc \r abc \t abc 1234!@#$%^&*()_+', + Email: 'a@b.com', + Url: 'https://www.abc.com', + Phone: '1-234-567-8910', + }; + + it('Create: all fields', async function () { + const rsp = await ncAxiosPost( + `/api/v1/base/${project.id}/tables/${table.id}`, + newRecord, + ); + + expect(rsp.body).to.deep.equal({ + Id: 401, + ...newRecord, + }); + }); + + it('Create: few fields left out', async function () { + const newRecord = { + SingleLineText: 'abc', + MultiLineText: 'abc abc \n abc \r abc \t abc 1234!@#$%^&*()_+', + }; + const rsp = await ncAxiosPost( + `/api/v1/base/${project.id}/tables/${table.id}`, + newRecord, + ); + + // fields left out should be null + expect(rsp.body).to.deep.equal({ + Id: 401, + ...newRecord, + Email: null, + Url: null, + Phone: null, + }); + }); + + it('Create: bulk', async function () { + const rsp = await ncAxiosPost( + `/api/v1/base/${project.id}/tables/${table.id}`, + [newRecord, newRecord, newRecord], + ); + + expect(rsp.body).to.deep.equal([{ Id: 401 }, { Id: 402 }, { Id: 403 }]); + }); + + // Error handling + it('Create: invalid ID', async function () { + // Invalid table ID + await ncAxiosPost(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + // Invalid project ID + await ncAxiosPost(`/api/v1/base/123456789/tables/123456789`, {}, 400); + // Invalid data - repeated ID + await ncAxiosPost( + `/api/v1/base/${project.id}/tables/${table.id}`, + { ...newRecord, Id: 300 }, + 400, + ); + // Invalid data - number instead of string + // await ncAxiosPost( + // `/api/v1/base/${project.id}/tables/${table.id}`, + // { ...newRecord, SingleLineText: 300 }, + // 400, + // ); + }); + + // TBD : default value handling + + ///////////////////////////////////////////////////////////////////////////// + + // READ + // + + ///////////////////////////////////////////////////////////////////////////// + + it.only('Read: all fields', async function () { + const rsp = await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/100`); + }); + + it('Read: invalid ID', async function () { + // Invalid table ID + await ncAxiosGet(`/api/v1/db/tables/123456789/rows/100`, {}, 400); + // Invalid row ID + await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/1000`, {}, 400); + }); + + ///////////////////////////////////////////////////////////////////////////// + + // UPDATE + // + + ///////////////////////////////////////////////////////////////////////////// + + it('Update: all fields', async function () { + const rsp = await ncAxiosPatch( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + Id: 1, + ...newRecord, + }, + ); + expect(rsp.body).to.deep.equal({ + Id: 1, + }); + }); + + it('Update: partial', async function () { + const recordBeforeUpdate = await ncAxiosGet( + `/api/v1/base/tables/${table.id}/rows/1`, + ); + + const rsp = await ncAxiosPatch( + `/api/v1/base/${project.id}/tables/${table.id}`, + { + Id: 1, + SingleLineText: 'some text', + MultiLineText: 'some more text', + }, + ); + expect(rsp.body).to.deep.equal({ + Id: 1, + }); + + const recordAfterUpdate = await ncAxiosGet( + `/api/v1/base/tables/${table.id}/rows/1`, + ); + expect(recordAfterUpdate.body).to.deep.equal({ + ...recordBeforeUpdate.body, + SingleLineText: 'some text', + MultiLineText: 'some more text', + }); + }); + + it('Update: bulk', async function () { + const rsp = await ncAxiosPatch( + `/api/v1/base/${project.id}/tables/${table.id}`, + [ + { + Id: 1, + SingleLineText: 'some text', + MultiLineText: 'some more text', + }, + { + Id: 2, + SingleLineText: 'some text', + MultiLineText: 'some more text', + }, + ], + ); + expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); + }); + + // Error handling + + it('Update: invalid ID', async function () { + // Invalid project ID + await ncAxiosPatch(`/api/v1/base/123456789/tables/${table.id}`, {}, 400); + // Invalid table ID + await ncAxiosPatch(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + // Invalid row ID + await ncAxiosPatch( + `/api/v1/base/${project.id}/tables/${table.id}`, + { Id: 123456789, SingleLineText: 'some text' }, + 400, + ); + }); + + ///////////////////////////////////////////////////////////////////////////// + + // DELETE + // + + ///////////////////////////////////////////////////////////////////////////// + + it('Delete: single', async function () { + const rsp = await ncAxiosDelete( + `/api/v1/base/${project.id}/tables/${table.id}`, + { Id: 1 }, + ); + expect(rsp.body).to.deep.equal({ Id: 1 }); + + // check that it's gone + await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/1`, {}, 400); + + }); + + it('Delete: bulk', async function () { + const rsp = await ncAxiosDelete( + `/api/v1/base/${project.id}/tables/${table.id}`, + [{ Id: 1 }, { Id: 2 }], + ); + expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); + + // check that it's gone + await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/1`, {}, 400); + await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/2`, {}, 400); + }); + + // Error handling + + it('Delete: invalid ID', async function () { + // Invalid project ID + await ncAxiosDelete(`/api/v1/base/123456789/tables/${table.id}`, {}, 400); + // Invalid table ID + await ncAxiosDelete(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + // Invalid row ID + await ncAxiosDelete( + `/api/v1/base/${project.id}/tables/${table.id}`, + { Id: 123456789 }, + 400, + ); + } } function numberBased() { From e3185c7048013677dd2bf27ab5edeacc57714cc8 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Mon, 29 May 2023 10:57:52 +0530 Subject: [PATCH 10/97] test: url clean-up Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 376 ++++++++++-------- 1 file changed, 200 insertions(+), 176 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index f79abb26a8..c314f73928 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -127,7 +127,11 @@ const verifyColumnsInRsp = (row, columns: ColumnType[]) => { return responseColumnsListStr === expectedColumnsListStr; }; -async function ncAxiosGet(url: string, query = {}, status = 200) { +async function ncAxiosGet({ + url = `/api/v1/base/${project.id}/tables/${table.id}`, + query = {}, + status = 200, +}: { url?: string; query?: any; status?: number } = {}) { const response = await request(context.app) .get(url) .set('xc-auth', context.token) @@ -136,7 +140,11 @@ async function ncAxiosGet(url: string, query = {}, status = 200) { .expect(status); return response; } -async function ncAxiosPost(url: string, body = {}, status = 200) { +async function ncAxiosPost({ + url = `/api/v1/base/${project.id}/tables/${table.id}`, + body = {}, + status = 200, +}: { url?: string; body?: any; status?: number } = {}) { const response = await request(context.app) .post(url) .set('xc-auth', context.token) @@ -144,7 +152,11 @@ async function ncAxiosPost(url: string, body = {}, status = 200) { // .expect(status); return response; } -async function ncAxiosPatch(url: string, body = {}, status = 200) { +async function ncAxiosPatch({ + url = `/api/v1/base/${project.id}/tables/${table.id}`, + body = {}, + status = 200, +}: { url?: string; body?: any; status?: number } = {}) { const response = await request(context.app) .patch(url) .set('xc-auth', context.token) @@ -152,7 +164,11 @@ async function ncAxiosPatch(url: string, body = {}, status = 200) { .expect(status); return response; } -async function ncAxiosDelete(url: string, body = {}, status = 200) { +async function ncAxiosDelete({ + url = `/api/v1/base/${project.id}/tables/${table.id}`, + body = {}, + status = 200, +}: { url?: string; body?: any; status?: number } = {}) { const response = await request(context.app) .delete(url) .set('xc-auth', context.token) @@ -229,9 +245,7 @@ function textBased() { ///////////////////////////////////////////////////////////////////////////// it('List: default', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - ); + const rsp = await ncAxiosGet(); const expectedPageInfo = { totalRows: 400, @@ -245,10 +259,7 @@ function textBased() { }); it('List: offset, limit', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { offset: 200, limit: 100 }, - ); + const rsp = await ncAxiosGet({ query: { offset: 200, limit: 100 } }); const expectedPageInfo = { totalRows: 400, @@ -261,10 +272,9 @@ function textBased() { }); it('List: fields, single', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { fields: 'SingleLineText' }, - ); + const rsp = await ncAxiosGet({ + query: { fields: 'SingleLineText' }, + }); expect( verifyColumnsInRsp(rsp.body.list[0], [{ title: 'SingleLineText' }]), @@ -272,10 +282,9 @@ function textBased() { }); it('List: fields, multiple', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { fields: ['SingleLineText', 'MultiLineText'] }, - ); + const rsp = await ncAxiosGet({ + query: { fields: ['SingleLineText', 'MultiLineText'] }, + }); expect( verifyColumnsInRsp(rsp.body.list[0], [ @@ -287,10 +296,9 @@ function textBased() { it('List: sort, ascending', async function () { const sortColumn = columns.find((c) => c.title === 'SingleLineText'); - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { sort: 'SingleLineText', limit: 400 }, - ); + const rsp = await ncAxiosGet({ + query: { sort: 'SingleLineText', limit: 400 }, + }); expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); const sortedArray = rsp.body.list.map((r) => r[sortColumn.title]); @@ -299,10 +307,9 @@ function textBased() { it('List: sort, descending', async function () { const sortColumn = columns.find((c) => c.title === 'SingleLineText'); - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { sort: '-SingleLineText', limit: 400 }, - ); + const rsp = await ncAxiosGet({ + query: { sort: '-SingleLineText', limit: 400 }, + }); expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); const descSortedArray = rsp.body.list.map((r) => r[sortColumn.title]); @@ -310,10 +317,12 @@ function textBased() { }); it('List: sort, multiple', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { sort: ['-SingleLineText', '-MultiLineText'], limit: 400 }, - ); + const rsp = await ncAxiosGet({ + query: { + sort: ['-SingleLineText', '-MultiLineText'], + limit: 400, + }, + }); expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); // Combination of SingleLineText & MultiLineText should be in descending order @@ -324,10 +333,12 @@ function textBased() { }); it('List: filter, single', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { where: '(SingleLineText,eq,Afghanistan)', limit: 400 }, - ); + const rsp = await ncAxiosGet({ + query: { + where: '(SingleLineText,eq,Afghanistan)', + limit: 400, + }, + }); expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); const filteredArray = rsp.body.list.map((r) => r.SingleLineText); @@ -335,14 +346,13 @@ function textBased() { }); it('List: filter, multiple', async function () { - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + const rsp = await ncAxiosGet({ + query: { where: '(SingleLineText,eq,Afghanistan)~and(MultiLineText,eq,Allahabad, India)', limit: 400, }, - ); + }); expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); const filteredArray = rsp.body.list.map( @@ -375,10 +385,7 @@ function textBased() { }); // fetch records from view - let rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { viewId: gridView.id }, - ); + let rsp = await ncAxiosGet({ query: { viewId: gridView.id } }); expect(rsp.body.pageInfo.totalRows).to.equal(31); await updateView(context, { @@ -395,8 +402,10 @@ function textBased() { }); // fetch records from view - rsp = await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { - viewId: gridView.id, + rsp = await ncAxiosGet({ + query: { + viewId: gridView.id, + }, }); expect(rsp.body.pageInfo.totalRows).to.equal(61); @@ -414,8 +423,10 @@ function textBased() { }); // fetch records from view - rsp = await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { - viewId: gridView.id, + rsp = await ncAxiosGet({ + query: { + viewId: gridView.id, + }, }); expect(rsp.body.pageInfo.totalRows).to.equal(61); @@ -432,8 +443,10 @@ function textBased() { }); // fetch records from view - rsp = await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { - viewId: gridView.id, + rsp = await ncAxiosGet({ + query: { + viewId: gridView.id, + }, }); const displayColumns = columns.filter((c) => c.title !== 'SingleLineText'); expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true); @@ -475,10 +488,7 @@ function textBased() { }); // fetch records from view - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { viewId: gridView.id }, - ); + const rsp = await ncAxiosGet({ query: { viewId: gridView.id } }); expect(rsp.body.pageInfo.totalRows).to.equal(61); const displayColumns = columns.filter( (c) => c.title !== 'MultiLineText' && c.title !== 'Email', @@ -490,14 +500,13 @@ function textBased() { it('List: view ID + sort', async function () { const gridView = await prepareViewForTests(); - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + const rsp = await ncAxiosGet({ + query: { viewId: gridView.id, sort: 'Url', limit: 100, }, - ); + }); const displayColumns = columns.filter( (c) => c.title !== 'MultiLineText' && c.title !== 'Email', ); @@ -510,14 +519,13 @@ function textBased() { it('List: view ID + filter', async function () { const gridView = await prepareViewForTests(); - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + const rsp = await ncAxiosGet({ + query: { viewId: gridView.id, where: '(Phone,eq,1-541-754-3010)', limit: 100, }, - ); + }); const displayColumns = columns.filter( (c) => c.title !== 'MultiLineText' && c.title !== 'Email', ); @@ -530,14 +538,13 @@ function textBased() { it('List: view ID + fields', async function () { const gridView = await prepareViewForTests(); - const rsp = await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + const rsp = await ncAxiosGet({ + query: { viewId: gridView.id, fields: ['Phone', 'MultiLineText', 'SingleLineText', 'Email'], limit: 100, }, - ); + }); expect(rsp.body.pageInfo.totalRows).to.equal(61); expect( verifyColumnsInRsp(rsp.body.list[0], [ @@ -550,67 +557,75 @@ function textBased() { // Error handling it('List: invalid ID', async function () { // Invalid table ID - await ncAxiosGet(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + await ncAxiosGet({ + url: `/api/v1/base/${project.id}/tables/123456789`, + status: 400, + }); // Invalid project ID - await ncAxiosGet(`/api/v1/base/123456789/tables/123456789`, {}, 400); + await ncAxiosGet({ + url: `/api/v1/base/123456789/tables/123456789`, + status: 400, + }); // Invalid view ID - await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { viewId: '123456789' }, - 400, - ); + await ncAxiosGet({ + query: { + viewId: '123456789', + }, + status: 400, + }); }); it('List: invalid limit & offset', async function () { // Invalid limit - await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + await ncAxiosGet({ + query: { limit: -100, }, - 200, - ); - await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + status: 200, + }); + await ncAxiosGet({ + query: { limit: 'abc', }, - 200, - ); + status: 200, + }); // Invalid offset - await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + await ncAxiosGet({ + query: { offset: -100, }, - 200, - ); - await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + status: 200, + }); + await ncAxiosGet({ + query: { offset: 'abc', }, - 200, - ); - await ncAxiosGet( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + status: 200, + }); + await ncAxiosGet({ + query: { offset: 10000, }, - 200, - ); + status: 200, + }); }); it('List: invalid sort, filter, fields', async function () { - await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { - sort: 'abc', + await ncAxiosGet({ + query: { + sort: 'abc', + }, }); - await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { - where: 'abc', + await ncAxiosGet({ + query: { + where: 'abc', + }, }); - await ncAxiosGet(`/api/v1/base/${project.id}/tables/${table.id}`, { - fields: 'abc', + await ncAxiosGet({ + query: { + fields: 'abc', + }, }); }); @@ -629,10 +644,7 @@ function textBased() { }; it('Create: all fields', async function () { - const rsp = await ncAxiosPost( - `/api/v1/base/${project.id}/tables/${table.id}`, - newRecord, - ); + const rsp = await ncAxiosPost({ body: newRecord }); expect(rsp.body).to.deep.equal({ Id: 401, @@ -645,10 +657,7 @@ function textBased() { SingleLineText: 'abc', MultiLineText: 'abc abc \n abc \r abc \t abc 1234!@#$%^&*()_+', }; - const rsp = await ncAxiosPost( - `/api/v1/base/${project.id}/tables/${table.id}`, - newRecord, - ); + const rsp = await ncAxiosPost({ body: newRecord }); // fields left out should be null expect(rsp.body).to.deep.equal({ @@ -661,10 +670,7 @@ function textBased() { }); it('Create: bulk', async function () { - const rsp = await ncAxiosPost( - `/api/v1/base/${project.id}/tables/${table.id}`, - [newRecord, newRecord, newRecord], - ); + const rsp = await ncAxiosPost({ body: [newRecord, newRecord, newRecord] }); expect(rsp.body).to.deep.equal([{ Id: 401 }, { Id: 402 }, { Id: 403 }]); }); @@ -672,21 +678,25 @@ function textBased() { // Error handling it('Create: invalid ID', async function () { // Invalid table ID - await ncAxiosPost(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + await ncAxiosPost({ + url: `/api/v1/base/${project.id}/tables/123456789`, + status: 400, + }); // Invalid project ID - await ncAxiosPost(`/api/v1/base/123456789/tables/123456789`, {}, 400); + await ncAxiosPost({ + url: `/api/v1/base/123456789/tables/123456789`, + status: 400, + }); // Invalid data - repeated ID - await ncAxiosPost( - `/api/v1/base/${project.id}/tables/${table.id}`, - { ...newRecord, Id: 300 }, - 400, - ); + await ncAxiosPost({ + body: { ...newRecord, Id: 300 }, + status: 400, + }); // Invalid data - number instead of string - // await ncAxiosPost( - // `/api/v1/base/${project.id}/tables/${table.id}`, - // { ...newRecord, SingleLineText: 300 }, - // 400, - // ); + // await ncAxiosPost({ + // body: { ...newRecord, SingleLineText: 300 }, + // status: 400, + // }); }); // TBD : default value handling @@ -698,15 +708,23 @@ function textBased() { ///////////////////////////////////////////////////////////////////////////// - it.only('Read: all fields', async function () { - const rsp = await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/100`); + it('Read: all fields', async function () { + const rsp = await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/100`, + }); }); it('Read: invalid ID', async function () { // Invalid table ID - await ncAxiosGet(`/api/v1/db/tables/123456789/rows/100`, {}, 400); + await ncAxiosGet({ + url: `/api/v1/base/tables/123456789/rows/100`, + status: 400, + }); // Invalid row ID - await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/1000`, {}, 400); + await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/1000`, + status: 400, + }); }); ///////////////////////////////////////////////////////////////////////////// @@ -717,38 +735,36 @@ function textBased() { ///////////////////////////////////////////////////////////////////////////// it('Update: all fields', async function () { - const rsp = await ncAxiosPatch( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + const rsp = await ncAxiosPatch({ + body: { Id: 1, ...newRecord, }, - ); + }); expect(rsp.body).to.deep.equal({ Id: 1, }); }); it('Update: partial', async function () { - const recordBeforeUpdate = await ncAxiosGet( - `/api/v1/base/tables/${table.id}/rows/1`, - ); + const recordBeforeUpdate = await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/1`, + }); - const rsp = await ncAxiosPatch( - `/api/v1/base/${project.id}/tables/${table.id}`, - { + const rsp = await ncAxiosPatch({ + body: { Id: 1, SingleLineText: 'some text', MultiLineText: 'some more text', }, - ); + }); expect(rsp.body).to.deep.equal({ Id: 1, }); - const recordAfterUpdate = await ncAxiosGet( - `/api/v1/base/tables/${table.id}/rows/1`, - ); + const recordAfterUpdate = await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/1`, + }); expect(recordAfterUpdate.body).to.deep.equal({ ...recordBeforeUpdate.body, SingleLineText: 'some text', @@ -757,9 +773,8 @@ function textBased() { }); it('Update: bulk', async function () { - const rsp = await ncAxiosPatch( - `/api/v1/base/${project.id}/tables/${table.id}`, - [ + const rsp = await ncAxiosPatch({ + body: [ { Id: 1, SingleLineText: 'some text', @@ -771,7 +786,7 @@ function textBased() { MultiLineText: 'some more text', }, ], - ); + }); expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); }); @@ -779,15 +794,20 @@ function textBased() { it('Update: invalid ID', async function () { // Invalid project ID - await ncAxiosPatch(`/api/v1/base/123456789/tables/${table.id}`, {}, 400); + await ncAxiosPatch({ + url: `/api/v1/base/123456789/tables/${table.id}`, + status: 400, + }); // Invalid table ID - await ncAxiosPatch(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + await ncAxiosPatch({ + url: `/api/v1/base/${project.id}/tables/123456789`, + status: 400, + }); // Invalid row ID - await ncAxiosPatch( - `/api/v1/base/${project.id}/tables/${table.id}`, - { Id: 123456789, SingleLineText: 'some text' }, - 400, - ); + await ncAxiosPatch({ + body: { Id: 123456789, SingleLineText: 'some text' }, + status: 400, + }); }); ///////////////////////////////////////////////////////////////////////////// @@ -798,43 +818,47 @@ function textBased() { ///////////////////////////////////////////////////////////////////////////// it('Delete: single', async function () { - const rsp = await ncAxiosDelete( - `/api/v1/base/${project.id}/tables/${table.id}`, - { Id: 1 }, - ); + const rsp = await ncAxiosDelete({ body: { Id: 1 } }); expect(rsp.body).to.deep.equal({ Id: 1 }); // check that it's gone - await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/1`, {}, 400); - + await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/1`, + status: 400, + }); }); it('Delete: bulk', async function () { - const rsp = await ncAxiosDelete( - `/api/v1/base/${project.id}/tables/${table.id}`, - [{ Id: 1 }, { Id: 2 }], - ); + const rsp = await ncAxiosDelete({ body: [{ Id: 1 }, { Id: 2 }] }); expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); // check that it's gone - await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/1`, {}, 400); - await ncAxiosGet(`/api/v1/db/tables/${table.id}/rows/2`, {}, 400); + await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/1`, + status: 400, + }); + await ncAxiosGet({ + url: `/api/v1/base/tables/${table.id}/rows/2`, + status: 400, + }); }); // Error handling it('Delete: invalid ID', async function () { // Invalid project ID - await ncAxiosDelete(`/api/v1/base/123456789/tables/${table.id}`, {}, 400); + await ncAxiosDelete({ + url: `/api/v1/base/123456789/tables/${table.id}`, + status: 400, + }); // Invalid table ID - await ncAxiosDelete(`/api/v1/base/${project.id}/tables/123456789`, {}, 400); + await ncAxiosDelete({ + url: `/api/v1/base/${project.id}/tables/123456789`, + status: 400, + }); // Invalid row ID - await ncAxiosDelete( - `/api/v1/base/${project.id}/tables/${table.id}`, - { Id: 123456789 }, - 400, - ); - } + await ncAxiosDelete({ body: { Id: 123456789 }, status: 400 }); + }); } function numberBased() { From a419e115250cb575477112c5ed2fc603c3472c28 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 12:07:24 +0530 Subject: [PATCH 11/97] fix: correction in project id validation Signed-off-by: Pranav C --- packages/nocodb/src/controllers/data-table.controller.ts | 4 ---- packages/nocodb/src/services/data-table.service.ts | 7 ++----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 57f13b55a4..c16ac877a8 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -106,13 +106,11 @@ export class DataTableController { @Acl('dataDelete') async dataDelete( @Request() req, - @Param('projectId') projectId: string, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, ) { return await this.dataTableService.dataDelete({ - projectId: projectId, modelId: modelId, cookie: req, viewId, @@ -124,14 +122,12 @@ export class DataTableController { @Acl('dataRead') async dataRead( @Request() req, - @Param('projectId') projectId: string, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, ) { return await this.dataTableService.dataRead({ modelId, - projectId, rowId: rowId, query: req.query, viewId, diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index d89fe3b722..de015954d4 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -143,8 +143,6 @@ export class DataTableService { return { count }; } - - private async getModelAndView(param: { projectId?: string; viewId?: string; @@ -152,11 +150,11 @@ export class DataTableService { }) { const model = await Model.get(param.modelId); - if(!model) { + if (!model) { throw new Error('Model not found'); } - if (model.project_id && model.project_id !== param.projectId) { + if (param.projectId && model.project_id !== param.projectId) { throw new Error('Model not belong to project'); } @@ -171,5 +169,4 @@ export class DataTableService { return { model, view }; } - } From ffde216556ea4940f874877dd07007da5a37ad6a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 12:21:22 +0530 Subject: [PATCH 12/97] refactor: update path Signed-off-by: Pranav C --- .../noco-docs/content/en/developer-resources/rest-apis.md | 6 +++--- packages/nocodb/src/controllers/data-table.controller.ts | 6 +++--- packages/nocodb/src/schema/swagger.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/noco-docs/content/en/developer-resources/rest-apis.md b/packages/noco-docs/content/en/developer-resources/rest-apis.md index f18a57d348..d93daa069e 100644 --- a/packages/noco-docs/content/en/developer-resources/rest-apis.md +++ b/packages/noco-docs/content/en/developer-resources/rest-apis.md @@ -76,9 +76,9 @@ Currently, the default value for {orgs} is noco. Users will be able to ch | Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/group/{columnId} | | Data | Get | dbTableRow | tableRowList | /api/v1/base/{baseId}/tables/{tableId} | | Data | Post | dbTableRow | tableRowCreate | /api/v1/base/{baseId}/tables/{tableId} | -| Data | Get | dbTableRow | tableRowRead | /api/v1/db/tables/{tableId}/rows/{rowId} | -| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/db/tables/{tableId}/rows/{rowId} | -| Data | Delete| dbTableRow | tableRowDelete | /api/v1/db/tables/{tableId}/rows/{rowId} | +| Data | Get | dbTableRow | tableRowRead | /api/v1/base/tables/{tableId}/rows/{rowId} | +| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/base/tables/{tableId}/rows/{rowId} | +| Data | Delete| dbTableRow | tableRowDelete | /api/v1/base/tables/{tableId}/rows/{rowId} | | Data | Get | dbTableRow | tableRowCount | /api/v1/base/{baseId}/tables/{tableId}/count | ### Meta APIs diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index c16ac877a8..3073423c30 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -85,7 +85,7 @@ export class DataTableController { }); } - @Patch(['/api/v1/db/tables/:modelId/rows/:rowId']) + @Patch(['/api/v1/base/tables/:modelId/rows/:rowId']) @Acl('dataUpdate') async dataUpdate( @Request() req, @@ -102,7 +102,7 @@ export class DataTableController { }); } - @Delete(['/api/v1/db/tables/:modelId/rows/:rowId']) + @Delete(['/api/v1/base/tables/:modelId/rows/:rowId']) @Acl('dataDelete') async dataDelete( @Request() req, @@ -118,7 +118,7 @@ export class DataTableController { }); } - @Get(['/api/v1/db/tables/:modelId/rows/:rowId']) + @Get(['/api/v1/base/tables/:modelId/rows/:rowId']) @Acl('dataRead') async dataRead( @Request() req, diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 5fd19b9012..912a9fa6d9 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -14263,7 +14263,7 @@ } } }, - "/api/v1/db/tables/{tableId}/rows/{rowId}": { + "/api/v1/base/tables/{tableId}/rows/{rowId}": { "parameters": [ { "schema": { From 0d9c896ab61a1143ddf4d88aae188748bbec3f44 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 12:57:28 +0530 Subject: [PATCH 13/97] feat: if array of values passed then do bulk insert Signed-off-by: Pranav C --- packages/nocodb/src/services/data-table.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index de015954d4..b6a19d238b 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -65,7 +65,12 @@ export class DataTableService { dbDriver: await NcConnectionMgrv2.get(base), }); - return await baseModel.insert(param.body, null, param.cookie); + // if array then do bulk insert + if (Array.isArray(param.body)) { + return await baseModel.bulkInsert(param.body, { cookie: param.cookie }); + } else { + return await baseModel.insert(param.body, null, param.cookie); + } } async dataUpdate(param: { From bd9620c073dc07a5dbe1c2e26d0d5dcd18214c32 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 14:58:15 +0530 Subject: [PATCH 14/97] feat: if array of values passed then do bulk insert Signed-off-by: Pranav C --- .../en/developer-resources/rest-apis.md | 4 +- .../src/controllers/data-table.controller.ts | 9 +- packages/nocodb/src/schema/swagger.json | 159 +++++++++++------- .../nocodb/src/services/data-table.service.ts | 45 +++-- 4 files changed, 137 insertions(+), 80 deletions(-) diff --git a/packages/noco-docs/content/en/developer-resources/rest-apis.md b/packages/noco-docs/content/en/developer-resources/rest-apis.md index d93daa069e..3652646fd8 100644 --- a/packages/noco-docs/content/en/developer-resources/rest-apis.md +++ b/packages/noco-docs/content/en/developer-resources/rest-apis.md @@ -77,8 +77,8 @@ Currently, the default value for {orgs} is noco. Users will be able to ch | Data | Get | dbTableRow | tableRowList | /api/v1/base/{baseId}/tables/{tableId} | | Data | Post | dbTableRow | tableRowCreate | /api/v1/base/{baseId}/tables/{tableId} | | Data | Get | dbTableRow | tableRowRead | /api/v1/base/tables/{tableId}/rows/{rowId} | -| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/base/tables/{tableId}/rows/{rowId} | -| Data | Delete| dbTableRow | tableRowDelete | /api/v1/base/tables/{tableId}/rows/{rowId} | +| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/base/tables/{tableId}/rows | +| Data | Delete| dbTableRow | tableRowDelete | /api/v1/base/tables/{tableId}/rows | | Data | Get | dbTableRow | tableRowCount | /api/v1/base/{baseId}/tables/{tableId}/count | ### Meta APIs diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 3073423c30..15944d6982 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -85,7 +85,7 @@ export class DataTableController { }); } - @Patch(['/api/v1/base/tables/:modelId/rows/:rowId']) + @Patch(['/api/v1/base/tables/:modelId/rows']) @Acl('dataUpdate') async dataUpdate( @Request() req, @@ -98,11 +98,11 @@ export class DataTableController { body: req.body, cookie: req, viewId, - rowId: rowId, + // rowId: rowId, }); } - @Delete(['/api/v1/base/tables/:modelId/rows/:rowId']) + @Delete(['/api/v1/base/tables/:modelId/rows']) @Acl('dataDelete') async dataDelete( @Request() req, @@ -114,7 +114,8 @@ export class DataTableController { modelId: modelId, cookie: req, viewId, - rowId: rowId, + body: req.body, + // rowId: rowId, }); } diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 912a9fa6d9..3f003ca5d3 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -14261,56 +14261,26 @@ } } } - } - }, - "/api/v1/base/tables/{tableId}/rows/{rowId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "tableId", - "in": "path", - "required": true, - "description": "Table Id" - }, - { - "schema": { - "type": "string" - }, - "name": "viewId", - "in": "query", - "required": true - }, - { - "schema": { - "example": "1" - }, - "name": "rowId", - "in": "path", - "required": true, - "description": "Unique Row ID" - } - ], - "get": { - "summary": "Get Table View Row", - "operationId": "db-view-row-read", + }, + + "patch": { + "summary": "Update Table View Row", + "operationId": "table-row-update", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "type": "object", - "properties": {} + "type": "object" }, "examples": { "Example 1": { "value": { "Id": 1, - "Title": "foo", + "Title": "bar", "CreatedAt": "2023-03-11T09:11:47.437Z", - "UpdatedAt": "2023-03-11T09:11:47.784Z" + "UpdatedAt": "2023-03-11T09:20:21.133Z" } } } @@ -14321,33 +14291,55 @@ "$ref": "#/components/responses/BadRequest" } }, - "description": "Get the target Table View Row", "tags": ["DB Table Row"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "array", + "items": { + "type": "object" + } + } + ] + }, + "examples": { + "Example 1": { + "value": { + "Id": 1, + "Title": "bar" + } + } + } + } + } + }, + "description": "Update the target Table View Row", "parameters": [ { "$ref": "#/components/parameters/xc-auth" } ] }, - "patch": { - "summary": "Update Table View Row", - "operationId": "table-row-update", + "delete": { + "summary": "Delete Table View Row", + "operationId": "table-row-delete", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "type": "object" + "type": "number" }, "examples": { "Example 1": { - "value": { - "Id": 1, - "Title": "bar", - "CreatedAt": "2023-03-11T09:11:47.437Z", - "UpdatedAt": "2023-03-11T09:20:21.133Z" - } + "value": 1 } } } @@ -14357,44 +14349,91 @@ "$ref": "#/components/responses/BadRequest" } }, - "tags": ["DB Table Row"], + "requestBody": { "content": { "application/json": { "schema": { - "type": "object" + "oneOf": [ + { + "type": "object" + }, + { + "type": "array", + "items": { + "type": "object" + } + } + ] }, "examples": { "Example 1": { "value": { - "Title": "bar" + "Id": 1 } } } } } }, - "description": "Update the target Table View Row", + "tags": ["DB Table Row"], + "description": "Delete the target Table View Row", "parameters": [ { "$ref": "#/components/parameters/xc-auth" } ] - }, - "delete": { - "summary": "Delete Table View Row", - "operationId": "table-row-delete", + } + }, + "/api/v1/base/tables/{tableId}/rows/{rowId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "Table Id" + }, + { + "schema": { + "type": "string" + }, + "name": "viewId", + "in": "query", + "required": true + }, + { + "schema": { + "example": "1" + }, + "name": "rowId", + "in": "path", + "required": true, + "description": "Unique Row ID" + } + ], + "get": { + "summary": "Get Table View Row", + "operationId": "db-view-row-read", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "type": "number" + "type": "object", + "properties": {} }, "examples": { "Example 1": { - "value": 1 + "value": { + "Id": 1, + "Title": "foo", + "CreatedAt": "2023-03-11T09:11:47.437Z", + "UpdatedAt": "2023-03-11T09:11:47.784Z" + } } } } @@ -14404,8 +14443,8 @@ "$ref": "#/components/responses/BadRequest" } }, + "description": "Get the target Table View Row", "tags": ["DB Table Row"], - "description": "Delete the target Table View Row", "parameters": [ { "$ref": "#/components/parameters/xc-auth" diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index b6a19d238b..b0a3bb72fe 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { NcError } from '../helpers/catchError'; import { Base, Model, View } from '../models'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import projectAcl from '../utils/projectAcl'; import { DatasService } from './datas.service'; @Injectable() @@ -77,7 +78,7 @@ export class DataTableService { projectId?: string; modelId: string; viewId?: string; - rowId: string; + // rowId: string; body: any; cookie: any; }) { @@ -91,35 +92,51 @@ export class DataTableService { dbDriver: await NcConnectionMgrv2.get(base), }); - return await baseModel.updateByPk( - param.rowId, - param.body, - null, - param.cookie, + // return await baseModel.updateByPk( + // param.rowId, + // param.body, + // null, + // param.cookie, + // ); + + const res = await baseModel.bulkUpdate( + Array.isArray(param.body) ? param.body : [param.body], + { cookie: param.cookie }, ); + + return Array.isArray(param.body) ? res : res[0]; } async dataDelete(param: { projectId?: string; modelId: string; viewId?: string; - rowId: string; + // rowId: string; cookie: any; + body: any; }) { const { model, view } = await this.getModelAndView(param); const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, viewId: view?.id, - dbDriver: await NcConnectionMgrv2.get(base), + dbDriver: await NcConnectionMgrv2.get(base) }); + // + // // todo: Should have error http status code + // const message = await baseModel.hasLTARData(param.rowId, model); + // if (message.length) { + // return { message }; + // } + // return await baseModel.delByPk(param.rowId, null, param.cookie); + + + const res = await baseModel.bulkUpdate( + Array.isArray(param.body) ? param.body : [param.body], + { cookie: param.cookie }, + ); - // todo: Should have error http status code - const message = await baseModel.hasLTARData(param.rowId, model); - if (message.length) { - return { message }; - } - return await baseModel.delByPk(param.rowId, null, param.cookie); + return Array.isArray(param.body) ? res : res[0]; } async dataCount(param: { From d4b92a931658f6ddc1cc5d32815d6067190de44a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 15:07:07 +0530 Subject: [PATCH 15/97] feat: add `filter` query param as an alias for `where` Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 2 +- packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index d5f1f95852..0d6bec726a 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -1513,7 +1513,7 @@ class BaseModelSqlv2 { _getListArgs(args: XcFilterWithAlias): XcFilter { const obj: XcFilter = {}; - obj.where = args.where || args.w || ''; + obj.where = args.filter || args.where || args.w || ''; obj.having = args.having || args.h || ''; obj.shuffle = args.shuffle || args.r || ''; obj.condition = args.condition || args.c || {}; diff --git a/packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts b/packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts index 4da3d275f9..625ba6adab 100644 --- a/packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts +++ b/packages/nocodb/src/db/sql-data-mapper/lib/BaseModel.ts @@ -1506,6 +1506,7 @@ abstract class BaseModel { export interface XcFilter { where?: string; + filter?: string; having?: string; condition?: any; conditionGraph?: any; From e2ced4212d53a8a5b8adfed345ce526931a0580f Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 15:59:13 +0530 Subject: [PATCH 16/97] refactor: path correction Signed-off-by: Pranav C --- packages/nocodb/src/controllers/data-table.controller.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 15944d6982..06b9624c46 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -85,13 +85,14 @@ export class DataTableController { }); } - @Patch(['/api/v1/base/tables/:modelId/rows']) + @Patch(['/api/v1/base/:projectId/tables/:modelId']) @Acl('dataUpdate') async dataUpdate( @Request() req, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, + @Param('projectId') projectId: string, ) { return await this.dataTableService.dataUpdate({ modelId: modelId, @@ -99,16 +100,18 @@ export class DataTableController { cookie: req, viewId, // rowId: rowId, + projectId, }); } - @Delete(['/api/v1/base/tables/:modelId/rows']) + @Delete(['/api/v1/base/:projectId/tables/:modelId']) @Acl('dataDelete') async dataDelete( @Request() req, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, + @Param('projectId') projectId: string, ) { return await this.dataTableService.dataDelete({ modelId: modelId, @@ -116,6 +119,7 @@ export class DataTableController { viewId, body: req.body, // rowId: rowId, + projectId, }); } From 4a59a4e6d493a35a07bd9d05c6fdb4c90e0fd607 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Mon, 29 May 2023 16:19:56 +0530 Subject: [PATCH 17/97] test: number based List + CRUD Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 252 ++++++++++++++++-- 1 file changed, 225 insertions(+), 27 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index c314f73928..76c53e5050 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -136,8 +136,8 @@ async function ncAxiosGet({ .get(url) .set('xc-auth', context.token) .query(query) - .send({}) - .expect(status); + .send({}); + expect(response.status).to.equal(status); return response; } async function ncAxiosPost({ @@ -149,7 +149,7 @@ async function ncAxiosPost({ .post(url) .set('xc-auth', context.token) .send(body); - // .expect(status); + expect(response.status).to.equal(status); return response; } async function ncAxiosPatch({ @@ -160,8 +160,8 @@ async function ncAxiosPatch({ const response = await request(context.app) .patch(url) .set('xc-auth', context.token) - .send(body) - .expect(status); + .send(body); + expect(response.status).to.equal(status); return response; } async function ncAxiosDelete({ @@ -172,8 +172,8 @@ async function ncAxiosDelete({ const response = await request(context.app) .delete(url) .set('xc-auth', context.token) - .send(body) - .expect(status); + .send(body); + expect(response.status).to.equal(status); return response; } @@ -736,14 +736,18 @@ function textBased() { it('Update: all fields', async function () { const rsp = await ncAxiosPatch({ - body: { - Id: 1, - ...newRecord, - }, - }); - expect(rsp.body).to.deep.equal({ - Id: 1, + body: [ + { + Id: 1, + ...newRecord, + }, + ], }); + expect(rsp.body).to.deep.equal([ + { + Id: '1', + }, + ]); }); it('Update: partial', async function () { @@ -752,15 +756,19 @@ function textBased() { }); const rsp = await ncAxiosPatch({ - body: { - Id: 1, - SingleLineText: 'some text', - MultiLineText: 'some more text', - }, - }); - expect(rsp.body).to.deep.equal({ - Id: 1, + body: [ + { + Id: 1, + SingleLineText: 'some text', + MultiLineText: 'some more text', + }, + ], }); + expect(rsp.body).to.deep.equal([ + { + Id: '1', + }, + ]); const recordAfterUpdate = await ncAxiosGet({ url: `/api/v1/base/tables/${table.id}/rows/1`, @@ -787,7 +795,7 @@ function textBased() { }, ], }); - expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); + expect(rsp.body).to.deep.equal([{ Id: '1' }, { Id: '2' }]); }); // Error handling @@ -818,10 +826,10 @@ function textBased() { ///////////////////////////////////////////////////////////////////////////// it('Delete: single', async function () { - const rsp = await ncAxiosDelete({ body: { Id: 1 } }); - expect(rsp.body).to.deep.equal({ Id: 1 }); + const rsp = await ncAxiosDelete({ body: [{ Id: 1 }] }); + expect(rsp.body).to.deep.equal([{ Id: '1' }]); - // check that it's gone + // // check that it's gone await ncAxiosGet({ url: `/api/v1/base/tables/${table.id}/rows/1`, status: 400, @@ -830,7 +838,7 @@ function textBased() { it('Delete: bulk', async function () { const rsp = await ncAxiosDelete({ body: [{ Id: 1 }, { Id: 2 }] }); - expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); + expect(rsp.body).to.deep.equal([{ Id: '1' }, { Id: '2' }]); // check that it's gone await ncAxiosGet({ @@ -902,6 +910,196 @@ function numberBased() { // verify length of unfiltered records to be 400 expect(insertedRecords.length).to.equal(400); }); + + const records = [ + { + Id: 1, + Number: 33, + Decimal: 33.3, + Currency: 33.3, + Percent: 33, + Duration: 10, + Rating: 0, + }, + { + Id: 2, + Number: null, + Decimal: 456.34, + Currency: 456.34, + Percent: null, + Duration: 20, + Rating: 1, + }, + { + Id: 3, + Number: 456, + Decimal: 333.3, + Currency: 333.3, + Percent: 456, + Duration: 30, + Rating: 2, + }, + { + Id: 4, + Number: 333, + Decimal: null, + Currency: null, + Percent: 333, + Duration: 40, + Rating: 3, + }, + { + Id: 5, + Number: 267, + Decimal: 267.5674, + Currency: 267.5674, + Percent: 267, + Duration: 50, + Rating: null, + }, + { + Id: 6, + Number: 34, + Decimal: 34, + Currency: 34, + Percent: 34, + Duration: 60, + Rating: 0, + }, + { + Id: 7, + Number: 8754, + Decimal: 8754, + Currency: 8754, + Percent: 8754, + Duration: null, + Rating: 4, + }, + { + Id: 8, + Number: 3234, + Decimal: 3234.547, + Currency: 3234.547, + Percent: 3234, + Duration: 70, + Rating: 5, + }, + { + Id: 9, + Number: 44, + Decimal: 44.2647, + Currency: 44.2647, + Percent: 44, + Duration: 80, + Rating: 0, + }, + { + Id: 10, + Number: 33, + Decimal: 33.98, + Currency: 33.98, + Percent: 33, + Duration: 90, + Rating: 1, + }, + ]; + + it('Number based- List & CRUD', async function () { + // list 10 records + let rsp = await ncAxiosGet({ + query: { + limit: 10, + }, + }); + const pageInfo = { + totalRows: 400, + page: 1, + pageSize: 10, + isFirstPage: true, + isLastPage: false, + }; + expect(rsp.body.pageInfo).to.deep.equal(pageInfo); + expect(rsp.body.list).to.deep.equal(records); + + /////////////////////////////////////////////////////////////////////////// + + // insert 10 records + // remove Id's from record array + records.forEach((r) => delete r.Id); + rsp = await ncAxiosPost({ + body: records, + }); + + // prepare array with 10 Id's, from 401 to 410 + const ids = []; + for (let i = 401; i <= 410; i++) { + ids.push({ Id: i }); + } + expect(rsp.body).to.deep.equal(ids); + + /////////////////////////////////////////////////////////////////////////// + + // read record with Id 401 + rsp = await ncAxiosGet({ + url: `/api/v1/base/${project.id}/tables/${table.id}/rows/401`, + }); + expect(rsp.body).to.deep.equal(records[0]); + + /////////////////////////////////////////////////////////////////////////// + + // update record with Id 401 to 404 + const updatedRecord = { + Number: 55, + Decimal: 55.5, + Currency: 55.5, + Percent: 55, + Duration: 55, + Rating: 5, + }; + const updatedRecords = [ + { + id: 401, + ...updatedRecord, + }, + { + id: 402, + ...updatedRecord, + }, + { + id: 403, + ...updatedRecord, + }, + { + id: 404, + ...updatedRecord, + }, + ]; + rsp = await ncAxiosPatch({ + body: updatedRecords, + }); + expect(rsp.body).to.deep.equal( + updatedRecords.map((record) => ({ id: record.id })), + ); + + // verify updated records + rsp = await ncAxiosGet({ + query: { + limit: 4, + offset: 400, + }, + }); + expect(rsp.body.list).to.deep.equal(updatedRecords); + + /////////////////////////////////////////////////////////////////////////// + + // delete record with ID 401 to 404 + rsp = await ncAxiosDelete({ + body: updatedRecords.map((record) => ({ id: record.id })), + }); + expect(rsp.body).to.deep.equal( + updatedRecords.map((record) => ({ id: record.id })), + ); + }); } function selectBased() { From e7ff641b611578de1fadb7dea22a6269c64e34d4 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 17:55:54 +0530 Subject: [PATCH 18/97] refactor: path correction and error status code update Signed-off-by: Pranav C --- .../src/controllers/data-table.controller.ts | 22 +- packages/nocodb/src/db/BaseModelSqlv2.ts | 2529 +++++++++-------- packages/nocodb/src/helpers/catchError.ts | 8 + .../nocodb/src/services/data-table.service.ts | 7 +- 4 files changed, 1282 insertions(+), 1284 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 06b9624c46..599622f125 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -26,19 +26,17 @@ export class DataTableController { constructor(private readonly dataTableService: DataTableService) {} // todo: Handle the error case where view doesnt belong to model - @Get('/api/v1/base/:projectId/tables/:modelId') + @Get('/api/v1/tables/:modelId') @Acl('dataList') async dataList( @Request() req, @Response() res, - @Param('projectId') projectId: string, @Param('modelId') modelId: string, @Query('viewId') viewId: string, ) { const startTime = process.hrtime(); const responseData = await this.dataTableService.dataList({ query: req.query, - projectId: projectId, modelId: modelId, viewId: viewId, }); @@ -47,12 +45,11 @@ export class DataTableController { res.json(responseData); } - @Get(['/api/v1/base/:projectId/tables/:modelId/count']) + @Get(['/api/v1/tables/:modelId/count']) @Acl('dataCount') async dataCount( @Request() req, @Response() res, - @Param('projectId') projectId: string, @Param('modelId') modelId: string, @Query('viewId') viewId: string, ) { @@ -60,24 +57,21 @@ export class DataTableController { query: req.query, modelId, viewId, - projectId, }); res.json(countResult); } - @Post(['/api/v1/base/:projectId/tables/:modelId']) + @Post(['/api/v1/tables/:modelId']) @HttpCode(200) @Acl('dataInsert') async dataInsert( @Request() req, - @Param('projectId') projectId: string, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Body() body: any, ) { return await this.dataTableService.dataInsert({ - projectId: projectId, modelId: modelId, body: body, viewId, @@ -85,41 +79,35 @@ export class DataTableController { }); } - @Patch(['/api/v1/base/:projectId/tables/:modelId']) + @Patch(['/api/v1/tables/:modelId']) @Acl('dataUpdate') async dataUpdate( @Request() req, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, - @Param('projectId') projectId: string, ) { return await this.dataTableService.dataUpdate({ modelId: modelId, body: req.body, cookie: req, viewId, - // rowId: rowId, - projectId, }); } - @Delete(['/api/v1/base/:projectId/tables/:modelId']) + @Delete(['/api/v1/tables/:modelId']) @Acl('dataDelete') async dataDelete( @Request() req, @Param('modelId') modelId: string, @Query('viewId') viewId: string, @Param('rowId') rowId: string, - @Param('projectId') projectId: string, ) { return await this.dataTableService.dataDelete({ modelId: modelId, cookie: req, viewId, body: req.body, - // rowId: rowId, - projectId, }); } diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 0d6bec726a..162a8cc696 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -1,7 +1,7 @@ -import autoBind from 'auto-bind'; -import groupBy from 'lodash/groupBy'; -import DataLoader from 'dataloader'; -import { nocoExecute } from 'nc-help'; +import autoBind from 'auto-bind' +import groupBy from 'lodash/groupBy' +import DataLoader from 'dataloader' +import { nocoExecute } from 'nc-help' import { AuditOperationSubTypes, AuditOperationTypes, @@ -9,33 +9,33 @@ import { isVirtualCol, RelationTypes, UITypes, -} from 'nocodb-sdk'; -import Validator from 'validator'; -import { customAlphabet } from 'nanoid'; -import DOMPurify from 'isomorphic-dompurify'; -import { v4 as uuidv4 } from 'uuid'; -import { NcError } from '../helpers/catchError'; -import getAst from '../helpers/getAst'; - -import { Audit, Column, Filter, Model, Project, Sort, View } from '../models'; -import { sanitize, unsanitize } from '../helpers/sqlSanitize'; +} from 'nocodb-sdk' +import Validator from 'validator' +import { customAlphabet } from 'nanoid' +import DOMPurify from 'isomorphic-dompurify' +import { v4 as uuidv4 } from 'uuid' +import { NcError } from '../helpers/catchError' +import getAst from '../helpers/getAst' + +import { Audit, Column, Filter, Model, Project, Sort, View } from '../models' +import { sanitize, unsanitize } from '../helpers/sqlSanitize' import { COMPARISON_OPS, COMPARISON_SUB_OPS, IS_WITHIN_COMPARISON_SUB_OPS, -} from '../models/Filter'; -import Noco from '../Noco'; -import { HANDLE_WEBHOOK } from '../services/hook-handler.service'; -import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2'; -import genRollupSelectv2 from './genRollupSelectv2'; -import conditionV2 from './conditionV2'; -import sortV2 from './sortV2'; -import { customValidators } from './util/customValidators'; -import type { XKnex } from './CustomKnex'; +} from '../models/Filter' +import Noco from '../Noco' +import { HANDLE_WEBHOOK } from '../services/hook-handler.service' +import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2' +import genRollupSelectv2 from './genRollupSelectv2' +import conditionV2 from './conditionV2' +import sortV2 from './sortV2' +import { customValidators } from './util/customValidators' +import type { XKnex } from './CustomKnex' import type { XcFilter, XcFilterWithAlias, -} from './sql-data-mapper/lib/BaseModel'; +} from './sql-data-mapper/lib/BaseModel' import type { BarcodeColumn, FormulaColumn, @@ -44,41 +44,41 @@ import type { QrCodeColumn, RollupColumn, SelectOption, -} from '../models'; -import type { Knex } from 'knex'; -import type { SortType } from 'nocodb-sdk'; +} from '../models' +import type { Knex } from 'knex' +import type { SortType } from 'nocodb-sdk' -const GROUP_COL = '__nc_group_id'; +const GROUP_COL = '__nc_group_id' -const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14); +const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14) export async function getViewAndModelByAliasOrId(param: { projectName: string; tableName: string; viewName?: string; }) { - const project = await Project.getWithInfoByTitleOrId(param.projectName); + const project = await Project.getWithInfoByTitleOrId(param.projectName) const model = await Model.getByAliasOrId({ project_id: project.id, aliasOrId: param.tableName, - }); + }) const view = param.viewName && (await View.getByTitleOrId({ titleOrId: param.viewName, fk_model_id: model.id, - })); - if (!model) NcError.notFound('Table not found'); - return { model, view }; + })) + if (!model) NcError.notFound('Table not found') + return { model, view } } async function populatePk(model: Model, insertObj: any) { - await model.getColumns(); + await model.getColumns() for (const pkCol of model.primaryKeys) { - if (!pkCol.meta?.ag || insertObj[pkCol.title]) continue; + if (!pkCol.meta?.ag || insertObj[pkCol.title]) continue insertObj[pkCol.title] = - pkCol.meta?.ag === 'nc' ? `rc_${nanoidv2()}` : uuidv4(); + pkCol.meta?.ag === 'nc' ? `rc_${nanoidv2()}` : uuidv4() } } @@ -88,13 +88,13 @@ function checkColumnRequired( extractPkAndPv?: boolean, ) { // if primary key or foreign key included in fields, it's required - if (column.pk || column.uidt === UITypes.ForeignKey) return true; + if (column.pk || column.uidt === UITypes.ForeignKey) return true - if (extractPkAndPv && column.pv) return true; + if (extractPkAndPv && column.pv) return true // check fields defined and if not, then select all // if defined check if it is in the fields - return !fields || fields.includes(column.title); + return !fields || fields.includes(column.title) } /** @@ -104,30 +104,30 @@ function checkColumnRequired( * @classdesc Base class for models */ class BaseModelSqlv2 { - protected dbDriver: XKnex; - protected model: Model; - protected viewId: string; - private _proto: any; - private _columns = {}; + protected dbDriver: XKnex + protected model: Model + protected viewId: string + private _proto: any + private _columns = {} private config: any = { limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1), limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1), limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1), - }; + } constructor({ - dbDriver, - model, - viewId, - }: { + dbDriver, + model, + viewId, + }: { [key: string]: any; model: Model; }) { - this.dbDriver = dbDriver; - this.model = model; - this.viewId = viewId; - autoBind(this); + this.dbDriver = dbDriver + this.model = model + this.viewId = viewId + autoBind(this) } public async readByPk( @@ -135,55 +135,55 @@ class BaseModelSqlv2 { validateFormula = false, query: any = {}, ): Promise { - const qb = this.dbDriver(this.tnPath); + const qb = this.dbDriver(this.tnPath) const { ast, dependencyFields } = await getAst({ query, model: this.model, view: this.viewId && (await View.get(this.viewId)), - }); + }) await this.selectObject({ ...(dependencyFields ?? {}), qb, validateFormula, - }); + }) - qb.where(_wherePk(this.model.primaryKeys, id)); + qb.where(_wherePk(this.model.primaryKeys, id)) - let data; + let data try { - data = (await this.execAndParse(qb))?.[0]; + data = (await this.execAndParse(qb))?.[0] } catch (e) { if (validateFormula || !haveFormulaColumn(await this.model.getColumns())) - throw e; - console.log(e); - return this.readByPk(id, true); + throw e + console.log(e) + return this.readByPk(id, true) } if (data) { - const proto = await this.getProto(); - data.__proto__ = proto; + const proto = await this.getProto() + data.__proto__ = proto } - return data ? await nocoExecute(ast, data, {}, query) : {}; + return data ? await nocoExecute(ast, data, {}, query) : {} } public async exist(id?: any): Promise { - const qb = this.dbDriver(this.tnPath); - await this.model.getColumns(); - const pks = this.model.primaryKeys; + const qb = this.dbDriver(this.tnPath) + await this.model.getColumns() + const pks = this.model.primaryKeys - if (!pks.length) return false; + if (!pks.length) return false - qb.select(pks[0].column_name); + qb.select(pks[0].column_name) if ((id + '').split('___').length != pks?.length) { - return false; + return false } - qb.where(_wherePk(pks, id)).first(); - return !!(await qb); + qb.where(_wherePk(pks, id)).first() + return !!(await qb) } // todo: add support for sortArrJson @@ -195,13 +195,13 @@ class BaseModelSqlv2 { } = {}, validateFormula = false, ): Promise { - const { where, ...rest } = this._getListArgs(args as any); - const qb = this.dbDriver(this.tnPath); - await this.selectObject({ ...args, qb, validateFormula }); + const { where, ...rest } = this._getListArgs(args as any) + const qb = this.dbDriver(this.tnPath) + await this.selectObject({ ...args, qb, validateFormula }) - const aliasColObjMap = await this.model.getAliasColObjMap(); - const sorts = extractSortsObject(rest?.sort, aliasColObjMap); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await this.model.getAliasColObjMap() + const sorts = extractSortsObject(rest?.sort, aliasColObjMap) + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) await conditionV2( [ @@ -218,30 +218,30 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) if (Array.isArray(sorts) && sorts?.length) { - await sortV2(sorts, qb, this.dbDriver); + await sortV2(sorts, qb, this.dbDriver) } else if (this.model.primaryKey) { - qb.orderBy(this.model.primaryKey.column_name); + qb.orderBy(this.model.primaryKey.column_name) } - let data; + let data try { - data = await qb.first(); + data = await qb.first() } catch (e) { if (validateFormula || !haveFormulaColumn(await this.model.getColumns())) - throw e; - console.log(e); - return this.findOne(args, true); + throw e + console.log(e) + return this.findOne(args, true) } if (data) { - const proto = await this.getProto(); - data.__proto__ = proto; + const proto = await this.getProto() + data.__proto__ = proto } - return data; + return data } public async list( @@ -257,23 +257,23 @@ class BaseModelSqlv2 { ignoreViewFilterAndSort = false, validateFormula = false, ): Promise { - const { where, fields, ...rest } = this._getListArgs(args as any); + const { where, fields, ...rest } = this._getListArgs(args as any) - const qb = this.dbDriver(this.tnPath); + const qb = this.dbDriver(this.tnPath) await this.selectObject({ qb, fieldsSet: args.fieldsSet, viewId: this.viewId, validateFormula, - }); + }) if (+rest?.shuffle) { - await this.shuffle({ qb }); + await this.shuffle({ qb }) } - const aliasColObjMap = await this.model.getAliasColObjMap(); - let sorts = extractSortsObject(rest?.sort, aliasColObjMap); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await this.model.getAliasColObjMap() + let sorts = extractSortsObject(rest?.sort, aliasColObjMap) + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) // todo: replace with view id if (!ignoreViewFilterAndSort && this.viewId) { await conditionV2( @@ -296,14 +296,14 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) if (!sorts) sorts = args.sortArr?.length ? args.sortArr - : await Sort.list({ viewId: this.viewId }); + : await Sort.list({ viewId: this.viewId }) - await sortV2(sorts, qb, this.dbDriver); + await sortV2(sorts, qb, this.dbDriver) } else { await conditionV2( [ @@ -320,52 +320,52 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) - if (!sorts) sorts = args.sortArr; + if (!sorts) sorts = args.sortArr - await sortV2(sorts, qb, this.dbDriver); + await sortV2(sorts, qb, this.dbDriver) } // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (this.model.primaryKey && this.model.primaryKey.ai) { - qb.orderBy(this.model.primaryKey.column_name); + qb.orderBy(this.model.primaryKey.column_name) } else if (this.model.columns.find((c) => c.column_name === 'created_at')) { - qb.orderBy('created_at'); + qb.orderBy('created_at') } - if (!ignoreViewFilterAndSort) applyPaginate(qb, rest); - const proto = await this.getProto(); + if (!ignoreViewFilterAndSort) applyPaginate(qb, rest) + const proto = await this.getProto() - let data; + let data try { - data = await this.execAndParse(qb); + data = await this.execAndParse(qb) } catch (e) { if (validateFormula || !haveFormulaColumn(await this.model.getColumns())) - throw e; - console.log(e); - return this.list(args, ignoreViewFilterAndSort, true); + throw e + console.log(e) + return this.list(args, ignoreViewFilterAndSort, true) } return data?.map((d) => { - d.__proto__ = proto; - return d; - }); + d.__proto__ = proto + return d + }) } public async count( args: { where?: string; limit?; filterArr?: Filter[] } = {}, ignoreViewFilterAndSort = false, ): Promise { - await this.model.getColumns(); - const { where } = this._getListArgs(args); + await this.model.getColumns() + const { where } = this._getListArgs(args) - const qb = this.dbDriver(this.tnPath); + const qb = this.dbDriver(this.tnPath) // qb.xwhere(where, await this.model.getAliasColMapping()); - const aliasColObjMap = await this.model.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await this.model.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) if (!ignoreViewFilterAndSort && this.viewId) { await conditionV2( @@ -389,7 +389,7 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) } else { await conditionV2( [ @@ -407,22 +407,22 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) } qb.count(sanitize(this.model.primaryKey?.column_name) || '*', { as: 'count', - }).first(); + }).first() - let sql = sanitize(qb.toQuery()); + let sql = sanitize(qb.toQuery()) if (!this.isPg && !this.isMssql && !this.isSnowflake) { - sql = unsanitize(qb.toQuery()); + sql = unsanitize(qb.toQuery()) } - const res = (await this.dbDriver.raw(sql)) as any; + const res = (await this.dbDriver.raw(sql)) as any return (this.isPg || this.isSnowflake ? res.rows[0] : res[0][0] ?? res[0]) - .count; + .count } // todo: add support for sortArrJson and filterArrJson @@ -437,21 +437,21 @@ class BaseModelSqlv2 { column_name: '', }, ) { - const { where, ...rest } = this._getListArgs(args as any); + const { where, ...rest } = this._getListArgs(args as any) - const qb = this.dbDriver(this.tnPath); - qb.count(`${this.model.primaryKey?.column_name || '*'} as count`); - qb.select(args.column_name); + const qb = this.dbDriver(this.tnPath) + qb.count(`${this.model.primaryKey?.column_name || '*'} as count`) + qb.select(args.column_name) if (+rest?.shuffle) { - await this.shuffle({ qb }); + await this.shuffle({ qb }) } - const aliasColObjMap = await this.model.getAliasColObjMap(); + const aliasColObjMap = await this.model.getAliasColObjMap() - const sorts = extractSortsObject(rest?.sort, aliasColObjMap); + const sorts = extractSortsObject(rest?.sort, aliasColObjMap) - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) await conditionV2( [ new Filter({ @@ -462,11 +462,11 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); - qb.groupBy(args.column_name); - if (sorts) await sortV2(sorts, qb, this.dbDriver); - applyPaginate(qb, rest); - return await qb; + ) + qb.groupBy(args.column_name) + if (sorts) await sortV2(sorts, qb, this.dbDriver) + applyPaginate(qb, rest) + return await qb } async multipleHmList( @@ -474,38 +474,38 @@ class BaseModelSqlv2 { args: { limit?; offset?; fieldsSet?: Set } = {}, ) { try { - const { where, sort, ...rest } = this._getListArgs(args as any); + const { where, sort, ...rest } = this._getListArgs(args as any) // todo: get only required fields // const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {}; const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn(); - const childTable = await chilCol.getModel(); + ).getChildColumn() + const childTable = await chilCol.getModel() const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn(); - const parentTable = await parentCol.getModel(); + ).getParentColumn() + const parentTable = await parentCol.getModel() const childModel = await Model.getBaseModelSQL({ model: childTable, dbDriver: this.dbDriver, - }); - await parentTable.getColumns(); + }) + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const qb = this.dbDriver(childTn); + const qb = this.dbDriver(childTn) await childModel.selectObject({ qb, extractPkAndPv: true, fieldsSet: args.fieldsSet, - }); - await this.applySortAndFilter({ table: childTable, where, qb, sort }); + }) + await this.applySortAndFilter({ table: childTable, where, qb, sort }) const childQb = this.dbDriver.queryBuilder().from( this.dbDriver @@ -520,59 +520,59 @@ class BaseModelSqlv2 { .select(parentCol.column_name) // .where(parentTable.primaryKey.cn, p) .where(_wherePk(parentTable.primaryKeys, p)), - ); + ) // todo: sanitize - query.limit(+rest?.limit || 25); - query.offset(+rest?.offset || 0); + query.limit(+rest?.limit || 25) + query.offset(+rest?.offset || 0) - return this.isSqlite ? this.dbDriver.select().from(query) : query; + return this.isSqlite ? this.dbDriver.select().from(query) : query }), !this.isSqlite, ) .as('list'), - ); + ) // console.log(childQb.toQuery()) - const children = await this.execAndParse(childQb, childTable); + const children = await this.execAndParse(childQb, childTable) const proto = await ( await Model.getBaseModelSQL({ id: childTable.id, dbDriver: this.dbDriver, }) - ).getProto(); + ).getProto() return groupBy( children.map((c) => { - c.__proto__ = proto; - return c; + c.__proto__ = proto + return c }), GROUP_COL, - ); + ) } catch (e) { - console.log(e); - throw e; + console.log(e) + throw e } } private async applySortAndFilter({ - table, - where, - qb, - sort, - }: { + table, + where, + qb, + sort, + }: { table: Model; where: string; qb; sort: string; }) { - const childAliasColMap = await table.getAliasColObjMap(); + const childAliasColMap = await table.getAliasColObjMap() - const filter = extractFilterFromXwhere(where, childAliasColMap); - await conditionV2(filter, qb, this.dbDriver); - if (!sort) return; - const sortObj = extractSortsObject(sort, childAliasColMap); - if (sortObj) await sortV2(sortObj, qb, this.dbDriver); + const filter = extractFilterFromXwhere(where, childAliasColMap) + await conditionV2(filter, qb, this.dbDriver) + if (!sort) return + const sortObj = extractSortsObject(sort, childAliasColMap) + if (sortObj) await sortV2(sortObj, qb, this.dbDriver) } async multipleHmListCount({ colId, ids }) { @@ -580,19 +580,19 @@ class BaseModelSqlv2 { // const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {}; const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn(); - const childTable = await chilCol.getModel(); + ).getChildColumn() + const childTable = await chilCol.getModel() const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn(); - const parentTable = await parentCol.getModel(); - await parentTable.getColumns(); + ).getParentColumn() + const parentTable = await parentCol.getModel() + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) const children = await this.dbDriver.unionAll( ids.map((p) => { @@ -605,17 +605,17 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, p) .where(_wherePk(parentTable.primaryKeys, p)), ) - .first(); + .first() - return this.isSqlite ? this.dbDriver.select().from(query) : query; + return this.isSqlite ? this.dbDriver.select().from(query) : query }), !this.isSqlite, - ); + ) - return children.map(({ count }) => count); + return children.map(({ count }) => count) } catch (e) { - console.log(e); - throw e; + console.log(e) + throw e } } @@ -624,32 +624,32 @@ class BaseModelSqlv2 { args: { limit?; offset?; fieldSet?: Set } = {}, ) { try { - const { where, sort, ...rest } = this._getListArgs(args as any); + const { where, sort, ...rest } = this._getListArgs(args as any) // todo: get only required fields const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn(); - const childTable = await chilCol.getModel(); + ).getChildColumn() + const childTable = await chilCol.getModel() const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn(); - const parentTable = await parentCol.getModel(); + ).getParentColumn() + const parentTable = await parentCol.getModel() const childModel = await Model.getBaseModelSQL({ model: childTable, dbDriver: this.dbDriver, - }); - await parentTable.getColumns(); + }) + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const qb = this.dbDriver(childTn); - await this.applySortAndFilter({ table: childTable, where, qb, sort }); + const qb = this.dbDriver(childTn) + await this.applySortAndFilter({ table: childTable, where, qb, sort }) qb.whereIn( chilCol.column_name, @@ -657,29 +657,29 @@ class BaseModelSqlv2 { .select(parentCol.column_name) // .where(parentTable.primaryKey.cn, p) .where(_wherePk(parentTable.primaryKeys, id)), - ); + ) // todo: sanitize - qb.limit(+rest?.limit || 25); - qb.offset(+rest?.offset || 0); + qb.limit(+rest?.limit || 25) + qb.offset(+rest?.offset || 0) - await childModel.selectObject({ qb, fieldsSet: args.fieldSet }); + await childModel.selectObject({ qb, fieldsSet: args.fieldSet }) - const children = await this.execAndParse(qb, childTable); + const children = await this.execAndParse(qb, childTable) const proto = await ( await Model.getBaseModelSQL({ id: childTable.id, dbDriver: this.dbDriver, }) - ).getProto(); + ).getProto() return children.map((c) => { - c.__proto__ = proto; - return c; - }); + c.__proto__ = proto + return c + }) } catch (e) { - console.log(e); - throw e; + console.log(e) + throw e } } @@ -688,19 +688,19 @@ class BaseModelSqlv2 { // const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {}; const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn(); - const childTable = await chilCol.getModel(); + ).getChildColumn() + const childTable = await chilCol.getModel() const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn(); - const parentTable = await parentCol.getModel(); - await parentTable.getColumns(); + ).getParentColumn() + const parentTable = await parentCol.getModel() + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) const query = this.dbDriver(childTn) .count(`${chilCol?.column_name} as count`) @@ -710,12 +710,12 @@ class BaseModelSqlv2 { .select(parentCol.column_name) .where(_wherePk(parentTable.primaryKeys, id)), ) - .first(); - const { count } = await query; - return count; + .first() + const { count } = await query + return count } catch (e) { - console.log(e); - throw e; + console.log(e) + throw e } } @@ -723,40 +723,40 @@ class BaseModelSqlv2 { { colId, parentIds }, args: { limit?; offset?; fieldsSet?: Set } = {}, ) { - const { where, sort, ...rest } = this._getListArgs(args as any); + const { where, sort, ...rest } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn // const tn = this.model.tn; // const cn = (await relColOptions.getChildColumn()).title; - const mmTable = await relColOptions.getMMModel(); - const vtn = this.getTnPath(mmTable); - const vcn = (await relColOptions.getMMChildColumn()).column_name; - const vrcn = (await relColOptions.getMMParentColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getParentColumn()).getModel(); - const parentTable = await (await relColOptions.getChildColumn()).getModel(); - await parentTable.getColumns(); + const mmTable = await relColOptions.getMMModel() + const vtn = this.getTnPath(mmTable) + const vcn = (await relColOptions.getMMChildColumn()).column_name + const vrcn = (await relColOptions.getMMParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getParentColumn()).getModel() + const parentTable = await (await relColOptions.getChildColumn()).getModel() + await parentTable.getColumns() const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }); + }) - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = childTn; - const rtnId = childTable.id; + const rtn = childTn + const rtnId = childTable.id - const qb = this.dbDriver(rtn).join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`); + const qb = this.dbDriver(rtn).join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) - await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }); + await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }) - await this.applySortAndFilter({ table: childTable, where, qb, sort }); + await this.applySortAndFilter({ table: childTable, where, qb, sort }) const finalQb = this.dbDriver.unionAll( parentIds.map((id) => { @@ -769,69 +769,69 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, id)), ) - .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])); + .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])) // todo: sanitize - query.limit(+rest?.limit || 25); - query.offset(+rest?.offset || 0); + query.limit(+rest?.limit || 25) + query.offset(+rest?.offset || 0) - return this.isSqlite ? this.dbDriver.select().from(query) : query; + return this.isSqlite ? this.dbDriver.select().from(query) : query }), !this.isSqlite, - ); + ) - let children = await this.execAndParse(finalQb, childTable); + let children = await this.execAndParse(finalQb, childTable) if (this.isMySQL) { - children = children[0]; + children = children[0] } const proto = await ( await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver, }) - ).getProto(); + ).getProto() const gs = groupBy( children.map((c) => { - c.__proto__ = proto; - return c; + c.__proto__ = proto + return c }), GROUP_COL, - ); - return parentIds.map((id) => gs[id] || []); + ) + return parentIds.map((id) => gs[id] || []) } public async mmList( { colId, parentId }, args: { limit?; offset?; fieldsSet?: Set } = {}, ) { - const { where, sort, ...rest } = this._getListArgs(args as any); + const { where, sort, ...rest } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn // const tn = this.model.tn; // const cn = (await relColOptions.getChildColumn()).title; - const mmTable = await relColOptions.getMMModel(); - const vtn = this.getTnPath(mmTable); - const vcn = (await relColOptions.getMMChildColumn()).column_name; - const vrcn = (await relColOptions.getMMParentColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getParentColumn()).getModel(); - const parentTable = await (await relColOptions.getChildColumn()).getModel(); - await parentTable.getColumns(); + const mmTable = await relColOptions.getMMModel() + const vtn = this.getTnPath(mmTable) + const vcn = (await relColOptions.getMMChildColumn()).column_name + const vrcn = (await relColOptions.getMMParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getParentColumn()).getModel() + const parentTable = await (await relColOptions.getChildColumn()).getModel() + await parentTable.getColumns() const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }); + }) - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = childTn; - const rtnId = childTable.id; + const rtn = childTn + const rtnId = childTable.id const qb = this.dbDriver(rtn) .join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) @@ -841,55 +841,55 @@ class BaseModelSqlv2 { .select(cn) // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, parentId)), - ); + ) - await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }); + await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }) - await this.applySortAndFilter({ table: childTable, where, qb, sort }); + await this.applySortAndFilter({ table: childTable, where, qb, sort }) // todo: sanitize - qb.limit(+rest?.limit || 25); - qb.offset(+rest?.offset || 0); + qb.limit(+rest?.limit || 25) + qb.offset(+rest?.offset || 0) - const children = await this.execAndParse(qb, childTable); + const children = await this.execAndParse(qb, childTable) const proto = await ( await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver }) - ).getProto(); + ).getProto() return children.map((c) => { - c.__proto__ = proto; - return c; - }); + c.__proto__ = proto + return c + }) } public async multipleMmListCount({ colId, parentIds }) { const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - const mmTable = await relColOptions.getMMModel(); - const vtn = this.getTnPath(mmTable); - const vcn = (await relColOptions.getMMChildColumn()).column_name; - const vrcn = (await relColOptions.getMMParentColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getParentColumn()).getModel(); - const parentTable = await (await relColOptions.getChildColumn()).getModel(); - await parentTable.getColumns(); + const mmTable = await relColOptions.getMMModel() + const vtn = this.getTnPath(mmTable) + const vcn = (await relColOptions.getMMChildColumn()).column_name + const vrcn = (await relColOptions.getMMParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getParentColumn()).getModel() + const parentTable = await (await relColOptions.getChildColumn()).getModel() + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = childTn; + const rtn = childTn const qb = this.dbDriver(rtn) .join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) // .select({ // [`${tn}_${vcn}`]: `${vtn}.${vcn}` // }) - .count(`${vtn}.${vcn}`, { as: 'count' }); + .count(`${vtn}.${vcn}`, { as: 'count' }) // await childModel.selectObject({ qb }); const children = await this.dbDriver.unionAll( @@ -903,38 +903,38 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, id)), ) - .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])); + .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])) // this._paginateAndSort(query, { sort, limit, offset }, null, true); - return this.isSqlite ? this.dbDriver.select().from(query) : query; + return this.isSqlite ? this.dbDriver.select().from(query) : query }), !this.isSqlite, - ); + ) - const gs = groupBy(children, GROUP_COL); - return parentIds.map((id) => gs?.[id]?.[0] || []); + const gs = groupBy(children, GROUP_COL) + return parentIds.map((id) => gs?.[id]?.[0] || []) } public async mmListCount({ colId, parentId }) { const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - const mmTable = await relColOptions.getMMModel(); - const vtn = this.getTnPath(mmTable); - const vcn = (await relColOptions.getMMChildColumn()).column_name; - const vrcn = (await relColOptions.getMMParentColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getParentColumn()).getModel(); - const parentTable = await (await relColOptions.getChildColumn()).getModel(); - await parentTable.getColumns(); + const mmTable = await relColOptions.getMMModel() + const vtn = this.getTnPath(mmTable) + const vcn = (await relColOptions.getMMChildColumn()).column_name + const vrcn = (await relColOptions.getMMParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getParentColumn()).getModel() + const parentTable = await (await relColOptions.getChildColumn()).getModel() + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = childTn; + const rtn = childTn const qb = this.dbDriver(rtn) .join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) @@ -949,11 +949,11 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, parentId)), ) - .first(); + .first() - const { count } = await qb; + const { count } = await qb - return count; + return count } // todo: naming & optimizing @@ -961,27 +961,27 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where } = this._getListArgs(args as any); + const { where } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - - const mmTable = await relColOptions.getMMModel(); - const vtn = this.getTnPath(mmTable); - const vcn = (await relColOptions.getMMChildColumn()).column_name; - const vrcn = (await relColOptions.getMMParentColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getParentColumn()).getModel(); - const parentTable = await (await relColOptions.getChildColumn()).getModel(); - await parentTable.getColumns(); - - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); - - const rtn = childTn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + + const mmTable = await relColOptions.getMMModel() + const vtn = this.getTnPath(mmTable) + const vcn = (await relColOptions.getMMChildColumn()).column_name + const vrcn = (await relColOptions.getMMParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getParentColumn()).getModel() + const parentTable = await (await relColOptions.getChildColumn()).getModel() + await parentTable.getColumns() + + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) + + const rtn = childTn const qb = this.dbDriver(rtn) .count(`*`, { as: 'count' }) .where((qb) => { @@ -997,14 +997,14 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, pid) .where(_wherePk(parentTable.primaryKeys, pid)), ), - ).orWhereNull(rcn); - }); + ).orWhereNull(rcn) + }) - const aliasColObjMap = await childTable.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await childTable.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) - await conditionV2(filterObj, qb, this.dbDriver); - return (await qb.first())?.count; + await conditionV2(filterObj, qb, this.dbDriver) + return (await qb.first())?.count } // todo: naming & optimizing @@ -1012,31 +1012,31 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where, ...rest } = this._getListArgs(args as any); + const { where, ...rest } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - - const mmTable = await relColOptions.getMMModel(); - const vtn = this.getTnPath(mmTable); - const vcn = (await relColOptions.getMMChildColumn()).column_name; - const vrcn = (await relColOptions.getMMParentColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getParentColumn()).getModel(); + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + + const mmTable = await relColOptions.getMMModel() + const vtn = this.getTnPath(mmTable) + const vcn = (await relColOptions.getMMChildColumn()).column_name + const vrcn = (await relColOptions.getMMParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getParentColumn()).getModel() const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }); - const parentTable = await (await relColOptions.getChildColumn()).getModel(); - await parentTable.getColumns(); + }) + const parentTable = await (await relColOptions.getChildColumn()).getModel() + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = childTn; + const rtn = childTn const qb = this.dbDriver(rtn).where((qb) => qb @@ -1054,34 +1054,34 @@ class BaseModelSqlv2 { ), ) .orWhereNull(rcn), - ); + ) if (+rest?.shuffle) { - await this.shuffle({ qb }); + await this.shuffle({ qb }) } - await childModel.selectObject({ qb }); + await childModel.selectObject({ qb }) - const aliasColObjMap = await childTable.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2(filterObj, qb, this.dbDriver); + const aliasColObjMap = await childTable.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + await conditionV2(filterObj, qb, this.dbDriver) // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (childTable.primaryKey && childTable.primaryKey.ai) { - qb.orderBy(childTable.primaryKey.column_name); + qb.orderBy(childTable.primaryKey.column_name) } else if (childTable.columns.find((c) => c.column_name === 'created_at')) { - qb.orderBy('created_at'); + qb.orderBy('created_at') } - applyPaginate(qb, rest); + applyPaginate(qb, rest) - const proto = await childModel.getProto(); - const data = await qb; + const proto = await childModel.getProto() + const data = await qb return data.map((c) => { - c.__proto__ = proto; - return c; - }); + c.__proto__ = proto + return c + }) } // todo: naming & optimizing @@ -1089,26 +1089,26 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where } = this._getListArgs(args as any); + const { where } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - const cn = (await relColOptions.getChildColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const childTable = await (await relColOptions.getChildColumn()).getModel(); + const cn = (await relColOptions.getChildColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const childTable = await (await relColOptions.getChildColumn()).getModel() const parentTable = await ( await relColOptions.getParentColumn() - ).getModel(); + ).getModel() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const tn = childTn; - const rtn = parentTn; - await parentTable.getColumns(); + const tn = childTn + const rtn = parentTn + await parentTable.getColumns() const qb = this.dbDriver(tn) .count(`*`, { as: 'count' }) @@ -1119,15 +1119,15 @@ class BaseModelSqlv2 { .select(rcn) // .where(parentTable.primaryKey.cn, pid) .where(_wherePk(parentTable.primaryKeys, pid)), - ).orWhereNull(cn); - }); + ).orWhereNull(cn) + }) - const aliasColObjMap = await childTable.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await childTable.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) - await conditionV2(filterObj, qb, this.dbDriver); + await conditionV2(filterObj, qb, this.dbDriver) - return (await qb.first())?.count; + return (await qb.first())?.count } // todo: naming & optimizing @@ -1135,30 +1135,30 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where, ...rest } = this._getListArgs(args as any); + const { where, ...rest } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - const cn = (await relColOptions.getChildColumn()).column_name; - const rcn = (await relColOptions.getParentColumn()).column_name; - const childTable = await (await relColOptions.getChildColumn()).getModel(); + const cn = (await relColOptions.getChildColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name + const childTable = await (await relColOptions.getChildColumn()).getModel() const parentTable = await ( await relColOptions.getParentColumn() - ).getModel(); + ).getModel() const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }); - await parentTable.getColumns(); + }) + await parentTable.getColumns() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const tn = childTn; - const rtn = parentTn; + const tn = childTn + const rtn = parentTn const qb = this.dbDriver(tn).where((qb) => { qb.whereNotIn( @@ -1167,36 +1167,36 @@ class BaseModelSqlv2 { .select(rcn) // .where(parentTable.primaryKey.cn, pid) .where(_wherePk(parentTable.primaryKeys, pid)), - ).orWhereNull(cn); - }); + ).orWhereNull(cn) + }) if (+rest?.shuffle) { - await this.shuffle({ qb }); + await this.shuffle({ qb }) } - await childModel.selectObject({ qb }); + await childModel.selectObject({ qb }) - const aliasColObjMap = await childTable.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2(filterObj, qb, this.dbDriver); + const aliasColObjMap = await childTable.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + await conditionV2(filterObj, qb, this.dbDriver) // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (childTable.primaryKey && childTable.primaryKey.ai) { - qb.orderBy(childTable.primaryKey.column_name); + qb.orderBy(childTable.primaryKey.column_name) } else if (childTable.columns.find((c) => c.column_name === 'created_at')) { - qb.orderBy('created_at'); + qb.orderBy('created_at') } - applyPaginate(qb, rest); + applyPaginate(qb, rest) - const proto = await childModel.getProto(); - const data = await this.execAndParse(qb, childTable); + const proto = await childModel.getProto() + const data = await this.execAndParse(qb, childTable) return data.map((c) => { - c.__proto__ = proto; - return c; - }); + c.__proto__ = proto + return c + }) } // todo: naming & optimizing @@ -1204,26 +1204,26 @@ class BaseModelSqlv2 { { colId, cid = null }, args, ): Promise { - const { where } = this._getListArgs(args as any); + const { where } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - const rcn = (await relColOptions.getParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name const parentTable = await ( await relColOptions.getParentColumn() - ).getModel(); - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getChildColumn()).getModel(); + ).getModel() + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getChildColumn()).getModel() - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = parentTn; - const tn = childTn; - await childTable.getColumns(); + const rtn = parentTn + const tn = childTn + await childTable.getColumns() const qb = this.dbDriver(rtn) .where((qb) => { @@ -1234,15 +1234,15 @@ class BaseModelSqlv2 { // .where(childTable.primaryKey.cn, cid) .where(_wherePk(childTable.primaryKeys, cid)) .whereNotNull(cn), - ); + ) }) - .count(`*`, { as: 'count' }); + .count(`*`, { as: 'count' }) - const aliasColObjMap = await parentTable.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await parentTable.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) - await conditionV2(filterObj, qb, this.dbDriver); - return (await qb.first())?.count; + await conditionV2(filterObj, qb, this.dbDriver) + return (await qb.first())?.count } // todo: naming & optimizing @@ -1250,30 +1250,30 @@ class BaseModelSqlv2 { { colId, cid = null }, args, ): Promise { - const { where, ...rest } = this._getListArgs(args as any); + const { where, ...rest } = this._getListArgs(args as any) const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ); + ) const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - const rcn = (await relColOptions.getParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name const parentTable = await ( await relColOptions.getParentColumn() - ).getModel(); - const cn = (await relColOptions.getChildColumn()).column_name; - const childTable = await (await relColOptions.getChildColumn()).getModel(); + ).getModel() + const cn = (await relColOptions.getChildColumn()).column_name + const childTable = await (await relColOptions.getChildColumn()).getModel() const parentModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: parentTable, - }); + }) - const childTn = this.getTnPath(childTable); - const parentTn = this.getTnPath(parentTable); + const childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const rtn = parentTn; - const tn = childTn; - await childTable.getColumns(); + const rtn = parentTn + const tn = childTn + await childTable.getColumns() const qb = this.dbDriver(rtn).where((qb) => { qb.whereNotIn( @@ -1283,38 +1283,38 @@ class BaseModelSqlv2 { // .where(childTable.primaryKey.cn, cid) .where(_wherePk(childTable.primaryKeys, cid)) .whereNotNull(cn), - ).orWhereNull(rcn); - }); + ).orWhereNull(rcn) + }) if (+rest?.shuffle) { - await this.shuffle({ qb }); + await this.shuffle({ qb }) } - await parentModel.selectObject({ qb }); + await parentModel.selectObject({ qb }) - const aliasColObjMap = await parentTable.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2(filterObj, qb, this.dbDriver); + const aliasColObjMap = await parentTable.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + await conditionV2(filterObj, qb, this.dbDriver) // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (parentTable.primaryKey && parentTable.primaryKey.ai) { - qb.orderBy(parentTable.primaryKey.column_name); + qb.orderBy(parentTable.primaryKey.column_name) } else if ( parentTable.columns.find((c) => c.column_name === 'created_at') ) { - qb.orderBy('created_at'); + qb.orderBy('created_at') } - applyPaginate(qb, rest); + applyPaginate(qb, rest) - const proto = await parentModel.getProto(); - const data = await this.execAndParse(qb, childTable); + const proto = await parentModel.getProto() + const data = await this.execAndParse(qb, childTable) return data.map((c) => { - c.__proto__ = proto; - return c; - }); + c.__proto__ = proto + return c + }) } private async getSelectQueryBuilderForFormula( @@ -1323,8 +1323,8 @@ class BaseModelSqlv2 { validateFormula = false, aliasToColumnBuilder = {}, ) { - const formula = await column.getColOptions(); - if (formula.error) throw new Error(`Formula error: ${formula.error}`); + const formula = await column.getColOptions() + if (formula.error) throw new Error(`Formula error: ${formula.error}`) const qb = await formulaQueryBuilderv2( formula.formula, null, @@ -1334,210 +1334,207 @@ class BaseModelSqlv2 { aliasToColumnBuilder, tableAlias, validateFormula, - ); - return qb; + ) + return qb } async getProto() { if (this._proto) { - return this._proto; + return this._proto } - const proto: any = { __columnAliases: {} }; - const columns = await this.model.getColumns(); + const proto: any = { __columnAliases: {} } + const columns = await this.model.getColumns() for (const column of columns) { switch (column.uidt) { - case UITypes.Rollup: - { - // @ts-ignore - const colOptions: RollupColumn = await column.getColOptions(); - } - break; - case UITypes.Lookup: - { - // @ts-ignore - const colOptions: LookupColumn = await column.getColOptions(); - proto.__columnAliases[column.title] = { - path: [ - (await Column.get({ colId: colOptions.fk_relation_column_id })) - ?.title, - (await Column.get({ colId: colOptions.fk_lookup_column_id })) - ?.title, - ], - }; + case UITypes.Rollup: { + // @ts-ignore + const colOptions: RollupColumn = await column.getColOptions() + } + break + case UITypes.Lookup: { + // @ts-ignore + const colOptions: LookupColumn = await column.getColOptions() + proto.__columnAliases[column.title] = { + path: [ + (await Column.get({ colId: colOptions.fk_relation_column_id })) + ?.title, + (await Column.get({ colId: colOptions.fk_lookup_column_id })) + ?.title, + ], } - break; - case UITypes.LinkToAnotherRecord: - { - this._columns[column.title] = column; - const colOptions = - (await column.getColOptions()) as LinkToAnotherRecordColumn; - // const parentColumn = await colOptions.getParentColumn(); - - if (colOptions?.type === 'hm') { - const listLoader = new DataLoader(async (ids: string[]) => { - try { - if (ids.length > 1) { - const data = await this.multipleHmList( + } + break + case UITypes.LinkToAnotherRecord: { + this._columns[column.title] = column + const colOptions = + (await column.getColOptions()) as LinkToAnotherRecordColumn + // const parentColumn = await colOptions.getParentColumn(); + + if (colOptions?.type === 'hm') { + const listLoader = new DataLoader(async (ids: string[]) => { + try { + if (ids.length > 1) { + const data = await this.multipleHmList( + { + colId: column.id, + ids, + }, + (listLoader as any).args, + ) + return ids.map((id: string) => (data[id] ? data[id] : [])) + } else { + return [ + await this.hmList( { colId: column.id, - ids, + id: ids[0], }, (listLoader as any).args, - ); - return ids.map((id: string) => (data[id] ? data[id] : [])); - } else { - return [ - await this.hmList( - { - colId: column.id, - id: ids[0], - }, - (listLoader as any).args, - ), - ]; - } - } catch (e) { - console.log(e); - return []; + ), + ] } - }); - const self: BaseModelSqlv2 = this; - - proto[column.title] = async function (args): Promise { - (listLoader as any).args = args; - return listLoader.load( - getCompositePk(self.model.primaryKeys, this), - ); - }; - - // defining HasMany count method within GQL Type class - // Object.defineProperty(type.prototype, column.alias, { - // async value(): Promise { - // return listLoader.load(this[model.pk.alias]); - // }, - // configurable: true - // }); - } else if (colOptions.type === 'mm') { - const listLoader = new DataLoader(async (ids: string[]) => { - try { - if (ids?.length > 1) { - const data = await this.multipleMmList( + } catch (e) { + console.log(e) + return [] + } + }) + const self: BaseModelSqlv2 = this + + proto[column.title] = async function(args): Promise { + (listLoader as any).args = args + return listLoader.load( + getCompositePk(self.model.primaryKeys, this), + ) + } + + // defining HasMany count method within GQL Type class + // Object.defineProperty(type.prototype, column.alias, { + // async value(): Promise { + // return listLoader.load(this[model.pk.alias]); + // }, + // configurable: true + // }); + } else if (colOptions.type === 'mm') { + const listLoader = new DataLoader(async (ids: string[]) => { + try { + if (ids?.length > 1) { + const data = await this.multipleMmList( + { + parentIds: ids, + colId: column.id, + }, + (listLoader as any).args, + ) + + return data + } else { + return [ + await this.mmList( { - parentIds: ids, + parentId: ids[0], colId: column.id, }, (listLoader as any).args, - ); - - return data; - } else { - return [ - await this.mmList( - { - parentId: ids[0], - colId: column.id, - }, - (listLoader as any).args, - ), - ]; - } - } catch (e) { - console.log(e); - return []; + ), + ] } - }); - - const self: BaseModelSqlv2 = this; - // const childColumn = await colOptions.getChildColumn(); - proto[column.title] = async function (args): Promise { - (listLoader as any).args = args; - return await listLoader.load( - getCompositePk(self.model.primaryKeys, this), - ); - }; - } else if (colOptions.type === 'bt') { - // @ts-ignore - const colOptions = - (await column.getColOptions()) as LinkToAnotherRecordColumn; - const pCol = await Column.get({ - colId: colOptions.fk_parent_column_id, - }); - const cCol = await Column.get({ - colId: colOptions.fk_child_column_id, - }); - const readLoader = new DataLoader(async (ids: string[]) => { - try { - const data = await ( - await Model.getBaseModelSQL({ - id: pCol.fk_model_id, - dbDriver: this.dbDriver, - }) - ).list( - { - // limit: ids.length, - where: `(${pCol.column_name},in,${ids.join(',')})`, - fieldsSet: (readLoader as any).args?.fieldsSet, - }, - true, - ); - const gs = groupBy(data, pCol.title); - return ids.map(async (id: string) => gs?.[id]?.[0]); - } catch (e) { - console.log(e); - return []; - } - }); + } catch (e) { + console.log(e) + return [] + } + }) - // defining HasMany count method within GQL Type class - proto[column.title] = async function (args?: any) { - if ( - this?.[cCol?.title] === null || - this?.[cCol?.title] === undefined + const self: BaseModelSqlv2 = this + // const childColumn = await colOptions.getChildColumn(); + proto[column.title] = async function(args): Promise { + (listLoader as any).args = args + return await listLoader.load( + getCompositePk(self.model.primaryKeys, this), + ) + } + } else if (colOptions.type === 'bt') { + // @ts-ignore + const colOptions = + (await column.getColOptions()) as LinkToAnotherRecordColumn + const pCol = await Column.get({ + colId: colOptions.fk_parent_column_id, + }) + const cCol = await Column.get({ + colId: colOptions.fk_child_column_id, + }) + const readLoader = new DataLoader(async (ids: string[]) => { + try { + const data = await ( + await Model.getBaseModelSQL({ + id: pCol.fk_model_id, + dbDriver: this.dbDriver, + }) + ).list( + { + // limit: ids.length, + where: `(${pCol.column_name},in,${ids.join(',')})`, + fieldsSet: (readLoader as any).args?.fieldsSet, + }, + true, ) - return null; + const gs = groupBy(data, pCol.title) + return ids.map(async (id: string) => gs?.[id]?.[0]) + } catch (e) { + console.log(e) + return [] + } + }) + + // defining HasMany count method within GQL Type class + proto[column.title] = async function(args?: any) { + if ( + this?.[cCol?.title] === null || + this?.[cCol?.title] === undefined + ) + return null; - (readLoader as any).args = args; + (readLoader as any).args = args - return await readLoader.load(this?.[cCol?.title]); - }; - // todo : handle mm + return await readLoader.load(this?.[cCol?.title]) } + // todo : handle mm } - break; + } + break } } - this._proto = proto; - return proto; + this._proto = proto + return proto } _getListArgs(args: XcFilterWithAlias): XcFilter { - const obj: XcFilter = {}; - obj.where = args.filter || args.where || args.w || ''; - obj.having = args.having || args.h || ''; - obj.shuffle = args.shuffle || args.r || ''; - obj.condition = args.condition || args.c || {}; - obj.conditionGraph = args.conditionGraph || {}; + const obj: XcFilter = {} + obj.where = args.filter || args.where || args.w || '' + obj.having = args.having || args.h || '' + obj.shuffle = args.shuffle || args.r || '' + obj.condition = args.condition || args.c || {} + obj.conditionGraph = args.conditionGraph || {} obj.limit = Math.max( Math.min( args.limit || args.l || this.config.limitDefault, this.config.limitMax, ), this.config.limitMin, - ); - obj.offset = Math.max(+(args.offset || args.o) || 0, 0); - obj.fields = args.fields || args.f; - obj.sort = args.sort || args.s; - return obj; + ) + obj.offset = Math.max(+(args.offset || args.o) || 0, 0) + obj.fields = args.fields || args.f + obj.sort = args.sort || args.s + return obj } public async shuffle({ qb }: { qb: Knex.QueryBuilder }): Promise { if (this.isMySQL) { - qb.orderByRaw('RAND()'); + qb.orderByRaw('RAND()') } else if (this.isPg || this.isSqlite) { - qb.orderByRaw('RANDOM()'); + qb.orderByRaw('RANDOM()') } else if (this.isMssql) { - qb.orderByRaw('NEWID()'); + qb.orderByRaw('NEWID()') } } @@ -1545,15 +1542,15 @@ class BaseModelSqlv2 { // pass view id as argument // add option to get only pk and pv public async selectObject({ - qb, - columns: _columns, - fields: _fields, - extractPkAndPv, - viewId, - fieldsSet, - alias, - validateFormula, - }: { + qb, + columns: _columns, + fields: _fields, + extractPkAndPv, + viewId, + fieldsSet, + alias, + validateFormula, + }: { fieldsSet?: Set; qb: Knex.QueryBuilder & Knex.QueryInterface; columns?: Column[]; @@ -1564,32 +1561,32 @@ class BaseModelSqlv2 { validateFormula?: boolean; }): Promise { // keep a common object for all columns to share across all columns - const aliasToColumnBuilder = {}; - let viewOrTableColumns: Column[] | { fk_column_id?: string }[]; + const aliasToColumnBuilder = {} + let viewOrTableColumns: Column[] | { fk_column_id?: string }[] - const res = {}; - let view: View; - let fields: string[]; + const res = {} + let view: View + let fields: string[] if (fieldsSet?.size) { - viewOrTableColumns = _columns || (await this.model.getColumns()); + viewOrTableColumns = _columns || (await this.model.getColumns()) } else { - view = await View.get(viewId); - const viewColumns = viewId && (await View.getColumns(viewId)); - fields = Array.isArray(_fields) ? _fields : _fields?.split(','); + view = await View.get(viewId) + const viewColumns = viewId && (await View.getColumns(viewId)) + fields = Array.isArray(_fields) ? _fields : _fields?.split(',') // const columns = _columns ?? (await this.model.getColumns()); // for (const column of columns) { viewOrTableColumns = - _columns || viewColumns || (await this.model.getColumns()); + _columns || viewColumns || (await this.model.getColumns()) } for (const viewOrTableColumn of viewOrTableColumns) { const column = viewOrTableColumn instanceof Column ? viewOrTableColumn : await Column.get({ - colId: (viewOrTableColumn as GridViewColumn).fk_column_id, - }); + colId: (viewOrTableColumn as GridViewColumn).fk_column_id, + }) // hide if column marked as hidden in view // of if column is system field and system field is hidden if ( @@ -1601,24 +1598,24 @@ class BaseModelSqlv2 { extractPkAndPv, ) ) { - continue; + continue } - if (!checkColumnRequired(column, fields, extractPkAndPv)) continue; + if (!checkColumnRequired(column, fields, extractPkAndPv)) continue switch (column.uidt) { case 'LinkToAnotherRecord': case 'Lookup': - break; + break case 'QrCode': { - const qrCodeColumn = await column.getColOptions(); + const qrCodeColumn = await column.getColOptions() const qrValueColumn = await Column.get({ colId: qrCodeColumn.fk_qr_value_column_id, - }); + }) // If the referenced value cannot be found: cancel current iteration if (qrValueColumn == null) { - break; + break } switch (qrValueColumn.uidt) { @@ -1629,31 +1626,31 @@ class BaseModelSqlv2 { alias, validateFormula, aliasToColumnBuilder, - ); + ) qb.select({ [column.column_name]: selectQb.builder, - }); + }) } catch { - continue; + continue } - break; + break default: { - qb.select({ [column.column_name]: qrValueColumn.column_name }); - break; + qb.select({ [column.column_name]: qrValueColumn.column_name }) + break } } - break; + break } case 'Barcode': { - const barcodeColumn = await column.getColOptions(); + const barcodeColumn = await column.getColOptions() const barcodeValueColumn = await Column.get({ colId: barcodeColumn.fk_barcode_value_column_id, - }); + }) // If the referenced value cannot be found: cancel current iteration if (barcodeValueColumn == null) { - break; + break } switch (barcodeValueColumn.uidt) { @@ -1664,48 +1661,47 @@ class BaseModelSqlv2 { alias, validateFormula, aliasToColumnBuilder, - ); + ) qb.select({ [column.column_name]: selectQb.builder, - }); + }) } catch { - continue; + continue } - break; + break default: { qb.select({ [column.column_name]: barcodeValueColumn.column_name, - }); - break; + }) + break } } - break; + break } - case 'Formula': - { - try { - const selectQb = await this.getSelectQueryBuilderForFormula( - column, - alias, - validateFormula, - aliasToColumnBuilder, - ); - qb.select( - this.dbDriver.raw(`?? as ??`, [ - selectQb.builder, - sanitize(column.title), - ]), - ); - } catch (e) { - console.log(e); - // return dummy select - qb.select( - this.dbDriver.raw(`'ERR' as ??`, [sanitize(column.title)]), - ); - } + case 'Formula': { + try { + const selectQb = await this.getSelectQueryBuilderForFormula( + column, + alias, + validateFormula, + aliasToColumnBuilder, + ) + qb.select( + this.dbDriver.raw(`?? as ??`, [ + selectQb.builder, + sanitize(column.title), + ]), + ) + } catch (e) { + console.log(e) + // return dummy select + qb.select( + this.dbDriver.raw(`'ERR' as ??`, [sanitize(column.title)]), + ) } - break; + } + break case 'Rollup': qb.select( ( @@ -1717,62 +1713,62 @@ class BaseModelSqlv2 { columnOptions: (await column.getColOptions()) as RollupColumn, }) ).builder.as(sanitize(column.title)), - ); - break; + ) + break default: res[sanitize(column.title || column.column_name)] = sanitize( `${alias || this.model.table_name}.${column.column_name}`, - ); - break; + ) + break } } - qb.select(res); + qb.select(res) } async insert(data, trx?, cookie?) { try { - await populatePk(this.model, data); + await populatePk(this.model, data) // todo: filter based on view - const insertObj = await this.model.mapAliasToColumn(data); + const insertObj = await this.model.mapAliasToColumn(data) - await this.validate(insertObj); + await this.validate(insertObj) if ('beforeInsert' in this) { - await this.beforeInsert(insertObj, trx, cookie); + await this.beforeInsert(insertObj, trx, cookie) } - await this.model.getColumns(); - let response; + await this.model.getColumns() + let response // const driver = trx ? trx : this.dbDriver; - const query = this.dbDriver(this.tnPath).insert(insertObj); + const query = this.dbDriver(this.tnPath).insert(insertObj) if ((this.isPg || this.isMssql) && this.model.primaryKey) { query.returning( `${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`, - ); - response = await this.execAndParse(query); + ) + response = await this.execAndParse(query) } - const ai = this.model.columns.find((c) => c.ai); + const ai = this.model.columns.find((c) => c.ai) - let ag: Column; - if (!ai) ag = this.model.columns.find((c) => c.meta?.ag); + let ag: Column + if (!ai) ag = this.model.columns.find((c) => c.meta?.ag) // handle if autogenerated primary key is used if (ag) { - if (!response) await this.execAndParse(query); - response = await this.readByPk(data[ag.title]); + if (!response) await this.execAndParse(query) + response = await this.readByPk(data[ag.title]) } else if ( !response || (typeof response?.[0] !== 'object' && response?.[0] !== null) ) { - let id; + let id if (response?.length) { - id = response[0]; + id = response[0] } else { - const res = await this.execAndParse(query); - id = res?.id ?? res[0]?.insertId; + const res = await this.execAndParse(query) + id = res?.id ?? res[0]?.insertId } if (ai) { @@ -1782,263 +1778,261 @@ class BaseModelSqlv2 { await this.dbDriver(this.tnPath) .select(ai.column_name) .max(ai.column_name, { as: 'id' }) - )[0].id; + )[0].id } else if (this.isSnowflake) { id = ( (await this.dbDriver(this.tnPath).max(ai.column_name, { as: 'id', })) as any - )[0].id; + )[0].id } - response = await this.readByPk(id); + response = await this.readByPk(id) } else { - response = data; + response = data } } else if (ai) { response = await this.readByPk( Array.isArray(response) ? response?.[0]?.[ai.title] : response?.[ai.title], - ); + ) } - await this.afterInsert(response, trx, cookie); - return Array.isArray(response) ? response[0] : response; + await this.afterInsert(response, trx, cookie) + return Array.isArray(response) ? response[0] : response } catch (e) { - console.log(e); - await this.errorInsert(e, data, trx, cookie); - throw e; + console.log(e) + await this.errorInsert(e, data, trx, cookie) + throw e } } async delByPk(id, trx?, cookie?) { try { // retrieve data for handling params in hook - const data = await this.readByPk(id); - await this.beforeDelete(id, trx, cookie); + const data = await this.readByPk(id) + await this.beforeDelete(id, trx, cookie) const response = await this.dbDriver(this.tnPath) .del() - .where(await this._wherePk(id)); - await this.afterDelete(data, trx, cookie); - return response; + .where(await this._wherePk(id)) + await this.afterDelete(data, trx, cookie) + return response } catch (e) { - console.log(e); - await this.errorDelete(e, id, trx, cookie); - throw e; + console.log(e) + await this.errorDelete(e, id, trx, cookie) + throw e } } async hasLTARData(rowId, model: Model): Promise { - const res = []; + const res = [] const LTARColumns = (await model.getColumns()).filter( (c) => c.uidt === UITypes.LinkToAnotherRecord, - ); - let i = 0; + ) + let i = 0 for (const column of LTARColumns) { const colOptions = - (await column.getColOptions()) as LinkToAnotherRecordColumn; - const childColumn = await colOptions.getChildColumn(); - const parentColumn = await colOptions.getParentColumn(); - const childModel = await childColumn.getModel(); - await childModel.getColumns(); - const parentModel = await parentColumn.getModel(); - await parentModel.getColumns(); - let cnt = 0; + (await column.getColOptions()) as LinkToAnotherRecordColumn + const childColumn = await colOptions.getChildColumn() + const parentColumn = await colOptions.getParentColumn() + const childModel = await childColumn.getModel() + await childModel.getColumns() + const parentModel = await parentColumn.getModel() + await parentModel.getColumns() + let cnt = 0 if (colOptions.type === RelationTypes.HAS_MANY) { cnt = +( await this.dbDriver(childModel.table_name) .count(childColumn.column_name, { as: 'cnt' }) .where(childColumn.column_name, rowId) - )[0].cnt; + )[0].cnt } else if (colOptions.type === RelationTypes.MANY_TO_MANY) { - const mmModel = await colOptions.getMMModel(); - const mmChildColumn = await colOptions.getMMChildColumn(); + const mmModel = await colOptions.getMMModel() + const mmChildColumn = await colOptions.getMMChildColumn() cnt = +( await this.dbDriver(mmModel.table_name) .where(`${mmModel.table_name}.${mmChildColumn.column_name}`, rowId) .count(mmChildColumn.column_name, { as: 'cnt' }) - )[0].cnt; + )[0].cnt } if (cnt) { res.push( `${i++ + 1}. ${model.title}.${ column.title } is a LinkToAnotherRecord of ${childModel.title}`, - ); + ) } } - return res; + return res } async updateByPk(id, data, trx?, cookie?) { try { - const updateObj = await this.model.mapAliasToColumn(data); + const updateObj = await this.model.mapAliasToColumn(data) - await this.validate(data); + await this.validate(data) - await this.beforeUpdate(data, trx, cookie); + await this.beforeUpdate(data, trx, cookie) - const prevData = await this.readByPk(id); + const prevData = await this.readByPk(id) const query = this.dbDriver(this.tnPath) .update(updateObj) - .where(await this._wherePk(id)); + .where(await this._wherePk(id)) - await this.execAndParse(query); + await this.execAndParse(query) - const newData = await this.readByPk(id); - await this.afterUpdate(prevData, newData, trx, cookie, updateObj); - return newData; + const newData = await this.readByPk(id) + await this.afterUpdate(prevData, newData, trx, cookie, updateObj) + return newData } catch (e) { - console.log(e); - await this.errorUpdate(e, data, trx, cookie); - throw e; + console.log(e) + await this.errorUpdate(e, data, trx, cookie) + throw e } } async _wherePk(id) { - await this.model.getColumns(); - return _wherePk(this.model.primaryKeys, id); + await this.model.getColumns() + return _wherePk(this.model.primaryKeys, id) } private getTnPath(tb: Model) { - const schema = (this.dbDriver as any).searchPath?.(); + const schema = (this.dbDriver as any).searchPath?.() if (this.isMssql && schema) { - return this.dbDriver.raw('??.??', [schema, tb.table_name]); + return this.dbDriver.raw('??.??', [schema, tb.table_name]) } else if (this.isSnowflake) { return [ this.dbDriver.client.config.connection.database, this.dbDriver.client.config.connection.schema, tb.table_name, - ].join('.'); + ].join('.') } else { - return tb.table_name; + return tb.table_name } } public get tnPath() { - return this.getTnPath(this.model); + return this.getTnPath(this.model) } get isSqlite() { - return this.clientType === 'sqlite3'; + return this.clientType === 'sqlite3' } get isMssql() { - return this.clientType === 'mssql'; + return this.clientType === 'mssql' } get isPg() { - return this.clientType === 'pg'; + return this.clientType === 'pg' } get isMySQL() { - return this.clientType === 'mysql2' || this.clientType === 'mysql'; + return this.clientType === 'mysql2' || this.clientType === 'mysql' } get isSnowflake() { - return this.clientType === 'snowflake'; + return this.clientType === 'snowflake' } get clientType() { - return this.dbDriver.clientType(); + return this.dbDriver.clientType() } async nestedInsert(data, _trx = null, cookie?) { // const driver = trx ? trx : await this.dbDriver.transaction(); try { - await populatePk(this.model, data); - const insertObj = await this.model.mapAliasToColumn(data); + await populatePk(this.model, data) + const insertObj = await this.model.mapAliasToColumn(data) - let rowId = null; - const postInsertOps = []; + let rowId = null + const postInsertOps = [] const nestedCols = (await this.model.getColumns()).filter( (c) => c.uidt === UITypes.LinkToAnotherRecord, - ); + ) for (const col of nestedCols) { if (col.title in data) { const colOptions = - await col.getColOptions(); + await col.getColOptions() // parse data if it's JSON string const nestedData = typeof data[col.title] === 'string' ? JSON.parse(data[col.title]) - : data[col.title]; + : data[col.title] switch (colOptions.type) { - case RelationTypes.BELONGS_TO: - { - const childCol = await colOptions.getChildColumn(); - const parentCol = await colOptions.getParentColumn(); - insertObj[childCol.column_name] = nestedData?.[parentCol.title]; - } - break; - case RelationTypes.HAS_MANY: - { - const childCol = await colOptions.getChildColumn(); - const childModel = await childCol.getModel(); - await childModel.getColumns(); - - postInsertOps.push(async () => { - await this.dbDriver(childModel.table_name) - .update({ - [childCol.column_name]: rowId, - }) - .whereIn( - childModel.primaryKey.column_name, - nestedData?.map((r) => r[childModel.primaryKey.title]), - ); - }); - } - break; + case RelationTypes.BELONGS_TO: { + const childCol = await colOptions.getChildColumn() + const parentCol = await colOptions.getParentColumn() + insertObj[childCol.column_name] = nestedData?.[parentCol.title] + } + break + case RelationTypes.HAS_MANY: { + const childCol = await colOptions.getChildColumn() + const childModel = await childCol.getModel() + await childModel.getColumns() + + postInsertOps.push(async () => { + await this.dbDriver(childModel.table_name) + .update({ + [childCol.column_name]: rowId, + }) + .whereIn( + childModel.primaryKey.column_name, + nestedData?.map((r) => r[childModel.primaryKey.title]), + ) + }) + } + break case RelationTypes.MANY_TO_MANY: { postInsertOps.push(async () => { const parentModel = await colOptions .getParentColumn() - .then((c) => c.getModel()); - await parentModel.getColumns(); - const parentMMCol = await colOptions.getMMParentColumn(); - const childMMCol = await colOptions.getMMChildColumn(); - const mmModel = await colOptions.getMMModel(); + .then((c) => c.getModel()) + await parentModel.getColumns() + const parentMMCol = await colOptions.getMMParentColumn() + const childMMCol = await colOptions.getMMChildColumn() + const mmModel = await colOptions.getMMModel() const rows = nestedData.map((r) => ({ [parentMMCol.column_name]: r[parentModel.primaryKey.title], [childMMCol.column_name]: rowId, - })); - await this.dbDriver(mmModel.table_name).insert(rows); - }); + })) + await this.dbDriver(mmModel.table_name).insert(rows) + }) } } } } - await this.validate(insertObj); + await this.validate(insertObj) - await this.beforeInsert(insertObj, this.dbDriver, cookie); + await this.beforeInsert(insertObj, this.dbDriver, cookie) - let response; - const query = this.dbDriver(this.tnPath).insert(insertObj); + let response + const query = this.dbDriver(this.tnPath).insert(insertObj) if (this.isPg || this.isMssql) { query.returning( `${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`, - ); - response = await query; + ) + response = await query } - const ai = this.model.columns.find((c) => c.ai); + const ai = this.model.columns.find((c) => c.ai) if ( !response || (typeof response?.[0] !== 'object' && response?.[0] !== null) ) { - let id; + let id if (response?.length) { - id = response[0]; + id = response[0] } else { - id = (await query)[0]; + id = (await query)[0] } if (ai) { @@ -2048,39 +2042,39 @@ class BaseModelSqlv2 { await this.dbDriver(this.tnPath) .select(ai.column_name) .max(ai.column_name, { as: 'id' }) - )[0].id; + )[0].id } else if (this.isSnowflake) { id = ( (await this.dbDriver(this.tnPath).max(ai.column_name, { as: 'id', })) as any - ).rows[0].id; + ).rows[0].id } - response = await this.readByPk(id); + response = await this.readByPk(id) } else { - response = data; + response = data } } else if (ai) { response = await this.readByPk( Array.isArray(response) ? response?.[0]?.[ai.title] : response?.[ai.title], - ); + ) } - response = Array.isArray(response) ? response[0] : response; + response = Array.isArray(response) ? response[0] : response if (response) rowId = response[this.model.primaryKey.title] || - response[this.model.primaryKey.column_name]; + response[this.model.primaryKey.column_name] - await Promise.all(postInsertOps.map((f) => f())); + await Promise.all(postInsertOps.map((f) => f())) - await this.afterInsert(response, this.dbDriver, cookie); + await this.afterInsert(response, this.dbDriver, cookie) - return response; + return response } catch (e) { - console.log(e); - throw e; + console.log(e) + throw e } } @@ -2098,65 +2092,78 @@ class BaseModelSqlv2 { raw?: boolean; } = {}, ) { - let trx; + let trx try { // TODO: ag column handling for raw bulk insert const insertDatas = raw ? datas : await Promise.all( - datas.map(async (d) => { - await populatePk(this.model, d); - return this.model.mapAliasToColumn(d); - }), - ); + datas.map(async (d) => { + await populatePk(this.model, d) + return this.model.mapAliasToColumn(d) + }), + ) // await this.beforeInsertb(insertDatas, null); if (!raw) { for (const data of datas) { - await this.validate(data); + await this.validate(data) } } // fallbacks to `10` if database client is sqlite // to avoid `too many SQL variables` error // refer : https://www.sqlite.org/limits.html - const chunkSize = this.isSqlite ? 10 : _chunkSize; + const chunkSize = this.isSqlite ? 10 : _chunkSize - trx = await this.dbDriver.transaction(); + trx = await this.dbDriver.transaction() if (!foreign_key_checks) { if (this.isPg) { - await trx.raw('set session_replication_role to replica;'); + await trx.raw('set session_replication_role to replica;') } else if (this.isMySQL) { - await trx.raw('SET foreign_key_checks = 0;'); + await trx.raw('SET foreign_key_checks = 0;') } } - const response = - this.isPg || this.isMssql - ? await trx + let response + + if (this.isSqlite) { + // sqlite doesnt support returning, so insert one by one and return ids + response = [] + + for (const insertData of insertDatas) { + const query = trx(this.tnPath).insert(insertData) + const id = (await query)[0] + response.push(await this.readByPk(id)) + } + } else { + response = + this.isPg || this.isMssql + ? await trx .batchInsert(this.tnPath, insertDatas, chunkSize) .returning(this.model.primaryKey?.column_name) - : await trx.batchInsert(this.tnPath, insertDatas, chunkSize); + : await trx.batchInsert(this.tnPath, insertDatas, chunkSize) + } if (!foreign_key_checks) { if (this.isPg) { - await trx.raw('set session_replication_role to origin;'); + await trx.raw('set session_replication_role to origin;') } else if (this.isMySQL) { - await trx.raw('SET foreign_key_checks = 1;'); + await trx.raw('SET foreign_key_checks = 1;') } } - await trx.commit(); + await trx.commit() - if (!raw) await this.afterBulkInsert(insertDatas, this.dbDriver, cookie); + if (!raw) await this.afterBulkInsert(insertDatas, this.dbDriver, cookie) - return response; + return response } catch (e) { - await trx?.rollback(); + await trx?.rollback() // await this.errorInsertb(e, data, null); - throw e; + throw e } } @@ -2164,54 +2171,54 @@ class BaseModelSqlv2 { datas: any[], { cookie, raw = false }: { cookie?: any; raw?: boolean } = {}, ) { - let transaction; + let transaction try { - if (raw) await this.model.getColumns(); + if (raw) await this.model.getColumns() const updateDatas = raw ? datas - : await Promise.all(datas.map((d) => this.model.mapAliasToColumn(d))); + : await Promise.all(datas.map((d) => this.model.mapAliasToColumn(d))) - const prevData = []; - const newData = []; - const updatePkValues = []; - const toBeUpdated = []; - const res = []; + const prevData = [] + const newData = [] + const updatePkValues = [] + const toBeUpdated = [] + const res = [] for (const d of updateDatas) { - if (!raw) await this.validate(d); - const pkValues = await this._extractPksValues(d); + if (!raw) await this.validate(d) + const pkValues = await this._extractPksValues(d) if (!pkValues) { // pk not specified - bypass - continue; + continue } - if (!raw) prevData.push(await this.readByPk(pkValues)); - const wherePk = await this._wherePk(pkValues); - res.push(wherePk); - toBeUpdated.push({ d, wherePk }); - updatePkValues.push(pkValues); + if (!raw) prevData.push(await this.readByPk(pkValues)) + const wherePk = await this._wherePk(pkValues) + res.push(wherePk) + toBeUpdated.push({ d, wherePk }) + updatePkValues.push(pkValues) } - transaction = await this.dbDriver.transaction(); + transaction = await this.dbDriver.transaction() for (const o of toBeUpdated) { - await transaction(this.tnPath).update(o.d).where(o.wherePk); + await transaction(this.tnPath).update(o.d).where(o.wherePk) } - await transaction.commit(); + await transaction.commit() if (!raw) { for (const pkValues of updatePkValues) { - newData.push(await this.readByPk(pkValues)); + newData.push(await this.readByPk(pkValues)) } } if (!raw) - await this.afterBulkUpdate(prevData, newData, this.dbDriver, cookie); + await this.afterBulkUpdate(prevData, newData, this.dbDriver, cookie) - return res; + return res } catch (e) { - if (transaction) await transaction.rollback(); - throw e; + if (transaction) await transaction.rollback() + throw e } } @@ -2221,18 +2228,18 @@ class BaseModelSqlv2 { { cookie }: { cookie?: any } = {}, ) { try { - let count = 0; - const updateData = await this.model.mapAliasToColumn(data); - await this.validate(updateData); - const pkValues = await this._extractPksValues(updateData); + let count = 0 + const updateData = await this.model.mapAliasToColumn(data) + await this.validate(updateData) + const pkValues = await this._extractPksValues(updateData) if (pkValues) { // pk is specified - by pass } else { - await this.model.getColumns(); - const { where } = this._getListArgs(args); - const qb = this.dbDriver(this.tnPath); - const aliasColObjMap = await this.model.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + await this.model.getColumns() + const { where } = this._getListArgs(args) + const qb = this.dbDriver(this.tnPath) + const aliasColObjMap = await this.model.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) await conditionV2( [ @@ -2249,55 +2256,55 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) - qb.update(updateData); + qb.update(updateData) - count = (await qb) as any; + count = (await qb) as any } - await this.afterBulkUpdate(null, count, this.dbDriver, cookie, true); + await this.afterBulkUpdate(null, count, this.dbDriver, cookie, true) - return count; + return count } catch (e) { - throw e; + throw e } } async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) { - let transaction; + let transaction try { const deleteIds = await Promise.all( ids.map((d) => this.model.mapAliasToColumn(d)), - ); + ) - const deleted = []; - const res = []; + const deleted = [] + const res = [] for (const d of deleteIds) { - const pkValues = await this._extractPksValues(d); + const pkValues = await this._extractPksValues(d) if (!pkValues) { // pk not specified - bypass - continue; + continue } - deleted.push(await this.readByPk(pkValues)); - res.push(d); + deleted.push(await this.readByPk(pkValues)) + res.push(d) } - transaction = await this.dbDriver.transaction(); + transaction = await this.dbDriver.transaction() for (const d of res) { - await transaction(this.tnPath).del().where(d); + await transaction(this.tnPath).del().where(d) } - await transaction.commit(); + await transaction.commit() - await this.afterBulkDelete(deleted, this.dbDriver, cookie); + await this.afterBulkDelete(deleted, this.dbDriver, cookie) - return res; + return res } catch (e) { - if (transaction) await transaction.rollback(); - console.log(e); - throw e; + if (transaction) await transaction.rollback() + console.log(e) + throw e } } @@ -2306,11 +2313,11 @@ class BaseModelSqlv2 { { cookie }: { cookie?: any } = {}, ) { try { - await this.model.getColumns(); - const { where } = this._getListArgs(args); - const qb = this.dbDriver(this.tnPath); - const aliasColObjMap = await this.model.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + await this.model.getColumns() + const { where } = this._getListArgs(args) + const qb = this.dbDriver(this.tnPath) + const aliasColObjMap = await this.model.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) await conditionV2( [ @@ -2327,17 +2334,17 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) - qb.del(); + qb.del() - const count = (await qb) as any; + const count = (await qb) as any - await this.afterBulkDelete(count, this.dbDriver, cookie, true); + await this.afterBulkDelete(count, this.dbDriver, cookie, true) - return count; + return count } catch (e) { - throw e; + throw e } } @@ -2346,12 +2353,12 @@ class BaseModelSqlv2 { * */ public async beforeInsert(data: any, _trx: any, req): Promise { - await this.handleHooks('before.insert', null, data, req); + await this.handleHooks('before.insert', null, data, req) } public async afterInsert(data: any, _trx: any, req): Promise { - await this.handleHooks('after.insert', null, data, req); - const id = this._extractPksValues(data); + await this.handleHooks('after.insert', null, data, req) + const id = this._extractPksValues(data) await Audit.insert({ fk_model_id: this.model.id, row_id: id, @@ -2363,7 +2370,7 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }); + }) } public async afterBulkUpdate( @@ -2373,10 +2380,10 @@ class BaseModelSqlv2 { req, isBulkAllOperation = false, ): Promise { - let noOfUpdatedRecords = newData; + let noOfUpdatedRecords = newData if (!isBulkAllOperation) { - noOfUpdatedRecords = newData.length; - await this.handleHooks('after.bulkUpdate', prevData, newData, req); + noOfUpdatedRecords = newData.length + await this.handleHooks('after.bulkUpdate', prevData, newData, req) } await Audit.insert({ @@ -2391,7 +2398,7 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }); + }) } public async afterBulkDelete( @@ -2400,10 +2407,10 @@ class BaseModelSqlv2 { req, isBulkAllOperation = false, ): Promise { - let noOfDeletedRecords = data; + let noOfDeletedRecords = data if (!isBulkAllOperation) { - noOfDeletedRecords = data.length; - await this.handleHooks('after.bulkDelete', null, data, req); + noOfDeletedRecords = data.length + await this.handleHooks('after.bulkDelete', null, data, req) } await Audit.insert({ @@ -2418,11 +2425,11 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }); + }) } public async afterBulkInsert(data: any[], _trx: any, req): Promise { - await this.handleHooks('after.bulkInsert', null, data, req); + await this.handleHooks('after.bulkInsert', null, data, req) await Audit.insert({ fk_model_id: this.model.id, @@ -2436,18 +2443,18 @@ class BaseModelSqlv2 { // 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; + const ignoreWebhook = req.query?.ignoreWebhook if (ignoreWebhook) { if (ignoreWebhook != 'true' && ignoreWebhook != 'false') { - throw new Error('ignoreWebhook value can be either true or false'); + throw new Error('ignoreWebhook value can be either true or false') } } if (ignoreWebhook === undefined || ignoreWebhook === 'false') { - await this.handleHooks('before.update', null, data, req); + await this.handleHooks('before.update', null, data, req) } } @@ -2458,25 +2465,25 @@ class BaseModelSqlv2 { req, updateObj?: Record, ): Promise { - const id = this._extractPksValues(newData); - let desc = `Record with ID ${id} has been updated in Table ${this.model.title}.`; - let details = ''; + const id = this._extractPksValues(newData) + let desc = `Record with ID ${id} has been updated in Table ${this.model.title}.` + let details = '' if (updateObj) { - updateObj = await this.model.mapColumnToAlias(updateObj); + updateObj = await this.model.mapColumnToAlias(updateObj) for (const k of Object.keys(updateObj)) { const prevValue = typeof prevData[k] === 'object' ? JSON.stringify(prevData[k]) - : prevData[k]; + : prevData[k] const newValue = typeof newData[k] === 'object' ? JSON.stringify(newData[k]) - : newData[k]; - desc += `\n`; - desc += `Column "${k}" got changed from "${prevValue}" to "${newValue}"`; + : newData[k] + desc += `\n` + desc += `Column "${k}" got changed from "${prevValue}" to "${newValue}"` details += DOMPurify.sanitize(`${k} : ${prevValue} - ${newValue}`); + ${newValue}`) } } await Audit.insert({ @@ -2488,25 +2495,25 @@ class BaseModelSqlv2 { details, ip: req?.clientIp, user: req?.user?.email, - }); + }) - const ignoreWebhook = req.query?.ignoreWebhook; + const ignoreWebhook = req.query?.ignoreWebhook if (ignoreWebhook) { if (ignoreWebhook != 'true' && ignoreWebhook != 'false') { - throw new Error('ignoreWebhook value can be either true or false'); + throw new Error('ignoreWebhook value can be either true or false') } } if (ignoreWebhook === undefined || ignoreWebhook === 'false') { - await this.handleHooks('after.update', prevData, newData, req); + await this.handleHooks('after.update', prevData, newData, req) } } public async beforeDelete(data: any, _trx: any, req): Promise { - await this.handleHooks('before.delete', null, data, req); + await this.handleHooks('before.delete', null, data, req) } public async afterDelete(data: any, _trx: any, req): Promise { - const id = req?.params?.id; + const id = req?.params?.id await Audit.insert({ fk_model_id: this.model.id, row_id: id, @@ -2518,8 +2525,8 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }); - await this.handleHooks('after.delete', null, data, req); + }) + await this.handleHooks('after.delete', null, data, req) } private async handleHooks(hookName, prevData, newData, req): Promise { @@ -2531,7 +2538,7 @@ class BaseModelSqlv2 { viewId: this.viewId, modelId: this.model.id, tnPath: this.tnPath, - }); + }) /* const view = await View.get(this.viewId); @@ -2627,49 +2634,52 @@ class BaseModelSqlv2 { } // @ts-ignore - protected async errorInsert(e, data, trx, cookie) {} + protected async errorInsert(e, data, trx, cookie) { + } // @ts-ignore - protected async errorUpdate(e, data, trx, cookie) {} + protected async errorUpdate(e, data, trx, cookie) { + } // todo: handle composite primary key protected _extractPksValues(data: any) { // data can be still inserted without PK // TODO: return a meaningful value - if (!this.model.primaryKey) return 'N/A'; + if (!this.model.primaryKey) return 'N/A' return ( data[this.model.primaryKey.title] || data[this.model.primaryKey.column_name] - ); + ) } // @ts-ignore - protected async errorDelete(e, id, trx, cookie) {} + protected async errorDelete(e, id, trx, cookie) { + } async validate(columns) { - await this.model.getColumns(); + await this.model.getColumns() // let cols = Object.keys(this.columns); for (let i = 0; i < this.model.columns.length; ++i) { - const column = this.model.columns[i]; + const column = this.model.columns[i] // skip validation if `validate` is undefined or false - if (!column?.meta?.validate || !column?.validate) continue; + if (!column?.meta?.validate || !column?.validate) continue - const validate = column.getValidators(); - const cn = column.column_name; - const columnTitle = column.title; - if (!validate) continue; + const validate = column.getValidators() + const cn = column.column_name + const columnTitle = column.title + if (!validate) continue - const { func, msg } = validate; + const { func, msg } = validate for (let j = 0; j < func.length; ++j) { const fn = typeof func[j] === 'string' ? customValidators[func[j]] ? customValidators[func[j]] : Validator[func[j]] - : func[j]; - const columnValue = columns?.[cn] || columns?.[columnTitle]; + : func[j] + const columnValue = columns?.[cn] || columns?.[columnTitle] const arg = - typeof func[j] === 'string' ? columnValue + '' : columnValue; + typeof func[j] === 'string' ? columnValue + '' : columnValue if ( ![null, undefined, ''].includes(columnValue) && !(fn.constructor.name === 'AsyncFunction' ? await fn(arg) : fn(arg)) @@ -2678,115 +2688,112 @@ class BaseModelSqlv2 { msg[j] .replace(/\{VALUE}/g, columnValue) .replace(/\{cn}/g, columnTitle), - ); + ) } } } - return true; + return true } async addChild({ - colId, - rowId, - childId, - cookie, - }: { + 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); + 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'); + NcError.notFound('Column not found') - const colOptions = await column.getColOptions(); + 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 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 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(); + 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) - const vTn = this.getTnPath(vTable); + if (this.isSnowflake) { + const parentPK = this.dbDriver(parentTn) + .select(parentColumn.column_name) + .where(_wherePk(parentTable.primaryKeys, childId)) + .first() - if (this.isSnowflake) { - const parentPK = this.dbDriver(parentTn) + 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(); - - const childPK = this.dbDriver(childTn) + .first(), + [vChildCol.column_name]: 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) + .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(), - [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; + .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); + const response = await this.readByPk(rowId) + await this.afterInsert(response, this.dbDriver, cookie) + await this.afterAddChild(rowId, childId, cookie) } public async afterAddChild(rowId, childId, req): Promise { @@ -2801,94 +2808,91 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }); + }) } async removeChild({ - colId, - rowId, - childId, - cookie, - }: { + 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); + 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'); + NcError.notFound('Column not found') - const colOptions = await column.getColOptions(); + 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 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 childTn = this.getTnPath(childTable) + const parentTn = this.getTnPath(parentTable) - const prevData = await this.readByPk(rowId); + 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(); + 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); + 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; + 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); + const newData = await this.readByPk(rowId) + await this.afterUpdate(prevData, newData, this.dbDriver, cookie) + await this.afterRemoveChild(rowId, childId, cookie) } public async afterRemoveChild(rowId, childId, req): Promise { @@ -2903,7 +2907,7 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }); + }) } public async groupedList( @@ -2912,34 +2916,32 @@ class BaseModelSqlv2 { ignoreViewFilterAndSort?: boolean; options?: (string | number | null | boolean)[]; } & Partial, - ): Promise< - { - key: string; - value: Record[]; - }[] - > { + ): Promise<{ + key: string; + value: Record[]; + }[]> { try { - const { where, ...rest } = this._getListArgs(args as any); + const { where, ...rest } = this._getListArgs(args as any) const column = await this.model .getColumns() - .then((cols) => cols?.find((col) => col.id === args.groupColumnId)); + .then((cols) => cols?.find((col) => col.id === args.groupColumnId)) - if (!column) NcError.notFound('Column not found'); + if (!column) NcError.notFound('Column not found') if (isVirtualCol(column)) - NcError.notImplemented('Grouping for virtual columns not implemented'); + NcError.notImplemented('Grouping for virtual columns not implemented') // extract distinct group column values - let groupingValues: Set; + let groupingValues: Set if (args.options?.length) { - groupingValues = new Set(args.options); + groupingValues = new Set(args.options) } else if (column.uidt === UITypes.SingleSelect) { const colOptions = await column.getColOptions<{ options: SelectOption[]; - }>(); + }>() groupingValues = new Set( (colOptions?.options ?? []).map((opt) => opt.title), - ); - groupingValues.add(null); + ) + groupingValues.add(null) } else { groupingValues = new Set( ( @@ -2947,20 +2949,20 @@ class BaseModelSqlv2 { .select(column.column_name) .distinct() ).map((row) => row[column.column_name]), - ); - groupingValues.add(null); + ) + groupingValues.add(null) } - const qb = this.dbDriver(this.tnPath); - qb.limit(+rest?.limit || 25); - qb.offset(+rest?.offset || 0); + const qb = this.dbDriver(this.tnPath) + qb.limit(+rest?.limit || 25) + qb.offset(+rest?.offset || 0) - await this.selectObject({ qb, extractPkAndPv: true }); + await this.selectObject({ qb, extractPkAndPv: true }) // todo: refactor and move to a method (applyFilterAndSort) - const aliasColObjMap = await this.model.getAliasColObjMap(); - let sorts = extractSortsObject(args?.sort, aliasColObjMap); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + const aliasColObjMap = await this.model.getAliasColObjMap() + let sorts = extractSortsObject(args?.sort, aliasColObjMap) + const filterObj = extractFilterFromXwhere(where, aliasColObjMap) // todo: replace with view id if (!args.ignoreViewFilterAndSort && this.viewId) { await conditionV2( @@ -2983,14 +2985,14 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) if (!sorts) sorts = args.sortArr?.length ? args.sortArr - : await Sort.list({ viewId: this.viewId }); + : await Sort.list({ viewId: this.viewId }) - if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver); + if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver) } else { await conditionV2( [ @@ -3007,71 +3009,71 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) - if (!sorts) sorts = args.sortArr; + if (!sorts) sorts = args.sortArr - if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver); + if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver) } // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (this.model.primaryKey && this.model.primaryKey.ai) { - qb.orderBy(this.model.primaryKey.column_name); + qb.orderBy(this.model.primaryKey.column_name) } else if ( this.model.columns.find((c) => c.column_name === 'created_at') ) { - qb.orderBy('created_at'); + qb.orderBy('created_at') } const groupedQb = this.dbDriver.from( this.dbDriver .unionAll( [...groupingValues].map((r) => { - const query = qb.clone(); + const query = qb.clone() if (r === null) { - query.whereNull(column.column_name); + query.whereNull(column.column_name) } else { - query.where(column.column_name, r); + query.where(column.column_name, r) } - return this.isSqlite ? this.dbDriver.select().from(query) : query; + return this.isSqlite ? this.dbDriver.select().from(query) : query }), !this.isSqlite, ) .as('__nc_grouped_list'), - ); + ) - const proto = await this.getProto(); + const proto = await this.getProto() - const data = await groupedQb; + const data = await groupedQb const result = data?.map((d) => { - d.__proto__ = proto; - return d; - }); + d.__proto__ = proto + return d + }) const groupedResult = result.reduce>( (aggObj, row) => { if (!aggObj.has(row[column.title])) { - aggObj.set(row[column.title], []); + aggObj.set(row[column.title], []) } - aggObj.get(row[column.title]).push(row); + aggObj.get(row[column.title]).push(row) - return aggObj; + return aggObj }, new Map(), - ); + ) const r = [...groupingValues].map((key) => ({ key, value: groupedResult.get(key) ?? [], - })); + })) - return r; + return r } catch (e) { - console.log(e); - throw e; + console.log(e) + throw e } } @@ -3083,19 +3085,19 @@ class BaseModelSqlv2 { ) { const column = await this.model .getColumns() - .then((cols) => cols?.find((col) => col.id === args.groupColumnId)); + .then((cols) => cols?.find((col) => col.id === args.groupColumnId)) - if (!column) NcError.notFound('Column not found'); + if (!column) NcError.notFound('Column not found') if (isVirtualCol(column)) - NcError.notImplemented('Grouping for virtual columns not implemented'); + NcError.notImplemented('Grouping for virtual columns not implemented') const qb = this.dbDriver(this.tnPath) .count('*', { as: 'count' }) - .groupBy(column.column_name); + .groupBy(column.column_name) // todo: refactor and move to a common method (applyFilterAndSort) - const aliasColObjMap = await this.model.getAliasColObjMap(); - const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap); + const aliasColObjMap = await this.model.getAliasColObjMap() + const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap) // todo: replace with view id if (!args.ignoreViewFilterAndSort && this.viewId) { @@ -3119,7 +3121,7 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) } else { await conditionV2( [ @@ -3136,34 +3138,34 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ); + ) } await this.selectObject({ qb, columns: [new Column({ ...column, title: 'key' })], - }); + }) - return await qb; + return await qb } private async execAndParse(qb: Knex.QueryBuilder, childTable?: Model) { - let query = qb.toQuery(); + let query = qb.toQuery() if (!this.isPg && !this.isMssql && !this.isSnowflake) { - query = unsanitize(qb.toQuery()); + query = unsanitize(qb.toQuery()) } else { - query = sanitize(query); + query = sanitize(query) } return this.convertAttachmentType( this.isPg || this.isSnowflake ? (await this.dbDriver.raw(query))?.rows : query.slice(0, 6) === 'select' && !this.isMssql - ? await this.dbDriver.from( + ? await this.dbDriver.from( this.dbDriver.raw(query).wrap('(', ') __nc_alias'), ) - : await this.dbDriver.raw(query), + : await this.dbDriver.raw(query), childTable, - ); + ) } private _convertAttachmentType( @@ -3174,12 +3176,13 @@ class BaseModelSqlv2 { if (d) { attachmentColumns.forEach((col) => { if (d[col.title] && typeof d[col.title] === 'string') { - d[col.title] = JSON.parse(d[col.title]); + d[col.title] = JSON.parse(d[col.title]) } - }); + }) } - } catch {} - return d; + } catch { + } + return d } private convertAttachmentType(data: Record, childTable?: Model) { @@ -3188,18 +3191,18 @@ class BaseModelSqlv2 { if (data) { const attachmentColumns = ( childTable ? childTable.columns : this.model.columns - ).filter((c) => c.uidt === UITypes.Attachment); + ).filter((c) => c.uidt === UITypes.Attachment) if (attachmentColumns.length) { if (Array.isArray(data)) { data = data.map((d) => this._convertAttachmentType(attachmentColumns, d), - ); + ) } else { - this._convertAttachmentType(attachmentColumns, data); + this._convertAttachmentType(attachmentColumns, data) } } } - return data; + return data } } @@ -3207,21 +3210,21 @@ function extractSortsObject( _sorts: string | string[], aliasColObjMap: { [columnAlias: string]: Column }, ): Sort[] | void { - if (!_sorts?.length) return; + if (!_sorts?.length) return - let sorts = _sorts; + let sorts = _sorts - if (!Array.isArray(sorts)) sorts = sorts.split(','); + if (!Array.isArray(sorts)) sorts = sorts.split(',') return sorts.map((s) => { - const sort: SortType = { direction: 'asc' }; + const sort: SortType = { direction: 'asc' } if (s.startsWith('-')) { - sort.direction = 'desc'; - sort.fk_column_id = aliasColObjMap[s.slice(1)]?.id; - } else sort.fk_column_id = aliasColObjMap[s]?.id; + sort.direction = 'desc' + sort.fk_column_id = aliasColObjMap[s.slice(1)]?.id + } else sort.fk_column_id = aliasColObjMap[s]?.id - return new Sort(sort); - }); + return new Sort(sort) + }) } function extractFilterFromXwhere( @@ -3229,26 +3232,26 @@ function extractFilterFromXwhere( aliasColObjMap: { [columnAlias: string]: Column }, ) { if (!str) { - return []; + return [] } - let nestedArrayConditions = []; + let nestedArrayConditions = [] - let openIndex = str.indexOf('(('); + let openIndex = str.indexOf('((') - if (openIndex === -1) openIndex = str.indexOf('(~'); + if (openIndex === -1) openIndex = str.indexOf('(~') - let nextOpenIndex = openIndex; + let nextOpenIndex = openIndex - let closingIndex = str.indexOf('))'); + let closingIndex = str.indexOf('))') // if it's a simple query simply return array of conditions if (openIndex === -1) { if (str && str != '~not') nestedArrayConditions = str.split( /(?=~(?:or(?:not)?|and(?:not)?|not)\()/, - ); - return extractCondition(nestedArrayConditions || [], aliasColObjMap); + ) + return extractCondition(nestedArrayConditions || [], aliasColObjMap) } // iterate until finding right closing @@ -3256,8 +3259,8 @@ function extractFilterFromXwhere( (nextOpenIndex = str .substring(0, closingIndex) .indexOf('((', nextOpenIndex + 1)) != -1 - ) { - closingIndex = str.indexOf('))', closingIndex + 1); + ) { + closingIndex = str.indexOf('))', closingIndex + 1) } if (closingIndex === -1) @@ -3265,15 +3268,15 @@ function extractFilterFromXwhere( `${str .substring(0, openIndex + 1) .slice(-10)} : Closing bracket not found`, - ); + ) // getting operand starting index - const operandStartIndex = str.lastIndexOf('~', openIndex); + const operandStartIndex = str.lastIndexOf('~', openIndex) const operator = operandStartIndex != -1 ? str.substring(operandStartIndex + 1, openIndex) - : ''; - const lhsOfNestedQuery = str.substring(0, openIndex); + : '' + const lhsOfNestedQuery = str.substring(0, openIndex) nestedArrayConditions.push( ...extractFilterFromXwhere(lhsOfNestedQuery, aliasColObjMap), @@ -3288,28 +3291,28 @@ function extractFilterFromXwhere( }), // RHS of nested query(recursion) ...extractFilterFromXwhere(str.substring(closingIndex + 2), aliasColObjMap), - ); - return nestedArrayConditions; + ) + return nestedArrayConditions } // mark `op` and `sub_op` any for being assignable to parameter of type function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) { if (!COMPARISON_OPS.includes(op)) { - NcError.badRequest(`${op} is not supported.`); + NcError.badRequest(`${op} is not supported.`) } if (sub_op) { if (![UITypes.Date, UITypes.DateTime].includes(uidt)) { - NcError.badRequest(`'${sub_op}' is not supported for UI Type'${uidt}'.`); + NcError.badRequest(`'${sub_op}' is not supported for UI Type'${uidt}'.`) } if (!COMPARISON_SUB_OPS.includes(sub_op)) { - NcError.badRequest(`'${sub_op}' is not supported.`); + NcError.badRequest(`'${sub_op}' is not supported.`) } if ( (op === 'isWithin' && !IS_WITHIN_COMPARISON_SUB_OPS.includes(sub_op)) || (op !== 'isWithin' && IS_WITHIN_COMPARISON_SUB_OPS.includes(sub_op)) ) { - NcError.badRequest(`'${sub_op}' is not supported for '${op}'`); + NcError.badRequest(`'${sub_op}' is not supported for '${op}'`) } } } @@ -3317,30 +3320,30 @@ function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) { function extractCondition(nestedArrayConditions, aliasColObjMap) { return nestedArrayConditions?.map((str) => { let [logicOp, alias, op, value] = - str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || []; + str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || [] if (!alias && !op && !value) { // try match with blank filter format [logicOp, alias, op, value] = - str.match(/(?:~(and|or|not))?\((.*?),(\w+)\)/)?.slice(1) || []; + str.match(/(?:~(and|or|not))?\((.*?),(\w+)\)/)?.slice(1) || [] } - let sub_op = null; + let sub_op = null if (aliasColObjMap[alias]) { if ( [UITypes.Date, UITypes.DateTime].includes(aliasColObjMap[alias].uidt) ) { - value = value?.split(','); + value = value?.split(',') // the first element would be sub_op - sub_op = value?.[0]; + sub_op = value?.[0] // remove the first element which is sub_op - value?.shift(); - value = value?.[0]; + value?.shift() + value = value?.[0] } else if (op === 'in') { - value = value.split(','); + value = value.split(',') } - validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op); + validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op) } return new Filter({ @@ -3349,8 +3352,8 @@ function extractCondition(nestedArrayConditions, aliasColObjMap) { fk_column_id: aliasColObjMap[alias]?.id, logical_op: logicOp, value, - }); - }); + }) + }) } function applyPaginate( @@ -3361,26 +3364,26 @@ function applyPaginate( ignoreLimit = false, }: XcFilter & { ignoreLimit?: boolean }, ) { - query.offset(offset); - if (!ignoreLimit) query.limit(limit); - return query; + query.offset(offset) + if (!ignoreLimit) query.limit(limit) + return query } function _wherePk(primaryKeys: Column[], id) { - const ids = (id + '').split('___'); - const where = {}; + const ids = (id + '').split('___') + const where = {} for (let i = 0; i < primaryKeys.length; ++i) { - where[primaryKeys[i].column_name] = ids[i]; + where[primaryKeys[i].column_name] = ids[i] } - return where; + return where } function getCompositePk(primaryKeys: Column[], row) { - return primaryKeys.map((c) => row[c.title]).join('___'); + return primaryKeys.map((c) => row[c.title]).join('___') } function haveFormulaColumn(columns: Column[]) { - return columns.some((c) => c.uidt === UITypes.Formula); + return columns.some((c) => c.uidt === UITypes.Formula) } function shouldSkipField( @@ -3391,7 +3394,7 @@ function shouldSkipField( extractPkAndPv, ) { if (fieldsSet) { - return !fieldsSet.has(column.title); + return !fieldsSet.has(column.title) } else { if (!extractPkAndPv) { if (!(viewOrTableColumn instanceof Column)) { @@ -3401,18 +3404,18 @@ function shouldSkipField( !column.pk && column.uidt !== UITypes.ForeignKey ) - return true; + return true if ( !view?.show_system_fields && column.uidt !== UITypes.ForeignKey && !column.pk && isSystemColumn(column) ) - return true; + return true } } - return false; + return false } } -export { BaseModelSqlv2 }; +export { BaseModelSqlv2 } diff --git a/packages/nocodb/src/helpers/catchError.ts b/packages/nocodb/src/helpers/catchError.ts index 279d042828..0165fae6c7 100644 --- a/packages/nocodb/src/helpers/catchError.ts +++ b/packages/nocodb/src/helpers/catchError.ts @@ -413,6 +413,8 @@ export default function ( return res.status(501).json({ msg: e.message }); } else if (e instanceof AjvError) { return res.status(400).json({ msg: e.message, errors: e.errors }); + } else if (e instanceof UnprocessableEntity) { + return res.status(422).json({ msg: e.message }); } next(e); } @@ -431,6 +433,8 @@ export class InternalServerError extends Error {} export class NotImplemented extends Error {} +export class UnprocessableEntity extends Error {} + export class AjvError extends Error { constructor(param: { message: string; errors: ErrorObject[] }) { super(param.message); @@ -468,4 +472,8 @@ export class NcError { static ajvValidationError(param: { message: string; errors: ErrorObject[] }) { throw new AjvError(param); } + + static unprocessableEntity(message = 'Unprocessable entity') { + throw new UnprocessableEntity(message); + } } diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index b0a3bb72fe..92e39b6315 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { NcError } from '../helpers/catchError'; import { Base, Model, View } from '../models'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; -import projectAcl from '../utils/projectAcl'; import { DatasService } from './datas.service'; @Injectable() @@ -173,7 +172,7 @@ export class DataTableService { const model = await Model.get(param.modelId); if (!model) { - throw new Error('Model not found'); + NcError.notFound('Model not found'); } if (param.projectId && model.project_id !== param.projectId) { @@ -184,8 +183,8 @@ export class DataTableService { if (param.viewId) { view = await View.get(param.viewId); - if (view.fk_model_id && view.fk_model_id !== param.modelId) { - throw new Error('View not belong to model'); + if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) { + NcError.unprocessableEntity('View not belong to model'); } } From e495a98f60d74cbc9ddf197db945cad29001782c Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Mon, 29 May 2023 18:07:55 +0530 Subject: [PATCH 19/97] test: update error codes Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 76c53e5050..f2d278e01c 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -559,19 +559,19 @@ function textBased() { // Invalid table ID await ncAxiosGet({ url: `/api/v1/base/${project.id}/tables/123456789`, - status: 400, + status: 404, }); // Invalid project ID await ncAxiosGet({ url: `/api/v1/base/123456789/tables/123456789`, - status: 400, + status: 404, }); // Invalid view ID await ncAxiosGet({ query: { viewId: '123456789', }, - status: 400, + status: 422, }); }); @@ -607,7 +607,7 @@ function textBased() { query: { offset: 10000, }, - status: 200, + status: 422, }); }); @@ -680,17 +680,17 @@ function textBased() { // Invalid table ID await ncAxiosPost({ url: `/api/v1/base/${project.id}/tables/123456789`, - status: 400, + status: 404, }); // Invalid project ID await ncAxiosPost({ url: `/api/v1/base/123456789/tables/123456789`, - status: 400, + status: 404, }); - // Invalid data - repeated ID + // Invalid data - create should not specify ID await ncAxiosPost({ body: { ...newRecord, Id: 300 }, - status: 400, + status: 422, }); // Invalid data - number instead of string // await ncAxiosPost({ @@ -718,12 +718,12 @@ function textBased() { // Invalid table ID await ncAxiosGet({ url: `/api/v1/base/tables/123456789/rows/100`, - status: 400, + status: 404, }); // Invalid row ID await ncAxiosGet({ url: `/api/v1/base/tables/${table.id}/rows/1000`, - status: 400, + status: 404, }); }); @@ -804,17 +804,17 @@ function textBased() { // Invalid project ID await ncAxiosPatch({ url: `/api/v1/base/123456789/tables/${table.id}`, - status: 400, + status: 404, }); // Invalid table ID await ncAxiosPatch({ url: `/api/v1/base/${project.id}/tables/123456789`, - status: 400, + status: 404, }); // Invalid row ID await ncAxiosPatch({ body: { Id: 123456789, SingleLineText: 'some text' }, - status: 400, + status: 422, }); }); @@ -832,7 +832,7 @@ function textBased() { // // check that it's gone await ncAxiosGet({ url: `/api/v1/base/tables/${table.id}/rows/1`, - status: 400, + status: 404, }); }); @@ -843,11 +843,11 @@ function textBased() { // check that it's gone await ncAxiosGet({ url: `/api/v1/base/tables/${table.id}/rows/1`, - status: 400, + status: 404, }); await ncAxiosGet({ url: `/api/v1/base/tables/${table.id}/rows/2`, - status: 400, + status: 404, }); }); @@ -857,15 +857,15 @@ function textBased() { // Invalid project ID await ncAxiosDelete({ url: `/api/v1/base/123456789/tables/${table.id}`, - status: 400, + status: 404, }); // Invalid table ID await ncAxiosDelete({ url: `/api/v1/base/${project.id}/tables/123456789`, - status: 400, + status: 404, }); // Invalid row ID - await ncAxiosDelete({ body: { Id: 123456789 }, status: 400 }); + await ncAxiosDelete({ body: { Id: 123456789 }, status: 422 }); }); } From 71cef4eb6781db810fae03f751768ef94efecf9b Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 18:29:01 +0530 Subject: [PATCH 20/97] chore: lint Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 2524 +++++++++++----------- 1 file changed, 1267 insertions(+), 1257 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 162a8cc696..ecda3a98e6 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -1,7 +1,7 @@ -import autoBind from 'auto-bind' -import groupBy from 'lodash/groupBy' -import DataLoader from 'dataloader' -import { nocoExecute } from 'nc-help' +import autoBind from 'auto-bind'; +import groupBy from 'lodash/groupBy'; +import DataLoader from 'dataloader'; +import { nocoExecute } from 'nc-help'; import { AuditOperationSubTypes, AuditOperationTypes, @@ -9,33 +9,33 @@ import { isVirtualCol, RelationTypes, UITypes, -} from 'nocodb-sdk' -import Validator from 'validator' -import { customAlphabet } from 'nanoid' -import DOMPurify from 'isomorphic-dompurify' -import { v4 as uuidv4 } from 'uuid' -import { NcError } from '../helpers/catchError' -import getAst from '../helpers/getAst' - -import { Audit, Column, Filter, Model, Project, Sort, View } from '../models' -import { sanitize, unsanitize } from '../helpers/sqlSanitize' +} from 'nocodb-sdk'; +import Validator from 'validator'; +import { customAlphabet } from 'nanoid'; +import DOMPurify from 'isomorphic-dompurify'; +import { v4 as uuidv4 } from 'uuid'; +import { NcError } from '../helpers/catchError'; +import getAst from '../helpers/getAst'; + +import { Audit, Column, Filter, Model, Project, Sort, View } from '../models'; +import { sanitize, unsanitize } from '../helpers/sqlSanitize'; import { COMPARISON_OPS, COMPARISON_SUB_OPS, IS_WITHIN_COMPARISON_SUB_OPS, -} from '../models/Filter' -import Noco from '../Noco' -import { HANDLE_WEBHOOK } from '../services/hook-handler.service' -import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2' -import genRollupSelectv2 from './genRollupSelectv2' -import conditionV2 from './conditionV2' -import sortV2 from './sortV2' -import { customValidators } from './util/customValidators' -import type { XKnex } from './CustomKnex' +} from '../models/Filter'; +import Noco from '../Noco'; +import { HANDLE_WEBHOOK } from '../services/hook-handler.service'; +import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2'; +import genRollupSelectv2 from './genRollupSelectv2'; +import conditionV2 from './conditionV2'; +import sortV2 from './sortV2'; +import { customValidators } from './util/customValidators'; +import type { XKnex } from './CustomKnex'; import type { XcFilter, XcFilterWithAlias, -} from './sql-data-mapper/lib/BaseModel' +} from './sql-data-mapper/lib/BaseModel'; import type { BarcodeColumn, FormulaColumn, @@ -44,41 +44,41 @@ import type { QrCodeColumn, RollupColumn, SelectOption, -} from '../models' -import type { Knex } from 'knex' -import type { SortType } from 'nocodb-sdk' +} from '../models'; +import type { Knex } from 'knex'; +import type { SortType } from 'nocodb-sdk'; -const GROUP_COL = '__nc_group_id' +const GROUP_COL = '__nc_group_id'; -const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14) +const nanoidv2 = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 14); export async function getViewAndModelByAliasOrId(param: { projectName: string; tableName: string; viewName?: string; }) { - const project = await Project.getWithInfoByTitleOrId(param.projectName) + const project = await Project.getWithInfoByTitleOrId(param.projectName); const model = await Model.getByAliasOrId({ project_id: project.id, aliasOrId: param.tableName, - }) + }); const view = param.viewName && (await View.getByTitleOrId({ titleOrId: param.viewName, fk_model_id: model.id, - })) - if (!model) NcError.notFound('Table not found') - return { model, view } + })); + if (!model) NcError.notFound('Table not found'); + return { model, view }; } async function populatePk(model: Model, insertObj: any) { - await model.getColumns() + await model.getColumns(); for (const pkCol of model.primaryKeys) { - if (!pkCol.meta?.ag || insertObj[pkCol.title]) continue + if (!pkCol.meta?.ag || insertObj[pkCol.title]) continue; insertObj[pkCol.title] = - pkCol.meta?.ag === 'nc' ? `rc_${nanoidv2()}` : uuidv4() + pkCol.meta?.ag === 'nc' ? `rc_${nanoidv2()}` : uuidv4(); } } @@ -88,13 +88,13 @@ function checkColumnRequired( extractPkAndPv?: boolean, ) { // if primary key or foreign key included in fields, it's required - if (column.pk || column.uidt === UITypes.ForeignKey) return true + if (column.pk || column.uidt === UITypes.ForeignKey) return true; - if (extractPkAndPv && column.pv) return true + if (extractPkAndPv && column.pv) return true; // check fields defined and if not, then select all // if defined check if it is in the fields - return !fields || fields.includes(column.title) + return !fields || fields.includes(column.title); } /** @@ -104,30 +104,30 @@ function checkColumnRequired( * @classdesc Base class for models */ class BaseModelSqlv2 { - protected dbDriver: XKnex - protected model: Model - protected viewId: string - private _proto: any - private _columns = {} + protected dbDriver: XKnex; + protected model: Model; + protected viewId: string; + private _proto: any; + private _columns = {}; private config: any = { limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1), limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1), limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1), - } + }; constructor({ - dbDriver, - model, - viewId, - }: { + dbDriver, + model, + viewId, + }: { [key: string]: any; model: Model; }) { - this.dbDriver = dbDriver - this.model = model - this.viewId = viewId - autoBind(this) + this.dbDriver = dbDriver; + this.model = model; + this.viewId = viewId; + autoBind(this); } public async readByPk( @@ -135,55 +135,55 @@ class BaseModelSqlv2 { validateFormula = false, query: any = {}, ): Promise { - const qb = this.dbDriver(this.tnPath) + const qb = this.dbDriver(this.tnPath); const { ast, dependencyFields } = await getAst({ query, model: this.model, view: this.viewId && (await View.get(this.viewId)), - }) + }); await this.selectObject({ ...(dependencyFields ?? {}), qb, validateFormula, - }) + }); - qb.where(_wherePk(this.model.primaryKeys, id)) + qb.where(_wherePk(this.model.primaryKeys, id)); - let data + let data; try { - data = (await this.execAndParse(qb))?.[0] + data = (await this.execAndParse(qb))?.[0]; } catch (e) { if (validateFormula || !haveFormulaColumn(await this.model.getColumns())) - throw e - console.log(e) - return this.readByPk(id, true) + throw e; + console.log(e); + return this.readByPk(id, true); } if (data) { - const proto = await this.getProto() - data.__proto__ = proto + const proto = await this.getProto(); + data.__proto__ = proto; } - return data ? await nocoExecute(ast, data, {}, query) : {} + return data ? await nocoExecute(ast, data, {}, query) : {}; } public async exist(id?: any): Promise { - const qb = this.dbDriver(this.tnPath) - await this.model.getColumns() - const pks = this.model.primaryKeys + const qb = this.dbDriver(this.tnPath); + await this.model.getColumns(); + const pks = this.model.primaryKeys; - if (!pks.length) return false + if (!pks.length) return false; - qb.select(pks[0].column_name) + qb.select(pks[0].column_name); if ((id + '').split('___').length != pks?.length) { - return false + return false; } - qb.where(_wherePk(pks, id)).first() - return !!(await qb) + qb.where(_wherePk(pks, id)).first(); + return !!(await qb); } // todo: add support for sortArrJson @@ -195,13 +195,13 @@ class BaseModelSqlv2 { } = {}, validateFormula = false, ): Promise { - const { where, ...rest } = this._getListArgs(args as any) - const qb = this.dbDriver(this.tnPath) - await this.selectObject({ ...args, qb, validateFormula }) + const { where, ...rest } = this._getListArgs(args as any); + const qb = this.dbDriver(this.tnPath); + await this.selectObject({ ...args, qb, validateFormula }); - const aliasColObjMap = await this.model.getAliasColObjMap() - const sorts = extractSortsObject(rest?.sort, aliasColObjMap) - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await this.model.getAliasColObjMap(); + const sorts = extractSortsObject(rest?.sort, aliasColObjMap); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); await conditionV2( [ @@ -218,30 +218,30 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); if (Array.isArray(sorts) && sorts?.length) { - await sortV2(sorts, qb, this.dbDriver) + await sortV2(sorts, qb, this.dbDriver); } else if (this.model.primaryKey) { - qb.orderBy(this.model.primaryKey.column_name) + qb.orderBy(this.model.primaryKey.column_name); } - let data + let data; try { - data = await qb.first() + data = await qb.first(); } catch (e) { if (validateFormula || !haveFormulaColumn(await this.model.getColumns())) - throw e - console.log(e) - return this.findOne(args, true) + throw e; + console.log(e); + return this.findOne(args, true); } if (data) { - const proto = await this.getProto() - data.__proto__ = proto + const proto = await this.getProto(); + data.__proto__ = proto; } - return data + return data; } public async list( @@ -257,23 +257,23 @@ class BaseModelSqlv2 { ignoreViewFilterAndSort = false, validateFormula = false, ): Promise { - const { where, fields, ...rest } = this._getListArgs(args as any) + const { where, fields, ...rest } = this._getListArgs(args as any); - const qb = this.dbDriver(this.tnPath) + const qb = this.dbDriver(this.tnPath); await this.selectObject({ qb, fieldsSet: args.fieldsSet, viewId: this.viewId, validateFormula, - }) + }); if (+rest?.shuffle) { - await this.shuffle({ qb }) + await this.shuffle({ qb }); } - const aliasColObjMap = await this.model.getAliasColObjMap() - let sorts = extractSortsObject(rest?.sort, aliasColObjMap) - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await this.model.getAliasColObjMap(); + let sorts = extractSortsObject(rest?.sort, aliasColObjMap); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); // todo: replace with view id if (!ignoreViewFilterAndSort && this.viewId) { await conditionV2( @@ -296,14 +296,14 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); if (!sorts) sorts = args.sortArr?.length ? args.sortArr - : await Sort.list({ viewId: this.viewId }) + : await Sort.list({ viewId: this.viewId }); - await sortV2(sorts, qb, this.dbDriver) + await sortV2(sorts, qb, this.dbDriver); } else { await conditionV2( [ @@ -320,52 +320,52 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); - if (!sorts) sorts = args.sortArr + if (!sorts) sorts = args.sortArr; - await sortV2(sorts, qb, this.dbDriver) + await sortV2(sorts, qb, this.dbDriver); } // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (this.model.primaryKey && this.model.primaryKey.ai) { - qb.orderBy(this.model.primaryKey.column_name) + qb.orderBy(this.model.primaryKey.column_name); } else if (this.model.columns.find((c) => c.column_name === 'created_at')) { - qb.orderBy('created_at') + qb.orderBy('created_at'); } - if (!ignoreViewFilterAndSort) applyPaginate(qb, rest) - const proto = await this.getProto() + if (!ignoreViewFilterAndSort) applyPaginate(qb, rest); + const proto = await this.getProto(); - let data + let data; try { - data = await this.execAndParse(qb) + data = await this.execAndParse(qb); } catch (e) { if (validateFormula || !haveFormulaColumn(await this.model.getColumns())) - throw e - console.log(e) - return this.list(args, ignoreViewFilterAndSort, true) + throw e; + console.log(e); + return this.list(args, ignoreViewFilterAndSort, true); } return data?.map((d) => { - d.__proto__ = proto - return d - }) + d.__proto__ = proto; + return d; + }); } public async count( args: { where?: string; limit?; filterArr?: Filter[] } = {}, ignoreViewFilterAndSort = false, ): Promise { - await this.model.getColumns() - const { where } = this._getListArgs(args) + await this.model.getColumns(); + const { where } = this._getListArgs(args); - const qb = this.dbDriver(this.tnPath) + const qb = this.dbDriver(this.tnPath); // qb.xwhere(where, await this.model.getAliasColMapping()); - const aliasColObjMap = await this.model.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await this.model.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); if (!ignoreViewFilterAndSort && this.viewId) { await conditionV2( @@ -389,7 +389,7 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); } else { await conditionV2( [ @@ -407,22 +407,22 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); } qb.count(sanitize(this.model.primaryKey?.column_name) || '*', { as: 'count', - }).first() + }).first(); - let sql = sanitize(qb.toQuery()) + let sql = sanitize(qb.toQuery()); if (!this.isPg && !this.isMssql && !this.isSnowflake) { - sql = unsanitize(qb.toQuery()) + sql = unsanitize(qb.toQuery()); } - const res = (await this.dbDriver.raw(sql)) as any + const res = (await this.dbDriver.raw(sql)) as any; return (this.isPg || this.isSnowflake ? res.rows[0] : res[0][0] ?? res[0]) - .count + .count; } // todo: add support for sortArrJson and filterArrJson @@ -437,21 +437,21 @@ class BaseModelSqlv2 { column_name: '', }, ) { - const { where, ...rest } = this._getListArgs(args as any) + const { where, ...rest } = this._getListArgs(args as any); - const qb = this.dbDriver(this.tnPath) - qb.count(`${this.model.primaryKey?.column_name || '*'} as count`) - qb.select(args.column_name) + const qb = this.dbDriver(this.tnPath); + qb.count(`${this.model.primaryKey?.column_name || '*'} as count`); + qb.select(args.column_name); if (+rest?.shuffle) { - await this.shuffle({ qb }) + await this.shuffle({ qb }); } - const aliasColObjMap = await this.model.getAliasColObjMap() + const aliasColObjMap = await this.model.getAliasColObjMap(); - const sorts = extractSortsObject(rest?.sort, aliasColObjMap) + const sorts = extractSortsObject(rest?.sort, aliasColObjMap); - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); await conditionV2( [ new Filter({ @@ -462,11 +462,11 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) - qb.groupBy(args.column_name) - if (sorts) await sortV2(sorts, qb, this.dbDriver) - applyPaginate(qb, rest) - return await qb + ); + qb.groupBy(args.column_name); + if (sorts) await sortV2(sorts, qb, this.dbDriver); + applyPaginate(qb, rest); + return await qb; } async multipleHmList( @@ -474,38 +474,38 @@ class BaseModelSqlv2 { args: { limit?; offset?; fieldsSet?: Set } = {}, ) { try { - const { where, sort, ...rest } = this._getListArgs(args as any) + const { where, sort, ...rest } = this._getListArgs(args as any); // todo: get only required fields // const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {}; const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn() - const childTable = await chilCol.getModel() + ).getChildColumn(); + const childTable = await chilCol.getModel(); const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn() - const parentTable = await parentCol.getModel() + ).getParentColumn(); + const parentTable = await parentCol.getModel(); const childModel = await Model.getBaseModelSQL({ model: childTable, dbDriver: this.dbDriver, - }) - await parentTable.getColumns() + }); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const qb = this.dbDriver(childTn) + const qb = this.dbDriver(childTn); await childModel.selectObject({ qb, extractPkAndPv: true, fieldsSet: args.fieldsSet, - }) - await this.applySortAndFilter({ table: childTable, where, qb, sort }) + }); + await this.applySortAndFilter({ table: childTable, where, qb, sort }); const childQb = this.dbDriver.queryBuilder().from( this.dbDriver @@ -520,59 +520,59 @@ class BaseModelSqlv2 { .select(parentCol.column_name) // .where(parentTable.primaryKey.cn, p) .where(_wherePk(parentTable.primaryKeys, p)), - ) + ); // todo: sanitize - query.limit(+rest?.limit || 25) - query.offset(+rest?.offset || 0) + query.limit(+rest?.limit || 25); + query.offset(+rest?.offset || 0); - return this.isSqlite ? this.dbDriver.select().from(query) : query + return this.isSqlite ? this.dbDriver.select().from(query) : query; }), !this.isSqlite, ) .as('list'), - ) + ); // console.log(childQb.toQuery()) - const children = await this.execAndParse(childQb, childTable) + const children = await this.execAndParse(childQb, childTable); const proto = await ( await Model.getBaseModelSQL({ id: childTable.id, dbDriver: this.dbDriver, }) - ).getProto() + ).getProto(); return groupBy( children.map((c) => { - c.__proto__ = proto - return c + c.__proto__ = proto; + return c; }), GROUP_COL, - ) + ); } catch (e) { - console.log(e) - throw e + console.log(e); + throw e; } } private async applySortAndFilter({ - table, - where, - qb, - sort, - }: { + table, + where, + qb, + sort, + }: { table: Model; where: string; qb; sort: string; }) { - const childAliasColMap = await table.getAliasColObjMap() + const childAliasColMap = await table.getAliasColObjMap(); - const filter = extractFilterFromXwhere(where, childAliasColMap) - await conditionV2(filter, qb, this.dbDriver) - if (!sort) return - const sortObj = extractSortsObject(sort, childAliasColMap) - if (sortObj) await sortV2(sortObj, qb, this.dbDriver) + const filter = extractFilterFromXwhere(where, childAliasColMap); + await conditionV2(filter, qb, this.dbDriver); + if (!sort) return; + const sortObj = extractSortsObject(sort, childAliasColMap); + if (sortObj) await sortV2(sortObj, qb, this.dbDriver); } async multipleHmListCount({ colId, ids }) { @@ -580,19 +580,19 @@ class BaseModelSqlv2 { // const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {}; const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn() - const childTable = await chilCol.getModel() + ).getChildColumn(); + const childTable = await chilCol.getModel(); const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn() - const parentTable = await parentCol.getModel() - await parentTable.getColumns() + ).getParentColumn(); + const parentTable = await parentCol.getModel(); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); const children = await this.dbDriver.unionAll( ids.map((p) => { @@ -605,17 +605,17 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, p) .where(_wherePk(parentTable.primaryKeys, p)), ) - .first() + .first(); - return this.isSqlite ? this.dbDriver.select().from(query) : query + return this.isSqlite ? this.dbDriver.select().from(query) : query; }), !this.isSqlite, - ) + ); - return children.map(({ count }) => count) + return children.map(({ count }) => count); } catch (e) { - console.log(e) - throw e + console.log(e); + throw e; } } @@ -624,32 +624,32 @@ class BaseModelSqlv2 { args: { limit?; offset?; fieldSet?: Set } = {}, ) { try { - const { where, sort, ...rest } = this._getListArgs(args as any) + const { where, sort, ...rest } = this._getListArgs(args as any); // todo: get only required fields const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn() - const childTable = await chilCol.getModel() + ).getChildColumn(); + const childTable = await chilCol.getModel(); const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn() - const parentTable = await parentCol.getModel() + ).getParentColumn(); + const parentTable = await parentCol.getModel(); const childModel = await Model.getBaseModelSQL({ model: childTable, dbDriver: this.dbDriver, - }) - await parentTable.getColumns() + }); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const qb = this.dbDriver(childTn) - await this.applySortAndFilter({ table: childTable, where, qb, sort }) + const qb = this.dbDriver(childTn); + await this.applySortAndFilter({ table: childTable, where, qb, sort }); qb.whereIn( chilCol.column_name, @@ -657,29 +657,29 @@ class BaseModelSqlv2 { .select(parentCol.column_name) // .where(parentTable.primaryKey.cn, p) .where(_wherePk(parentTable.primaryKeys, id)), - ) + ); // todo: sanitize - qb.limit(+rest?.limit || 25) - qb.offset(+rest?.offset || 0) + qb.limit(+rest?.limit || 25); + qb.offset(+rest?.offset || 0); - await childModel.selectObject({ qb, fieldsSet: args.fieldSet }) + await childModel.selectObject({ qb, fieldsSet: args.fieldSet }); - const children = await this.execAndParse(qb, childTable) + const children = await this.execAndParse(qb, childTable); const proto = await ( await Model.getBaseModelSQL({ id: childTable.id, dbDriver: this.dbDriver, }) - ).getProto() + ).getProto(); return children.map((c) => { - c.__proto__ = proto - return c - }) + c.__proto__ = proto; + return c; + }); } catch (e) { - console.log(e) - throw e + console.log(e); + throw e; } } @@ -688,19 +688,19 @@ class BaseModelSqlv2 { // const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {}; const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const chilCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getChildColumn() - const childTable = await chilCol.getModel() + ).getChildColumn(); + const childTable = await chilCol.getModel(); const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - ).getParentColumn() - const parentTable = await parentCol.getModel() - await parentTable.getColumns() + ).getParentColumn(); + const parentTable = await parentCol.getModel(); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); const query = this.dbDriver(childTn) .count(`${chilCol?.column_name} as count`) @@ -710,12 +710,12 @@ class BaseModelSqlv2 { .select(parentCol.column_name) .where(_wherePk(parentTable.primaryKeys, id)), ) - .first() - const { count } = await query - return count + .first(); + const { count } = await query; + return count; } catch (e) { - console.log(e) - throw e + console.log(e); + throw e; } } @@ -723,40 +723,40 @@ class BaseModelSqlv2 { { colId, parentIds }, args: { limit?; offset?; fieldsSet?: Set } = {}, ) { - const { where, sort, ...rest } = this._getListArgs(args as any) + const { where, sort, ...rest } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; // const tn = this.model.tn; // const cn = (await relColOptions.getChildColumn()).title; - const mmTable = await relColOptions.getMMModel() - const vtn = this.getTnPath(mmTable) - const vcn = (await relColOptions.getMMChildColumn()).column_name - const vrcn = (await relColOptions.getMMParentColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getParentColumn()).getModel() - const parentTable = await (await relColOptions.getChildColumn()).getModel() - await parentTable.getColumns() + const mmTable = await relColOptions.getMMModel(); + const vtn = this.getTnPath(mmTable); + const vcn = (await relColOptions.getMMChildColumn()).column_name; + const vrcn = (await relColOptions.getMMParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getParentColumn()).getModel(); + const parentTable = await (await relColOptions.getChildColumn()).getModel(); + await parentTable.getColumns(); const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }) + }); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = childTn - const rtnId = childTable.id + const rtn = childTn; + const rtnId = childTable.id; - const qb = this.dbDriver(rtn).join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) + const qb = this.dbDriver(rtn).join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`); - await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }) + await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }); - await this.applySortAndFilter({ table: childTable, where, qb, sort }) + await this.applySortAndFilter({ table: childTable, where, qb, sort }); const finalQb = this.dbDriver.unionAll( parentIds.map((id) => { @@ -769,69 +769,69 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, id)), ) - .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])) + .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])); // todo: sanitize - query.limit(+rest?.limit || 25) - query.offset(+rest?.offset || 0) + query.limit(+rest?.limit || 25); + query.offset(+rest?.offset || 0); - return this.isSqlite ? this.dbDriver.select().from(query) : query + return this.isSqlite ? this.dbDriver.select().from(query) : query; }), !this.isSqlite, - ) + ); - let children = await this.execAndParse(finalQb, childTable) + let children = await this.execAndParse(finalQb, childTable); if (this.isMySQL) { - children = children[0] + children = children[0]; } const proto = await ( await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver, }) - ).getProto() + ).getProto(); const gs = groupBy( children.map((c) => { - c.__proto__ = proto - return c + c.__proto__ = proto; + return c; }), GROUP_COL, - ) - return parentIds.map((id) => gs[id] || []) + ); + return parentIds.map((id) => gs[id] || []); } public async mmList( { colId, parentId }, args: { limit?; offset?; fieldsSet?: Set } = {}, ) { - const { where, sort, ...rest } = this._getListArgs(args as any) + const { where, sort, ...rest } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; // const tn = this.model.tn; // const cn = (await relColOptions.getChildColumn()).title; - const mmTable = await relColOptions.getMMModel() - const vtn = this.getTnPath(mmTable) - const vcn = (await relColOptions.getMMChildColumn()).column_name - const vrcn = (await relColOptions.getMMParentColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getParentColumn()).getModel() - const parentTable = await (await relColOptions.getChildColumn()).getModel() - await parentTable.getColumns() + const mmTable = await relColOptions.getMMModel(); + const vtn = this.getTnPath(mmTable); + const vcn = (await relColOptions.getMMChildColumn()).column_name; + const vrcn = (await relColOptions.getMMParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getParentColumn()).getModel(); + const parentTable = await (await relColOptions.getChildColumn()).getModel(); + await parentTable.getColumns(); const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }) + }); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = childTn - const rtnId = childTable.id + const rtn = childTn; + const rtnId = childTable.id; const qb = this.dbDriver(rtn) .join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) @@ -841,55 +841,55 @@ class BaseModelSqlv2 { .select(cn) // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, parentId)), - ) + ); - await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }) + await childModel.selectObject({ qb, fieldsSet: args.fieldsSet }); - await this.applySortAndFilter({ table: childTable, where, qb, sort }) + await this.applySortAndFilter({ table: childTable, where, qb, sort }); // todo: sanitize - qb.limit(+rest?.limit || 25) - qb.offset(+rest?.offset || 0) + qb.limit(+rest?.limit || 25); + qb.offset(+rest?.offset || 0); - const children = await this.execAndParse(qb, childTable) + const children = await this.execAndParse(qb, childTable); const proto = await ( await Model.getBaseModelSQL({ id: rtnId, dbDriver: this.dbDriver }) - ).getProto() + ).getProto(); return children.map((c) => { - c.__proto__ = proto - return c - }) + c.__proto__ = proto; + return c; + }); } public async multipleMmListCount({ colId, parentIds }) { const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - const mmTable = await relColOptions.getMMModel() - const vtn = this.getTnPath(mmTable) - const vcn = (await relColOptions.getMMChildColumn()).column_name - const vrcn = (await relColOptions.getMMParentColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getParentColumn()).getModel() - const parentTable = await (await relColOptions.getChildColumn()).getModel() - await parentTable.getColumns() + const mmTable = await relColOptions.getMMModel(); + const vtn = this.getTnPath(mmTable); + const vcn = (await relColOptions.getMMChildColumn()).column_name; + const vrcn = (await relColOptions.getMMParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getParentColumn()).getModel(); + const parentTable = await (await relColOptions.getChildColumn()).getModel(); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = childTn + const rtn = childTn; const qb = this.dbDriver(rtn) .join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) // .select({ // [`${tn}_${vcn}`]: `${vtn}.${vcn}` // }) - .count(`${vtn}.${vcn}`, { as: 'count' }) + .count(`${vtn}.${vcn}`, { as: 'count' }); // await childModel.selectObject({ qb }); const children = await this.dbDriver.unionAll( @@ -903,38 +903,38 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, id)), ) - .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])) + .select(this.dbDriver.raw('? as ??', [id, GROUP_COL])); // this._paginateAndSort(query, { sort, limit, offset }, null, true); - return this.isSqlite ? this.dbDriver.select().from(query) : query + return this.isSqlite ? this.dbDriver.select().from(query) : query; }), !this.isSqlite, - ) + ); - const gs = groupBy(children, GROUP_COL) - return parentIds.map((id) => gs?.[id]?.[0] || []) + const gs = groupBy(children, GROUP_COL); + return parentIds.map((id) => gs?.[id]?.[0] || []); } public async mmListCount({ colId, parentId }) { const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - const mmTable = await relColOptions.getMMModel() - const vtn = this.getTnPath(mmTable) - const vcn = (await relColOptions.getMMChildColumn()).column_name - const vrcn = (await relColOptions.getMMParentColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getParentColumn()).getModel() - const parentTable = await (await relColOptions.getChildColumn()).getModel() - await parentTable.getColumns() + const mmTable = await relColOptions.getMMModel(); + const vtn = this.getTnPath(mmTable); + const vcn = (await relColOptions.getMMChildColumn()).column_name; + const vrcn = (await relColOptions.getMMParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getParentColumn()).getModel(); + const parentTable = await (await relColOptions.getChildColumn()).getModel(); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = childTn + const rtn = childTn; const qb = this.dbDriver(rtn) .join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) @@ -949,11 +949,11 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, id) .where(_wherePk(parentTable.primaryKeys, parentId)), ) - .first() + .first(); - const { count } = await qb + const { count } = await qb; - return count + return count; } // todo: naming & optimizing @@ -961,27 +961,27 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where } = this._getListArgs(args as any) + const { where } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - - const mmTable = await relColOptions.getMMModel() - const vtn = this.getTnPath(mmTable) - const vcn = (await relColOptions.getMMChildColumn()).column_name - const vrcn = (await relColOptions.getMMParentColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getParentColumn()).getModel() - const parentTable = await (await relColOptions.getChildColumn()).getModel() - await parentTable.getColumns() - - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) - - const rtn = childTn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + + const mmTable = await relColOptions.getMMModel(); + const vtn = this.getTnPath(mmTable); + const vcn = (await relColOptions.getMMChildColumn()).column_name; + const vrcn = (await relColOptions.getMMParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getParentColumn()).getModel(); + const parentTable = await (await relColOptions.getChildColumn()).getModel(); + await parentTable.getColumns(); + + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); + + const rtn = childTn; const qb = this.dbDriver(rtn) .count(`*`, { as: 'count' }) .where((qb) => { @@ -997,14 +997,14 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, pid) .where(_wherePk(parentTable.primaryKeys, pid)), ), - ).orWhereNull(rcn) - }) + ).orWhereNull(rcn); + }); - const aliasColObjMap = await childTable.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await childTable.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2(filterObj, qb, this.dbDriver) - return (await qb.first())?.count + await conditionV2(filterObj, qb, this.dbDriver); + return (await qb.first())?.count; } // todo: naming & optimizing @@ -1012,31 +1012,31 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where, ...rest } = this._getListArgs(args as any) + const { where, ...rest } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn - - const mmTable = await relColOptions.getMMModel() - const vtn = this.getTnPath(mmTable) - const vcn = (await relColOptions.getMMChildColumn()).column_name - const vrcn = (await relColOptions.getMMParentColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getParentColumn()).getModel() + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; + + const mmTable = await relColOptions.getMMModel(); + const vtn = this.getTnPath(mmTable); + const vcn = (await relColOptions.getMMChildColumn()).column_name; + const vrcn = (await relColOptions.getMMParentColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getParentColumn()).getModel(); const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }) - const parentTable = await (await relColOptions.getChildColumn()).getModel() - await parentTable.getColumns() + }); + const parentTable = await (await relColOptions.getChildColumn()).getModel(); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = childTn + const rtn = childTn; const qb = this.dbDriver(rtn).where((qb) => qb @@ -1054,34 +1054,34 @@ class BaseModelSqlv2 { ), ) .orWhereNull(rcn), - ) + ); if (+rest?.shuffle) { - await this.shuffle({ qb }) + await this.shuffle({ qb }); } - await childModel.selectObject({ qb }) + await childModel.selectObject({ qb }); - const aliasColObjMap = await childTable.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) - await conditionV2(filterObj, qb, this.dbDriver) + const aliasColObjMap = await childTable.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + await conditionV2(filterObj, qb, this.dbDriver); // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (childTable.primaryKey && childTable.primaryKey.ai) { - qb.orderBy(childTable.primaryKey.column_name) + qb.orderBy(childTable.primaryKey.column_name); } else if (childTable.columns.find((c) => c.column_name === 'created_at')) { - qb.orderBy('created_at') + qb.orderBy('created_at'); } - applyPaginate(qb, rest) + applyPaginate(qb, rest); - const proto = await childModel.getProto() - const data = await qb + const proto = await childModel.getProto(); + const data = await qb; return data.map((c) => { - c.__proto__ = proto - return c - }) + c.__proto__ = proto; + return c; + }); } // todo: naming & optimizing @@ -1089,26 +1089,26 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where } = this._getListArgs(args as any) + const { where } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - const cn = (await relColOptions.getChildColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const childTable = await (await relColOptions.getChildColumn()).getModel() + const cn = (await relColOptions.getChildColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const childTable = await (await relColOptions.getChildColumn()).getModel(); const parentTable = await ( await relColOptions.getParentColumn() - ).getModel() + ).getModel(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const tn = childTn - const rtn = parentTn - await parentTable.getColumns() + const tn = childTn; + const rtn = parentTn; + await parentTable.getColumns(); const qb = this.dbDriver(tn) .count(`*`, { as: 'count' }) @@ -1119,15 +1119,15 @@ class BaseModelSqlv2 { .select(rcn) // .where(parentTable.primaryKey.cn, pid) .where(_wherePk(parentTable.primaryKeys, pid)), - ).orWhereNull(cn) - }) + ).orWhereNull(cn); + }); - const aliasColObjMap = await childTable.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await childTable.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2(filterObj, qb, this.dbDriver) + await conditionV2(filterObj, qb, this.dbDriver); - return (await qb.first())?.count + return (await qb.first())?.count; } // todo: naming & optimizing @@ -1135,30 +1135,30 @@ class BaseModelSqlv2 { { colId, pid = null }, args, ): Promise { - const { where, ...rest } = this._getListArgs(args as any) + const { where, ...rest } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - const cn = (await relColOptions.getChildColumn()).column_name - const rcn = (await relColOptions.getParentColumn()).column_name - const childTable = await (await relColOptions.getChildColumn()).getModel() + const cn = (await relColOptions.getChildColumn()).column_name; + const rcn = (await relColOptions.getParentColumn()).column_name; + const childTable = await (await relColOptions.getChildColumn()).getModel(); const parentTable = await ( await relColOptions.getParentColumn() - ).getModel() + ).getModel(); const childModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: childTable, - }) - await parentTable.getColumns() + }); + await parentTable.getColumns(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const tn = childTn - const rtn = parentTn + const tn = childTn; + const rtn = parentTn; const qb = this.dbDriver(tn).where((qb) => { qb.whereNotIn( @@ -1167,36 +1167,36 @@ class BaseModelSqlv2 { .select(rcn) // .where(parentTable.primaryKey.cn, pid) .where(_wherePk(parentTable.primaryKeys, pid)), - ).orWhereNull(cn) - }) + ).orWhereNull(cn); + }); if (+rest?.shuffle) { - await this.shuffle({ qb }) + await this.shuffle({ qb }); } - await childModel.selectObject({ qb }) + await childModel.selectObject({ qb }); - const aliasColObjMap = await childTable.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) - await conditionV2(filterObj, qb, this.dbDriver) + const aliasColObjMap = await childTable.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + await conditionV2(filterObj, qb, this.dbDriver); // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (childTable.primaryKey && childTable.primaryKey.ai) { - qb.orderBy(childTable.primaryKey.column_name) + qb.orderBy(childTable.primaryKey.column_name); } else if (childTable.columns.find((c) => c.column_name === 'created_at')) { - qb.orderBy('created_at') + qb.orderBy('created_at'); } - applyPaginate(qb, rest) + applyPaginate(qb, rest); - const proto = await childModel.getProto() - const data = await this.execAndParse(qb, childTable) + const proto = await childModel.getProto(); + const data = await this.execAndParse(qb, childTable); return data.map((c) => { - c.__proto__ = proto - return c - }) + c.__proto__ = proto; + return c; + }); } // todo: naming & optimizing @@ -1204,26 +1204,26 @@ class BaseModelSqlv2 { { colId, cid = null }, args, ): Promise { - const { where } = this._getListArgs(args as any) + const { where } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - const rcn = (await relColOptions.getParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name; const parentTable = await ( await relColOptions.getParentColumn() - ).getModel() - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getChildColumn()).getModel() + ).getModel(); + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getChildColumn()).getModel(); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = parentTn - const tn = childTn - await childTable.getColumns() + const rtn = parentTn; + const tn = childTn; + await childTable.getColumns(); const qb = this.dbDriver(rtn) .where((qb) => { @@ -1234,15 +1234,15 @@ class BaseModelSqlv2 { // .where(childTable.primaryKey.cn, cid) .where(_wherePk(childTable.primaryKeys, cid)) .whereNotNull(cn), - ) + ); }) - .count(`*`, { as: 'count' }) + .count(`*`, { as: 'count' }); - const aliasColObjMap = await parentTable.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await parentTable.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); - await conditionV2(filterObj, qb, this.dbDriver) - return (await qb.first())?.count + await conditionV2(filterObj, qb, this.dbDriver); + return (await qb.first())?.count; } // todo: naming & optimizing @@ -1250,30 +1250,30 @@ class BaseModelSqlv2 { { colId, cid = null }, args, ): Promise { - const { where, ...rest } = this._getListArgs(args as any) + const { where, ...rest } = this._getListArgs(args as any); const relColumn = (await this.model.getColumns()).find( (c) => c.id === colId, - ) + ); const relColOptions = - (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn; - const rcn = (await relColOptions.getParentColumn()).column_name + const rcn = (await relColOptions.getParentColumn()).column_name; const parentTable = await ( await relColOptions.getParentColumn() - ).getModel() - const cn = (await relColOptions.getChildColumn()).column_name - const childTable = await (await relColOptions.getChildColumn()).getModel() + ).getModel(); + const cn = (await relColOptions.getChildColumn()).column_name; + const childTable = await (await relColOptions.getChildColumn()).getModel(); const parentModel = await Model.getBaseModelSQL({ dbDriver: this.dbDriver, model: parentTable, - }) + }); - const childTn = this.getTnPath(childTable) - const parentTn = this.getTnPath(parentTable) + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const rtn = parentTn - const tn = childTn - await childTable.getColumns() + const rtn = parentTn; + const tn = childTn; + await childTable.getColumns(); const qb = this.dbDriver(rtn).where((qb) => { qb.whereNotIn( @@ -1283,38 +1283,38 @@ class BaseModelSqlv2 { // .where(childTable.primaryKey.cn, cid) .where(_wherePk(childTable.primaryKeys, cid)) .whereNotNull(cn), - ).orWhereNull(rcn) - }) + ).orWhereNull(rcn); + }); if (+rest?.shuffle) { - await this.shuffle({ qb }) + await this.shuffle({ qb }); } - await parentModel.selectObject({ qb }) + await parentModel.selectObject({ qb }); - const aliasColObjMap = await parentTable.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) - await conditionV2(filterObj, qb, this.dbDriver) + const aliasColObjMap = await parentTable.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); + await conditionV2(filterObj, qb, this.dbDriver); // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (parentTable.primaryKey && parentTable.primaryKey.ai) { - qb.orderBy(parentTable.primaryKey.column_name) + qb.orderBy(parentTable.primaryKey.column_name); } else if ( parentTable.columns.find((c) => c.column_name === 'created_at') ) { - qb.orderBy('created_at') + qb.orderBy('created_at'); } - applyPaginate(qb, rest) + applyPaginate(qb, rest); - const proto = await parentModel.getProto() - const data = await this.execAndParse(qb, childTable) + const proto = await parentModel.getProto(); + const data = await this.execAndParse(qb, childTable); return data.map((c) => { - c.__proto__ = proto - return c - }) + c.__proto__ = proto; + return c; + }); } private async getSelectQueryBuilderForFormula( @@ -1323,8 +1323,8 @@ class BaseModelSqlv2 { validateFormula = false, aliasToColumnBuilder = {}, ) { - const formula = await column.getColOptions() - if (formula.error) throw new Error(`Formula error: ${formula.error}`) + const formula = await column.getColOptions(); + if (formula.error) throw new Error(`Formula error: ${formula.error}`); const qb = await formulaQueryBuilderv2( formula.formula, null, @@ -1334,207 +1334,210 @@ class BaseModelSqlv2 { aliasToColumnBuilder, tableAlias, validateFormula, - ) - return qb + ); + return qb; } async getProto() { if (this._proto) { - return this._proto + return this._proto; } - const proto: any = { __columnAliases: {} } - const columns = await this.model.getColumns() + const proto: any = { __columnAliases: {} }; + const columns = await this.model.getColumns(); for (const column of columns) { switch (column.uidt) { - case UITypes.Rollup: { - // @ts-ignore - const colOptions: RollupColumn = await column.getColOptions() - } - break - case UITypes.Lookup: { - // @ts-ignore - const colOptions: LookupColumn = await column.getColOptions() - proto.__columnAliases[column.title] = { - path: [ - (await Column.get({ colId: colOptions.fk_relation_column_id })) - ?.title, - (await Column.get({ colId: colOptions.fk_lookup_column_id })) - ?.title, - ], + case UITypes.Rollup: + { + // @ts-ignore + const colOptions: RollupColumn = await column.getColOptions(); } - } - break - case UITypes.LinkToAnotherRecord: { - this._columns[column.title] = column - const colOptions = - (await column.getColOptions()) as LinkToAnotherRecordColumn - // const parentColumn = await colOptions.getParentColumn(); - - if (colOptions?.type === 'hm') { - const listLoader = new DataLoader(async (ids: string[]) => { - try { - if (ids.length > 1) { - const data = await this.multipleHmList( - { - colId: column.id, - ids, - }, - (listLoader as any).args, - ) - return ids.map((id: string) => (data[id] ? data[id] : [])) - } else { - return [ - await this.hmList( + break; + case UITypes.Lookup: + { + // @ts-ignore + const colOptions: LookupColumn = await column.getColOptions(); + proto.__columnAliases[column.title] = { + path: [ + (await Column.get({ colId: colOptions.fk_relation_column_id })) + ?.title, + (await Column.get({ colId: colOptions.fk_lookup_column_id })) + ?.title, + ], + }; + } + break; + case UITypes.LinkToAnotherRecord: + { + this._columns[column.title] = column; + const colOptions = + (await column.getColOptions()) as LinkToAnotherRecordColumn; + // const parentColumn = await colOptions.getParentColumn(); + + if (colOptions?.type === 'hm') { + const listLoader = new DataLoader(async (ids: string[]) => { + try { + if (ids.length > 1) { + const data = await this.multipleHmList( { colId: column.id, - id: ids[0], + ids, }, (listLoader as any).args, - ), - ] + ); + return ids.map((id: string) => (data[id] ? data[id] : [])); + } else { + return [ + await this.hmList( + { + colId: column.id, + id: ids[0], + }, + (listLoader as any).args, + ), + ]; + } + } catch (e) { + console.log(e); + return []; } - } catch (e) { - console.log(e) - return [] - } - }) - const self: BaseModelSqlv2 = this - - proto[column.title] = async function(args): Promise { - (listLoader as any).args = args - return listLoader.load( - getCompositePk(self.model.primaryKeys, this), - ) - } - - // defining HasMany count method within GQL Type class - // Object.defineProperty(type.prototype, column.alias, { - // async value(): Promise { - // return listLoader.load(this[model.pk.alias]); - // }, - // configurable: true - // }); - } else if (colOptions.type === 'mm') { - const listLoader = new DataLoader(async (ids: string[]) => { - try { - if (ids?.length > 1) { - const data = await this.multipleMmList( - { - parentIds: ids, - colId: column.id, - }, - (listLoader as any).args, - ) - - return data - } else { - return [ - await this.mmList( + }); + const self: BaseModelSqlv2 = this; + + proto[column.title] = async function (args): Promise { + (listLoader as any).args = args; + return listLoader.load( + getCompositePk(self.model.primaryKeys, this), + ); + }; + + // defining HasMany count method within GQL Type class + // Object.defineProperty(type.prototype, column.alias, { + // async value(): Promise { + // return listLoader.load(this[model.pk.alias]); + // }, + // configurable: true + // }); + } else if (colOptions.type === 'mm') { + const listLoader = new DataLoader(async (ids: string[]) => { + try { + if (ids?.length > 1) { + const data = await this.multipleMmList( { - parentId: ids[0], + parentIds: ids, colId: column.id, }, (listLoader as any).args, - ), - ] + ); + + return data; + } else { + return [ + await this.mmList( + { + parentId: ids[0], + colId: column.id, + }, + (listLoader as any).args, + ), + ]; + } + } catch (e) { + console.log(e); + return []; } - } catch (e) { - console.log(e) - return [] - } - }) + }); + + const self: BaseModelSqlv2 = this; + // const childColumn = await colOptions.getChildColumn(); + proto[column.title] = async function (args): Promise { + (listLoader as any).args = args; + return await listLoader.load( + getCompositePk(self.model.primaryKeys, this), + ); + }; + } else if (colOptions.type === 'bt') { + // @ts-ignore + const colOptions = + (await column.getColOptions()) as LinkToAnotherRecordColumn; + const pCol = await Column.get({ + colId: colOptions.fk_parent_column_id, + }); + const cCol = await Column.get({ + colId: colOptions.fk_child_column_id, + }); + const readLoader = new DataLoader(async (ids: string[]) => { + try { + const data = await ( + await Model.getBaseModelSQL({ + id: pCol.fk_model_id, + dbDriver: this.dbDriver, + }) + ).list( + { + // limit: ids.length, + where: `(${pCol.column_name},in,${ids.join(',')})`, + fieldsSet: (readLoader as any).args?.fieldsSet, + }, + true, + ); + const gs = groupBy(data, pCol.title); + return ids.map(async (id: string) => gs?.[id]?.[0]); + } catch (e) { + console.log(e); + return []; + } + }); - const self: BaseModelSqlv2 = this - // const childColumn = await colOptions.getChildColumn(); - proto[column.title] = async function(args): Promise { - (listLoader as any).args = args - return await listLoader.load( - getCompositePk(self.model.primaryKeys, this), - ) - } - } else if (colOptions.type === 'bt') { - // @ts-ignore - const colOptions = - (await column.getColOptions()) as LinkToAnotherRecordColumn - const pCol = await Column.get({ - colId: colOptions.fk_parent_column_id, - }) - const cCol = await Column.get({ - colId: colOptions.fk_child_column_id, - }) - const readLoader = new DataLoader(async (ids: string[]) => { - try { - const data = await ( - await Model.getBaseModelSQL({ - id: pCol.fk_model_id, - dbDriver: this.dbDriver, - }) - ).list( - { - // limit: ids.length, - where: `(${pCol.column_name},in,${ids.join(',')})`, - fieldsSet: (readLoader as any).args?.fieldsSet, - }, - true, + // defining HasMany count method within GQL Type class + proto[column.title] = async function (args?: any) { + if ( + this?.[cCol?.title] === null || + this?.[cCol?.title] === undefined ) - const gs = groupBy(data, pCol.title) - return ids.map(async (id: string) => gs?.[id]?.[0]) - } catch (e) { - console.log(e) - return [] - } - }) - - // defining HasMany count method within GQL Type class - proto[column.title] = async function(args?: any) { - if ( - this?.[cCol?.title] === null || - this?.[cCol?.title] === undefined - ) - return null; + return null; - (readLoader as any).args = args + (readLoader as any).args = args; - return await readLoader.load(this?.[cCol?.title]) + return await readLoader.load(this?.[cCol?.title]); + }; + // todo : handle mm } - // todo : handle mm } - } - break + break; } } - this._proto = proto - return proto + this._proto = proto; + return proto; } _getListArgs(args: XcFilterWithAlias): XcFilter { - const obj: XcFilter = {} - obj.where = args.filter || args.where || args.w || '' - obj.having = args.having || args.h || '' - obj.shuffle = args.shuffle || args.r || '' - obj.condition = args.condition || args.c || {} - obj.conditionGraph = args.conditionGraph || {} + const obj: XcFilter = {}; + obj.where = args.filter || args.where || args.w || ''; + obj.having = args.having || args.h || ''; + obj.shuffle = args.shuffle || args.r || ''; + obj.condition = args.condition || args.c || {}; + obj.conditionGraph = args.conditionGraph || {}; obj.limit = Math.max( Math.min( args.limit || args.l || this.config.limitDefault, this.config.limitMax, ), this.config.limitMin, - ) - obj.offset = Math.max(+(args.offset || args.o) || 0, 0) - obj.fields = args.fields || args.f - obj.sort = args.sort || args.s - return obj + ); + obj.offset = Math.max(+(args.offset || args.o) || 0, 0); + obj.fields = args.fields || args.f; + obj.sort = args.sort || args.s; + return obj; } public async shuffle({ qb }: { qb: Knex.QueryBuilder }): Promise { if (this.isMySQL) { - qb.orderByRaw('RAND()') + qb.orderByRaw('RAND()'); } else if (this.isPg || this.isSqlite) { - qb.orderByRaw('RANDOM()') + qb.orderByRaw('RANDOM()'); } else if (this.isMssql) { - qb.orderByRaw('NEWID()') + qb.orderByRaw('NEWID()'); } } @@ -1542,15 +1545,15 @@ class BaseModelSqlv2 { // pass view id as argument // add option to get only pk and pv public async selectObject({ - qb, - columns: _columns, - fields: _fields, - extractPkAndPv, - viewId, - fieldsSet, - alias, - validateFormula, - }: { + qb, + columns: _columns, + fields: _fields, + extractPkAndPv, + viewId, + fieldsSet, + alias, + validateFormula, + }: { fieldsSet?: Set; qb: Knex.QueryBuilder & Knex.QueryInterface; columns?: Column[]; @@ -1561,32 +1564,32 @@ class BaseModelSqlv2 { validateFormula?: boolean; }): Promise { // keep a common object for all columns to share across all columns - const aliasToColumnBuilder = {} - let viewOrTableColumns: Column[] | { fk_column_id?: string }[] + const aliasToColumnBuilder = {}; + let viewOrTableColumns: Column[] | { fk_column_id?: string }[]; - const res = {} - let view: View - let fields: string[] + const res = {}; + let view: View; + let fields: string[]; if (fieldsSet?.size) { - viewOrTableColumns = _columns || (await this.model.getColumns()) + viewOrTableColumns = _columns || (await this.model.getColumns()); } else { - view = await View.get(viewId) - const viewColumns = viewId && (await View.getColumns(viewId)) - fields = Array.isArray(_fields) ? _fields : _fields?.split(',') + view = await View.get(viewId); + const viewColumns = viewId && (await View.getColumns(viewId)); + fields = Array.isArray(_fields) ? _fields : _fields?.split(','); // const columns = _columns ?? (await this.model.getColumns()); // for (const column of columns) { viewOrTableColumns = - _columns || viewColumns || (await this.model.getColumns()) + _columns || viewColumns || (await this.model.getColumns()); } for (const viewOrTableColumn of viewOrTableColumns) { const column = viewOrTableColumn instanceof Column ? viewOrTableColumn : await Column.get({ - colId: (viewOrTableColumn as GridViewColumn).fk_column_id, - }) + colId: (viewOrTableColumn as GridViewColumn).fk_column_id, + }); // hide if column marked as hidden in view // of if column is system field and system field is hidden if ( @@ -1598,24 +1601,24 @@ class BaseModelSqlv2 { extractPkAndPv, ) ) { - continue + continue; } - if (!checkColumnRequired(column, fields, extractPkAndPv)) continue + if (!checkColumnRequired(column, fields, extractPkAndPv)) continue; switch (column.uidt) { case 'LinkToAnotherRecord': case 'Lookup': - break + break; case 'QrCode': { - const qrCodeColumn = await column.getColOptions() + const qrCodeColumn = await column.getColOptions(); const qrValueColumn = await Column.get({ colId: qrCodeColumn.fk_qr_value_column_id, - }) + }); // If the referenced value cannot be found: cancel current iteration if (qrValueColumn == null) { - break + break; } switch (qrValueColumn.uidt) { @@ -1626,31 +1629,31 @@ class BaseModelSqlv2 { alias, validateFormula, aliasToColumnBuilder, - ) + ); qb.select({ [column.column_name]: selectQb.builder, - }) + }); } catch { - continue + continue; } - break + break; default: { - qb.select({ [column.column_name]: qrValueColumn.column_name }) - break + qb.select({ [column.column_name]: qrValueColumn.column_name }); + break; } } - break + break; } case 'Barcode': { - const barcodeColumn = await column.getColOptions() + const barcodeColumn = await column.getColOptions(); const barcodeValueColumn = await Column.get({ colId: barcodeColumn.fk_barcode_value_column_id, - }) + }); // If the referenced value cannot be found: cancel current iteration if (barcodeValueColumn == null) { - break + break; } switch (barcodeValueColumn.uidt) { @@ -1661,47 +1664,48 @@ class BaseModelSqlv2 { alias, validateFormula, aliasToColumnBuilder, - ) + ); qb.select({ [column.column_name]: selectQb.builder, - }) + }); } catch { - continue + continue; } - break + break; default: { qb.select({ [column.column_name]: barcodeValueColumn.column_name, - }) - break + }); + break; } } - break + break; } - case 'Formula': { - try { - const selectQb = await this.getSelectQueryBuilderForFormula( - column, - alias, - validateFormula, - aliasToColumnBuilder, - ) - qb.select( - this.dbDriver.raw(`?? as ??`, [ - selectQb.builder, - sanitize(column.title), - ]), - ) - } catch (e) { - console.log(e) - // return dummy select - qb.select( - this.dbDriver.raw(`'ERR' as ??`, [sanitize(column.title)]), - ) + case 'Formula': + { + try { + const selectQb = await this.getSelectQueryBuilderForFormula( + column, + alias, + validateFormula, + aliasToColumnBuilder, + ); + qb.select( + this.dbDriver.raw(`?? as ??`, [ + selectQb.builder, + sanitize(column.title), + ]), + ); + } catch (e) { + console.log(e); + // return dummy select + qb.select( + this.dbDriver.raw(`'ERR' as ??`, [sanitize(column.title)]), + ); + } } - } - break + break; case 'Rollup': qb.select( ( @@ -1713,62 +1717,62 @@ class BaseModelSqlv2 { columnOptions: (await column.getColOptions()) as RollupColumn, }) ).builder.as(sanitize(column.title)), - ) - break + ); + break; default: res[sanitize(column.title || column.column_name)] = sanitize( `${alias || this.model.table_name}.${column.column_name}`, - ) - break + ); + break; } } - qb.select(res) + qb.select(res); } async insert(data, trx?, cookie?) { try { - await populatePk(this.model, data) + await populatePk(this.model, data); // todo: filter based on view - const insertObj = await this.model.mapAliasToColumn(data) + const insertObj = await this.model.mapAliasToColumn(data); - await this.validate(insertObj) + await this.validate(insertObj); if ('beforeInsert' in this) { - await this.beforeInsert(insertObj, trx, cookie) + await this.beforeInsert(insertObj, trx, cookie); } - await this.model.getColumns() - let response + await this.model.getColumns(); + let response; // const driver = trx ? trx : this.dbDriver; - const query = this.dbDriver(this.tnPath).insert(insertObj) + const query = this.dbDriver(this.tnPath).insert(insertObj); if ((this.isPg || this.isMssql) && this.model.primaryKey) { query.returning( `${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`, - ) - response = await this.execAndParse(query) + ); + response = await this.execAndParse(query); } - const ai = this.model.columns.find((c) => c.ai) + const ai = this.model.columns.find((c) => c.ai); - let ag: Column - if (!ai) ag = this.model.columns.find((c) => c.meta?.ag) + let ag: Column; + if (!ai) ag = this.model.columns.find((c) => c.meta?.ag); // handle if autogenerated primary key is used if (ag) { - if (!response) await this.execAndParse(query) - response = await this.readByPk(data[ag.title]) + if (!response) await this.execAndParse(query); + response = await this.readByPk(data[ag.title]); } else if ( !response || (typeof response?.[0] !== 'object' && response?.[0] !== null) ) { - let id + let id; if (response?.length) { - id = response[0] + id = response[0]; } else { - const res = await this.execAndParse(query) - id = res?.id ?? res[0]?.insertId + const res = await this.execAndParse(query); + id = res?.id ?? res[0]?.insertId; } if (ai) { @@ -1778,261 +1782,263 @@ class BaseModelSqlv2 { await this.dbDriver(this.tnPath) .select(ai.column_name) .max(ai.column_name, { as: 'id' }) - )[0].id + )[0].id; } else if (this.isSnowflake) { id = ( (await this.dbDriver(this.tnPath).max(ai.column_name, { as: 'id', })) as any - )[0].id + )[0].id; } - response = await this.readByPk(id) + response = await this.readByPk(id); } else { - response = data + response = data; } } else if (ai) { response = await this.readByPk( Array.isArray(response) ? response?.[0]?.[ai.title] : response?.[ai.title], - ) + ); } - await this.afterInsert(response, trx, cookie) - return Array.isArray(response) ? response[0] : response + await this.afterInsert(response, trx, cookie); + return Array.isArray(response) ? response[0] : response; } catch (e) { - console.log(e) - await this.errorInsert(e, data, trx, cookie) - throw e + console.log(e); + await this.errorInsert(e, data, trx, cookie); + throw e; } } async delByPk(id, trx?, cookie?) { try { // retrieve data for handling params in hook - const data = await this.readByPk(id) - await this.beforeDelete(id, trx, cookie) + const data = await this.readByPk(id); + await this.beforeDelete(id, trx, cookie); const response = await this.dbDriver(this.tnPath) .del() - .where(await this._wherePk(id)) - await this.afterDelete(data, trx, cookie) - return response + .where(await this._wherePk(id)); + await this.afterDelete(data, trx, cookie); + return response; } catch (e) { - console.log(e) - await this.errorDelete(e, id, trx, cookie) - throw e + console.log(e); + await this.errorDelete(e, id, trx, cookie); + throw e; } } async hasLTARData(rowId, model: Model): Promise { - const res = [] + const res = []; const LTARColumns = (await model.getColumns()).filter( (c) => c.uidt === UITypes.LinkToAnotherRecord, - ) - let i = 0 + ); + let i = 0; for (const column of LTARColumns) { const colOptions = - (await column.getColOptions()) as LinkToAnotherRecordColumn - const childColumn = await colOptions.getChildColumn() - const parentColumn = await colOptions.getParentColumn() - const childModel = await childColumn.getModel() - await childModel.getColumns() - const parentModel = await parentColumn.getModel() - await parentModel.getColumns() - let cnt = 0 + (await column.getColOptions()) as LinkToAnotherRecordColumn; + const childColumn = await colOptions.getChildColumn(); + const parentColumn = await colOptions.getParentColumn(); + const childModel = await childColumn.getModel(); + await childModel.getColumns(); + const parentModel = await parentColumn.getModel(); + await parentModel.getColumns(); + let cnt = 0; if (colOptions.type === RelationTypes.HAS_MANY) { cnt = +( await this.dbDriver(childModel.table_name) .count(childColumn.column_name, { as: 'cnt' }) .where(childColumn.column_name, rowId) - )[0].cnt + )[0].cnt; } else if (colOptions.type === RelationTypes.MANY_TO_MANY) { - const mmModel = await colOptions.getMMModel() - const mmChildColumn = await colOptions.getMMChildColumn() + const mmModel = await colOptions.getMMModel(); + const mmChildColumn = await colOptions.getMMChildColumn(); cnt = +( await this.dbDriver(mmModel.table_name) .where(`${mmModel.table_name}.${mmChildColumn.column_name}`, rowId) .count(mmChildColumn.column_name, { as: 'cnt' }) - )[0].cnt + )[0].cnt; } if (cnt) { res.push( `${i++ + 1}. ${model.title}.${ column.title } is a LinkToAnotherRecord of ${childModel.title}`, - ) + ); } } - return res + return res; } async updateByPk(id, data, trx?, cookie?) { try { - const updateObj = await this.model.mapAliasToColumn(data) + const updateObj = await this.model.mapAliasToColumn(data); - await this.validate(data) + await this.validate(data); - await this.beforeUpdate(data, trx, cookie) + await this.beforeUpdate(data, trx, cookie); - const prevData = await this.readByPk(id) + const prevData = await this.readByPk(id); const query = this.dbDriver(this.tnPath) .update(updateObj) - .where(await this._wherePk(id)) + .where(await this._wherePk(id)); - await this.execAndParse(query) + await this.execAndParse(query); - const newData = await this.readByPk(id) - await this.afterUpdate(prevData, newData, trx, cookie, updateObj) - return newData + const newData = await this.readByPk(id); + await this.afterUpdate(prevData, newData, trx, cookie, updateObj); + return newData; } catch (e) { - console.log(e) - await this.errorUpdate(e, data, trx, cookie) - throw e + console.log(e); + await this.errorUpdate(e, data, trx, cookie); + throw e; } } async _wherePk(id) { - await this.model.getColumns() - return _wherePk(this.model.primaryKeys, id) + await this.model.getColumns(); + return _wherePk(this.model.primaryKeys, id); } private getTnPath(tb: Model) { - const schema = (this.dbDriver as any).searchPath?.() + const schema = (this.dbDriver as any).searchPath?.(); if (this.isMssql && schema) { - return this.dbDriver.raw('??.??', [schema, tb.table_name]) + return this.dbDriver.raw('??.??', [schema, tb.table_name]); } else if (this.isSnowflake) { return [ this.dbDriver.client.config.connection.database, this.dbDriver.client.config.connection.schema, tb.table_name, - ].join('.') + ].join('.'); } else { - return tb.table_name + return tb.table_name; } } public get tnPath() { - return this.getTnPath(this.model) + return this.getTnPath(this.model); } get isSqlite() { - return this.clientType === 'sqlite3' + return this.clientType === 'sqlite3'; } get isMssql() { - return this.clientType === 'mssql' + return this.clientType === 'mssql'; } get isPg() { - return this.clientType === 'pg' + return this.clientType === 'pg'; } get isMySQL() { - return this.clientType === 'mysql2' || this.clientType === 'mysql' + return this.clientType === 'mysql2' || this.clientType === 'mysql'; } get isSnowflake() { - return this.clientType === 'snowflake' + return this.clientType === 'snowflake'; } get clientType() { - return this.dbDriver.clientType() + return this.dbDriver.clientType(); } async nestedInsert(data, _trx = null, cookie?) { // const driver = trx ? trx : await this.dbDriver.transaction(); try { - await populatePk(this.model, data) - const insertObj = await this.model.mapAliasToColumn(data) + await populatePk(this.model, data); + const insertObj = await this.model.mapAliasToColumn(data); - let rowId = null - const postInsertOps = [] + let rowId = null; + const postInsertOps = []; const nestedCols = (await this.model.getColumns()).filter( (c) => c.uidt === UITypes.LinkToAnotherRecord, - ) + ); for (const col of nestedCols) { if (col.title in data) { const colOptions = - await col.getColOptions() + await col.getColOptions(); // parse data if it's JSON string const nestedData = typeof data[col.title] === 'string' ? JSON.parse(data[col.title]) - : data[col.title] + : data[col.title]; switch (colOptions.type) { - case RelationTypes.BELONGS_TO: { - const childCol = await colOptions.getChildColumn() - const parentCol = await colOptions.getParentColumn() - insertObj[childCol.column_name] = nestedData?.[parentCol.title] - } - break - case RelationTypes.HAS_MANY: { - const childCol = await colOptions.getChildColumn() - const childModel = await childCol.getModel() - await childModel.getColumns() - - postInsertOps.push(async () => { - await this.dbDriver(childModel.table_name) - .update({ - [childCol.column_name]: rowId, - }) - .whereIn( - childModel.primaryKey.column_name, - nestedData?.map((r) => r[childModel.primaryKey.title]), - ) - }) - } - break + case RelationTypes.BELONGS_TO: + { + const childCol = await colOptions.getChildColumn(); + const parentCol = await colOptions.getParentColumn(); + insertObj[childCol.column_name] = nestedData?.[parentCol.title]; + } + break; + case RelationTypes.HAS_MANY: + { + const childCol = await colOptions.getChildColumn(); + const childModel = await childCol.getModel(); + await childModel.getColumns(); + + postInsertOps.push(async () => { + await this.dbDriver(childModel.table_name) + .update({ + [childCol.column_name]: rowId, + }) + .whereIn( + childModel.primaryKey.column_name, + nestedData?.map((r) => r[childModel.primaryKey.title]), + ); + }); + } + break; case RelationTypes.MANY_TO_MANY: { postInsertOps.push(async () => { const parentModel = await colOptions .getParentColumn() - .then((c) => c.getModel()) - await parentModel.getColumns() - const parentMMCol = await colOptions.getMMParentColumn() - const childMMCol = await colOptions.getMMChildColumn() - const mmModel = await colOptions.getMMModel() + .then((c) => c.getModel()); + await parentModel.getColumns(); + const parentMMCol = await colOptions.getMMParentColumn(); + const childMMCol = await colOptions.getMMChildColumn(); + const mmModel = await colOptions.getMMModel(); const rows = nestedData.map((r) => ({ [parentMMCol.column_name]: r[parentModel.primaryKey.title], [childMMCol.column_name]: rowId, - })) - await this.dbDriver(mmModel.table_name).insert(rows) - }) + })); + await this.dbDriver(mmModel.table_name).insert(rows); + }); } } } } - await this.validate(insertObj) + await this.validate(insertObj); - await this.beforeInsert(insertObj, this.dbDriver, cookie) + await this.beforeInsert(insertObj, this.dbDriver, cookie); - let response - const query = this.dbDriver(this.tnPath).insert(insertObj) + let response; + const query = this.dbDriver(this.tnPath).insert(insertObj); if (this.isPg || this.isMssql) { query.returning( `${this.model.primaryKey.column_name} as ${this.model.primaryKey.title}`, - ) - response = await query + ); + response = await query; } - const ai = this.model.columns.find((c) => c.ai) + const ai = this.model.columns.find((c) => c.ai); if ( !response || (typeof response?.[0] !== 'object' && response?.[0] !== null) ) { - let id + let id; if (response?.length) { - id = response[0] + id = response[0]; } else { - id = (await query)[0] + id = (await query)[0]; } if (ai) { @@ -2042,39 +2048,39 @@ class BaseModelSqlv2 { await this.dbDriver(this.tnPath) .select(ai.column_name) .max(ai.column_name, { as: 'id' }) - )[0].id + )[0].id; } else if (this.isSnowflake) { id = ( (await this.dbDriver(this.tnPath).max(ai.column_name, { as: 'id', })) as any - ).rows[0].id + ).rows[0].id; } - response = await this.readByPk(id) + response = await this.readByPk(id); } else { - response = data + response = data; } } else if (ai) { response = await this.readByPk( Array.isArray(response) ? response?.[0]?.[ai.title] : response?.[ai.title], - ) + ); } - response = Array.isArray(response) ? response[0] : response + response = Array.isArray(response) ? response[0] : response; if (response) rowId = response[this.model.primaryKey.title] || - response[this.model.primaryKey.column_name] + response[this.model.primaryKey.column_name]; - await Promise.all(postInsertOps.map((f) => f())) + await Promise.all(postInsertOps.map((f) => f())); - await this.afterInsert(response, this.dbDriver, cookie) + await this.afterInsert(response, this.dbDriver, cookie); - return response + return response; } catch (e) { - console.log(e) - throw e + console.log(e); + throw e; } } @@ -2092,78 +2098,78 @@ class BaseModelSqlv2 { raw?: boolean; } = {}, ) { - let trx + let trx; try { // TODO: ag column handling for raw bulk insert const insertDatas = raw ? datas : await Promise.all( - datas.map(async (d) => { - await populatePk(this.model, d) - return this.model.mapAliasToColumn(d) - }), - ) + datas.map(async (d) => { + await populatePk(this.model, d); + return this.model.mapAliasToColumn(d); + }), + ); // await this.beforeInsertb(insertDatas, null); if (!raw) { for (const data of datas) { - await this.validate(data) + await this.validate(data); } } // fallbacks to `10` if database client is sqlite // to avoid `too many SQL variables` error // refer : https://www.sqlite.org/limits.html - const chunkSize = this.isSqlite ? 10 : _chunkSize + const chunkSize = this.isSqlite ? 10 : _chunkSize; - trx = await this.dbDriver.transaction() + trx = await this.dbDriver.transaction(); if (!foreign_key_checks) { if (this.isPg) { - await trx.raw('set session_replication_role to replica;') + await trx.raw('set session_replication_role to replica;'); } else if (this.isMySQL) { - await trx.raw('SET foreign_key_checks = 0;') + await trx.raw('SET foreign_key_checks = 0;'); } } - let response + let response; if (this.isSqlite) { // sqlite doesnt support returning, so insert one by one and return ids - response = [] + response = []; for (const insertData of insertDatas) { - const query = trx(this.tnPath).insert(insertData) - const id = (await query)[0] - response.push(await this.readByPk(id)) + const query = trx(this.tnPath).insert(insertData); + const id = (await query)[0]; + response.push(await this.readByPk(id)); } } else { response = this.isPg || this.isMssql ? await trx - .batchInsert(this.tnPath, insertDatas, chunkSize) - .returning(this.model.primaryKey?.column_name) - : await trx.batchInsert(this.tnPath, insertDatas, chunkSize) + .batchInsert(this.tnPath, insertDatas, chunkSize) + .returning(this.model.primaryKey?.column_name) + : await trx.batchInsert(this.tnPath, insertDatas, chunkSize); } if (!foreign_key_checks) { if (this.isPg) { - await trx.raw('set session_replication_role to origin;') + await trx.raw('set session_replication_role to origin;'); } else if (this.isMySQL) { - await trx.raw('SET foreign_key_checks = 1;') + await trx.raw('SET foreign_key_checks = 1;'); } } - await trx.commit() + await trx.commit(); - if (!raw) await this.afterBulkInsert(insertDatas, this.dbDriver, cookie) + if (!raw) await this.afterBulkInsert(insertDatas, this.dbDriver, cookie); - return response + return response; } catch (e) { - await trx?.rollback() + await trx?.rollback(); // await this.errorInsertb(e, data, null); - throw e + throw e; } } @@ -2171,54 +2177,54 @@ class BaseModelSqlv2 { datas: any[], { cookie, raw = false }: { cookie?: any; raw?: boolean } = {}, ) { - let transaction + let transaction; try { - if (raw) await this.model.getColumns() + if (raw) await this.model.getColumns(); const updateDatas = raw ? datas - : await Promise.all(datas.map((d) => this.model.mapAliasToColumn(d))) + : await Promise.all(datas.map((d) => this.model.mapAliasToColumn(d))); - const prevData = [] - const newData = [] - const updatePkValues = [] - const toBeUpdated = [] - const res = [] + const prevData = []; + const newData = []; + const updatePkValues = []; + const toBeUpdated = []; + const res = []; for (const d of updateDatas) { - if (!raw) await this.validate(d) - const pkValues = await this._extractPksValues(d) + if (!raw) await this.validate(d); + const pkValues = await this._extractPksValues(d); if (!pkValues) { // pk not specified - bypass - continue + continue; } - if (!raw) prevData.push(await this.readByPk(pkValues)) - const wherePk = await this._wherePk(pkValues) - res.push(wherePk) - toBeUpdated.push({ d, wherePk }) - updatePkValues.push(pkValues) + if (!raw) prevData.push(await this.readByPk(pkValues)); + const wherePk = await this._wherePk(pkValues); + res.push(wherePk); + toBeUpdated.push({ d, wherePk }); + updatePkValues.push(pkValues); } - transaction = await this.dbDriver.transaction() + transaction = await this.dbDriver.transaction(); for (const o of toBeUpdated) { - await transaction(this.tnPath).update(o.d).where(o.wherePk) + await transaction(this.tnPath).update(o.d).where(o.wherePk); } - await transaction.commit() + await transaction.commit(); if (!raw) { for (const pkValues of updatePkValues) { - newData.push(await this.readByPk(pkValues)) + newData.push(await this.readByPk(pkValues)); } } if (!raw) - await this.afterBulkUpdate(prevData, newData, this.dbDriver, cookie) + await this.afterBulkUpdate(prevData, newData, this.dbDriver, cookie); - return res + return res; } catch (e) { - if (transaction) await transaction.rollback() - throw e + if (transaction) await transaction.rollback(); + throw e; } } @@ -2228,18 +2234,18 @@ class BaseModelSqlv2 { { cookie }: { cookie?: any } = {}, ) { try { - let count = 0 - const updateData = await this.model.mapAliasToColumn(data) - await this.validate(updateData) - const pkValues = await this._extractPksValues(updateData) + let count = 0; + const updateData = await this.model.mapAliasToColumn(data); + await this.validate(updateData); + const pkValues = await this._extractPksValues(updateData); if (pkValues) { // pk is specified - by pass } else { - await this.model.getColumns() - const { where } = this._getListArgs(args) - const qb = this.dbDriver(this.tnPath) - const aliasColObjMap = await this.model.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + await this.model.getColumns(); + const { where } = this._getListArgs(args); + const qb = this.dbDriver(this.tnPath); + const aliasColObjMap = await this.model.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); await conditionV2( [ @@ -2256,55 +2262,55 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); - qb.update(updateData) + qb.update(updateData); - count = (await qb) as any + count = (await qb) as any; } - await this.afterBulkUpdate(null, count, this.dbDriver, cookie, true) + await this.afterBulkUpdate(null, count, this.dbDriver, cookie, true); - return count + return count; } catch (e) { - throw e + throw e; } } async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) { - let transaction + let transaction; try { const deleteIds = await Promise.all( ids.map((d) => this.model.mapAliasToColumn(d)), - ) + ); - const deleted = [] - const res = [] + const deleted = []; + const res = []; for (const d of deleteIds) { - const pkValues = await this._extractPksValues(d) + const pkValues = await this._extractPksValues(d); if (!pkValues) { // pk not specified - bypass - continue + continue; } - deleted.push(await this.readByPk(pkValues)) - res.push(d) + deleted.push(await this.readByPk(pkValues)); + res.push(d); } - transaction = await this.dbDriver.transaction() + transaction = await this.dbDriver.transaction(); for (const d of res) { - await transaction(this.tnPath).del().where(d) + await transaction(this.tnPath).del().where(d); } - await transaction.commit() + await transaction.commit(); - await this.afterBulkDelete(deleted, this.dbDriver, cookie) + await this.afterBulkDelete(deleted, this.dbDriver, cookie); - return res + return res; } catch (e) { - if (transaction) await transaction.rollback() - console.log(e) - throw e + if (transaction) await transaction.rollback(); + console.log(e); + throw e; } } @@ -2313,11 +2319,11 @@ class BaseModelSqlv2 { { cookie }: { cookie?: any } = {}, ) { try { - await this.model.getColumns() - const { where } = this._getListArgs(args) - const qb = this.dbDriver(this.tnPath) - const aliasColObjMap = await this.model.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + await this.model.getColumns(); + const { where } = this._getListArgs(args); + const qb = this.dbDriver(this.tnPath); + const aliasColObjMap = await this.model.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); await conditionV2( [ @@ -2334,17 +2340,17 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); - qb.del() + qb.del(); - const count = (await qb) as any + const count = (await qb) as any; - await this.afterBulkDelete(count, this.dbDriver, cookie, true) + await this.afterBulkDelete(count, this.dbDriver, cookie, true); - return count + return count; } catch (e) { - throw e + throw e; } } @@ -2353,12 +2359,12 @@ class BaseModelSqlv2 { * */ public async beforeInsert(data: any, _trx: any, req): Promise { - await this.handleHooks('before.insert', null, data, req) + await this.handleHooks('before.insert', null, data, req); } public async afterInsert(data: any, _trx: any, req): Promise { - await this.handleHooks('after.insert', null, data, req) - const id = this._extractPksValues(data) + await this.handleHooks('after.insert', null, data, req); + const id = this._extractPksValues(data); await Audit.insert({ fk_model_id: this.model.id, row_id: id, @@ -2370,7 +2376,7 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }) + }); } public async afterBulkUpdate( @@ -2380,10 +2386,10 @@ class BaseModelSqlv2 { req, isBulkAllOperation = false, ): Promise { - let noOfUpdatedRecords = newData + let noOfUpdatedRecords = newData; if (!isBulkAllOperation) { - noOfUpdatedRecords = newData.length - await this.handleHooks('after.bulkUpdate', prevData, newData, req) + noOfUpdatedRecords = newData.length; + await this.handleHooks('after.bulkUpdate', prevData, newData, req); } await Audit.insert({ @@ -2398,7 +2404,7 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }) + }); } public async afterBulkDelete( @@ -2407,10 +2413,10 @@ class BaseModelSqlv2 { req, isBulkAllOperation = false, ): Promise { - let noOfDeletedRecords = data + let noOfDeletedRecords = data; if (!isBulkAllOperation) { - noOfDeletedRecords = data.length - await this.handleHooks('after.bulkDelete', null, data, req) + noOfDeletedRecords = data.length; + await this.handleHooks('after.bulkDelete', null, data, req); } await Audit.insert({ @@ -2425,11 +2431,11 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }) + }); } public async afterBulkInsert(data: any[], _trx: any, req): Promise { - await this.handleHooks('after.bulkInsert', null, data, req) + await this.handleHooks('after.bulkInsert', null, data, req); await Audit.insert({ fk_model_id: this.model.id, @@ -2443,18 +2449,18 @@ class BaseModelSqlv2 { // 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 + const ignoreWebhook = req.query?.ignoreWebhook; if (ignoreWebhook) { if (ignoreWebhook != 'true' && ignoreWebhook != 'false') { - throw new Error('ignoreWebhook value can be either true or false') + throw new Error('ignoreWebhook value can be either true or false'); } } if (ignoreWebhook === undefined || ignoreWebhook === 'false') { - await this.handleHooks('before.update', null, data, req) + await this.handleHooks('before.update', null, data, req); } } @@ -2465,25 +2471,25 @@ class BaseModelSqlv2 { req, updateObj?: Record, ): Promise { - const id = this._extractPksValues(newData) - let desc = `Record with ID ${id} has been updated in Table ${this.model.title}.` - let details = '' + const id = this._extractPksValues(newData); + let desc = `Record with ID ${id} has been updated in Table ${this.model.title}.`; + let details = ''; if (updateObj) { - updateObj = await this.model.mapColumnToAlias(updateObj) + updateObj = await this.model.mapColumnToAlias(updateObj); for (const k of Object.keys(updateObj)) { const prevValue = typeof prevData[k] === 'object' ? JSON.stringify(prevData[k]) - : prevData[k] + : prevData[k]; const newValue = typeof newData[k] === 'object' ? JSON.stringify(newData[k]) - : newData[k] - desc += `\n` - desc += `Column "${k}" got changed from "${prevValue}" to "${newValue}"` + : newData[k]; + desc += `\n`; + desc += `Column "${k}" got changed from "${prevValue}" to "${newValue}"`; details += DOMPurify.sanitize(`${k} : ${prevValue} - ${newValue}`) + ${newValue}`); } } await Audit.insert({ @@ -2495,25 +2501,25 @@ class BaseModelSqlv2 { details, ip: req?.clientIp, user: req?.user?.email, - }) + }); - const ignoreWebhook = req.query?.ignoreWebhook + const ignoreWebhook = req.query?.ignoreWebhook; if (ignoreWebhook) { if (ignoreWebhook != 'true' && ignoreWebhook != 'false') { - throw new Error('ignoreWebhook value can be either true or false') + throw new Error('ignoreWebhook value can be either true or false'); } } if (ignoreWebhook === undefined || ignoreWebhook === 'false') { - await this.handleHooks('after.update', prevData, newData, req) + await this.handleHooks('after.update', prevData, newData, req); } } public async beforeDelete(data: any, _trx: any, req): Promise { - await this.handleHooks('before.delete', null, data, req) + await this.handleHooks('before.delete', null, data, req); } public async afterDelete(data: any, _trx: any, req): Promise { - const id = req?.params?.id + const id = req?.params?.id; await Audit.insert({ fk_model_id: this.model.id, row_id: id, @@ -2525,8 +2531,8 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }) - await this.handleHooks('after.delete', null, data, req) + }); + await this.handleHooks('after.delete', null, data, req); } private async handleHooks(hookName, prevData, newData, req): Promise { @@ -2538,7 +2544,7 @@ class BaseModelSqlv2 { viewId: this.viewId, modelId: this.model.id, tnPath: this.tnPath, - }) + }); /* const view = await View.get(this.viewId); @@ -2634,52 +2640,49 @@ class BaseModelSqlv2 { } // @ts-ignore - protected async errorInsert(e, data, trx, cookie) { - } + protected async errorInsert(e, data, trx, cookie) {} // @ts-ignore - protected async errorUpdate(e, data, trx, cookie) { - } + protected async errorUpdate(e, data, trx, cookie) {} // todo: handle composite primary key protected _extractPksValues(data: any) { // data can be still inserted without PK // TODO: return a meaningful value - if (!this.model.primaryKey) return 'N/A' + if (!this.model.primaryKey) return 'N/A'; return ( data[this.model.primaryKey.title] || data[this.model.primaryKey.column_name] - ) + ); } // @ts-ignore - protected async errorDelete(e, id, trx, cookie) { - } + protected async errorDelete(e, id, trx, cookie) {} async validate(columns) { - await this.model.getColumns() + await this.model.getColumns(); // let cols = Object.keys(this.columns); for (let i = 0; i < this.model.columns.length; ++i) { - const column = this.model.columns[i] + const column = this.model.columns[i]; // skip validation if `validate` is undefined or false - if (!column?.meta?.validate || !column?.validate) continue + if (!column?.meta?.validate || !column?.validate) continue; - const validate = column.getValidators() - const cn = column.column_name - const columnTitle = column.title - if (!validate) continue + const validate = column.getValidators(); + const cn = column.column_name; + const columnTitle = column.title; + if (!validate) continue; - const { func, msg } = validate + const { func, msg } = validate; for (let j = 0; j < func.length; ++j) { const fn = typeof func[j] === 'string' ? customValidators[func[j]] ? customValidators[func[j]] : Validator[func[j]] - : func[j] - const columnValue = columns?.[cn] || columns?.[columnTitle] + : func[j]; + const columnValue = columns?.[cn] || columns?.[columnTitle]; const arg = - typeof func[j] === 'string' ? columnValue + '' : columnValue + typeof func[j] === 'string' ? columnValue + '' : columnValue; if ( ![null, undefined, ''].includes(columnValue) && !(fn.constructor.name === 'AsyncFunction' ? await fn(arg) : fn(arg)) @@ -2688,112 +2691,115 @@ class BaseModelSqlv2 { msg[j] .replace(/\{VALUE}/g, columnValue) .replace(/\{cn}/g, columnTitle), - ) + ); } } } - return true + return true; } async addChild({ - colId, - rowId, - childId, - cookie, - }: { + 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) + 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') + NcError.notFound('Column not found'); - const colOptions = await column.getColOptions() + 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 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 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() + case RelationTypes.MANY_TO_MANY: + { + const vChildCol = await colOptions.getMMChildColumn(); + const vParentCol = await colOptions.getMMParentColumn(); + const vTable = await colOptions.getMMModel(); - const childPK = this.dbDriver(childTn) - .select(childColumn.column_name) - .where(_wherePk(childTable.primaryKeys, rowId)) - .first() + const vTn = this.getTnPath(vTable); - 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) + if (this.isSnowflake) { + const parentPK = this.dbDriver(parentTn) .select(parentColumn.column_name) .where(_wherePk(parentTable.primaryKeys, childId)) - .first(), - [vChildCol.column_name]: this.dbDriver(childTn) + .first(); + + const childPK = 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) + .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() - .as('___cn_alias'), - ), - }) - .where(_wherePk(childTable.primaryKeys, rowId)) - } - break + .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) + const response = await this.readByPk(rowId); + await this.afterInsert(response, this.dbDriver, cookie); + await this.afterAddChild(rowId, childId, cookie); } public async afterAddChild(rowId, childId, req): Promise { @@ -2808,91 +2814,94 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }) + }); } async removeChild({ - colId, - rowId, - childId, - cookie, - }: { + 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) + 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') + NcError.notFound('Column not found'); - const colOptions = await column.getColOptions() + 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 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 childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); - const prevData = await this.readByPk(rowId) + 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() + 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) + 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 + 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) + const newData = await this.readByPk(rowId); + await this.afterUpdate(prevData, newData, this.dbDriver, cookie); + await this.afterRemoveChild(rowId, childId, cookie); } public async afterRemoveChild(rowId, childId, req): Promise { @@ -2907,7 +2916,7 @@ class BaseModelSqlv2 { // details: JSON.stringify(data), ip: req?.clientIp, user: req?.user?.email, - }) + }); } public async groupedList( @@ -2916,32 +2925,34 @@ class BaseModelSqlv2 { ignoreViewFilterAndSort?: boolean; options?: (string | number | null | boolean)[]; } & Partial, - ): Promise<{ - key: string; - value: Record[]; - }[]> { + ): Promise< + { + key: string; + value: Record[]; + }[] + > { try { - const { where, ...rest } = this._getListArgs(args as any) + const { where, ...rest } = this._getListArgs(args as any); const column = await this.model .getColumns() - .then((cols) => cols?.find((col) => col.id === args.groupColumnId)) + .then((cols) => cols?.find((col) => col.id === args.groupColumnId)); - if (!column) NcError.notFound('Column not found') + if (!column) NcError.notFound('Column not found'); if (isVirtualCol(column)) - NcError.notImplemented('Grouping for virtual columns not implemented') + NcError.notImplemented('Grouping for virtual columns not implemented'); // extract distinct group column values - let groupingValues: Set + let groupingValues: Set; if (args.options?.length) { - groupingValues = new Set(args.options) + groupingValues = new Set(args.options); } else if (column.uidt === UITypes.SingleSelect) { const colOptions = await column.getColOptions<{ options: SelectOption[]; - }>() + }>(); groupingValues = new Set( (colOptions?.options ?? []).map((opt) => opt.title), - ) - groupingValues.add(null) + ); + groupingValues.add(null); } else { groupingValues = new Set( ( @@ -2949,20 +2960,20 @@ class BaseModelSqlv2 { .select(column.column_name) .distinct() ).map((row) => row[column.column_name]), - ) - groupingValues.add(null) + ); + groupingValues.add(null); } - const qb = this.dbDriver(this.tnPath) - qb.limit(+rest?.limit || 25) - qb.offset(+rest?.offset || 0) + const qb = this.dbDriver(this.tnPath); + qb.limit(+rest?.limit || 25); + qb.offset(+rest?.offset || 0); - await this.selectObject({ qb, extractPkAndPv: true }) + await this.selectObject({ qb, extractPkAndPv: true }); // todo: refactor and move to a method (applyFilterAndSort) - const aliasColObjMap = await this.model.getAliasColObjMap() - let sorts = extractSortsObject(args?.sort, aliasColObjMap) - const filterObj = extractFilterFromXwhere(where, aliasColObjMap) + const aliasColObjMap = await this.model.getAliasColObjMap(); + let sorts = extractSortsObject(args?.sort, aliasColObjMap); + const filterObj = extractFilterFromXwhere(where, aliasColObjMap); // todo: replace with view id if (!args.ignoreViewFilterAndSort && this.viewId) { await conditionV2( @@ -2985,14 +2996,14 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); if (!sorts) sorts = args.sortArr?.length ? args.sortArr - : await Sort.list({ viewId: this.viewId }) + : await Sort.list({ viewId: this.viewId }); - if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver) + if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver); } else { await conditionV2( [ @@ -3009,71 +3020,71 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); - if (!sorts) sorts = args.sortArr + if (!sorts) sorts = args.sortArr; - if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver) + if (sorts?.['length']) await sortV2(sorts, qb, this.dbDriver); } // sort by primary key if not autogenerated string // if autogenerated string sort by created_at column if present if (this.model.primaryKey && this.model.primaryKey.ai) { - qb.orderBy(this.model.primaryKey.column_name) + qb.orderBy(this.model.primaryKey.column_name); } else if ( this.model.columns.find((c) => c.column_name === 'created_at') ) { - qb.orderBy('created_at') + qb.orderBy('created_at'); } const groupedQb = this.dbDriver.from( this.dbDriver .unionAll( [...groupingValues].map((r) => { - const query = qb.clone() + const query = qb.clone(); if (r === null) { - query.whereNull(column.column_name) + query.whereNull(column.column_name); } else { - query.where(column.column_name, r) + query.where(column.column_name, r); } - return this.isSqlite ? this.dbDriver.select().from(query) : query + return this.isSqlite ? this.dbDriver.select().from(query) : query; }), !this.isSqlite, ) .as('__nc_grouped_list'), - ) + ); - const proto = await this.getProto() + const proto = await this.getProto(); - const data = await groupedQb + const data = await groupedQb; const result = data?.map((d) => { - d.__proto__ = proto - return d - }) + d.__proto__ = proto; + return d; + }); const groupedResult = result.reduce>( (aggObj, row) => { if (!aggObj.has(row[column.title])) { - aggObj.set(row[column.title], []) + aggObj.set(row[column.title], []); } - aggObj.get(row[column.title]).push(row) + aggObj.get(row[column.title]).push(row); - return aggObj + return aggObj; }, new Map(), - ) + ); const r = [...groupingValues].map((key) => ({ key, value: groupedResult.get(key) ?? [], - })) + })); - return r + return r; } catch (e) { - console.log(e) - throw e + console.log(e); + throw e; } } @@ -3085,19 +3096,19 @@ class BaseModelSqlv2 { ) { const column = await this.model .getColumns() - .then((cols) => cols?.find((col) => col.id === args.groupColumnId)) + .then((cols) => cols?.find((col) => col.id === args.groupColumnId)); - if (!column) NcError.notFound('Column not found') + if (!column) NcError.notFound('Column not found'); if (isVirtualCol(column)) - NcError.notImplemented('Grouping for virtual columns not implemented') + NcError.notImplemented('Grouping for virtual columns not implemented'); const qb = this.dbDriver(this.tnPath) .count('*', { as: 'count' }) - .groupBy(column.column_name) + .groupBy(column.column_name); // todo: refactor and move to a common method (applyFilterAndSort) - const aliasColObjMap = await this.model.getAliasColObjMap() - const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap) + const aliasColObjMap = await this.model.getAliasColObjMap(); + const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap); // todo: replace with view id if (!args.ignoreViewFilterAndSort && this.viewId) { @@ -3121,7 +3132,7 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); } else { await conditionV2( [ @@ -3138,34 +3149,34 @@ class BaseModelSqlv2 { ], qb, this.dbDriver, - ) + ); } await this.selectObject({ qb, columns: [new Column({ ...column, title: 'key' })], - }) + }); - return await qb + return await qb; } private async execAndParse(qb: Knex.QueryBuilder, childTable?: Model) { - let query = qb.toQuery() + let query = qb.toQuery(); if (!this.isPg && !this.isMssql && !this.isSnowflake) { - query = unsanitize(qb.toQuery()) + query = unsanitize(qb.toQuery()); } else { - query = sanitize(query) + query = sanitize(query); } return this.convertAttachmentType( this.isPg || this.isSnowflake ? (await this.dbDriver.raw(query))?.rows : query.slice(0, 6) === 'select' && !this.isMssql - ? await this.dbDriver.from( + ? await this.dbDriver.from( this.dbDriver.raw(query).wrap('(', ') __nc_alias'), ) - : await this.dbDriver.raw(query), + : await this.dbDriver.raw(query), childTable, - ) + ); } private _convertAttachmentType( @@ -3176,13 +3187,12 @@ class BaseModelSqlv2 { if (d) { attachmentColumns.forEach((col) => { if (d[col.title] && typeof d[col.title] === 'string') { - d[col.title] = JSON.parse(d[col.title]) + d[col.title] = JSON.parse(d[col.title]); } - }) + }); } - } catch { - } - return d + } catch {} + return d; } private convertAttachmentType(data: Record, childTable?: Model) { @@ -3191,18 +3201,18 @@ class BaseModelSqlv2 { if (data) { const attachmentColumns = ( childTable ? childTable.columns : this.model.columns - ).filter((c) => c.uidt === UITypes.Attachment) + ).filter((c) => c.uidt === UITypes.Attachment); if (attachmentColumns.length) { if (Array.isArray(data)) { data = data.map((d) => this._convertAttachmentType(attachmentColumns, d), - ) + ); } else { - this._convertAttachmentType(attachmentColumns, data) + this._convertAttachmentType(attachmentColumns, data); } } } - return data + return data; } } @@ -3210,21 +3220,21 @@ function extractSortsObject( _sorts: string | string[], aliasColObjMap: { [columnAlias: string]: Column }, ): Sort[] | void { - if (!_sorts?.length) return + if (!_sorts?.length) return; - let sorts = _sorts + let sorts = _sorts; - if (!Array.isArray(sorts)) sorts = sorts.split(',') + if (!Array.isArray(sorts)) sorts = sorts.split(','); return sorts.map((s) => { - const sort: SortType = { direction: 'asc' } + const sort: SortType = { direction: 'asc' }; if (s.startsWith('-')) { - sort.direction = 'desc' - sort.fk_column_id = aliasColObjMap[s.slice(1)]?.id - } else sort.fk_column_id = aliasColObjMap[s]?.id + sort.direction = 'desc'; + sort.fk_column_id = aliasColObjMap[s.slice(1)]?.id; + } else sort.fk_column_id = aliasColObjMap[s]?.id; - return new Sort(sort) - }) + return new Sort(sort); + }); } function extractFilterFromXwhere( @@ -3232,26 +3242,26 @@ function extractFilterFromXwhere( aliasColObjMap: { [columnAlias: string]: Column }, ) { if (!str) { - return [] + return []; } - let nestedArrayConditions = [] + let nestedArrayConditions = []; - let openIndex = str.indexOf('((') + let openIndex = str.indexOf('(('); - if (openIndex === -1) openIndex = str.indexOf('(~') + if (openIndex === -1) openIndex = str.indexOf('(~'); - let nextOpenIndex = openIndex + let nextOpenIndex = openIndex; - let closingIndex = str.indexOf('))') + let closingIndex = str.indexOf('))'); // if it's a simple query simply return array of conditions if (openIndex === -1) { if (str && str != '~not') nestedArrayConditions = str.split( /(?=~(?:or(?:not)?|and(?:not)?|not)\()/, - ) - return extractCondition(nestedArrayConditions || [], aliasColObjMap) + ); + return extractCondition(nestedArrayConditions || [], aliasColObjMap); } // iterate until finding right closing @@ -3259,8 +3269,8 @@ function extractFilterFromXwhere( (nextOpenIndex = str .substring(0, closingIndex) .indexOf('((', nextOpenIndex + 1)) != -1 - ) { - closingIndex = str.indexOf('))', closingIndex + 1) + ) { + closingIndex = str.indexOf('))', closingIndex + 1); } if (closingIndex === -1) @@ -3268,15 +3278,15 @@ function extractFilterFromXwhere( `${str .substring(0, openIndex + 1) .slice(-10)} : Closing bracket not found`, - ) + ); // getting operand starting index - const operandStartIndex = str.lastIndexOf('~', openIndex) + const operandStartIndex = str.lastIndexOf('~', openIndex); const operator = operandStartIndex != -1 ? str.substring(operandStartIndex + 1, openIndex) - : '' - const lhsOfNestedQuery = str.substring(0, openIndex) + : ''; + const lhsOfNestedQuery = str.substring(0, openIndex); nestedArrayConditions.push( ...extractFilterFromXwhere(lhsOfNestedQuery, aliasColObjMap), @@ -3291,28 +3301,28 @@ function extractFilterFromXwhere( }), // RHS of nested query(recursion) ...extractFilterFromXwhere(str.substring(closingIndex + 2), aliasColObjMap), - ) - return nestedArrayConditions + ); + return nestedArrayConditions; } // mark `op` and `sub_op` any for being assignable to parameter of type function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) { if (!COMPARISON_OPS.includes(op)) { - NcError.badRequest(`${op} is not supported.`) + NcError.badRequest(`${op} is not supported.`); } if (sub_op) { if (![UITypes.Date, UITypes.DateTime].includes(uidt)) { - NcError.badRequest(`'${sub_op}' is not supported for UI Type'${uidt}'.`) + NcError.badRequest(`'${sub_op}' is not supported for UI Type'${uidt}'.`); } if (!COMPARISON_SUB_OPS.includes(sub_op)) { - NcError.badRequest(`'${sub_op}' is not supported.`) + NcError.badRequest(`'${sub_op}' is not supported.`); } if ( (op === 'isWithin' && !IS_WITHIN_COMPARISON_SUB_OPS.includes(sub_op)) || (op !== 'isWithin' && IS_WITHIN_COMPARISON_SUB_OPS.includes(sub_op)) ) { - NcError.badRequest(`'${sub_op}' is not supported for '${op}'`) + NcError.badRequest(`'${sub_op}' is not supported for '${op}'`); } } } @@ -3320,30 +3330,30 @@ function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) { function extractCondition(nestedArrayConditions, aliasColObjMap) { return nestedArrayConditions?.map((str) => { let [logicOp, alias, op, value] = - str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || [] + str.match(/(?:~(and|or|not))?\((.*?),(\w+),(.*)\)/)?.slice(1) || []; if (!alias && !op && !value) { // try match with blank filter format [logicOp, alias, op, value] = - str.match(/(?:~(and|or|not))?\((.*?),(\w+)\)/)?.slice(1) || [] + str.match(/(?:~(and|or|not))?\((.*?),(\w+)\)/)?.slice(1) || []; } - let sub_op = null + let sub_op = null; if (aliasColObjMap[alias]) { if ( [UITypes.Date, UITypes.DateTime].includes(aliasColObjMap[alias].uidt) ) { - value = value?.split(',') + value = value?.split(','); // the first element would be sub_op - sub_op = value?.[0] + sub_op = value?.[0]; // remove the first element which is sub_op - value?.shift() - value = value?.[0] + value?.shift(); + value = value?.[0]; } else if (op === 'in') { - value = value.split(',') + value = value.split(','); } - validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op) + validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op); } return new Filter({ @@ -3352,8 +3362,8 @@ function extractCondition(nestedArrayConditions, aliasColObjMap) { fk_column_id: aliasColObjMap[alias]?.id, logical_op: logicOp, value, - }) - }) + }); + }); } function applyPaginate( @@ -3364,26 +3374,26 @@ function applyPaginate( ignoreLimit = false, }: XcFilter & { ignoreLimit?: boolean }, ) { - query.offset(offset) - if (!ignoreLimit) query.limit(limit) - return query + query.offset(offset); + if (!ignoreLimit) query.limit(limit); + return query; } function _wherePk(primaryKeys: Column[], id) { - const ids = (id + '').split('___') - const where = {} + const ids = (id + '').split('___'); + const where = {}; for (let i = 0; i < primaryKeys.length; ++i) { - where[primaryKeys[i].column_name] = ids[i] + where[primaryKeys[i].column_name] = ids[i]; } - return where + return where; } function getCompositePk(primaryKeys: Column[], row) { - return primaryKeys.map((c) => row[c.title]).join('___') + return primaryKeys.map((c) => row[c.title]).join('___'); } function haveFormulaColumn(columns: Column[]) { - return columns.some((c) => c.uidt === UITypes.Formula) + return columns.some((c) => c.uidt === UITypes.Formula); } function shouldSkipField( @@ -3394,7 +3404,7 @@ function shouldSkipField( extractPkAndPv, ) { if (fieldsSet) { - return !fieldsSet.has(column.title) + return !fieldsSet.has(column.title); } else { if (!extractPkAndPv) { if (!(viewOrTableColumn instanceof Column)) { @@ -3404,18 +3414,18 @@ function shouldSkipField( !column.pk && column.uidt !== UITypes.ForeignKey ) - return true + return true; if ( !view?.show_system_fields && column.uidt !== UITypes.ForeignKey && !column.pk && isSystemColumn(column) ) - return true + return true; } } - return false + return false; } } -export { BaseModelSqlv2 } +export { BaseModelSqlv2 }; From 50a318a243554bfbf83a97d2fc48a11577de3b2a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 18:32:24 +0530 Subject: [PATCH 21/97] fix: sqlite bulk insert Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index ecda3a98e6..81157382b6 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -2142,7 +2142,7 @@ class BaseModelSqlv2 { for (const insertData of insertDatas) { const query = trx(this.tnPath).insert(insertData); const id = (await query)[0]; - response.push(await this.readByPk(id)); + response.push(id); } } else { response = From 7adece9931eaef6aaf603b5567829bbdcb28f2d1 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 29 May 2023 18:38:10 +0530 Subject: [PATCH 22/97] refactor: update apis path Signed-off-by: Pranav C --- .../nocodb/src/controllers/data-table.controller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 599622f125..95bcc310ce 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -26,7 +26,7 @@ export class DataTableController { constructor(private readonly dataTableService: DataTableService) {} // todo: Handle the error case where view doesnt belong to model - @Get('/api/v1/tables/:modelId') + @Get('/api/v1/tables/:modelId/rows') @Acl('dataList') async dataList( @Request() req, @@ -45,7 +45,7 @@ export class DataTableController { res.json(responseData); } - @Get(['/api/v1/tables/:modelId/count']) + @Get(['/api/v1/tables/:modelId/rows/count']) @Acl('dataCount') async dataCount( @Request() req, @@ -62,7 +62,7 @@ export class DataTableController { res.json(countResult); } - @Post(['/api/v1/tables/:modelId']) + @Post(['/api/v1/tables/:modelId/rows']) @HttpCode(200) @Acl('dataInsert') async dataInsert( @@ -79,7 +79,7 @@ export class DataTableController { }); } - @Patch(['/api/v1/tables/:modelId']) + @Patch(['/api/v1/tables/:modelId/rows']) @Acl('dataUpdate') async dataUpdate( @Request() req, @@ -95,7 +95,7 @@ export class DataTableController { }); } - @Delete(['/api/v1/tables/:modelId']) + @Delete(['/api/v1/tables/:modelId/rows']) @Acl('dataDelete') async dataDelete( @Request() req, From 1c304a2285508c510dec068fabf1b9705f35a3d0 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 00:52:49 +0530 Subject: [PATCH 23/97] fix: handle non-existing view id in view get method Signed-off-by: Pranav C --- packages/nocodb/src/models/View.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/models/View.ts b/packages/nocodb/src/models/View.ts index 86abcae743..058a30f723 100644 --- a/packages/nocodb/src/models/View.ts +++ b/packages/nocodb/src/models/View.ts @@ -126,8 +126,10 @@ export default class View implements ViewType { )); if (!view) { view = await ncMeta.metaGet2(null, null, MetaTable.VIEWS, viewId); - view.meta = parseMetaProp(view); - await NocoCache.set(`${CacheScope.VIEW}:${view.id}`, view); + if (view) { + view.meta = parseMetaProp(view); + await NocoCache.set(`${CacheScope.VIEW}:${view.id}`, view); + } } return view && new View(view); From 030cebaa6b67a8d76697fa2f205d83648364764b Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 00:53:17 +0530 Subject: [PATCH 24/97] fix: handle UnprocessableEntity exception Signed-off-by: Pranav C --- .../src/filters/global-exception/global-exception.filter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nocodb/src/filters/global-exception/global-exception.filter.ts b/packages/nocodb/src/filters/global-exception/global-exception.filter.ts index ac9b6e5692..7f8ac8adc5 100644 --- a/packages/nocodb/src/filters/global-exception/global-exception.filter.ts +++ b/packages/nocodb/src/filters/global-exception/global-exception.filter.ts @@ -8,6 +8,7 @@ import { NotFound, NotImplemented, Unauthorized, + UnprocessableEntity, } from '../../helpers/catchError'; import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common'; import type { Response } from 'express'; @@ -15,6 +16,7 @@ import type { Response } from 'express'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { private logger = new Logger(GlobalExceptionFilter.name); + catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); @@ -58,6 +60,8 @@ export class GlobalExceptionFilter implements ExceptionFilter { return response .status(400) .json({ msg: exception.message, errors: exception.errors }); + } else if (exception instanceof UnprocessableEntity) { + return response.status(422).json({ msg: exception.message }); } // handle different types of exceptions From 464fd325162b3e18c70835948d4a832045233ad8 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 00:55:50 +0530 Subject: [PATCH 25/97] fix: path correction in test Signed-off-by: Pranav C --- .../tests/unit/rest/tests/newDataApis.test.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index f2d278e01c..b4c09654d7 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -128,7 +128,7 @@ const verifyColumnsInRsp = (row, columns: ColumnType[]) => { }; async function ncAxiosGet({ - url = `/api/v1/base/${project.id}/tables/${table.id}`, + url = `/api/v1/tables/${table.id}/rows`, query = {}, status = 200, }: { url?: string; query?: any; status?: number } = {}) { @@ -141,7 +141,7 @@ async function ncAxiosGet({ return response; } async function ncAxiosPost({ - url = `/api/v1/base/${project.id}/tables/${table.id}`, + url = `/api/v1/tables/${table.id}/rows`, body = {}, status = 200, }: { url?: string; body?: any; status?: number } = {}) { @@ -153,7 +153,7 @@ async function ncAxiosPost({ return response; } async function ncAxiosPatch({ - url = `/api/v1/base/${project.id}/tables/${table.id}`, + url = `/api/v1/tables/${table.id}`, body = {}, status = 200, }: { url?: string; body?: any; status?: number } = {}) { @@ -165,7 +165,7 @@ async function ncAxiosPatch({ return response; } async function ncAxiosDelete({ - url = `/api/v1/base/${project.id}/tables/${table.id}`, + url = `/api/v1/tables/${table.id}/rows`, body = {}, status = 200, }: { url?: string; body?: any; status?: number } = {}) { @@ -558,12 +558,12 @@ function textBased() { it('List: invalid ID', async function () { // Invalid table ID await ncAxiosGet({ - url: `/api/v1/base/${project.id}/tables/123456789`, + url: `/api/v1/tables/123456789`, status: 404, }); // Invalid project ID await ncAxiosGet({ - url: `/api/v1/base/123456789/tables/123456789`, + url: `/api/v1/tables/123456789/rows`, status: 404, }); // Invalid view ID @@ -679,12 +679,12 @@ function textBased() { it('Create: invalid ID', async function () { // Invalid table ID await ncAxiosPost({ - url: `/api/v1/base/${project.id}/tables/123456789`, + url: `/api/v1/tables/123456789`, status: 404, }); // Invalid project ID await ncAxiosPost({ - url: `/api/v1/base/123456789/tables/123456789`, + url: `/api/v1/tables/123456789`, status: 404, }); // Invalid data - create should not specify ID @@ -710,19 +710,19 @@ function textBased() { it('Read: all fields', async function () { const rsp = await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/100`, + url: `/api/v1/tables/${table.id}/rows/100`, }); }); it('Read: invalid ID', async function () { // Invalid table ID await ncAxiosGet({ - url: `/api/v1/base/tables/123456789/rows/100`, + url: `/api/v1/tables/123456789/rows/100`, status: 404, }); // Invalid row ID await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/1000`, + url: `/api/v1/tables/${table.id}/rows/1000`, status: 404, }); }); @@ -752,7 +752,7 @@ function textBased() { it('Update: partial', async function () { const recordBeforeUpdate = await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/1`, + url: `/api/v1/tables/${table.id}/rows/1`, }); const rsp = await ncAxiosPatch({ @@ -771,7 +771,7 @@ function textBased() { ]); const recordAfterUpdate = await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/1`, + url: `/api/v1/tables/${table.id}/rows/1`, }); expect(recordAfterUpdate.body).to.deep.equal({ ...recordBeforeUpdate.body, @@ -803,12 +803,12 @@ function textBased() { it('Update: invalid ID', async function () { // Invalid project ID await ncAxiosPatch({ - url: `/api/v1/base/123456789/tables/${table.id}`, + url: `/api/v1/tables/${table.id}`, status: 404, }); // Invalid table ID await ncAxiosPatch({ - url: `/api/v1/base/${project.id}/tables/123456789`, + url: `/api/v1/tables/123456789`, status: 404, }); // Invalid row ID @@ -831,7 +831,7 @@ function textBased() { // // check that it's gone await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/1`, + url: `/api/v1/tables/${table.id}/rows/1`, status: 404, }); }); @@ -842,11 +842,11 @@ function textBased() { // check that it's gone await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/1`, + url: `/api/v1/tables/${table.id}/rows/1`, status: 404, }); await ncAxiosGet({ - url: `/api/v1/base/tables/${table.id}/rows/2`, + url: `/api/v1/tables/${table.id}/rows/2`, status: 404, }); }); @@ -856,12 +856,12 @@ function textBased() { it('Delete: invalid ID', async function () { // Invalid project ID await ncAxiosDelete({ - url: `/api/v1/base/123456789/tables/${table.id}`, + url: `/api/v1/tables/${table.id}`, status: 404, }); // Invalid table ID await ncAxiosDelete({ - url: `/api/v1/base/${project.id}/tables/123456789`, + url: `/api/v1/tables/123456789`, status: 404, }); // Invalid row ID @@ -1041,7 +1041,7 @@ function numberBased() { // read record with Id 401 rsp = await ncAxiosGet({ - url: `/api/v1/base/${project.id}/tables/${table.id}/rows/401`, + url: `/api/v1/tables/${table.id}/rows/401`, }); expect(rsp.body).to.deep.equal(records[0]); From a9aa3128257c329dbd2a9ff7041acae95b264355 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 00:58:16 +0530 Subject: [PATCH 26/97] chore: swagger.json path update Signed-off-by: Pranav C --- packages/nocodb/src/schema/swagger.json | 155 +----------------------- 1 file changed, 6 insertions(+), 149 deletions(-) diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 3f003ca5d3..d96a756153 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -13964,17 +13964,8 @@ ] } }, - "/api/v1/base/{projectId}/tables/{tableId}": { + "/api/v1/tables/{tableId}/rows": { "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "projectId", - "in": "path", - "required": true, - "description": "Project Id" - }, { "schema": { "type": "string" @@ -13989,8 +13980,7 @@ "type": "string" }, "name": "viewId", - "in": "query", - "required": true + "in": "query" } ], "get": { @@ -14182,17 +14172,8 @@ "description": "Create a new row in the given Table View" } }, - "/api/v1/base/{projectId}/tables/{tableId}/count": { + "/api/v1/tables/{tableId}/rows/count": { "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "projectId", - "in": "path", - "required": true, - "description": "Project Id" - }, { "schema": { "type": "string" @@ -14207,8 +14188,7 @@ "type": "string" }, "name": "viewId", - "in": "query", - "required": true + "in": "query" } ], "get": { @@ -14261,131 +14241,9 @@ } } } - }, - - "patch": { - "summary": "Update Table View Row", - "operationId": "table-row-update", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object" - }, - "examples": { - "Example 1": { - "value": { - "Id": 1, - "Title": "bar", - "CreatedAt": "2023-03-11T09:11:47.437Z", - "UpdatedAt": "2023-03-11T09:20:21.133Z" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - }, - "tags": ["DB Table Row"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "type": "array", - "items": { - "type": "object" - } - } - ] - }, - "examples": { - "Example 1": { - "value": { - "Id": 1, - "Title": "bar" - } - } - } - } - } - }, - "description": "Update the target Table View Row", - "parameters": [ - { - "$ref": "#/components/parameters/xc-auth" - } - ] - }, - "delete": { - "summary": "Delete Table View Row", - "operationId": "table-row-delete", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "number" - }, - "examples": { - "Example 1": { - "value": 1 - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - }, - - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object" - }, - { - "type": "array", - "items": { - "type": "object" - } - } - ] - }, - "examples": { - "Example 1": { - "value": { - "Id": 1 - } - } - } - } - } - }, - "tags": ["DB Table Row"], - "description": "Delete the target Table View Row", - "parameters": [ - { - "$ref": "#/components/parameters/xc-auth" - } - ] } }, - "/api/v1/base/tables/{tableId}/rows/{rowId}": { + "/api/v1/tables/{tableId}/rows/{rowId}": { "parameters": [ { "schema": { @@ -14401,8 +14259,7 @@ "type": "string" }, "name": "viewId", - "in": "query", - "required": true + "in": "query" }, { "schema": { From c106fd39b3bfe88430bb6e0893ddd70705b4c7da Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 01:01:56 +0530 Subject: [PATCH 27/97] chore: rest-apis docs path update Signed-off-by: Pranav C --- .../content/en/developer-resources/rest-apis.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/noco-docs/content/en/developer-resources/rest-apis.md b/packages/noco-docs/content/en/developer-resources/rest-apis.md index 3652646fd8..f1467eea06 100644 --- a/packages/noco-docs/content/en/developer-resources/rest-apis.md +++ b/packages/noco-docs/content/en/developer-resources/rest-apis.md @@ -74,12 +74,12 @@ Currently, the default value for {orgs} is noco. Users will be able to ch | Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} | | Data | Get | dbViewRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count | | Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/group/{columnId} | -| Data | Get | dbTableRow | tableRowList | /api/v1/base/{baseId}/tables/{tableId} | -| Data | Post | dbTableRow | tableRowCreate | /api/v1/base/{baseId}/tables/{tableId} | -| Data | Get | dbTableRow | tableRowRead | /api/v1/base/tables/{tableId}/rows/{rowId} | -| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/base/tables/{tableId}/rows | -| Data | Delete| dbTableRow | tableRowDelete | /api/v1/base/tables/{tableId}/rows | -| Data | Get | dbTableRow | tableRowCount | /api/v1/base/{baseId}/tables/{tableId}/count | +| Data | Get | dbTableRow | tableRowList | /api/v1/tables/{tableId} | +| Data | Post | dbTableRow | tableRowCreate | /api/v1/tables/{tableId} | +| Data | Get | dbTableRow | tableRowRead | /api/v1/tables/{tableId}/rows/{rowId} | +| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/tables/{tableId}/rows | +| Data | Delete| dbTableRow | tableRowDelete | /api/v1/tables/{tableId}/rows | +| Data | Get | dbTableRow | tableRowCount | /api/v1/tables/{tableId}/rows/count | ### Meta APIs From 5df92404e286aa42079b61d1877c384833dcd376 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 01:10:11 +0530 Subject: [PATCH 28/97] fix: insert one by one in mysql and sqlite Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 11 ++++-- .../nocodb/src/services/data-table.service.ts | 39 +++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 81157382b6..21ec90095c 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -2091,11 +2091,13 @@ class BaseModelSqlv2 { cookie, foreign_key_checks = true, raw = false, + insertOneByOneAsFallback = false, }: { chunkSize?: number; cookie?: any; foreign_key_checks?: boolean; raw?: boolean; + insertOneByOneAsFallback?: boolean; } = {}, ) { let trx; @@ -2135,14 +2137,17 @@ class BaseModelSqlv2 { let response; - if (this.isSqlite) { - // sqlite doesnt support returning, so insert one by one and return ids + // insert one by one as fallback to get ids for sqlite and mysql + if (insertOneByOneAsFallback && (this.isSqlite || this.isMySQL)) { + // sqlite and mysql doesnt support returning, so insert one by one and return ids response = []; + const aiPkCol = this.model.primaryKeys.find((pk) => pk.ai); + for (const insertData of insertDatas) { const query = trx(this.tnPath).insert(insertData); const id = (await query)[0]; - response.push(id); + response.push(aiPkCol ? { [aiPkCol.title]: id } : id); } } else { response = diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 92e39b6315..72dab7cc9f 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -66,11 +66,12 @@ export class DataTableService { }); // if array then do bulk insert - if (Array.isArray(param.body)) { - return await baseModel.bulkInsert(param.body, { cookie: param.cookie }); - } else { - return await baseModel.insert(param.body, null, param.cookie); - } + const result = await baseModel.bulkInsert( + Array.isArray(param.body) ? param.body : [param.body], + { cookie: param.cookie, insertOneByOneAsFallback: true }, + ); + + return Array.isArray(param.body) ? result : result[0]; } async dataUpdate(param: { @@ -119,7 +120,7 @@ export class DataTableService { const baseModel = await Model.getBaseModelSQL({ id: model.id, viewId: view?.id, - dbDriver: await NcConnectionMgrv2.get(base) + dbDriver: await NcConnectionMgrv2.get(base), }); // // // todo: Should have error http status code @@ -129,7 +130,6 @@ export class DataTableService { // } // return await baseModel.delByPk(param.rowId, null, param.cookie); - const res = await baseModel.bulkUpdate( Array.isArray(param.body) ? param.body : [param.body], { cookie: param.cookie }, @@ -190,4 +190,29 @@ export class DataTableService { return { model, view }; } + + private async extractPks({ model, rows }: { rows: any[]; model?: Model }) { + return await Promise.all( + rows.map(async (row) => { + // if not object then return the value + if (typeof row !== 'object') return row; + + let pk; + + // if model is passed then use the model to get the pk columns and extract the pk values + if (model) { + pk = await model.getColumns().then((cols) => + cols + .filter((col) => col.pk) + .map((col) => row[col.title]) + .join('___'), + ); + } else { + // if model is not passed then get all the values and join them + pk = Object.values(row).join('___'); + } + return pk; + }), + ); + } } From 6116a9edca875212775f37af9d84883b81f46d3e Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 01:12:45 +0530 Subject: [PATCH 29/97] fix: read api path correction Signed-off-by: Pranav C --- packages/nocodb/src/controllers/data-table.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 95bcc310ce..808650b20b 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -111,7 +111,7 @@ export class DataTableController { }); } - @Get(['/api/v1/base/tables/:modelId/rows/:rowId']) + @Get(['/api/v1/tables/:modelId/rows/:rowId']) @Acl('dataRead') async dataRead( @Request() req, From 1093be5732ff1721640e35331b53208f97b5fed1 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Tue, 30 May 2023 10:45:30 +0530 Subject: [PATCH 30/97] test: URL corrections, select & date based tests Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/tests/unit/factory/row.ts | 2 + .../tests/unit/rest/tests/newDataApis.test.ts | 359 ++++++++++++++++-- 2 files changed, 329 insertions(+), 32 deletions(-) diff --git a/packages/nocodb/tests/unit/factory/row.ts b/packages/nocodb/tests/unit/factory/row.ts index 4f4f42abbe..8bfd7f2fc8 100644 --- a/packages/nocodb/tests/unit/factory/row.ts +++ b/packages/nocodb/tests/unit/factory/row.ts @@ -184,6 +184,8 @@ const rowMixedValue = (column: ColumnType, index: number) => { // eslint-disable-next-line no-case-declarations const d2 = new Date(); d2.setDate(d2.getDate() - 400 + index); + // set time to 12:00:00 + d2.setHours(12, 0, 0, 0); return d2.toISOString(); case UITypes.URL: return urls[index % urls.length]; diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index b4c09654d7..54dc2a6a5a 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -153,7 +153,7 @@ async function ncAxiosPost({ return response; } async function ncAxiosPatch({ - url = `/api/v1/tables/${table.id}`, + url = `/api/v1/tables/${table.id}/rows`, body = {}, status = 200, }: { url?: string; body?: any; status?: number } = {}) { @@ -557,15 +557,15 @@ function textBased() { // Error handling it('List: invalid ID', async function () { // Invalid table ID - await ncAxiosGet({ - url: `/api/v1/tables/123456789`, - status: 404, - }); - // Invalid project ID await ncAxiosGet({ url: `/api/v1/tables/123456789/rows`, status: 404, }); + // Invalid project ID + // await ncAxiosGet({ + // url: `/api/v1/base/123456789/tables/123456789`, + // status: 404, + // }); // Invalid view ID await ncAxiosGet({ query: { @@ -595,13 +595,13 @@ function textBased() { query: { offset: -100, }, - status: 200, + status: 422, }); await ncAxiosGet({ query: { offset: 'abc', }, - status: 200, + status: 422, }); await ncAxiosGet({ query: { @@ -672,25 +672,25 @@ function textBased() { it('Create: bulk', async function () { const rsp = await ncAxiosPost({ body: [newRecord, newRecord, newRecord] }); - expect(rsp.body).to.deep.equal([{ Id: 401 }, { Id: 402 }, { Id: 403 }]); + expect(rsp.body).to.deep.equal([401, 402, 403]); }); // Error handling it('Create: invalid ID', async function () { // Invalid table ID await ncAxiosPost({ - url: `/api/v1/tables/123456789`, + url: `/api/v1/tables/123456789/rows`, status: 404, }); // Invalid project ID - await ncAxiosPost({ - url: `/api/v1/tables/123456789`, - status: 404, - }); + // await ncAxiosPost({ + // url: `/api/v1/base/123456789/tables/123456789`, + // status: 404, + // }); // Invalid data - create should not specify ID await ncAxiosPost({ body: { ...newRecord, Id: 300 }, - status: 422, + status: 400, }); // Invalid data - number instead of string // await ncAxiosPost({ @@ -801,20 +801,21 @@ function textBased() { // Error handling it('Update: invalid ID', async function () { - // Invalid project ID - await ncAxiosPatch({ - url: `/api/v1/tables/${table.id}`, - status: 404, - }); + // // Invalid project ID + // await ncAxiosPatch({ + // url: `/api/v1/base/123456789/tables/${table.id}`, + // status: 404, + // }); // Invalid table ID await ncAxiosPatch({ - url: `/api/v1/tables/123456789`, + url: `/api/v1/tables/123456789/rows`, + body: { Id: 100, SingleLineText: 'some text' }, status: 404, }); // Invalid row ID await ncAxiosPatch({ body: { Id: 123456789, SingleLineText: 'some text' }, - status: 422, + status: 400, }); }); @@ -854,18 +855,19 @@ function textBased() { // Error handling it('Delete: invalid ID', async function () { - // Invalid project ID - await ncAxiosDelete({ - url: `/api/v1/tables/${table.id}`, - status: 404, - }); + // // Invalid project ID + // await ncAxiosDelete({ + // url: `/api/v1/tables/${table.id}/rows`, + // status: 404, + // }); // Invalid table ID await ncAxiosDelete({ - url: `/api/v1/tables/123456789`, + url: `/api/v1/tables/123456789/rows`, + body: { Id: 100 }, status: 404, }); // Invalid row ID - await ncAxiosDelete({ body: { Id: 123456789 }, status: 422 }); + await ncAxiosDelete({ body: { Id: 123456789 }, status: 400 }); }); } @@ -1139,6 +1141,152 @@ function selectBased() { // verify length of unfiltered records to be 400 expect(insertedRecords.length).to.equal(400); }); + + const records = [ + { + Id: 1, + SingleSelect: 'jan', + MultiSelect: 'jan,feb,mar', + }, + { + Id: 2, + SingleSelect: 'feb', + MultiSelect: 'apr,may,jun', + }, + { + Id: 3, + SingleSelect: 'mar', + MultiSelect: 'jul,aug,sep', + }, + { + Id: 4, + SingleSelect: 'apr', + MultiSelect: 'oct,nov,dec', + }, + { + Id: 5, + SingleSelect: 'may', + MultiSelect: 'jan,feb,mar', + }, + { + Id: 6, + SingleSelect: 'jun', + MultiSelect: null, + }, + { + Id: 7, + SingleSelect: 'jul', + MultiSelect: 'jan,feb,mar', + }, + { + Id: 8, + SingleSelect: 'aug', + MultiSelect: 'apr,may,jun', + }, + { + Id: 9, + SingleSelect: 'sep', + MultiSelect: 'jul,aug,sep', + }, + { + Id: 10, + SingleSelect: 'oct', + MultiSelect: 'oct,nov,dec', + }, + ]; + + it('Select based- List & CRUD', async function () { + // list 10 records + let rsp = await ncAxiosGet({ + query: { + limit: 10, + }, + }); + const pageInfo = { + totalRows: 400, + page: 1, + pageSize: 10, + isFirstPage: true, + isLastPage: false, + }; + expect(rsp.body.pageInfo).to.deep.equal(pageInfo); + expect(rsp.body.list).to.deep.equal(records); + + /////////////////////////////////////////////////////////////////////////// + + // insert 10 records + // remove Id's from record array + records.forEach((r) => delete r.Id); + rsp = await ncAxiosPost({ + body: records, + }); + + // prepare array with 10 Id's, from 401 to 410 + const ids = []; + for (let i = 401; i <= 410; i++) { + ids.push({ Id: i }); + } + expect(rsp.body).to.deep.equal(ids); + + /////////////////////////////////////////////////////////////////////////// + + // read record with Id 401 + rsp = await ncAxiosGet({ + url: `/api/v1/tables/${table.id}/rows/401`, + }); + expect(rsp.body).to.deep.equal(records[0]); + + /////////////////////////////////////////////////////////////////////////// + + // update record with Id 401 to 404 + const updatedRecord = { + SingleSelect: 'jan', + MultiSelect: 'jan,feb,mar', + }; + const updatedRecords = [ + { + id: 401, + ...updatedRecord, + }, + { + id: 402, + ...updatedRecord, + }, + { + id: 403, + ...updatedRecord, + }, + { + id: 404, + ...updatedRecord, + }, + ]; + rsp = await ncAxiosPatch({ + body: updatedRecords, + }); + expect(rsp.body).to.deep.equal( + updatedRecords.map((record) => ({ id: record.id })), + ); + + // verify updated records + rsp = await ncAxiosGet({ + query: { + limit: 4, + offset: 400, + }, + }); + expect(rsp.body.list).to.deep.equal(updatedRecords); + + /////////////////////////////////////////////////////////////////////////// + + // delete record with ID 401 to 404 + rsp = await ncAxiosDelete({ + body: updatedRecords.map((record) => ({ id: record.id })), + }); + expect(rsp.body).to.deep.equal( + updatedRecords.map((record) => ({ id: record.id })), + ); + }); } function dateBased() { @@ -1161,6 +1309,7 @@ function dateBased() { for (let i = 0; i < 800; i++) { const row = { Date: rowMixedValue(columns[1], i), + DateTime: rowMixedValue(columns[2], i), }; rowAttributes.push(row); } @@ -1178,6 +1327,152 @@ function dateBased() { // verify length of unfiltered records to be 800 expect(insertedRecords.length).to.equal(800); }); + + const records = [ + { + Id: 1, + Date: '2022-04-25', + DateTime: '2022-04-25T06:30:00.000Z', + }, + { + Id: 2, + Date: '2022-04-26', + DateTime: '2022-04-26T06:30:00.000Z', + }, + { + Id: 3, + Date: '2022-04-27', + DateTime: '2022-04-27T06:30:00.000Z', + }, + { + Id: 4, + Date: '2022-04-28', + DateTime: '2022-04-28T06:30:00.000Z', + }, + { + Id: 5, + Date: '2022-04-29', + DateTime: '2022-04-29T06:30:00.000Z', + }, + { + Id: 6, + Date: '2022-04-30', + DateTime: '2022-04-30T06:30:00.000Z', + }, + { + Id: 7, + Date: '2022-05-01', + DateTime: '2022-05-01T06:30:00.000Z', + }, + { + Id: 8, + Date: '2022-05-02', + DateTime: '2022-05-02T06:30:00.000Z', + }, + { + Id: 9, + Date: '2022-05-03', + DateTime: '2022-05-03T06:30:00.000Z', + }, + { + Id: 10, + Date: '2022-05-04', + DateTime: '2022-05-04T06:30:00.000Z', + }, + ]; + + it('Date based- List & CRUD', async function () { + // list 10 records + let rsp = await ncAxiosGet({ + query: { + limit: 10, + }, + }); + const pageInfo = { + totalRows: 400, + page: 1, + pageSize: 10, + isFirstPage: true, + isLastPage: false, + }; + expect(rsp.body.pageInfo).to.deep.equal(pageInfo); + expect(rsp.body.list).to.deep.equal(records); + + /////////////////////////////////////////////////////////////////////////// + + // insert 10 records + // remove Id's from record array + records.forEach((r) => delete r.Id); + rsp = await ncAxiosPost({ + body: records, + }); + + // prepare array with 10 Id's, from 401 to 410 + const ids = []; + for (let i = 401; i <= 410; i++) { + ids.push({ Id: i }); + } + expect(rsp.body).to.deep.equal(ids); + + /////////////////////////////////////////////////////////////////////////// + + // read record with Id 401 + rsp = await ncAxiosGet({ + url: `/api/v1/tables/${table.id}/rows/401`, + }); + expect(rsp.body).to.deep.equal(records[0]); + + /////////////////////////////////////////////////////////////////////////// + + // update record with Id 401 to 404 + const updatedRecord = { + Date: '2022-04-25', + DateTime: '2022-04-25T06:30:00.000Z', + }; + const updatedRecords = [ + { + id: 401, + ...updatedRecord, + }, + { + id: 402, + ...updatedRecord, + }, + { + id: 403, + ...updatedRecord, + }, + { + id: 404, + ...updatedRecord, + }, + ]; + rsp = await ncAxiosPatch({ + body: updatedRecords, + }); + expect(rsp.body).to.deep.equal( + updatedRecords.map((record) => ({ id: record.id })), + ); + + // verify updated records + rsp = await ncAxiosGet({ + query: { + limit: 4, + offset: 400, + }, + }); + expect(rsp.body.list).to.deep.equal(updatedRecords); + + /////////////////////////////////////////////////////////////////////////// + + // delete record with ID 401 to 404 + rsp = await ncAxiosDelete({ + body: updatedRecords.map((record) => ({ id: record.id })), + }); + expect(rsp.body).to.deep.equal( + updatedRecords.map((record) => ({ id: record.id })), + ); + }); } /////////////////////////////////////////////////////////////////////////////// @@ -1187,9 +1482,9 @@ function dateBased() { export default function () { // describe('General', generalDb); describe('Text based', textBased); - // describe('Numerical', numberBased); - // describe('Select based', selectBased); - // describe('Date based', dateBased); + describe('Numerical', numberBased); + describe('Select based', selectBased); + describe('Date based', dateBased); } /////////////////////////////////////////////////////////////////////////////// From 4108f77eb60ce309b2ca656f39e0beb3d5a76933 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Tue, 30 May 2023 10:46:24 +0530 Subject: [PATCH 31/97] test: response format correction Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 54dc2a6a5a..861c44580e 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -672,7 +672,7 @@ function textBased() { it('Create: bulk', async function () { const rsp = await ncAxiosPost({ body: [newRecord, newRecord, newRecord] }); - expect(rsp.body).to.deep.equal([401, 402, 403]); + expect(rsp.body).to.deep.equal([{ Id: 401 }, { Id: 402 }, { Id: 403 }]); }); // Error handling From a9d9012876979b9c1406276fe93c1c088b948910 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 11:30:47 +0530 Subject: [PATCH 32/97] refactor: update/delete - return with array of object containing id Signed-off-by: Pranav C --- .../nocodb/src/services/data-table.service.ts | 54 +++++-------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 72dab7cc9f..d3ca4aada6 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -92,19 +92,12 @@ export class DataTableService { dbDriver: await NcConnectionMgrv2.get(base), }); - // return await baseModel.updateByPk( - // param.rowId, - // param.body, - // null, - // param.cookie, - // ); - const res = await baseModel.bulkUpdate( Array.isArray(param.body) ? param.body : [param.body], { cookie: param.cookie }, ); - return Array.isArray(param.body) ? res : res[0]; + return this.extractIdObj(param.body) } async dataDelete(param: { @@ -122,20 +115,13 @@ export class DataTableService { viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); - // - // // todo: Should have error http status code - // const message = await baseModel.hasLTARData(param.rowId, model); - // if (message.length) { - // return { message }; - // } - // return await baseModel.delByPk(param.rowId, null, param.cookie); - const res = await baseModel.bulkUpdate( + await baseModel.bulkUpdate( Array.isArray(param.body) ? param.body : [param.body], { cookie: param.cookie }, ); - return Array.isArray(param.body) ? res : res[0]; + return this.extractIdObj(param.body); } async dataCount(param: { @@ -191,28 +177,16 @@ export class DataTableService { return { model, view }; } - private async extractPks({ model, rows }: { rows: any[]; model?: Model }) { - return await Promise.all( - rows.map(async (row) => { - // if not object then return the value - if (typeof row !== 'object') return row; - - let pk; - - // if model is passed then use the model to get the pk columns and extract the pk values - if (model) { - pk = await model.getColumns().then((cols) => - cols - .filter((col) => col.pk) - .map((col) => row[col.title]) - .join('___'), - ); - } else { - // if model is not passed then get all the values and join them - pk = Object.values(row).join('___'); - } - return pk; - }), - ); + private async extractIdObj({ model, body }: { body: Record | Record[]; model: Model }) { + const pkColumns = await model.getColumns().then((cols) => cols.filter((col) => col.pk)); + + const result = (Array.isArray(body) ? body : [body]).map((row) => { + return pkColumns.reduce((acc, col) => { + acc[col.title] = row[col.title]; + return acc; + }) + }) + + return Array.isArray(body) ? result : result[0]; } } From 66f8be523a55946a3c2f2f73e7df0b7523537fb0 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 11:58:46 +0530 Subject: [PATCH 33/97] refactor: ignore invalid limit and offset Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 21ec90095c..d1230d2b3f 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -1518,14 +1518,25 @@ class BaseModelSqlv2 { obj.shuffle = args.shuffle || args.r || ''; obj.condition = args.condition || args.c || {}; obj.conditionGraph = args.conditionGraph || {}; + + // use default value if invalid limit + // for example, if limit is not a number, it will be ignored + // if limit is less than 1, it will be ignored + const limit = +(args.limit || args.l); obj.limit = Math.max( Math.min( - args.limit || args.l || this.config.limitDefault, + limit && limit > 0 && Number.isInteger(limit) + ? limit + : this.config.limitDefault, this.config.limitMax, ), this.config.limitMin, ); - obj.offset = Math.max(+(args.offset || args.o) || 0, 0); + + // skip any invalid offset, ignore negative and non-integer values + const offset = +(args.offset || args.o) || 0; + obj.offset = Math.max(Number.isInteger(offset) ? offset : 0, 0); + obj.fields = args.fields || args.f; obj.sort = args.sort || args.s; return obj; From 56461ed92f97d69feb4c2a0e2db1dae58068f918 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 12:09:37 +0530 Subject: [PATCH 34/97] refactor: avoid repetition and move to helpers Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 22 +---------- packages/nocodb/src/helpers/PagedResponse.ts | 15 ++----- .../src/helpers/extractLimitAndOffset.ts | 39 +++++++++++++++++++ packages/nocodb/src/helpers/index.ts | 1 + 4 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 packages/nocodb/src/helpers/extractLimitAndOffset.ts diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index d1230d2b3f..908d22129c 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -14,6 +14,7 @@ import Validator from 'validator'; import { customAlphabet } from 'nanoid'; import DOMPurify from 'isomorphic-dompurify'; import { v4 as uuidv4 } from 'uuid'; +import { extractLimitAndOffset } from '../helpers' import { NcError } from '../helpers/catchError'; import getAst from '../helpers/getAst'; @@ -1512,31 +1513,12 @@ class BaseModelSqlv2 { } _getListArgs(args: XcFilterWithAlias): XcFilter { - const obj: XcFilter = {}; + const obj: XcFilter = extractLimitAndOffset(args); obj.where = args.filter || args.where || args.w || ''; obj.having = args.having || args.h || ''; obj.shuffle = args.shuffle || args.r || ''; obj.condition = args.condition || args.c || {}; obj.conditionGraph = args.conditionGraph || {}; - - // use default value if invalid limit - // for example, if limit is not a number, it will be ignored - // if limit is less than 1, it will be ignored - const limit = +(args.limit || args.l); - obj.limit = Math.max( - Math.min( - limit && limit > 0 && Number.isInteger(limit) - ? limit - : this.config.limitDefault, - this.config.limitMax, - ), - this.config.limitMin, - ); - - // skip any invalid offset, ignore negative and non-integer values - const offset = +(args.offset || args.o) || 0; - obj.offset = Math.max(Number.isInteger(offset) ? offset : 0, 0); - obj.fields = args.fields || args.f; obj.sort = args.sort || args.s; return obj; diff --git a/packages/nocodb/src/helpers/PagedResponse.ts b/packages/nocodb/src/helpers/PagedResponse.ts index 1600d749dd..cd9da62d45 100644 --- a/packages/nocodb/src/helpers/PagedResponse.ts +++ b/packages/nocodb/src/helpers/PagedResponse.ts @@ -1,11 +1,6 @@ +import { extractLimitAndOffset } from '.'; import type { PaginatedType } from 'nocodb-sdk'; -const config: any = { - limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1), - limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1), - limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1), -}; - export class PagedResponseImpl { constructor( list: T[], @@ -17,12 +12,7 @@ export class PagedResponseImpl { o?: number; } = {}, ) { - const limit = Math.max( - Math.min(args.limit || args.l || config.limitDefault, config.limitMax), - config.limitMin, - ); - - const offset = Math.max(+(args.offset || args.o) || 0, 0); + const { offset, limit } = extractLimitAndOffset(args); let count = args.count ?? null; @@ -44,4 +34,5 @@ export class PagedResponseImpl { list: Array; pageInfo: PaginatedType; + errors?: any[]; } diff --git a/packages/nocodb/src/helpers/extractLimitAndOffset.ts b/packages/nocodb/src/helpers/extractLimitAndOffset.ts new file mode 100644 index 0000000000..1cf5072aa3 --- /dev/null +++ b/packages/nocodb/src/helpers/extractLimitAndOffset.ts @@ -0,0 +1,39 @@ +const config = { + limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1), + limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1), + limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1), +}; + +export function extractLimitAndOffset( + args: { + limit?: number | string; + offset?: number | string; + l?: number | string; + o?: number | string; + } = {}, +) { + const obj: { + limit?: number; + offset?: number; + } = {}; + + // use default value if invalid limit + // for example, if limit is not a number, it will be ignored + // if limit is less than 1, it will be ignored + const limit = +(args.limit || args.l); + obj.limit = Math.max( + Math.min( + limit && limit > 0 && Number.isInteger(limit) + ? limit + : config.limitDefault, + config.limitMax, + ), + config.limitMin, + ); + + // skip any invalid offset, ignore negative and non-integer values + const offset = +(args.offset || args.o) || 0; + obj.offset = Math.max(Number.isInteger(offset) ? offset : 0, 0); + + return obj; +} diff --git a/packages/nocodb/src/helpers/index.ts b/packages/nocodb/src/helpers/index.ts index ee77b070e2..adf2eeecba 100644 --- a/packages/nocodb/src/helpers/index.ts +++ b/packages/nocodb/src/helpers/index.ts @@ -2,5 +2,6 @@ import { populateMeta } from './populateMeta'; export * from './columnHelpers'; export * from './apiHelpers'; export * from './cacheHelpers'; +export * from './extractLimitAndOffset'; export { populateMeta }; From fc44f646728e655492ac57eece33eec2cdd4b813 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 12:11:35 +0530 Subject: [PATCH 35/97] refactor: add error in response if offset if beyond the limit Signed-off-by: Pranav C --- packages/nocodb/src/helpers/PagedResponse.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/nocodb/src/helpers/PagedResponse.ts b/packages/nocodb/src/helpers/PagedResponse.ts index cd9da62d45..97f8e3b658 100644 --- a/packages/nocodb/src/helpers/PagedResponse.ts +++ b/packages/nocodb/src/helpers/PagedResponse.ts @@ -30,6 +30,14 @@ export class PagedResponseImpl { this.pageInfo.page === (Math.ceil(this.pageInfo.totalRows / this.pageInfo.pageSize) || 1); } + + if (offset && offset >= count) { + this.errors = [ + { + message: 'Offset is beyond the total number of rows', + }, + ]; + } } list: Array; From e98abee16736f7433c857d626a63c93a70d2ef8a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 13:10:44 +0530 Subject: [PATCH 36/97] fix: pass right props Signed-off-by: Pranav C --- .../nocodb/src/services/data-table.service.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index d3ca4aada6..020d3fa77f 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -97,7 +97,7 @@ export class DataTableService { { cookie: param.cookie }, ); - return this.extractIdObj(param.body) + return this.extractIdObj({ body: param.body, model }); } async dataDelete(param: { @@ -121,7 +121,7 @@ export class DataTableService { { cookie: param.cookie }, ); - return this.extractIdObj(param.body); + return this.extractIdObj({ body: param.body, model }); } async dataCount(param: { @@ -177,15 +177,23 @@ export class DataTableService { return { model, view }; } - private async extractIdObj({ model, body }: { body: Record | Record[]; model: Model }) { - const pkColumns = await model.getColumns().then((cols) => cols.filter((col) => col.pk)); + private async extractIdObj({ + model, + body, + }: { + body: Record | Record[]; + model: Model; + }) { + const pkColumns = await model + .getColumns() + .then((cols) => cols.filter((col) => col.pk)); const result = (Array.isArray(body) ? body : [body]).map((row) => { - return pkColumns.reduce((acc, col) => { + return pkColumns.reduce((acc, col) => { acc[col.title] = row[col.title]; return acc; - }) - }) + }); + }); return Array.isArray(body) ? result : result[0]; } From 220dc863e5ac308d1cacc6805f7f7887bed1443c Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 13:14:58 +0530 Subject: [PATCH 37/97] fix: provide initial value in reduce method Signed-off-by: Pranav C --- packages/nocodb/src/services/data-table.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 020d3fa77f..9251af6004 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -192,7 +192,7 @@ export class DataTableService { return pkColumns.reduce((acc, col) => { acc[col.title] = row[col.title]; return acc; - }); + }, {}); }); return Array.isArray(body) ? result : result[0]; From 9db124c65f291a1524ed2e7db1c04a7020eaed80 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 13:29:21 +0530 Subject: [PATCH 38/97] fix: if row not found return `null` rather than empty object Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 908d22129c..973e1c9eb0 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -168,7 +168,7 @@ class BaseModelSqlv2 { data.__proto__ = proto; } - return data ? await nocoExecute(ast, data, {}, query) : {}; + return data ? await nocoExecute(ast, data, {}, query) : null; } public async exist(id?: any): Promise { From 49f868e45d61b7233c8e023ef01ba3f9d856c3a6 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 16:31:52 +0530 Subject: [PATCH 39/97] feat: throw exception if user passed a non-existing rowId Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 32 ++++++++++++++++--- .../nocodb/src/services/data-table.service.ts | 4 +-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 973e1c9eb0..c26d67a876 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -14,7 +14,7 @@ import Validator from 'validator'; import { customAlphabet } from 'nanoid'; import DOMPurify from 'isomorphic-dompurify'; import { v4 as uuidv4 } from 'uuid'; -import { extractLimitAndOffset } from '../helpers' +import { extractLimitAndOffset } from '../helpers'; import { NcError } from '../helpers/catchError'; import getAst from '../helpers/getAst'; @@ -2173,7 +2173,11 @@ class BaseModelSqlv2 { async bulkUpdate( datas: any[], - { cookie, raw = false }: { cookie?: any; raw?: boolean } = {}, + { + cookie, + raw = false, + throwExceptionIfNotExist = false, + }: { cookie?: any; raw?: boolean; throwExceptionIfNotExist?: boolean } = {}, ) { let transaction; try { @@ -2212,7 +2216,12 @@ class BaseModelSqlv2 { if (!raw) { for (const pkValues of updatePkValues) { - newData.push(await this.readByPk(pkValues)); + const oldRecord = await this.readByPk(pkValues); + if (!oldRecord && throwExceptionIfNotExist) + NcError.unprocessableEntity( + `Record with pk ${JSON.stringify(pkValues)} not found`, + ); + newData.push(oldRecord); } } @@ -2275,7 +2284,13 @@ class BaseModelSqlv2 { } } - async bulkDelete(ids: any[], { cookie }: { cookie?: any } = {}) { + async bulkDelete( + ids: any[], + { + cookie, + throwExceptionIfNotExist = false, + }: { cookie?: any; throwExceptionIfNotExist?: boolean } = {}, + ) { let transaction; try { const deleteIds = await Promise.all( @@ -2290,7 +2305,14 @@ class BaseModelSqlv2 { // pk not specified - bypass continue; } - deleted.push(await this.readByPk(pkValues)); + + const oldRecord = await this.readByPk(pkValues); + if (!oldRecord && throwExceptionIfNotExist) + NcError.unprocessableEntity( + `Record with pk ${JSON.stringify(pkValues)} not found`, + ); + deleted.push(oldRecord); + res.push(d); } diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 9251af6004..ad68fef149 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -94,7 +94,7 @@ export class DataTableService { const res = await baseModel.bulkUpdate( Array.isArray(param.body) ? param.body : [param.body], - { cookie: param.cookie }, + { cookie: param.cookie, throwExceptionIfNotExist: true }, ); return this.extractIdObj({ body: param.body, model }); @@ -118,7 +118,7 @@ export class DataTableService { await baseModel.bulkUpdate( Array.isArray(param.body) ? param.body : [param.body], - { cookie: param.cookie }, + { cookie: param.cookie, throwExceptionIfNotExist: true }, ); return this.extractIdObj({ body: param.body, model }); From 8818535e385fbbdf21497fde634acbb5bd28505b Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 16:56:07 +0530 Subject: [PATCH 40/97] fix: typo correction - use the right method Signed-off-by: Pranav C --- packages/nocodb/src/services/data-table.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index ad68fef149..f68f47fd9b 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -116,7 +116,7 @@ export class DataTableService { dbDriver: await NcConnectionMgrv2.get(base), }); - await baseModel.bulkUpdate( + await baseModel.bulkDelete( Array.isArray(param.body) ? param.body : [param.body], { cookie: param.cookie, throwExceptionIfNotExist: true }, ); From 549d22bada26e12d6a032970f4f6cdb902595f7e Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 16:58:28 +0530 Subject: [PATCH 41/97] refactor: better error message Signed-off-by: Pranav C --- packages/nocodb/src/services/data-table.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index f68f47fd9b..00da4967c7 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -158,7 +158,7 @@ export class DataTableService { const model = await Model.get(param.modelId); if (!model) { - NcError.notFound('Model not found'); + NcError.notFound(`Table '${param.modelId}' not found`); } if (param.projectId && model.project_id !== param.projectId) { @@ -170,7 +170,7 @@ export class DataTableService { if (param.viewId) { view = await View.get(param.viewId); if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) { - NcError.unprocessableEntity('View not belong to model'); + NcError.unprocessableEntity(`View '${param.viewId}' not found`); } } From 6ad1ce8d63b1eeb152de20177f46c911b42caa6f Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Tue, 30 May 2023 17:02:38 +0530 Subject: [PATCH 42/97] test: general error correction Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 102 ++++++++---------- 1 file changed, 44 insertions(+), 58 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 861c44580e..2e21b16bda 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -193,6 +193,10 @@ function generalDb() { }); customerColumns = await customerTable.getColumns(); }); + + it('should list all records', async function () { + console.log('should list all records'); + }); } function textBased() { @@ -561,11 +565,7 @@ function textBased() { url: `/api/v1/tables/123456789/rows`, status: 404, }); - // Invalid project ID - // await ncAxiosGet({ - // url: `/api/v1/base/123456789/tables/123456789`, - // status: 404, - // }); + // Invalid view ID await ncAxiosGet({ query: { @@ -576,39 +576,56 @@ function textBased() { }); it('List: invalid limit & offset', async function () { - // Invalid limit - await ncAxiosGet({ + const expectedPageInfo = { + totalRows: 400, + page: 1, + pageSize: 25, + isFirstPage: true, + isLastPage: false, + }; + + // Invalid limit : falls back to default value + let rsp = await ncAxiosGet({ query: { limit: -100, }, status: 200, }); - await ncAxiosGet({ + expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); + + rsp = await ncAxiosGet({ query: { limit: 'abc', }, status: 200, }); + expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); - // Invalid offset - await ncAxiosGet({ + // Invalid offset : falls back to default value + rsp = await ncAxiosGet({ query: { offset: -100, }, - status: 422, + status: 200, }); - await ncAxiosGet({ + expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); + + rsp = await ncAxiosGet({ query: { offset: 'abc', }, - status: 422, + status: 200, }); - await ncAxiosGet({ + expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); + + // Offset > totalRows : returns empty list + rsp = await ncAxiosGet({ query: { offset: 10000, }, - status: 422, + status: 200, }); + expect(rsp.body.list.length).to.equal(0); }); it('List: invalid sort, filter, fields', async function () { @@ -646,10 +663,7 @@ function textBased() { it('Create: all fields', async function () { const rsp = await ncAxiosPost({ body: newRecord }); - expect(rsp.body).to.deep.equal({ - Id: 401, - ...newRecord, - }); + expect(rsp.body).to.deep.equal({ Id: 401 }); }); it('Create: few fields left out', async function () { @@ -660,13 +674,7 @@ function textBased() { const rsp = await ncAxiosPost({ body: newRecord }); // fields left out should be null - expect(rsp.body).to.deep.equal({ - Id: 401, - ...newRecord, - Email: null, - Url: null, - Phone: null, - }); + expect(rsp.body).to.deep.equal({ Id: 401 }); }); it('Create: bulk', async function () { @@ -682,11 +690,7 @@ function textBased() { url: `/api/v1/tables/123456789/rows`, status: 404, }); - // Invalid project ID - // await ncAxiosPost({ - // url: `/api/v1/base/123456789/tables/123456789`, - // status: 404, - // }); + // Invalid data - create should not specify ID await ncAxiosPost({ body: { ...newRecord, Id: 300 }, @@ -743,11 +747,7 @@ function textBased() { }, ], }); - expect(rsp.body).to.deep.equal([ - { - Id: '1', - }, - ]); + expect(rsp.body).to.deep.equal([{ Id: 1 }]); }); it('Update: partial', async function () { @@ -764,11 +764,7 @@ function textBased() { }, ], }); - expect(rsp.body).to.deep.equal([ - { - Id: '1', - }, - ]); + expect(rsp.body).to.deep.equal([{ Id: 1 }]); const recordAfterUpdate = await ncAxiosGet({ url: `/api/v1/tables/${table.id}/rows/1`, @@ -795,17 +791,12 @@ function textBased() { }, ], }); - expect(rsp.body).to.deep.equal([{ Id: '1' }, { Id: '2' }]); + expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); }); // Error handling it('Update: invalid ID', async function () { - // // Invalid project ID - // await ncAxiosPatch({ - // url: `/api/v1/base/123456789/tables/${table.id}`, - // status: 404, - // }); // Invalid table ID await ncAxiosPatch({ url: `/api/v1/tables/123456789/rows`, @@ -815,7 +806,7 @@ function textBased() { // Invalid row ID await ncAxiosPatch({ body: { Id: 123456789, SingleLineText: 'some text' }, - status: 400, + status: 422, }); }); @@ -828,9 +819,9 @@ function textBased() { it('Delete: single', async function () { const rsp = await ncAxiosDelete({ body: [{ Id: 1 }] }); - expect(rsp.body).to.deep.equal([{ Id: '1' }]); + expect(rsp.body).to.deep.equal([{ Id: 1 }]); - // // check that it's gone + // check that it's gone await ncAxiosGet({ url: `/api/v1/tables/${table.id}/rows/1`, status: 404, @@ -839,7 +830,7 @@ function textBased() { it('Delete: bulk', async function () { const rsp = await ncAxiosDelete({ body: [{ Id: 1 }, { Id: 2 }] }); - expect(rsp.body).to.deep.equal([{ Id: '1' }, { Id: '2' }]); + expect(rsp.body).to.deep.equal([{ Id: 1 }, { Id: 2 }]); // check that it's gone await ncAxiosGet({ @@ -855,11 +846,6 @@ function textBased() { // Error handling it('Delete: invalid ID', async function () { - // // Invalid project ID - // await ncAxiosDelete({ - // url: `/api/v1/tables/${table.id}/rows`, - // status: 404, - // }); // Invalid table ID await ncAxiosDelete({ url: `/api/v1/tables/123456789/rows`, @@ -867,7 +853,7 @@ function textBased() { status: 404, }); // Invalid row ID - await ncAxiosDelete({ body: { Id: 123456789 }, status: 400 }); + await ncAxiosDelete({ body: { Id: '123456789' }, status: 422 }); }); } @@ -1480,11 +1466,11 @@ function dateBased() { /////////////////////////////////////////////////////////////////////////////// export default function () { - // describe('General', generalDb); describe('Text based', textBased); describe('Numerical', numberBased); describe('Select based', selectBased); describe('Date based', dateBased); + // describe('General', generalDb); } /////////////////////////////////////////////////////////////////////////////// From 3fd393203b8b219bcef8e753966cb728f389689a Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 30 May 2023 17:26:54 +0530 Subject: [PATCH 43/97] feat: add validation for duplicate row Signed-off-by: Pranav C --- .../nocodb/src/services/data-table.service.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 00da4967c7..b630300c93 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -84,6 +84,8 @@ export class DataTableService { }) { const { model, view } = await this.getModelAndView(param); + await this.checkForDuplicateRow({ rows: param.body, model }); + const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ @@ -109,6 +111,9 @@ export class DataTableService { body: any; }) { const { model, view } = await this.getModelAndView(param); + + await this.checkForDuplicateRow({ rows: param.body, model }); + const base = await Base.get(model.base_id); const baseModel = await Model.getBaseModelSQL({ id: model.id, @@ -197,4 +202,33 @@ export class DataTableService { return Array.isArray(body) ? result : result[0]; } + + private async checkForDuplicateRow({ + rows, + model, + }: { + rows: any[] | any; + model: Model; + }) { + if (!rows || !Array.isArray(rows) || rows.length === 1) { + return; + } + + await model.getColumns(); + + const keys = new Set(); + + for (const row of rows) { + let pk; + // if only one primary key then extract the value + if (model.primaryKeys.length === 1) pk = row[model.primaryKey.title]; + // if composite primary key then join the values with ___ + else pk = model.primaryKeys.map((pk) => row[pk.title]).join('___'); + // if duplicate then throw error + if (keys.has(pk)) { + NcError.unprocessableEntity('Duplicate row with id ' + pk); + } + keys.add(pk); + } + } } From 82e5377557ec15b43727a77d4c63a900699fe91f Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Tue, 30 May 2023 20:09:36 +0530 Subject: [PATCH 44/97] test: corrections (wip) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 2e21b16bda..8f19a8da56 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -1031,7 +1031,7 @@ function numberBased() { rsp = await ncAxiosGet({ url: `/api/v1/tables/${table.id}/rows/401`, }); - expect(rsp.body).to.deep.equal(records[0]); + expect(rsp.body).to.deep.equal({ Id: 401, ...records[0] }); /////////////////////////////////////////////////////////////////////////// @@ -1046,19 +1046,19 @@ function numberBased() { }; const updatedRecords = [ { - id: 401, + Id: 401, ...updatedRecord, }, { - id: 402, + Id: 402, ...updatedRecord, }, { - id: 403, + Id: 403, ...updatedRecord, }, { - id: 404, + Id: 404, ...updatedRecord, }, ]; @@ -1066,7 +1066,7 @@ function numberBased() { body: updatedRecords, }); expect(rsp.body).to.deep.equal( - updatedRecords.map((record) => ({ id: record.id })), + updatedRecords.map((record) => ({ Id: record.Id })), ); // verify updated records @@ -1082,10 +1082,10 @@ function numberBased() { // delete record with ID 401 to 404 rsp = await ncAxiosDelete({ - body: updatedRecords.map((record) => ({ id: record.id })), + body: updatedRecords.map((record) => ({ Id: record.Id })), }); expect(rsp.body).to.deep.equal( - updatedRecords.map((record) => ({ id: record.id })), + updatedRecords.map((record) => ({ Id: record.Id })), ); }); } @@ -1220,7 +1220,7 @@ function selectBased() { rsp = await ncAxiosGet({ url: `/api/v1/tables/${table.id}/rows/401`, }); - expect(rsp.body).to.deep.equal(records[0]); + expect(rsp.body).to.deep.equal({ Id: 401, ...records[0] }); /////////////////////////////////////////////////////////////////////////// @@ -1231,19 +1231,19 @@ function selectBased() { }; const updatedRecords = [ { - id: 401, + Id: 401, ...updatedRecord, }, { - id: 402, + Id: 402, ...updatedRecord, }, { - id: 403, + Id: 403, ...updatedRecord, }, { - id: 404, + Id: 404, ...updatedRecord, }, ]; @@ -1251,7 +1251,7 @@ function selectBased() { body: updatedRecords, }); expect(rsp.body).to.deep.equal( - updatedRecords.map((record) => ({ id: record.id })), + updatedRecords.map((record) => ({ Id: record.Id })), ); // verify updated records @@ -1267,10 +1267,10 @@ function selectBased() { // delete record with ID 401 to 404 rsp = await ncAxiosDelete({ - body: updatedRecords.map((record) => ({ id: record.id })), + body: updatedRecords.map((record) => ({ Id: record.Id })), }); expect(rsp.body).to.deep.equal( - updatedRecords.map((record) => ({ id: record.id })), + updatedRecords.map((record) => ({ Id: record.Id })), ); }); } @@ -1375,7 +1375,7 @@ function dateBased() { }, }); const pageInfo = { - totalRows: 400, + totalRows: 800, page: 1, pageSize: 10, isFirstPage: true, @@ -1393,43 +1393,43 @@ function dateBased() { body: records, }); - // prepare array with 10 Id's, from 401 to 410 + // prepare array with 10 Id's, from 801 to 810 const ids = []; - for (let i = 401; i <= 410; i++) { + for (let i = 801; i <= 810; i++) { ids.push({ Id: i }); } expect(rsp.body).to.deep.equal(ids); /////////////////////////////////////////////////////////////////////////// - // read record with Id 401 + // read record with Id 801 rsp = await ncAxiosGet({ - url: `/api/v1/tables/${table.id}/rows/401`, + url: `/api/v1/tables/${table.id}/rows/801`, }); - expect(rsp.body).to.deep.equal(records[0]); + expect(rsp.body).to.deep.equal({ Id: 801, ...records[0] }); /////////////////////////////////////////////////////////////////////////// - // update record with Id 401 to 404 + // update record with Id 801 to 804 const updatedRecord = { Date: '2022-04-25', DateTime: '2022-04-25T06:30:00.000Z', }; const updatedRecords = [ { - id: 401, + Id: 801, ...updatedRecord, }, { - id: 402, + Id: 802, ...updatedRecord, }, { - id: 403, + Id: 803, ...updatedRecord, }, { - id: 404, + Id: 804, ...updatedRecord, }, ]; @@ -1437,26 +1437,26 @@ function dateBased() { body: updatedRecords, }); expect(rsp.body).to.deep.equal( - updatedRecords.map((record) => ({ id: record.id })), + updatedRecords.map((record) => ({ Id: record.Id })), ); // verify updated records rsp = await ncAxiosGet({ query: { limit: 4, - offset: 400, + offset: 800, }, }); expect(rsp.body.list).to.deep.equal(updatedRecords); /////////////////////////////////////////////////////////////////////////// - // delete record with ID 401 to 404 + // delete record with ID 801 to 804 rsp = await ncAxiosDelete({ - body: updatedRecords.map((record) => ({ id: record.id })), + body: updatedRecords.map((record) => ({ Id: record.Id })), }); expect(rsp.body).to.deep.equal( - updatedRecords.map((record) => ({ id: record.id })), + updatedRecords.map((record) => ({ Id: record.Id })), ); }); } From c513226661b9eed13032124113276ab256230f93 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Tue, 30 May 2023 21:14:37 +0530 Subject: [PATCH 45/97] test: linked fields related Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 190 +++++++++++++++++- 1 file changed, 186 insertions(+), 4 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 8f19a8da56..efabb4ab13 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -89,7 +89,11 @@ import init from '../../init'; import { createProject, createSakilaProject } from '../../factory/project'; import { createTable, getTable } from '../../factory/table'; import { createBulkRows, listRow, rowMixedValue } from '../../factory/row'; -import { customColumns } from '../../factory/column'; +import { + createLookupColumn, + createRollupColumn, + customColumns, +} from '../../factory/column'; import { createView, updateView } from '../../factory/view'; import type { Api } from 'nocodb-sdk'; @@ -110,6 +114,12 @@ let insertedRecords: any[] = []; let sakilaProject: Project; let customerTable: Model; let customerColumns; +let actorTable: Model; +let actorColumns; +let countryTable: Model; +let countryColumns; +let cityTable: Model; +let cityColumns; // Optimisation scope for time reduction // 1. BeforeEach can be changed to BeforeAll for List and Read APIs @@ -192,10 +202,179 @@ function generalDb() { name: 'customer', }); customerColumns = await customerTable.getColumns(); + + actorTable = await getTable({ + project: sakilaProject, + name: 'actor', + }); + actorColumns = await actorTable.getColumns(); + + countryTable = await getTable({ + project: sakilaProject, + name: 'country', + }); + countryColumns = await countryTable.getColumns(); + + cityTable = await getTable({ + project: sakilaProject, + name: 'city', + }); + cityColumns = await cityTable.getColumns(); + }); + + it('Nested List - Link to another record', async function () { + const expectedRecords = [ + [ + { + CityId: 251, + City: 'Kabul', + }, + ], + [ + { + CityId: 59, + City: 'Batna', + }, + { + CityId: 63, + City: 'Bchar', + }, + { + CityId: 483, + City: 'Skikda', + }, + ], + [ + { + CityId: 516, + City: 'Tafuna', + }, + ], + [ + { + CityId: 67, + City: 'Benguela', + }, + { + CityId: 360, + City: 'Namibe', + }, + ], + ]; + + // read first 4 records + const records = await ncAxiosGet({ + url: `/api/v1/tables/${countryTable.id}/rows`, + query: { + limit: 4, + }, + }); + expect(records.body.list.length).to.equal(4); + + // extract LTAR column "City List" + const cityList = records.body.list.map((r) => r['City List']); + expect(cityList).to.deep.equal(expectedRecords); + }); + + it('Nested List - Lookup', async function () { + const lookupColumn = await createLookupColumn(context, { + project: sakilaProject, + title: 'Lookup', + table: countryTable, + relatedTableName: cityTable.table_name, + relatedTableColumnTitle: 'City', + }); + + const expectedRecords = [ + ['Kabul'], + ['Batna', 'Bchar', 'Skikda'], + ['Tafuna'], + ['Benguela', 'Namibe'], + ]; + + // read first 4 records + const records = await ncAxiosGet({ + url: `/api/v1/tables/${countryTable.id}/rows`, + query: { + limit: 4, + }, + }); + expect(records.body.list.length).to.equal(4); + + // extract Lookup column + const lookupData = records.body.list.map((record) => record.Lookup); + expect(lookupData).to.deep.equal(expectedRecords); + }); + + it('Nested List - Rollup', async function () { + const rollupColumn = await createRollupColumn(context, { + project: sakilaProject, + title: 'Rollup', + table: countryTable, + relatedTableName: cityTable.table_name, + relatedTableColumnTitle: 'City', + rollupFunction: 'count', + }); + + const expectedRecords = [1, 3, 1, 2]; + + // read first 4 records + const records = await ncAxiosGet({ + url: `/api/v1/tables/${countryTable.id}/rows`, + query: { + limit: 4, + }, + }); + expect(records.body.list.length).to.equal(4); + + // extract Lookup column + const rollupData = records.body.list.map((record) => record.Rollup); + expect(rollupData).to.deep.equal(expectedRecords); }); - it('should list all records', async function () { - console.log('should list all records'); + it('Nested Read - Link to another record', async function () { + const records = await ncAxiosGet({ + url: `/api/v1/tables/${countryTable.id}/rows/1`, + }); + + // extract LTAR column "City List" + expect(records.body['City List']).to.deep.equal([ + { + CityId: 251, + City: 'Kabul', + }, + ]); + }); + + it('Nested Read - Lookup', async function () { + const lookupColumn = await createLookupColumn(context, { + project: sakilaProject, + title: 'Lookup', + table: countryTable, + relatedTableName: cityTable.table_name, + relatedTableColumnTitle: 'City', + }); + + const records = await ncAxiosGet({ + url: `/api/v1/tables/${countryTable.id}/rows/1`, + }); + expect(records.body.Lookup).to.deep.equal(['Kabul']); + }); + + it('Nested Read - Rollup', async function () { + const rollupColumn = await createRollupColumn(context, { + project: sakilaProject, + title: 'Rollup', + table: countryTable, + relatedTableName: cityTable.table_name, + relatedTableColumnTitle: 'City', + rollupFunction: 'count', + }); + + const records = await ncAxiosGet({ + url: `/api/v1/tables/${countryTable.id}/rows/1`, + }); + expect(records.body.Rollup).to.equal(1); }); } @@ -1466,11 +1645,14 @@ function dateBased() { /////////////////////////////////////////////////////////////////////////////// export default function () { + // based out of sakila db, for link based tests + describe('General', generalDb); + + // standalone tables describe('Text based', textBased); describe('Numerical', numberBased); describe('Select based', selectBased); describe('Date based', dateBased); - // describe('General', generalDb); } /////////////////////////////////////////////////////////////////////////////// From 5256f9d79f4cf14ad35992e18c012cc7d01d017d Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:40:44 +0530 Subject: [PATCH 46/97] test: link api verification : has-many Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/tests/unit/factory/column.ts | 4 +- .../tests/unit/rest/tests/newDataApis.test.ts | 338 +++++++++++++++++- 2 files changed, 340 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/tests/unit/factory/column.ts b/packages/nocodb/tests/unit/factory/column.ts index 27818e0db2..6b810c7324 100644 --- a/packages/nocodb/tests/unit/factory/column.ts +++ b/packages/nocodb/tests/unit/factory/column.ts @@ -46,7 +46,7 @@ const defaultColumns = function (context) { ]; }; -const customColumns = function (type: string) { +const customColumns = function (type: string, options: any = {}) { switch (type) { case 'textBased': return [ @@ -157,6 +157,8 @@ const customColumns = function (type: string) { dtxp: "'jan','feb','mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'", }, ]; + case 'custom': + return [{ title: 'Id', uidt: UITypes.ID }, ...options]; } }; diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index efabb4ab13..1e14038b83 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -85,6 +85,7 @@ import 'mocha'; import { UITypes, ViewTypes } from 'nocodb-sdk'; import { expect } from 'chai'; import request from 'supertest'; +import { Api } from 'nocodb-sdk'; import init from '../../init'; import { createProject, createSakilaProject } from '../../factory/project'; import { createTable, getTable } from '../../factory/table'; @@ -95,7 +96,6 @@ import { customColumns, } from '../../factory/column'; import { createView, updateView } from '../../factory/view'; -import type { Api } from 'nocodb-sdk'; import type { ColumnType } from 'nocodb-sdk'; import type Project from '../../../../src/models/Project'; @@ -189,6 +189,51 @@ async function ncAxiosDelete({ /////////////////////////////////////////////////////////////////////////////// +async function ncAxiosLinkGet({ + urlParams: { tableId, linkId, rowId }, + query = {}, + status = 200, +}: { urlParams?: any; query?: any; status?: number } = {}) { + const urlParams = { tableId, linkId, rowId }; + const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; + const response = await request(context.app) + .get(url) + .set('xc-auth', context.token) + .query(query) + .send({}); + expect(response.status).to.equal(status); + return response; +} +async function ncAxiosLinkAdd({ + urlParams: { tableId, linkId, rowId }, + body = {}, + status = 200, +}: { urlParams?: any; body?: any; status?: number } = {}) { + const urlParams = { tableId, linkId, rowId }; + const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; + const response = await request(context.app) + .post(url) + .set('xc-auth', context.token) + .send(body); + expect(response.status).to.equal(status); + return response; +} +async function ncAxiosLinkRemove({ + urlParams: { tableId, linkId, rowId }, + body = {}, + status = 200, +}: { urlParams?: any; body?: any; status?: number } = {}) { + const urlParams = { tableId, linkId, rowId }; + const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; + const response = await request(context.app) + .delete(url) + .set('xc-auth', context.token) + .send(body); + expect(response.status).to.equal(status); + return response; +} + +/////////////////////////////////////////////////////////////////////////////// // generic table, sakila based function generalDb() { beforeEach(async function () { @@ -1640,6 +1685,296 @@ function dateBased() { }); } +function linkBased() { + let tblCity: Model; + let tblCountry: Model; + let tblActor: Model; + let tblFilm: Model; + + async function prepareRecords(title: string, count: number) { + const records = []; + for (let i = 1; i <= count; i++) { + records.push({ + Id: i, + [title]: `${title} ${i}`, + }); + } + return records; + } + + // prepare data for test cases + beforeEach(async function () { + context = await init(); + const project = await createProject(context); + + api = new Api({ + baseURL: `http://localhost:8080/`, + headers: { + 'xc-auth': context.token, + }, + }); + + const columns = [ + { title: 'Title', uidt: UITypes.SingleLineText, pv: true }, + ]; + + // Prepare City table + columns[0].title = 'City'; + tblCity = await createTable(context, project, { + title: 'City', + columns: customColumns('custom', columns), + }); + const cityRecords = await prepareRecords('City', 100); + await api.dbTableRow.bulkCreate( + 'noco', + project.id, + tblCity.id, + cityRecords, + ); + + // Prepare Country table + columns[0].title = 'Country'; + tblCountry = await createTable(context, project, { + title: 'Country', + columns: customColumns('custom', columns), + }); + const countryRecords = await prepareRecords('Country', 10); + await api.dbTableRow.bulkCreate( + 'noco', + project.id, + tblCountry.id, + countryRecords, + ); + + // Prepare Actor table + columns[0].title = 'Actor'; + tblActor = await createTable(context, project, { + title: 'Actor', + columns: customColumns('custom', columns), + }); + const actorRecords = await prepareRecords('Actor', 100); + await api.dbTableRow.bulkCreate( + 'noco', + project.id, + tblActor.id, + actorRecords, + ); + + // Prepare Movie table + columns[0].title = 'Film'; + tblFilm = await createTable(context, project, { + title: 'Film', + columns: customColumns('custom', columns), + }); + const filmRecords = await prepareRecords('Film', 100); + await api.dbTableRow.bulkCreate( + 'noco', + project.id, + tblFilm.id, + filmRecords, + ); + + // Create links + // Country City + await api.dbTableColumn.create(tblCountry.id, { + uidt: UITypes.LinkToAnotherRecord, + title: `Cities`, + parentId: tblCountry.id, + childId: tblCity.id, + type: 'hm', + }); + + // Actor Film + await api.dbTableColumn.create(tblActor.id, { + uidt: UITypes.LinkToAnotherRecord, + title: `Films`, + parentId: tblActor.id, + childId: tblFilm.id, + type: 'mm', + }); + }); + + // Create hm link between Country and City + // List them for a record & verify in both tables + it('Create Has-Many ', async function () { + for (let i = 1; i <= 10; i++) { + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: i, + }, + body: { + links: [10 * i + 1, 10 * i + 2, 10 * i + 3, 10 * i + 4, 10 * i + 5], + }, + }); + } + + // verify in Country table + for (let i = 1; i <= 10; i++) { + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: i, + }, + }); + expect(rsp.body).to.deep.equal({ + links: [10 * i + 1, 10 * i + 2, 10 * i + 3, 10 * i + 4, 10 * i + 5], + }); + } + + // verify in City table + for (let i = 1; i <= 100; i++) { + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCity.id, + linkId: tblCity.columns[2].id, + rowId: i, + }, + }); + if (i % 10 <= 5 && i % 10 > 0) { + expect(rsp.body).to.deep.equal({ + links: [Math.ceil(i / 10)], + }); + } else { + expect(rsp.body).to.deep.equal({ + links: [], + }); + } + } + }); + + // Update hm link between Country and City + // List them for a record & verify in both tables + it('Update Has-Many ', async function () { + for (let i = 1; i <= 10; i++) { + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: i, + }, + body: { + links: [10 * i + 6, 10 * i + 7], + }, + }); + } + + // verify in Country table + for (let i = 1; i <= 10; i++) { + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: i, + }, + }); + expect(rsp.body).to.deep.equal({ + links: [ + 10 * i + 1, + 10 * i + 2, + 10 * i + 3, + 10 * i + 4, + 10 * i + 5, + 10 * i + 6, + 10 * i + 7, + ], + }); + } + + // verify in City table + for (let i = 1; i <= 100; i++) { + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCity.id, + linkId: tblCity.columns[2].id, + rowId: i, + }, + }); + if (i % 10 <= 7 && i % 10 > 0) { + expect(rsp.body).to.deep.equal({ + links: [Math.ceil(i / 10)], + }); + } else { + expect(rsp.body).to.deep.equal({ + links: [], + }); + } + } + }); + + // Delete hm link between Country and City + // List them for a record & verify in both tables + it('Delete Has-Many ', async function () { + for (let i = 1; i <= 10; i++) { + await ncAxiosLinkRemove({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: i, + }, + body: { + links: [10 * i + 6, 10 * i + 7], + }, + }); + } + + // verify in Country table + for (let i = 1; i <= 10; i++) { + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: i, + }, + }); + expect(rsp.body).to.deep.equal({ + links: [10 * i + 1, 10 * i + 2, 10 * i + 3, 10 * i + 4, 10 * i + 5], + }); + } + + // verify in City table + for (let i = 1; i <= 100; i++) { + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCity.id, + linkId: tblCity.columns[2].id, + rowId: i, + }, + }); + if (i % 10 <= 5 && i % 10 > 0) { + expect(rsp.body).to.deep.equal({ + links: [Math.ceil(i / 10)], + }); + } else { + expect(rsp.body).to.deep.equal({ + links: [], + }); + } + } + }); + + // Create mm link between Actor and Film + // List them for a record & verify in both tables + it('Create Many-Many ', async function () {}); + + // Update mm link between Actor and Film + // List them for a record & verify in both tables + it('Update Many-Many ', async function () {}); + + // Delete mm link between Actor and Film + // List them for a record & verify in both tables + it('Delete Many-Many ', async function () {}); + + // Other scenarios + // Has-many : change an existing link to a new one + + // limit & offset verification + + // invalid link id +} + /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// @@ -1653,6 +1988,7 @@ export default function () { describe('Numerical', numberBased); describe('Select based', selectBased); describe('Date based', dateBased); + // describe('Link based', linkBased); } /////////////////////////////////////////////////////////////////////////////// From 94ac8705e2e58d216b40975f3a90ee001b9d7b61 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 15 Jun 2023 12:46:31 +0530 Subject: [PATCH 47/97] test: nested API for MM Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 419 ++++++++++++++---- 1 file changed, 332 insertions(+), 87 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 1e14038b83..f5fe5729ca 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -1794,22 +1794,64 @@ function linkBased() { }); }); - // Create hm link between Country and City - // List them for a record & verify in both tables - it('Create Has-Many ', async function () { + it('Has-Many ', async function () { + // Create hm link between Country and City + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: [1, 2, 3, 4, 5], + }, + }); + + // verify in Country table + let rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ + links: [1, 2, 3, 4, 5], + }); + + // verify in City table for (let i = 1; i <= 10; i++) { - await ncAxiosLinkAdd({ + const rsp = await ncAxiosLinkGet({ urlParams: { - tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + tableId: tblCity.id, + linkId: tblCity.columns[2].id, rowId: i, }, - body: { - links: [10 * i + 1, 10 * i + 2, 10 * i + 3, 10 * i + 4, 10 * i + 5], - }, }); + if (i <= 5) { + expect(rsp.body).to.deep.equal({ + links: [i], + }); + } else { + expect(rsp.body).to.deep.equal({ + links: [], + }); + } } + // Update hm link between Country and City + // List them for a record & verify in both tables + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: [6, 7], + }, + }); + // verify in Country table for (let i = 1; i <= 10; i++) { const rsp = await ncAxiosLinkGet({ @@ -1820,12 +1862,13 @@ function linkBased() { }, }); expect(rsp.body).to.deep.equal({ - links: [10 * i + 1, 10 * i + 2, 10 * i + 3, 10 * i + 4, 10 * i + 5], + links: [1, 2, 3, 4, 5, 6, 7], }); } // verify in City table - for (let i = 1; i <= 100; i++) { + // verify in City table + for (let i = 1; i <= 10; i++) { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, @@ -1833,9 +1876,9 @@ function linkBased() { rowId: i, }, }); - if (i % 10 <= 5 && i % 10 > 0) { + if (i <= 7) { expect(rsp.body).to.deep.equal({ - links: [Math.ceil(i / 10)], + links: [i], }); } else { expect(rsp.body).to.deep.equal({ @@ -1843,48 +1886,34 @@ function linkBased() { }); } } - }); - // Update hm link between Country and City - // List them for a record & verify in both tables - it('Update Has-Many ', async function () { - for (let i = 1; i <= 10; i++) { - await ncAxiosLinkAdd({ - urlParams: { - tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, - rowId: i, - }, - body: { - links: [10 * i + 6, 10 * i + 7], - }, - }); - } + // Delete hm link between Country and City + // List them for a record & verify in both tables + await ncAxiosLinkRemove({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: [1, 3, 5, 7], + }, + }); // verify in Country table - for (let i = 1; i <= 10; i++) { - const rsp = await ncAxiosLinkGet({ - urlParams: { - tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, - rowId: i, - }, - }); - expect(rsp.body).to.deep.equal({ - links: [ - 10 * i + 1, - 10 * i + 2, - 10 * i + 3, - 10 * i + 4, - 10 * i + 5, - 10 * i + 6, - 10 * i + 7, - ], - }); - } + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ + links: [2, 4, 6], + }); // verify in City table - for (let i = 1; i <= 100; i++) { + for (let i = 1; i <= 10; i++) { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, @@ -1892,7 +1921,7 @@ function linkBased() { rowId: i, }, }); - if (i % 10 <= 7 && i % 10 > 0) { + if (i % 2 === 0 && i <= 6) { expect(rsp.body).to.deep.equal({ links: [Math.ceil(i / 10)], }); @@ -1904,48 +1933,135 @@ function linkBased() { } }); - // Delete hm link between Country and City + // Create mm link between Actor and Film // List them for a record & verify in both tables - it('Delete Has-Many ', async function () { - for (let i = 1; i <= 10; i++) { - await ncAxiosLinkRemove({ - urlParams: { - tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, - rowId: i, - }, - body: { - links: [10 * i + 6, 10 * i + 7], - }, - }); - } - // verify in Country table - for (let i = 1; i <= 10; i++) { + function initializeArrayFromSequence(i, count) { + return Array.from({ length: count }, (_, index) => i + index); + } + + it('Create Many-Many ', async function () { + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblActor.id, + linkId: tblActor.columns[2].id, + rowId: 1, + }, + body: { + links: initializeArrayFromSequence(1, 20), + }, + }); + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblFilm.id, + linkId: tblFilm.columns[2].id, + rowId: 1, + }, + body: { + links: initializeArrayFromSequence(1, 20), + }, + }); + + // verify in Actor table + let rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblActor.id, + linkId: tblActor.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ + links: initializeArrayFromSequence(1, 20), + }); + + // verify in Film table + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblFilm.id, + linkId: tblFilm.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ + links: initializeArrayFromSequence(1, 20), + }); + + // Update mm link between Actor and Film + // List them for a record & verify in both tables + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblActor.id, + linkId: tblActor.columns[2].id, + rowId: 1, + }, + body: { + links: initializeArrayFromSequence(21, 30), + }, + }); + + // verify in Actor table + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblActor.id, + linkId: tblActor.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ + links: initializeArrayFromSequence(1, 30), + }); + + // verify in Film table + for (let i = 21; i <= 30; i++) { const rsp = await ncAxiosLinkGet({ urlParams: { - tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + tableId: tblFilm.id, + linkId: tblFilm.columns[2].id, rowId: i, }, }); expect(rsp.body).to.deep.equal({ - links: [10 * i + 1, 10 * i + 2, 10 * i + 3, 10 * i + 4, 10 * i + 5], + links: initializeArrayFromSequence(1, 1), }); } - // verify in City table - for (let i = 1; i <= 100; i++) { + // Delete mm link between Actor and Film + // List them for a record & verify in both tables + await ncAxiosLinkRemove({ + urlParams: { + tableId: tblActor.id, + linkId: tblActor.columns[2].id, + rowId: 1, + }, + body: { + links: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29], + }, + }); + + // verify in Actor table + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblActor.id, + linkId: tblActor.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ + links: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30], + }); + + // verify in Film table + for (let i = 2; i <= 30; i++) { const rsp = await ncAxiosLinkGet({ urlParams: { - tableId: tblCity.id, - linkId: tblCity.columns[2].id, + tableId: tblFilm.id, + linkId: tblFilm.columns[2].id, rowId: i, }, }); - if (i % 10 <= 5 && i % 10 > 0) { + if (i % 2 === 0) { expect(rsp.body).to.deep.equal({ - links: [Math.ceil(i / 10)], + links: [1], }); } else { expect(rsp.body).to.deep.equal({ @@ -1955,24 +2071,153 @@ function linkBased() { } }); - // Create mm link between Actor and Film - // List them for a record & verify in both tables - it('Create Many-Many ', async function () {}); + // Other scenarios + // Has-many : change an existing link to a new one + it('Change an existing link to a new one', async function () { + // add a link + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: [1, 2, 3], + }, + }); - // Update mm link between Actor and Film - // List them for a record & verify in both tables - it('Update Many-Many ', async function () {}); + // update the link + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 2, + }, + body: { + links: [2, 3], + }, + }); - // Delete mm link between Actor and Film - // List them for a record & verify in both tables - it('Delete Many-Many ', async function () {}); + // verify record 1 + let rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + }); + expect(rsp.body).to.deep.equal({ links: [1] }); - // Other scenarios - // Has-many : change an existing link to a new one + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 2, + }, + }); + expect(rsp.body).to.deep.equal({ links: [2, 3] }); + }); // limit & offset verification + it('Limit & offset verification', async function () { + // add a link + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: initializeArrayFromSequence(1, 50), + }, + }); + + // verify record 1 + let rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + query: { + limit: 10, + offset: 0, + }, + }); + expect(rsp.body).to.deep.equal({ + links: initializeArrayFromSequence(1, 10), + }); + + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + query: { + limit: 10, + offset: 10, + }, + }); + expect(rsp.body).to.deep.equal({ + links: initializeArrayFromSequence(11, 20), + }); + + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + query: { + limit: 10, + offset: 40, + }, + }); + expect(rsp.body).to.deep.equal({ + links: initializeArrayFromSequence(41, 50), + }); + }); // invalid link id + it('Invalid link id', async function () { + // Link Add: Invalid link ID + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: [9999], + }, + status: 400, + }); + + // Invalid link field ID + const rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: 9999, + rowId: 19, + }, + status: 400, + }); + expect(rsp.body).to.deep.equal({ links: [] }); + + // Link Remove: Invalid link ID + await ncAxiosLinkRemove({ + urlParams: { + tableId: tblCountry.id, + linkId: tblCountry.columns[2].id, + rowId: 1, + }, + body: { + links: [9999], + }, + status: 400, + }); + }); } /////////////////////////////////////////////////////////////////////////////// From 60e2ccad8c87620906f963c151a8f3c3bbb032d7 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 00:06:37 +0530 Subject: [PATCH 48/97] feat: add nested list api Signed-off-by: Pranav C --- .../src/controllers/data-table.controller.ts | 21 ++++++ .../nocodb/src/services/data-table.service.ts | 67 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index 808650b20b..7f46a68f3c 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -126,4 +126,25 @@ export class DataTableController { viewId, }); } + + + @Get(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Acl('nestedDataList') + async nestedDataList( + @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.nestedDataList({ + modelId, + rowId: rowId, + query: req.query, + viewId, + refRowIds, + columnId, + }); + } } diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index b630300c93..0042b0ae10 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; +import { RelationTypes, UITypes } from 'nocodb-sdk' import { NcError } from '../helpers/catchError'; -import { Base, Model, View } from '../models'; +import { PagedResponseImpl } from '../helpers/PagedResponse' +import { Base, Column, LinkToAnotherRecordColumn, Model, View } from '../models' +import { getColumnByIdOrName } from '../modules/datas/helpers' import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { DatasService } from './datas.service'; @@ -231,4 +234,66 @@ export class DataTableService { keys.add(pk); } } + + async nestedDataList(param: { + viewId: string; + modelId: string; + query: any; + rowId: string | string[] | number | number[]; + columnId: string; + }) { + const { model, view } = await this.getModelAndView(param); + 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 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 colOptions = await column.getColOptions( + ) + + let data:any[] ; + let count:number; + if(colOptions.type === RelationTypes.MANY_TO_MANY) { + data = await baseModel.mmList( + { + colId: column.id, + parentId: param.rowId, + }, + param.query as any, + ); + count = await baseModel.mmListCount({ + colId: column.id, + parentId: param.rowId, + }); + }else { + data = await baseModel.hmList( + { + colId: column.id, + id: param.rowId, + }, + param.query as any, + ); + count = await baseModel.hmListCount({ + colId: column.id, + id: param.rowId, + }); + } + return new PagedResponseImpl(data, { + count, + ...param.query, + }); + } } From 3c4850060434b6016db4d833bb950e981474a8d3 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 01:22:14 +0530 Subject: [PATCH 49/97] feat: add nested link/unlik (WIP) Signed-off-by: Pranav C --- .../src/controllers/data-table.controller.ts | 42 +++- packages/nocodb/src/db/BaseModelSqlv2.ts | 191 ++++++++++++++++++ .../nocodb/src/services/data-table.service.ts | 73 ++++++- 3 files changed, 294 insertions(+), 12 deletions(-) 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; + } } From 956f20a8f96126d5cfea1d3f9735a2c5be57e964 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 01:27:46 +0530 Subject: [PATCH 50/97] fix: update where condition in remove/add links method Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 36 ++++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 4f6157b6eb..c324f796a1 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3491,7 +3491,7 @@ class BaseModelSqlv2 { rowId, }: { cookie: any; - childIds: string | string[] | number | number[]; + childIds: (string|number)[]; colId: string; rowId: string; }) { @@ -3525,7 +3525,8 @@ class BaseModelSqlv2 { if (this.isSnowflake) { const parentPK = this.dbDriver(parentTn) .select(parentColumn.column_name) - .where(_wherePk(parentTable.primaryKeys, childId)) + // .where(_wherePk(parentTable.primaryKeys, childId)) + .whereIn(parentTable.primaryKey.column_name, childIds) .first(); const childPK = this.dbDriver(childTn) @@ -3541,7 +3542,8 @@ class BaseModelSqlv2 { await this.dbDriver(vTn).insert({ [vParentCol.column_name]: this.dbDriver(parentTn) .select(parentColumn.column_name) - .where(_wherePk(parentTable.primaryKeys, childId)) + // .where(_wherePk(parentTable.primaryKeys, childId)) + .where(parentTable.primaryKey.column_name, childIds) .first(), [vChildCol.column_name]: this.dbDriver(childTn) .select(childColumn.column_name) @@ -3563,7 +3565,8 @@ class BaseModelSqlv2 { .as('___cn_alias'), ), }) - .where(_wherePk(childTable.primaryKeys, childId)); + // .where(_wherePk(childTable.primaryKeys, childId)); + .whereIn(childTable.primaryKey.column_name, childIds); } break; case RelationTypes.BELONGS_TO: @@ -3573,7 +3576,8 @@ class BaseModelSqlv2 { [childColumn.column_name]: this.dbDriver.from( this.dbDriver(parentTn) .select(parentColumn.column_name) - .where(_wherePk(parentTable.primaryKeys, childId)) + // .where(_wherePk(parentTable.primaryKeys, childId)) + .whereIn(parentTable.primaryKey.column_name, childIds) .first() .as('___cn_alias'), ), @@ -3583,9 +3587,9 @@ class BaseModelSqlv2 { break; } - const response = await this.readByPk(rowId); - await this.afterInsert(response, this.dbDriver, cookie); - await this.afterAddChild(rowId, childId, cookie); + // const response = await this.readByPk(rowId); + // await this.afterInsert(response, this.dbDriver, cookie); + // await this.afterAddChild(rowId, childId, cookie); } async removeLinks({ @@ -3595,7 +3599,7 @@ class BaseModelSqlv2 { rowId, }: { cookie: any; - childIds: string | string[] | number | number[]; + childIds: (string|number)[]; colId: string; rowId: string; }) { @@ -3632,7 +3636,7 @@ class BaseModelSqlv2 { .where({ [vParentCol.column_name]: this.dbDriver(parentTn) .select(parentColumn.column_name) - .where(_wherePk(parentTable.primaryKeys, childId)) + .whereIn(parentTable.primaryKey.column_name,childIds) .first(), [vChildCol.column_name]: this.dbDriver(childTn) .select(childColumn.column_name) @@ -3651,7 +3655,8 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, rowId) // .first() // }) - .where(_wherePk(childTable.primaryKeys, childId)) + // .where(_wherePk(childTable.primaryKeys, childId)) + .whereIn(childTable.primaryKey.column_name, childIds) .update({ [childColumn.column_name]: null }); } break; @@ -3664,15 +3669,16 @@ class BaseModelSqlv2 { // .where(parentTable.primaryKey.cn, childId) // .first() // }) - .where(_wherePk(childTable.primaryKeys, rowId)) + // .where(_wherePk(childTable.primaryKeys, rowId)) + .where(childTable.primaryKey.column_name, 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); + // const newData = await this.readByPk(rowId); + // await this.afterUpdate(prevData, newData, this.dbDriver, cookie); + // await this.afterRemoveChild(rowId, childIds, cookie); } } From 8611b53a620018bf395378fa46a361f0ce8edc15 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 01:29:15 +0530 Subject: [PATCH 51/97] fix: pass values as array Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 6 +++--- packages/nocodb/src/services/data-table.service.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index c324f796a1..e73528f4fe 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3491,7 +3491,7 @@ class BaseModelSqlv2 { rowId, }: { cookie: any; - childIds: (string|number)[]; + childIds: (string | number)[]; colId: string; rowId: string; }) { @@ -3599,7 +3599,7 @@ class BaseModelSqlv2 { rowId, }: { cookie: any; - childIds: (string|number)[]; + childIds: (string | number)[]; colId: string; rowId: string; }) { @@ -3636,7 +3636,7 @@ class BaseModelSqlv2 { .where({ [vParentCol.column_name]: this.dbDriver(parentTn) .select(parentColumn.column_name) - .whereIn(parentTable.primaryKey.column_name,childIds) + .whereIn(parentTable.primaryKey.column_name, childIds) .first(), [vChildCol.column_name]: this.dbDriver(childTn) .select(childColumn.column_name) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index ddfb72efde..be733a49a5 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -317,7 +317,7 @@ export class DataTableService { await baseModel.addLinks({ colId: column.id, - childIds: param.refRowIds, + childIds: Array.isArray(param.refRowIds) ? param.refRowIds : [param.refRowIds], rowId: param.rowId, cookie: param.cookie, }); @@ -340,7 +340,7 @@ export class DataTableService { await baseModel.removeLinks({ colId: column.id, - childIds: param.refRowIds, + childIds: Array.isArray(param.refRowIds) ? param.refRowIds : [param.refRowIds], rowId: param.rowId, cookie: param.cookie, }); From 77f4b18723a0b93d23f39cd52c689572935ced24 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 13:17:03 +0530 Subject: [PATCH 52/97] feat: ids validation (WIP) Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index e73528f4fe..aa3a7ab640 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3501,6 +3501,13 @@ class BaseModelSqlv2 { if (!column || column.uidt !== UITypes.LinkToAnotherRecord) NcError.notFound('Column not found'); + // validate parentId + { + if (!this.dbDriver(this.tnPath).where(this._wherePk(rowId)).first()) { + NcError.notFound('Row not found'); + } + } + const colOptions = await column.getColOptions(); const childColumn = await colOptions.getChildColumn(); @@ -3522,6 +3529,30 @@ class BaseModelSqlv2 { const vTn = this.getTnPath(vTable); + // validate Ids + { + const childRowsQb = this.dbDriver(parentTn) + .select(parentColumn.column_name) + // .where(_wherePk(parentTable.primaryKeys, childId)) + .whereIn(parentTable.primaryKey.column_name, childIds); + + if (parentTable.primaryKey.column_name !== parentColumn.column_name) + childRowsQb.select(parentTable.primaryKey.column_name); + + const childRows = await childRowsQb; + + if (childRows.length !== childIds.length) { + const missingIds = childIds.filter( + (id) => + !childRows.find((r) => r[parentColumn.column_name] === id), + ); + + NcError.notFound( + `Child record with id ${missingIds.join(', ')} not found`, + ); + } + } + if (this.isSnowflake) { const parentPK = this.dbDriver(parentTn) .select(parentColumn.column_name) @@ -3555,6 +3586,26 @@ class BaseModelSqlv2 { break; case RelationTypes.HAS_MANY: { + // validate Ids + { + const childRowsQb = this.dbDriver(childTn) + .select(childTable.primaryKey.column_name) + .whereIn(childTable.primaryKey.column_name, childIds); + + const childRows = await childRowsQb; + + if (childRows.length !== childIds.length) { + const missingIds = childIds.filter( + (id) => + !childRows.find((r) => r[parentColumn.column_name] === id), + ); + + NcError.notFound( + `Child record with id ${missingIds.join(', ')} not found`, + ); + } + } + await this.dbDriver(childTn) .update({ [childColumn.column_name]: this.dbDriver.from( @@ -3571,6 +3622,27 @@ class BaseModelSqlv2 { break; case RelationTypes.BELONGS_TO: { + // validate Ids + { + const childRowsQb = this.dbDriver(parentTn) + .select(parentTable.primaryKey.column_name) + .whereIn(parentTable.primaryKey.column_name, childIds) + .first(); + + const childRows = await childRowsQb; + + if (childRows.length !== childIds.length) { + const missingIds = childIds.filter( + (id) => + !childRows.find((r) => r[parentColumn.column_name] === id), + ); + + NcError.notFound( + `Child record with id ${missingIds.join(', ')} not found`, + ); + } + } + await this.dbDriver(childTn) .update({ [childColumn.column_name]: this.dbDriver.from( From 2686dfd126a00b30aaaca630a6f77d5f200cadad Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 14:35:08 +0530 Subject: [PATCH 53/97] feat: ids validation - unlink (WIP) Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 69 ++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index aa3a7ab640..b4150f94e5 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3702,6 +3702,33 @@ class BaseModelSqlv2 { const vParentCol = await colOptions.getMMParentColumn(); const vTable = await colOptions.getMMModel(); + + + // validate Ids + { + const childRowsQb = this.dbDriver(parentTn) + .select(parentColumn.column_name) + // .where(_wherePk(parentTable.primaryKeys, childId)) + .whereIn(parentTable.primaryKey.column_name, childIds); + + if (parentTable.primaryKey.column_name !== parentColumn.column_name) + childRowsQb.select(parentTable.primaryKey.column_name); + + const childRows = await childRowsQb; + + if (childRows.length !== childIds.length) { + const missingIds = childIds.filter( + (id) => + !childRows.find((r) => r[parentColumn.column_name] === id), + ); + + NcError.notFound( + `Child record with id ${missingIds.join(', ')} not found`, + ); + } + } + + const vTn = this.getTnPath(vTable); await this.dbDriver(vTn) @@ -3720,6 +3747,27 @@ class BaseModelSqlv2 { break; case RelationTypes.HAS_MANY: { + + // validate Ids + { + const childRowsQb = this.dbDriver(childTn) + .select(childTable.primaryKey.column_name) + .whereIn(childTable.primaryKey.column_name, childIds); + + const childRows = await childRowsQb; + + if (childRows.length !== childIds.length) { + const missingIds = childIds.filter( + (id) => + !childRows.find((r) => r[parentColumn.column_name] === id), + ); + + NcError.notFound( + `Child record with id ${missingIds.join(', ')} not found`, + ); + } + } + await this.dbDriver(childTn) // .where({ // [childColumn.cn]: this.dbDriver(parentTable.tn) @@ -3734,6 +3782,27 @@ class BaseModelSqlv2 { break; case RelationTypes.BELONGS_TO: { + // validate Ids + { + const childRowsQb = this.dbDriver(parentTn) + .select(parentTable.primaryKey.column_name) + .whereIn(parentTable.primaryKey.column_name, childIds) + .first(); + + const childRows = await childRowsQb; + + if (childRows.length !== childIds.length) { + const missingIds = childIds.filter( + (id) => + !childRows.find((r) => r[parentColumn.column_name] === id), + ); + + NcError.notFound( + `Child record with id ${missingIds.join(', ')} not found`, + ); + } + } + await this.dbDriver(childTn) // .where({ // [childColumn.cn]: this.dbDriver(parentTable.tn) From bd1acf44268f5a950fb4da65c7e4346cddd8210b Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 15:30:54 +0530 Subject: [PATCH 54/97] feat: skip existing links while inserting and consider composite pk as well (WIP) Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 131 +++++++++++++++-------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index b4150f94e5..f362a6b779 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3501,11 +3501,13 @@ class BaseModelSqlv2 { if (!column || column.uidt !== UITypes.LinkToAnotherRecord) NcError.notFound('Column not found'); - // validate parentId - { - if (!this.dbDriver(this.tnPath).where(this._wherePk(rowId)).first()) { - NcError.notFound('Row not found'); - } + const row = await this.dbDriver(this.tnPath) + .where(this._wherePk(rowId)) + .first(); + + // validate rowId + if (!row) { + NcError.notFound('Row not found'); } const colOptions = await column.getColOptions(); @@ -3529,12 +3531,33 @@ class BaseModelSqlv2 { const vTn = this.getTnPath(vTable); + let insertData: Record[]; + // validate Ids { const childRowsQb = this.dbDriver(parentTn) .select(parentColumn.column_name) - // .where(_wherePk(parentTable.primaryKeys, childId)) - .whereIn(parentTable.primaryKey.column_name, childIds); + .select(`${vTable.table_name}.${vChildCol.column_name}`) + .leftJoin(vTn, (qb) => { + qb.on( + `${vTable.table_name}.${vParentCol.column_name}`, + `${parentTable.table_name}.${parentColumn.column_name}`, + ).orOn( + `${vTable.table_name}.${vChildCol.column_name}`, + row[childColumn.column_name], + ); + }); + // .where(_wherePk(parentTable.primaryKeys, childId)) + + if (parentTable.primaryKeys.length > 1) { + childRowsQb.where((qb) => { + for (const childId of childIds) { + qb.orWhere(_wherePk(parentTable.primaryKeys, childId)); + } + }); + } else { + childRowsQb.whereIn(parentTable.primaryKey.column_name, childIds); + } if (parentTable.primaryKey.column_name !== parentColumn.column_name) childRowsQb.select(parentTable.primaryKey.column_name); @@ -3551,46 +3574,66 @@ class BaseModelSqlv2 { `Child record with id ${missingIds.join(', ')} not found`, ); } - } - if (this.isSnowflake) { - const parentPK = this.dbDriver(parentTn) - .select(parentColumn.column_name) - // .where(_wherePk(parentTable.primaryKeys, childId)) - .whereIn(parentTable.primaryKey.column_name, childIds) - .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)) - .where(parentTable.primaryKey.column_name, childIds) - .first(), - [vChildCol.column_name]: this.dbDriver(childTn) - .select(childColumn.column_name) - .where(_wherePk(childTable.primaryKeys, rowId)) - .first(), - }); + insertData = childRows + // skip existing links + .filter((childRow) => !childRow[vChildCol.column_name]) + // generate insert data for new links + .map((childRow) => ({ + [vParentCol.column_name]: childRow[parentColumn.column_name], + [vChildCol.column_name]: row[childColumn.column_name], + })); } + + // if (this.isSnowflake) { + // const parentPK = this.dbDriver(parentTn) + // .select(parentColumn.column_name) + // // .where(_wherePk(parentTable.primaryKeys, childId)) + // .whereIn(parentTable.primaryKey.column_name, childIds) + // .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)) + // .where(parentTable.primaryKey.column_name, childIds) + // .first(), + // [vChildCol.column_name]: this.dbDriver(childTn) + // .select(childColumn.column_name) + // .where(_wherePk(childTable.primaryKeys, rowId)) + // .first(), + // }); + await this.dbDriver(vTn).bulkInsert(insertData); + // } } break; case RelationTypes.HAS_MANY: { // validate Ids { - const childRowsQb = this.dbDriver(childTn) - .select(childTable.primaryKey.column_name) - .whereIn(childTable.primaryKey.column_name, childIds); + const childRowsQb = this.dbDriver(childTn).select( + childTable.primaryKey.column_name, + ); + + if (childTable.primaryKeys.length > 1) { + childRowsQb.where((qb) => { + for (const childId of childIds) { + qb.orWhere(_wherePk(childTable.primaryKeys, childId)); + } + }); + } else { + childRowsQb.whereIn(parentTable.primaryKey.column_name, childIds); + } const childRows = await childRowsQb; @@ -3626,7 +3669,7 @@ class BaseModelSqlv2 { { const childRowsQb = this.dbDriver(parentTn) .select(parentTable.primaryKey.column_name) - .whereIn(parentTable.primaryKey.column_name, childIds) + .where(_wherePk(parentTable.primaryKeys, childIds[0])) .first(); const childRows = await childRowsQb; @@ -3648,8 +3691,8 @@ class BaseModelSqlv2 { [childColumn.column_name]: this.dbDriver.from( this.dbDriver(parentTn) .select(parentColumn.column_name) - // .where(_wherePk(parentTable.primaryKeys, childId)) - .whereIn(parentTable.primaryKey.column_name, childIds) + .where(_wherePk(parentTable.primaryKeys, childIds[0])) + // .whereIn(parentTable.primaryKey.column_name, childIds) .first() .as('___cn_alias'), ), @@ -3702,8 +3745,6 @@ class BaseModelSqlv2 { const vParentCol = await colOptions.getMMParentColumn(); const vTable = await colOptions.getMMModel(); - - // validate Ids { const childRowsQb = this.dbDriver(parentTn) @@ -3728,7 +3769,6 @@ class BaseModelSqlv2 { } } - const vTn = this.getTnPath(vTable); await this.dbDriver(vTn) @@ -3747,7 +3787,6 @@ class BaseModelSqlv2 { break; case RelationTypes.HAS_MANY: { - // validate Ids { const childRowsQb = this.dbDriver(childTn) From c9ebfad4671c8107ad6227b4cc08437cd6eddea0 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 15:58:29 +0530 Subject: [PATCH 55/97] fix: mm unlink implementation Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index f362a6b779..cae38e99ef 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3773,15 +3773,18 @@ class BaseModelSqlv2 { await this.dbDriver(vTn) .where({ - [vParentCol.column_name]: this.dbDriver(parentTn) - .select(parentColumn.column_name) - .whereIn(parentTable.primaryKey.column_name, childIds) - .first(), [vChildCol.column_name]: this.dbDriver(childTn) .select(childColumn.column_name) .where(_wherePk(childTable.primaryKeys, rowId)) .first(), }) + .whereIn( + [vParentCol.column_name], + this.dbDriver(parentTn) + .select(parentColumn.column_name) + .whereIn(parentTable.primaryKey.column_name, childIds) + .first(), + ) .delete(); } break; From ae1a31b4032d745ab935909ada3e611f7112a9e5 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 16:52:23 +0530 Subject: [PATCH 56/97] fix: typo corrections Signed-off-by: Pranav C --- .../src/controllers/data-table.controller.ts | 2 + packages/nocodb/src/db/BaseModelSqlv2.ts | 4 +- .../nocodb/src/modules/datas/datas.module.ts | 2 +- .../nocodb/src/services/data-table.service.ts | 77 ++++++++++++------- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index eaac1c6e6a..ba235db747 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -164,6 +164,7 @@ export class DataTableController { viewId, columnId, refRowIds, + cookie: req, }); } @@ -185,6 +186,7 @@ export class DataTableController { viewId, columnId, refRowIds, + cookie: req, }); } } diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index cae38e99ef..016e697b72 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3613,7 +3613,9 @@ class BaseModelSqlv2 { // .where(_wherePk(childTable.primaryKeys, rowId)) // .first(), // }); - await this.dbDriver(vTn).bulkInsert(insertData); + + // todo: use bulk insert + await this.dbDriver(vTn).insert(insertData); // } } break; diff --git a/packages/nocodb/src/modules/datas/datas.module.ts b/packages/nocodb/src/modules/datas/datas.module.ts index 3d72f2b1b4..9ee4adeecf 100644 --- a/packages/nocodb/src/modules/datas/datas.module.ts +++ b/packages/nocodb/src/modules/datas/datas.module.ts @@ -29,7 +29,7 @@ import { PublicDatasService } from '../../services/public-datas.service'; }), ], controllers: [ - process.env.NC_WORKER_CONTAINER !== 'true' + ...(process.env.NC_WORKER_CONTAINER !== 'true' ? [ DataTableController, DatasController, diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index be733a49a5..e301ed1267 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -1,11 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { RelationTypes, UITypes } from 'nocodb-sdk' +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, getViewAndModelByAliasOrId, PathParams } from '../modules/datas/helpers' +import { PagedResponseImpl } from '../helpers/PagedResponse'; +import { Base, Column, Model, View } from '../models'; +import { + getColumnByIdOrName, + getViewAndModelByAliasOrId, + PathParams, +} from '../modules/datas/helpers'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { DatasService } from './datas.service'; +import type { LinkToAnotherRecordColumn } from '../models'; @Injectable() export class DataTableService { @@ -250,14 +255,13 @@ export class DataTableService { viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); - const column = await this.getColumn(param) + const column = await this.getColumn(param); - const colOptions = await column.getColOptions( - ) + const colOptions = await column.getColOptions(); - let data:any[] ; - let count:number; - if(colOptions.type === RelationTypes.MANY_TO_MANY) { + let data: any[]; + let count: number; + if (colOptions.type === RelationTypes.MANY_TO_MANY) { data = await baseModel.mmList( { colId: column.id, @@ -265,11 +269,11 @@ export class DataTableService { }, param.query as any, ); - count = await baseModel.mmListCount({ + count = (await baseModel.mmListCount({ colId: column.id, parentId: param.rowId, - }); - }else { + })) as number; + } else { data = await baseModel.hmList( { colId: column.id, @@ -277,10 +281,10 @@ export class DataTableService { }, param.query as any, ); - count = await baseModel.hmListCount({ + count = (await baseModel.hmListCount({ colId: column.id, id: param.rowId, - }); + })) as number; } return new PagedResponseImpl(data, { count, @@ -288,20 +292,28 @@ export class DataTableService { }); } - private async getColumn(param: {modelId: string; columnId: string }) { - const column = await Column.get({ colId: param.columnId }) + 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) NcError.badRequest('Column not found'); if (column.fk_model_id !== param.modelId) - NcError.badRequest('Column not belong to model') + NcError.badRequest('Column not belong to model'); if (column.uidt !== UITypes.LinkToAnotherRecord) - NcError.badRequest('Column is not LTAR') - return column + 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 }) { + 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'); @@ -317,14 +329,25 @@ export class DataTableService { await baseModel.addLinks({ colId: column.id, - childIds: Array.isArray(param.refRowIds) ? param.refRowIds : [param.refRowIds], + childIds: Array.isArray(param.refRowIds) + ? param.refRowIds + : [param.refRowIds], rowId: param.rowId, cookie: param.cookie, }); - return true; } + return true; + } - async nestedUnlink(param: { cookie:any;viewId: string; modelId: string; columnId: string; query: any; refRowIds: string | string[] | number | number[]; rowId: string }) { + 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'); @@ -340,7 +363,9 @@ export class DataTableService { await baseModel.removeLinks({ colId: column.id, - childIds: Array.isArray(param.refRowIds) ? param.refRowIds : [param.refRowIds], + childIds: Array.isArray(param.refRowIds) + ? param.refRowIds + : [param.refRowIds], rowId: param.rowId, cookie: param.cookie, }); From efadf11e94ca4c63b05464ad4695ec8b047f1527 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 17:36:17 +0530 Subject: [PATCH 57/97] fix: typo correction Signed-off-by: Pranav C --- packages/nocodb/src/controllers/data-table.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index ba235db747..a67526c6f9 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -147,8 +147,8 @@ export class DataTableController { } - @Get(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) - @Post('nestedDataLink') + @Post(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Acl('nestedDataLink') async nestedLink( @Request() req, @Param('modelId') modelId: string, From 5b04a70d5b44ae617d1f85f8ba74a0b112e24a53 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 18:23:39 +0530 Subject: [PATCH 58/97] fix: link/unlink - mm Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 016e697b72..6b7b6424c7 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3542,7 +3542,7 @@ class BaseModelSqlv2 { qb.on( `${vTable.table_name}.${vParentCol.column_name}`, `${parentTable.table_name}.${parentColumn.column_name}`, - ).orOn( + ).andOn( `${vTable.table_name}.${vChildCol.column_name}`, row[childColumn.column_name], ); @@ -3583,6 +3583,9 @@ class BaseModelSqlv2 { [vParentCol.column_name]: childRow[parentColumn.column_name], [vChildCol.column_name]: row[childColumn.column_name], })); + + // if no new links, return true + if(!insertData.length) return true } // if (this.isSnowflake) { @@ -3785,7 +3788,6 @@ class BaseModelSqlv2 { this.dbDriver(parentTn) .select(parentColumn.column_name) .whereIn(parentTable.primaryKey.column_name, childIds) - .first(), ) .delete(); } From bb6f311b4873051e4b5c2cb86f1050476715f282 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 18:43:08 +0530 Subject: [PATCH 59/97] fix: add missing await and validation Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 6b7b6424c7..82ec7f99fc 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3502,7 +3502,7 @@ class BaseModelSqlv2 { NcError.notFound('Column not found'); const row = await this.dbDriver(this.tnPath) - .where(this._wherePk(rowId)) + .where(await this._wherePk(rowId)) .first(); // validate rowId @@ -3585,7 +3585,7 @@ class BaseModelSqlv2 { })); // if no new links, return true - if(!insertData.length) return true + if (!insertData.length) return true; } // if (this.isSnowflake) { @@ -3729,6 +3729,15 @@ class BaseModelSqlv2 { if (!column || column.uidt !== UITypes.LinkToAnotherRecord) NcError.notFound('Column not found'); + const row = await this.dbDriver(this.tnPath) + .where(await this._wherePk(rowId)) + .first(); + + // validate rowId + if (!row) { + NcError.notFound('Row not found'); + } + const colOptions = await column.getColOptions(); const childColumn = await colOptions.getChildColumn(); @@ -3787,7 +3796,7 @@ class BaseModelSqlv2 { [vParentCol.column_name], this.dbDriver(parentTn) .select(parentColumn.column_name) - .whereIn(parentTable.primaryKey.column_name, childIds) + .whereIn(parentTable.primaryKey.column_name, childIds), ) .delete(); } From d5e41e7621bd82ed22b3182aa23d247e8068bbf9 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 15 Jun 2023 18:57:16 +0530 Subject: [PATCH 60/97] fix: bt link api correction Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 28 ++++++------------------ 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 82ec7f99fc..f161de2d7c 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3677,17 +3677,10 @@ class BaseModelSqlv2 { .where(_wherePk(parentTable.primaryKeys, childIds[0])) .first(); - const childRows = await childRowsQb; + const childRow = await childRowsQb; - if (childRows.length !== childIds.length) { - const missingIds = childIds.filter( - (id) => - !childRows.find((r) => r[parentColumn.column_name] === id), - ); - - NcError.notFound( - `Child record with id ${missingIds.join(', ')} not found`, - ); + if (!childRow) { + NcError.notFound(`Child record with id ${childIds[0]} not found`); } } @@ -3841,20 +3834,13 @@ class BaseModelSqlv2 { { const childRowsQb = this.dbDriver(parentTn) .select(parentTable.primaryKey.column_name) - .whereIn(parentTable.primaryKey.column_name, childIds) + .where(_wherePk(parentTable.primaryKeys, childIds[0])) .first(); - const childRows = await childRowsQb; - - if (childRows.length !== childIds.length) { - const missingIds = childIds.filter( - (id) => - !childRows.find((r) => r[parentColumn.column_name] === id), - ); + const childRow = await childRowsQb; - NcError.notFound( - `Child record with id ${missingIds.join(', ')} not found`, - ); + if (!childRow) { + NcError.notFound(`Child record with id ${childIds[0]} not found`); } } From 7f11d6007ae3539999b4945f23e1499e9a638f64 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Fri, 16 Jun 2023 13:15:20 +0530 Subject: [PATCH 61/97] test: has-many corrections Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../src/controllers/data-table.controller.ts | 9 +- packages/nocodb/tests/unit/factory/column.ts | 2 +- .../tests/unit/rest/tests/newDataApis.test.ts | 396 ++++++++++-------- 3 files changed, 217 insertions(+), 190 deletions(-) diff --git a/packages/nocodb/src/controllers/data-table.controller.ts b/packages/nocodb/src/controllers/data-table.controller.ts index a67526c6f9..5d6b6ec573 100644 --- a/packages/nocodb/src/controllers/data-table.controller.ts +++ b/packages/nocodb/src/controllers/data-table.controller.ts @@ -127,8 +127,7 @@ export class DataTableController { }); } - - @Get(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Get(['/api/v1/tables/:modelId/links/:columnId/rows/:rowId']) @Acl('nestedDataList') async nestedDataList( @Request() req, @@ -146,8 +145,7 @@ export class DataTableController { }); } - - @Post(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Post(['/api/v1/tables/:modelId/links/:columnId/rows/:rowId']) @Acl('nestedDataLink') async nestedLink( @Request() req, @@ -168,8 +166,7 @@ export class DataTableController { }); } - - @Delete(['/api/v1/tables/:modelId/links/:columnId/row/:rowId']) + @Delete(['/api/v1/tables/:modelId/links/:columnId/rows/:rowId']) @Acl('nestedDataUnlink') async nestedUnlink( @Request() req, diff --git a/packages/nocodb/tests/unit/factory/column.ts b/packages/nocodb/tests/unit/factory/column.ts index 6b810c7324..0655e25c18 100644 --- a/packages/nocodb/tests/unit/factory/column.ts +++ b/packages/nocodb/tests/unit/factory/column.ts @@ -158,7 +158,7 @@ const customColumns = function (type: string, options: any = {}) { }, ]; case 'custom': - return [{ title: 'Id', uidt: UITypes.ID }, ...options]; + return [{ title: 'Id', column_name: 'Id', uidt: UITypes.ID }, ...options]; } }; diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index f5fe5729ca..9d1a732e23 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -85,13 +85,13 @@ import 'mocha'; import { UITypes, ViewTypes } from 'nocodb-sdk'; import { expect } from 'chai'; import request from 'supertest'; -import { Api } from 'nocodb-sdk'; import init from '../../init'; import { createProject, createSakilaProject } from '../../factory/project'; import { createTable, getTable } from '../../factory/table'; import { createBulkRows, listRow, rowMixedValue } from '../../factory/row'; import { createLookupColumn, + createLtarColumn, createRollupColumn, customColumns, } from '../../factory/column'; @@ -101,10 +101,6 @@ import type { ColumnType } from 'nocodb-sdk'; import type Project from '../../../../src/models/Project'; import type Model from '../../../../src/models/Model'; -let api: Api; - -const debugMode = true; - let context; let project: Project; let table: Model; @@ -201,6 +197,10 @@ async function ncAxiosLinkGet({ .set('xc-auth', context.token) .query(query) .send({}); + if (response.status !== status) { + console.log(response.body); + } + expect(response.status).to.equal(status); return response; } @@ -1691,6 +1691,11 @@ function linkBased() { let tblActor: Model; let tblFilm: Model; + let columnsFilm; + let columnsActor; + let columnsCountry; + let columnsCity; + async function prepareRecords(title: string, count: number) { const records = []; for (let i = 1; i <= count; i++) { @@ -1705,137 +1710,171 @@ function linkBased() { // prepare data for test cases beforeEach(async function () { context = await init(); - const project = await createProject(context); - - api = new Api({ - baseURL: `http://localhost:8080/`, - headers: { - 'xc-auth': context.token, - }, - }); + project = await createProject(context); const columns = [ - { title: 'Title', uidt: UITypes.SingleLineText, pv: true }, + { + title: 'Title', + column_name: 'Title', + uidt: UITypes.SingleLineText, + pv: true, + }, ]; - // Prepare City table - columns[0].title = 'City'; - tblCity = await createTable(context, project, { - title: 'City', - columns: customColumns('custom', columns), - }); - const cityRecords = await prepareRecords('City', 100); - await api.dbTableRow.bulkCreate( - 'noco', - project.id, - tblCity.id, - cityRecords, - ); + try { + // Prepare City table + columns[0].title = 'City'; + columns[0].column_name = 'City'; + tblCity = await createTable(context, project, { + title: 'City', + table_name: 'City', + columns: customColumns('custom', columns), + }); + const cityRecords = await prepareRecords('City', 100); - // Prepare Country table - columns[0].title = 'Country'; - tblCountry = await createTable(context, project, { - title: 'Country', - columns: customColumns('custom', columns), - }); - const countryRecords = await prepareRecords('Country', 10); - await api.dbTableRow.bulkCreate( - 'noco', - project.id, - tblCountry.id, - countryRecords, - ); + // insert records + await createBulkRows(context, { + project, + table: tblCity, + values: cityRecords, + }); - // Prepare Actor table - columns[0].title = 'Actor'; - tblActor = await createTable(context, project, { - title: 'Actor', - columns: customColumns('custom', columns), - }); - const actorRecords = await prepareRecords('Actor', 100); - await api.dbTableRow.bulkCreate( - 'noco', - project.id, - tblActor.id, - actorRecords, - ); + insertedRecords = await listRow({ project, table: tblCity }); - // Prepare Movie table - columns[0].title = 'Film'; - tblFilm = await createTable(context, project, { - title: 'Film', - columns: customColumns('custom', columns), - }); - const filmRecords = await prepareRecords('Film', 100); - await api.dbTableRow.bulkCreate( - 'noco', - project.id, - tblFilm.id, - filmRecords, - ); + // Prepare Country table + columns[0].title = 'Country'; + columns[0].column_name = 'Country'; + tblCountry = await createTable(context, project, { + title: 'Country', + table_name: 'Country', + columns: customColumns('custom', columns), + }); + const countryRecords = await prepareRecords('Country', 10); + // insert records + await createBulkRows(context, { + project, + table: tblCountry, + values: countryRecords, + }); - // Create links - // Country City - await api.dbTableColumn.create(tblCountry.id, { - uidt: UITypes.LinkToAnotherRecord, - title: `Cities`, - parentId: tblCountry.id, - childId: tblCity.id, - type: 'hm', - }); + // Prepare Actor table + columns[0].title = 'Actor'; + columns[0].column_name = 'Actor'; + tblActor = await createTable(context, project, { + title: 'Actor', + table_name: 'Actor', + columns: customColumns('custom', columns), + }); + const actorRecords = await prepareRecords('Actor', 100); + await createBulkRows(context, { + project, + table: tblActor, + values: actorRecords, + }); - // Actor Film - await api.dbTableColumn.create(tblActor.id, { - uidt: UITypes.LinkToAnotherRecord, - title: `Films`, - parentId: tblActor.id, - childId: tblFilm.id, - type: 'mm', - }); + // Prepare Movie table + columns[0].title = 'Film'; + columns[0].column_name = 'Film'; + tblFilm = await createTable(context, project, { + title: 'Film', + table_name: 'Film', + columns: customColumns('custom', columns), + }); + const filmRecords = await prepareRecords('Film', 100); + await createBulkRows(context, { + project, + table: tblFilm, + values: filmRecords, + }); + + // Create links + // Country City + await createLtarColumn(context, { + title: 'Cities', + parentTable: tblCountry, + childTable: tblCity, + type: 'hm', + }); + await createLtarColumn(context, { + title: 'Films', + parentTable: tblActor, + childTable: tblFilm, + type: 'mm', + }); + + columnsFilm = await tblFilm.getColumns(); + columnsActor = await tblActor.getColumns(); + columnsCountry = await tblCountry.getColumns(); + columnsCity = await tblCity.getColumns(); + } catch (e) { + console.log(e); + } }); + function getColumnId(columns, title: string) { + return columns.find((c) => c.title === title).id; + } + it('Has-Many ', async function () { // Create hm link between Country and City await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: [1, 2, 3, 4, 5], - }, + body: [1, 2, 3, 4, 5], + status: 201, }); // verify in Country table let rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ - links: [1, 2, 3, 4, 5], - }); + + // page Info + const pageInfo = { + totalRows: 5, + page: 1, + pageSize: 25, + isFirstPage: true, + isLastPage: true, + }; + expect(rsp.body.pageInfo).to.deep.equal(pageInfo); + + // links + let subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + for (let i = 1; i <= 5; i++) { + expect(subResponse[i - 1]).to.deep.equal({ + Id: i, + City: `City ${i}`, + }); + } // verify in City table for (let i = 1; i <= 10; i++) { - const rsp = await ncAxiosLinkGet({ + rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, - linkId: tblCity.columns[2].id, - rowId: i, + linkId: getColumnId(columnsCity, 'Country'), + rowId: i + 10, }, }); + subResponse = rsp.body.list.map(({ Id, Country }) => ({ + Id, + Country, + })); if (i <= 5) { - expect(rsp.body).to.deep.equal({ - links: [i], + expect(subResponse).to.deep.equal({ + Id: i, + Country: `Country 1`, }); } else { - expect(rsp.body).to.deep.equal({ - links: [], - }); + expect(subResponse.list.length).to.equal(0); } } @@ -1844,25 +1883,25 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: [6, 7], - }, + body: [6, 7], }); // verify in Country table - for (let i = 1; i <= 10; i++) { - const rsp = await ncAxiosLinkGet({ - urlParams: { - tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, - rowId: i, - }, - }); - expect(rsp.body).to.deep.equal({ - links: [1, 2, 3, 4, 5, 6, 7], + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + }); + subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + for (let i = 1; i <= 7; i++) { + expect(subResponse[i - 1]).to.deep.equal({ + Id: i, + City: `City ${i}`, }); } @@ -1872,18 +1911,21 @@ function linkBased() { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, - linkId: tblCity.columns[2].id, + linkId: getColumnId(columnsCity, 'Country'), rowId: i, }, }); + subResponse = rsp.body.list.map(({ Id, Country }) => ({ + Id, + Country, + })); if (i <= 7) { - expect(rsp.body).to.deep.equal({ - links: [i], + expect(subResponse).to.deep.equal({ + Id: 1, + Country: `Country 1`, }); } else { - expect(rsp.body).to.deep.equal({ - links: [], - }); + expect(subResponse.list.length).to.equal(0); } } @@ -1892,43 +1934,49 @@ function linkBased() { await ncAxiosLinkRemove({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: [1, 3, 5, 7], - }, + body: [1, 3, 5, 7], }); // verify in Country table rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ - links: [2, 4, 6], - }); - + subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + expect(subResponse.list.length).to.equal(3); + for (let i = 1; i <= 3; i++) { + expect(subResponse[i - 1]).to.deep.equal({ + Id: i * 2, + City: `City ${i * 2}`, + }); + } // verify in City table for (let i = 1; i <= 10; i++) { - const rsp = await ncAxiosLinkGet({ + rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, - linkId: tblCity.columns[2].id, + linkId: getColumnId(columnsCity, 'Country'), rowId: i, }, }); + subResponse = rsp.body.list.map(({ Id, Country }) => ({ + Id, + Country, + })); + if (i % 2 === 0 && i <= 6) { - expect(rsp.body).to.deep.equal({ - links: [Math.ceil(i / 10)], + expect(subResponse).to.deep.equal({ + Id: 1, + Country: `Country 1`, }); } else { - expect(rsp.body).to.deep.equal({ - links: [], - }); + expect(subResponse.list.length).to.equal(0); } } }); @@ -1944,29 +1992,25 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblActor.id, - linkId: tblActor.columns[2].id, + linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, - body: { - links: initializeArrayFromSequence(1, 20), - }, + body: initializeArrayFromSequence(1, 20), }); await ncAxiosLinkAdd({ urlParams: { tableId: tblFilm.id, - linkId: tblFilm.columns[2].id, + linkId: getColumnId(columnsFilm, 'Actor'), rowId: 1, }, - body: { - links: initializeArrayFromSequence(1, 20), - }, + body: initializeArrayFromSequence(1, 20), }); // verify in Actor table let rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblActor.id, - linkId: tblActor.columns[2].id, + linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, }); @@ -1978,7 +2022,7 @@ function linkBased() { rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblFilm.id, - linkId: tblFilm.columns[2].id, + linkId: getColumnId(columnsFilm, 'Actor'), rowId: 1, }, }); @@ -1991,19 +2035,17 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblActor.id, - linkId: tblActor.columns[2].id, + linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, - body: { - links: initializeArrayFromSequence(21, 30), - }, + body: initializeArrayFromSequence(21, 30), }); // verify in Actor table rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblActor.id, - linkId: tblActor.columns[2].id, + linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, }); @@ -2016,7 +2058,7 @@ function linkBased() { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblFilm.id, - linkId: tblFilm.columns[2].id, + linkId: getColumnId(columnsFilm, 'Actor'), rowId: i, }, }); @@ -2030,19 +2072,17 @@ function linkBased() { await ncAxiosLinkRemove({ urlParams: { tableId: tblActor.id, - linkId: tblActor.columns[2].id, + linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, - body: { - links: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29], - }, + body: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29], }); // verify in Actor table rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblActor.id, - linkId: tblActor.columns[2].id, + linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, }); @@ -2055,7 +2095,7 @@ function linkBased() { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblFilm.id, - linkId: tblFilm.columns[2].id, + linkId: getColumnId(columnsFilm, 'Actor'), rowId: i, }, }); @@ -2078,31 +2118,27 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: [1, 2, 3], - }, + body: [1, 2, 3], }); // update the link await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 2, }, - body: { - links: [2, 3], - }, + body: [2, 3], }); // verify record 1 let rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, }); @@ -2111,7 +2147,7 @@ function linkBased() { rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 2, }, }); @@ -2124,19 +2160,17 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: initializeArrayFromSequence(1, 50), - }, + body: initializeArrayFromSequence(1, 50), }); // verify record 1 let rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, query: { @@ -2151,7 +2185,7 @@ function linkBased() { rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, query: { @@ -2166,7 +2200,7 @@ function linkBased() { rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, query: { @@ -2185,12 +2219,10 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: [9999], - }, + body: [9999], status: 400, }); @@ -2209,12 +2241,10 @@ function linkBased() { await ncAxiosLinkRemove({ urlParams: { tableId: tblCountry.id, - linkId: tblCountry.columns[2].id, + linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: { - links: [9999], - }, + body: [9999], status: 400, }); }); @@ -2233,7 +2263,7 @@ export default function () { describe('Numerical', numberBased); describe('Select based', selectBased); describe('Date based', dateBased); - // describe('Link based', linkBased); + describe('Link based', linkBased); } /////////////////////////////////////////////////////////////////////////////// From 855c84f1bfd888e9d417dd22e4f22b4eefb33aae Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Fri, 16 Jun 2023 15:00:01 +0530 Subject: [PATCH 62/97] test: many-many list/add/remove Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 151 +++++++++++++----- 1 file changed, 111 insertions(+), 40 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 9d1a732e23..5cb54aa33f 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -207,7 +207,7 @@ async function ncAxiosLinkGet({ async function ncAxiosLinkAdd({ urlParams: { tableId, linkId, rowId }, body = {}, - status = 200, + status = 201, }: { urlParams?: any; body?: any; status?: number } = {}) { const urlParams = { tableId, linkId, rowId }; const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; @@ -2000,7 +2000,7 @@ function linkBased() { await ncAxiosLinkAdd({ urlParams: { tableId: tblFilm.id, - linkId: getColumnId(columnsFilm, 'Actor'), + linkId: getColumnId(columnsFilm, 'Actor List'), rowId: 1, }, body: initializeArrayFromSequence(1, 20), @@ -2014,21 +2014,55 @@ function linkBased() { rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(1, 20), + + // page info + const pageInfo = { + totalRows: 20, + page: 1, + pageSize: 25, + isFirstPage: true, + isLastPage: true, + }; + expect(rsp.body.pageInfo).to.deep.equal(pageInfo); + + // Links + expect(rsp.body.list.length).to.equal(20); + for (let i = 1; i <= 20; i++) { + expect(rsp.body.list[i - 1]).to.deep.equal({ + Id: i, + Film: `Film ${i}`, + }); + } + + // Second record + rsp = await ncAxiosLinkGet({ + urlParams: { + tableId: tblActor.id, + linkId: getColumnId(columnsActor, 'Films'), + rowId: 2, + }, + }); + expect(rsp.body.list.length).to.equal(1); + expect(rsp.body.list[0]).to.deep.equal({ + Id: 1, + Film: `Film 1`, }); // verify in Film table rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblFilm.id, - linkId: getColumnId(columnsFilm, 'Actor'), + linkId: getColumnId(columnsFilm, 'Actor List'), rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(1, 20), - }); + expect(rsp.body.list.length).to.equal(20); + for (let i = 1; i <= 20; i++) { + expect(rsp.body.list[i - 1]).to.deep.equal({ + Id: i, + Actor: `Actor ${i}`, + }); + } // Update mm link between Actor and Film // List them for a record & verify in both tables @@ -2038,7 +2072,7 @@ function linkBased() { linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, - body: initializeArrayFromSequence(21, 30), + body: initializeArrayFromSequence(21, 10), }); // verify in Actor table @@ -2049,21 +2083,28 @@ function linkBased() { rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(1, 30), - }); + expect(rsp.body.list.length).to.equal(25); + // paginated response, limit to 25 + for (let i = 1; i <= 25; i++) { + expect(rsp.body.list[i - 1]).to.deep.equal({ + Id: i, + Film: `Film ${i}`, + }); + } // verify in Film table for (let i = 21; i <= 30; i++) { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblFilm.id, - linkId: getColumnId(columnsFilm, 'Actor'), + linkId: getColumnId(columnsFilm, 'Actor List'), rowId: i, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(1, 1), + expect(rsp.body.list.length).to.equal(1); + expect(rsp.body.list[0]).to.deep.equal({ + Id: 1, + Actor: `Actor 1`, }); } @@ -2086,27 +2127,31 @@ function linkBased() { rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ - links: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30], - }); + expect(rsp.body.list.length).to.equal(15); + for (let i = 2; i <= 30; i += 2) { + expect(rsp.body.list[i / 2 - 1]).to.deep.equal({ + Id: i, + Film: `Film ${i}`, + }); + } // verify in Film table for (let i = 2; i <= 30; i++) { const rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblFilm.id, - linkId: getColumnId(columnsFilm, 'Actor'), + linkId: getColumnId(columnsFilm, 'Actor List'), rowId: i, }, }); if (i % 2 === 0) { - expect(rsp.body).to.deep.equal({ - links: [1], + expect(rsp.body.list.length).to.equal(1); + expect(rsp.body.list[0]).to.deep.equal({ + Id: 1, + Actor: `Actor 1`, }); } else { - expect(rsp.body).to.deep.equal({ - links: [], - }); + expect(rsp.body.list.length).to.equal(0); } } }); @@ -2142,7 +2187,12 @@ function linkBased() { rowId: 1, }, }); - expect(rsp.body).to.deep.equal({ links: [1] }); + let subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + expect(subResponse.length).to.equal(1); + expect(subResponse[0]).to.deep.equal({ + Id: 1, + City: 'City 1', + }); rsp = await ncAxiosLinkGet({ urlParams: { @@ -2151,7 +2201,14 @@ function linkBased() { rowId: 2, }, }); - expect(rsp.body).to.deep.equal({ links: [2, 3] }); + expect(rsp.body.list.length).to.equal(2); + subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + for (let i = 2; i <= 3; i++) { + expect(subResponse[i - 2]).to.deep.equal({ + Id: i, + City: `City ${i}`, + }); + } }); // limit & offset verification @@ -2178,9 +2235,14 @@ function linkBased() { offset: 0, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(1, 10), - }); + expect(rsp.body.list.length).to.equal(10); + let subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + for (let i = 1; i <= 10; i++) { + expect(subResponse[i - 1]).to.deep.equal({ + Id: i, + City: `City ${i}`, + }); + } rsp = await ncAxiosLinkGet({ urlParams: { @@ -2193,9 +2255,14 @@ function linkBased() { offset: 10, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(11, 20), - }); + subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + expect(subResponse.length).to.equal(10); + for (let i = 11; i <= 20; i++) { + expect(subResponse[i - 11]).to.deep.equal({ + Id: i, + City: `City ${i}`, + }); + } rsp = await ncAxiosLinkGet({ urlParams: { @@ -2204,13 +2271,18 @@ function linkBased() { rowId: 1, }, query: { - limit: 10, + limit: 100, offset: 40, }, }); - expect(rsp.body).to.deep.equal({ - links: initializeArrayFromSequence(41, 50), - }); + subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); + expect(subResponse.length).to.equal(10); + for (let i = 41; i <= 50; i++) { + expect(subResponse[i - 41]).to.deep.equal({ + Id: i, + City: `City ${i}`, + }); + } }); // invalid link id @@ -2223,11 +2295,11 @@ function linkBased() { rowId: 1, }, body: [9999], - status: 400, + status: 404, }); // Invalid link field ID - const rsp = await ncAxiosLinkGet({ + await ncAxiosLinkGet({ urlParams: { tableId: tblCountry.id, linkId: 9999, @@ -2235,7 +2307,6 @@ function linkBased() { }, status: 400, }); - expect(rsp.body).to.deep.equal({ links: [] }); // Link Remove: Invalid link ID await ncAxiosLinkRemove({ @@ -2245,7 +2316,7 @@ function linkBased() { rowId: 1, }, body: [9999], - status: 400, + status: 404, }); }); } From 1c2644d712d1aea186191fbccc58d5374d2fdcec Mon Sep 17 00:00:00 2001 From: Pranav C Date: Fri, 16 Jun 2023 23:20:19 +0530 Subject: [PATCH 63/97] feat: use nocoexecute with nested list Signed-off-by: Pranav C --- .../nocodb/src/services/data-table.service.ts | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index e301ed1267..1021efa023 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -1,13 +1,10 @@ import { Injectable } from '@nestjs/common'; import { RelationTypes, UITypes } from 'nocodb-sdk'; +import { nocoExecute } from 'nc-help'; import { NcError } from '../helpers/catchError'; +import getAst from '../helpers/getAst'; import { PagedResponseImpl } from '../helpers/PagedResponse'; import { Base, Column, Model, View } from '../models'; -import { - getColumnByIdOrName, - getViewAndModelByAliasOrId, - PathParams, -} from '../modules/datas/helpers'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; import { DatasService } from './datas.service'; import type { LinkToAnotherRecordColumn } from '../models'; @@ -259,6 +256,22 @@ export class DataTableService { const colOptions = await column.getColOptions(); + const relatedModel = await colOptions.getRelatedTable(); + + 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) {} + let data: any[]; let count: number; if (colOptions.type === RelationTypes.MANY_TO_MANY) { @@ -267,7 +280,7 @@ export class DataTableService { colId: column.id, parentId: param.rowId, }, - param.query as any, + listArgs as any, ); count = (await baseModel.mmListCount({ colId: column.id, @@ -286,6 +299,9 @@ export class DataTableService { id: param.rowId, })) as number; } + + data = await nocoExecute(ast, data, {}, listArgs); + return new PagedResponseImpl(data, { count, ...param.query, From c6ceef98dce47df3b59cda7be99936700aede0f4 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Jun 2023 00:33:58 +0530 Subject: [PATCH 64/97] feat: add bt read Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 57 +++++++++++++++++++ .../nocodb/src/services/data-table.service.ts | 16 +++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index f161de2d7c..23b8d1e500 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3862,6 +3862,63 @@ class BaseModelSqlv2 { // await this.afterUpdate(prevData, newData, this.dbDriver, cookie); // await this.afterRemoveChild(rowId, childIds, cookie); } + + async btRead( + { colId, id }: { colId; id }, + args: { limit?; offset?; fieldSet?: Set } = {}, + ) { + try { + const { where, sort } = this._getListArgs(args as any); + // todo: get only required fields + + const relColumn = (await this.model.getColumns()).find( + (c) => c.id === colId, + ); + + const parentCol = await ( + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + ).getParentColumn(); + const parentTable = await parentCol.getModel(); + const chilCol = await ( + (await relColumn.getColOptions()) as LinkToAnotherRecordColumn + ).getChildColumn(); + const childTable = await chilCol.getModel(); + + const parentModel = await Model.getBaseModelSQL({ + model: parentTable, + dbDriver: this.dbDriver, + }); + await childTable.getColumns(); + + const childTn = this.getTnPath(childTable); + const parentTn = this.getTnPath(parentTable); + + const qb = this.dbDriver(parentTn); + await this.applySortAndFilter({ table: parentTable, where, qb, sort }); + + qb.where( + parentCol.column_name, + this.dbDriver(childTn) + .select(chilCol.column_name) + // .where(parentTable.primaryKey.cn, p) + .where(_wherePk(childTable.primaryKeys, id)), + ); + + await parentModel.selectObject({ qb, fieldsSet: args.fieldSet }); + + const parent = (await this.execAndParse(qb, childTable))?.[0]; + + const proto = await parentModel.getProto(); + + if (parent) { + parent.__proto__ = proto; + } + return parent; + } catch (e) { + console.log(e); + throw e; + } + } } function extractSortsObject( diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 1021efa023..bb6dd95fbd 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -261,7 +261,7 @@ export class DataTableService { const { ast, dependencyFields } = await getAst({ model: relatedModel, query: param.query, - extractOnlyPrimaries: !!(param.query?.f || param.query?.fields), + extractOnlyPrimaries: !(param.query?.f || param.query?.fields), }); const listArgs: any = dependencyFields; @@ -286,22 +286,32 @@ export class DataTableService { colId: column.id, parentId: param.rowId, })) as number; - } else { + } else if (colOptions.type === RelationTypes.HAS_MANY) { data = await baseModel.hmList( { colId: column.id, id: param.rowId, }, - param.query as any, + listArgs as any, ); count = (await baseModel.hmListCount({ colId: column.id, id: param.rowId, })) as number; + } else { + data = await baseModel.btRead( + { + colId: column.id, + id: param.rowId, + }, + param.query as any, + ); } data = await nocoExecute(ast, data, {}, listArgs); + if(colOptions.type === RelationTypes.BELONGS_TO) return data; + return new PagedResponseImpl(data, { count, ...param.query, From 965282415107b71b2abb099f9575b858a983fa8a Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 11:14:43 +0530 Subject: [PATCH 65/97] test: error handling for nested apis Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 218 ++++++++++++++++-- 1 file changed, 201 insertions(+), 17 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 5cb54aa33f..5ac492fc01 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -197,10 +197,7 @@ async function ncAxiosLinkGet({ .set('xc-auth', context.token) .query(query) .send({}); - if (response.status !== status) { - console.log(response.body); - } - + console.log(status, response.status); expect(response.status).to.equal(status); return response; } @@ -215,6 +212,8 @@ async function ncAxiosLinkAdd({ .post(url) .set('xc-auth', context.token) .send(body); + + console.log(status, response.status); expect(response.status).to.equal(status); return response; } @@ -229,6 +228,7 @@ async function ncAxiosLinkRemove({ .delete(url) .set('xc-auth', context.token) .send(body); + console.log(status, response.status); expect(response.status).to.equal(status); return response; } @@ -1749,7 +1749,7 @@ function linkBased() { table_name: 'Country', columns: customColumns('custom', columns), }); - const countryRecords = await prepareRecords('Country', 10); + const countryRecords = await prepareRecords('Country', 100); // insert records await createBulkRows(context, { project, @@ -1855,13 +1855,15 @@ function linkBased() { }); } + /////////////////////////////////////////////////////////////////// + // verify in City table for (let i = 1; i <= 10; i++) { rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, linkId: getColumnId(columnsCity, 'Country'), - rowId: i + 10, + rowId: i, }, }); subResponse = rsp.body.list.map(({ Id, Country }) => ({ @@ -1905,7 +1907,6 @@ function linkBased() { }); } - // verify in City table // verify in City table for (let i = 1; i <= 10; i++) { const rsp = await ncAxiosLinkGet({ @@ -2285,39 +2286,222 @@ function linkBased() { } }); - // invalid link id - it('Invalid link id', async function () { + // Error handling + it('Error handling : Nested ADD', async function () { + const validParams = { + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }; + + // Link Add: Invalid table ID + await ncAxiosLinkAdd({ + ...validParams, + urlParams: { ...validParams.urlParams, tableId: 9999 }, + status: 404, + }); + // Link Add: Invalid link ID + await ncAxiosLinkAdd({ + ...validParams, + urlParams: { ...validParams.urlParams, linkId: 9999 }, + status: 404, + }); + + // Link Add: Invalid Source row ID + await ncAxiosLinkAdd({ + ...validParams, + urlParams: { ...validParams.urlParams, rowId: 9999 }, + status: 404, + }); + + // Body parameter error + // + + // Link Add: Invalid body parameter - empty body : ignore + await ncAxiosLinkAdd({ + ...validParams, + body: [], + status: 201, + }); + + // Link Add: Invalid body parameter - row id invalid + await ncAxiosLinkAdd({ + ...validParams, + body: [999, 998, 997], + status: 400, + }); + + // Link Add: Invalid body parameter - repeated row id + await ncAxiosLinkAdd({ + ...validParams, + body: [1, 2, 1, 2], + status: 400, + }); + }); + + it('Error handling : Nested REMOVE', async function () { + // Prepare data await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: [9999], - status: 404, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, }); - // Invalid link field ID - await ncAxiosLinkGet({ + const validParams = { urlParams: { tableId: tblCountry.id, - linkId: 9999, - rowId: 19, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, }, - status: 400, + body: [1, 2, 3], + status: 200, + }; + + // Link Remove: Invalid table ID + await ncAxiosLinkRemove({ + ...validParams, + urlParams: { ...validParams.urlParams, tableId: 9999 }, + status: 404, }); // Link Remove: Invalid link ID await ncAxiosLinkRemove({ + ...validParams, + urlParams: { ...validParams.urlParams, linkId: 9999 }, + status: 404, + }); + + // Link Remove: Invalid Source row ID + await ncAxiosLinkRemove({ + ...validParams, + urlParams: { ...validParams.urlParams, rowId: 9999 }, + status: 404, + }); + + // Body parameter error + // + + // Link Remove: Invalid body parameter - empty body : ignore + await ncAxiosLinkRemove({ + ...validParams, + body: [], + status: 404, + }); + + // Link Remove: Invalid body parameter - row id invalid + await ncAxiosLinkRemove({ + ...validParams, + body: [999, 998], + status: 404, + }); + + // Link Remove: Invalid body parameter - repeated row id + await ncAxiosLinkRemove({ + ...validParams, + body: [1, 2, 1, 2], + status: 404, + }); + }); + + it('Error handling : Nested List', async function () { + // Prepare data + await ncAxiosLinkAdd({ urlParams: { tableId: tblCountry.id, linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: [9999], + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + query: { + offset: 0, + limit: 10, + }, + status: 200, + }; + + // Link List: Invalid table ID + await ncAxiosLinkGet({ + ...validParams, + urlParams: { ...validParams.urlParams, tableId: 9999 }, + status: 404, + }); + + // Link List: Invalid link ID + await ncAxiosLinkGet({ + ...validParams, + urlParams: { ...validParams.urlParams, linkId: 9999 }, + status: 404, + }); + + // Link List: Invalid Source row ID + await ncAxiosLinkGet({ + ...validParams, + urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, }); + + // Query parameter error + // + + // Link List: Invalid query parameter - negative offset + await ncAxiosLinkGet({ + ...validParams, + query: { ...validParams.query, offset: -1 }, + status: 200, + }); + + // Link List: Invalid query parameter - string offset + await ncAxiosLinkGet({ + ...validParams, + query: { ...validParams.query, offset: 'abcd' }, + status: 200, + }); + + // Link List: Invalid query parameter - offset > total rows + await ncAxiosLinkGet({ + ...validParams, + query: { ...validParams.query, offset: 9999 }, + status: 200, + }); + + // Link List: Invalid query parameter - negative limit + await ncAxiosLinkGet({ + ...validParams, + query: { ...validParams.query, limit: -1 }, + status: 200, + }); + + // Link List: Invalid query parameter - string limit + await ncAxiosLinkGet({ + ...validParams, + query: { ...validParams.query, limit: 'abcd' }, + status: 200, + }); + + // Link List: Invalid query parameter - limit > total rows + await ncAxiosLinkGet({ + ...validParams, + query: { ...validParams.query, limit: 9999 }, + status: 200, + }); }); } From 35c67b2f7488d6dfbbe1ad9e4416201e17459626 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Jun 2023 12:35:16 +0530 Subject: [PATCH 66/97] feat: add validation for duplicate id Signed-off-by: Pranav C --- .../nocodb/src/services/data-table.service.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index bb6dd95fbd..6213e738a3 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -6,6 +6,7 @@ import getAst from '../helpers/getAst'; import { PagedResponseImpl } from '../helpers/PagedResponse'; import { Base, Column, Model, View } from '../models'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; +import { referencedRowIdParam } from './api-docs/swagger/templates/params'; import { DatasService } from './datas.service'; import type { LinkToAnotherRecordColumn } from '../models'; @@ -310,7 +311,7 @@ export class DataTableService { data = await nocoExecute(ast, data, {}, listArgs); - if(colOptions.type === RelationTypes.BELONGS_TO) return data; + if (colOptions.type === RelationTypes.BELONGS_TO) return data; return new PagedResponseImpl(data, { count, @@ -340,6 +341,8 @@ export class DataTableService { refRowIds: string | string[] | number | number[]; rowId: string; }) { + this.validateIds(param.refRowIds); + const { model, view } = await this.getModelAndView(param); if (!model) NcError.notFound('Table not found'); @@ -374,6 +377,8 @@ export class DataTableService { refRowIds: string | string[] | number | number[]; rowId: string; }) { + this.validateIds(param.refRowIds); + const { model, view } = await this.getModelAndView(param); if (!model) NcError.notFound('Table not found'); @@ -398,4 +403,20 @@ export class DataTableService { return true; } + + private validateIds(rowIds: any[] | any) { + if (Array.isArray(rowIds)) { + const map = new Map(); + for (const rowId of rowIds) { + if (rowId === undefined || rowId === null) + NcError.badRequest('Invalid row id ' + rowId); + if (map.has(rowId)) { + NcError.badRequest('Duplicate row with id ' + rowId); + } + map.set(rowId, true); + } + } else if (rowIds === undefined || rowIds === null) { + NcError.badRequest('Invalid row id ' + rowIds); + } + } } From d2af09133d3f62096e68ccd1212bed7efd24d163 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Jun 2023 13:11:46 +0530 Subject: [PATCH 67/97] refactor: error code corrections Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 12 ++++++------ packages/nocodb/src/services/data-table.service.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 7c12fdda91..071cb9b77d 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3699,7 +3699,7 @@ class BaseModelSqlv2 { !childRows.find((r) => r[parentColumn.column_name] === id), ); - NcError.notFound( + NcError.unprocessableEntity( `Child record with id ${missingIds.join(', ')} not found`, ); } @@ -3777,7 +3777,7 @@ class BaseModelSqlv2 { !childRows.find((r) => r[parentColumn.column_name] === id), ); - NcError.notFound( + NcError.unprocessableEntity( `Child record with id ${missingIds.join(', ')} not found`, ); } @@ -3809,7 +3809,7 @@ class BaseModelSqlv2 { const childRow = await childRowsQb; if (!childRow) { - NcError.notFound(`Child record with id ${childIds[0]} not found`); + NcError.unprocessableEntity(`Child record with id ${childIds[0]} not found`); } } @@ -3899,7 +3899,7 @@ class BaseModelSqlv2 { !childRows.find((r) => r[parentColumn.column_name] === id), ); - NcError.notFound( + NcError.unprocessableEntity( `Child record with id ${missingIds.join(', ')} not found`, ); } @@ -3939,7 +3939,7 @@ class BaseModelSqlv2 { !childRows.find((r) => r[parentColumn.column_name] === id), ); - NcError.notFound( + NcError.unprocessableEntity( `Child record with id ${missingIds.join(', ')} not found`, ); } @@ -3969,7 +3969,7 @@ class BaseModelSqlv2 { const childRow = await childRowsQb; if (!childRow) { - NcError.notFound(`Child record with id ${childIds[0]} not found`); + NcError.unprocessableEntity(`Child record with id ${childIds[0]} not found`); } } diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 6213e738a3..31f3920441 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -409,14 +409,14 @@ export class DataTableService { const map = new Map(); for (const rowId of rowIds) { if (rowId === undefined || rowId === null) - NcError.badRequest('Invalid row id ' + rowId); + NcError.unprocessableEntity('Invalid row id ' + rowId); if (map.has(rowId)) { - NcError.badRequest('Duplicate row with id ' + rowId); + NcError.unprocessableEntity('Duplicate row with id ' + rowId); } map.set(rowId, true); } } else if (rowIds === undefined || rowIds === null) { - NcError.badRequest('Invalid row id ' + rowIds); + NcError.unprocessableEntity('Invalid row id ' + rowIds); } } } From 46b9d0386ef8ba7d25eb30404673c39b5c2a2bce Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 13:33:30 +0530 Subject: [PATCH 68/97] test: error strings Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 106 +++++++++++++++--- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 5ac492fc01..e7df041c36 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -101,6 +101,8 @@ import type { ColumnType } from 'nocodb-sdk'; import type Project from '../../../../src/models/Project'; import type Model from '../../../../src/models/Model'; +const debugMode = false; + let context; let project: Project; let table: Model; @@ -189,7 +191,8 @@ async function ncAxiosLinkGet({ urlParams: { tableId, linkId, rowId }, query = {}, status = 200, -}: { urlParams?: any; query?: any; status?: number } = {}) { + msg, +}: { urlParams?: any; query?: any; status?: number; msg?: string } = {}) { const urlParams = { tableId, linkId, rowId }; const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; const response = await request(context.app) @@ -197,15 +200,24 @@ async function ncAxiosLinkGet({ .set('xc-auth', context.token) .query(query) .send({}); - console.log(status, response.status); - expect(response.status).to.equal(status); + if (debugMode && status !== response.status) + console.log('#### expected', status, 'received', response.status); + else expect(response.status).to.equal(status); + + // print error codes + if (debugMode && status !== 200) { + console.log('#### ', response.body.msg); + } + if (msg) expect(response.body.msg).to.equal(msg); + return response; } async function ncAxiosLinkAdd({ urlParams: { tableId, linkId, rowId }, body = {}, status = 201, -}: { urlParams?: any; body?: any; status?: number } = {}) { + msg, +}: { urlParams?: any; body?: any; status?: number; msg?: string } = {}) { const urlParams = { tableId, linkId, rowId }; const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; const response = await request(context.app) @@ -213,23 +225,41 @@ async function ncAxiosLinkAdd({ .set('xc-auth', context.token) .send(body); - console.log(status, response.status); - expect(response.status).to.equal(status); + if (debugMode && status !== response.status) + console.log('#### expected', status, 'received', response.status); + else expect(response.status).to.equal(status); + + // print error codes + if (debugMode && status !== 201) { + console.log('#### ', response.body.msg); + } + + if (msg) expect(response.body.msg).to.equal(msg); + return response; } async function ncAxiosLinkRemove({ urlParams: { tableId, linkId, rowId }, body = {}, status = 200, -}: { urlParams?: any; body?: any; status?: number } = {}) { + msg, +}: { urlParams?: any; body?: any; status?: number; msg?: string } = {}) { const urlParams = { tableId, linkId, rowId }; const url = `/api/v1/tables/${urlParams.tableId}/links/${urlParams.linkId}/rows/${urlParams.rowId}`; const response = await request(context.app) .delete(url) .set('xc-auth', context.token) .send(body); - console.log(status, response.status); - expect(response.status).to.equal(status); + if (debugMode && status !== response.status) + console.log('#### expected', status, 'received', response.status); + else expect(response.status).to.equal(status); + + // print error codes + if (debugMode && status !== 200) { + console.log('#### ', response.body.msg); + } + if (msg) expect(response.body.msg).to.equal(msg); + return response; } @@ -2286,7 +2316,7 @@ function linkBased() { } }); - // Error handling + // Error handling (has-many) it('Error handling : Nested ADD', async function () { const validParams = { urlParams: { @@ -2299,30 +2329,38 @@ function linkBased() { }; // Link Add: Invalid table ID + if (debugMode) console.log('Link Add: Invalid table ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, status: 404, + msg: "Table '9999' not found", }); // Link Add: Invalid link ID + if (debugMode) console.log('Link Add: Invalid link ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, status: 404, + msg: "Column '9999' not found", }); // Link Add: Invalid Source row ID + if (debugMode) console.log('Link Add: Invalid Source row ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, + msg: "Row '9999' not found", }); // Body parameter error // // Link Add: Invalid body parameter - empty body : ignore + if (debugMode) + console.log('Link Add: Invalid body parameter - empty body : ignore'); await ncAxiosLinkAdd({ ...validParams, body: [], @@ -2330,17 +2368,23 @@ function linkBased() { }); // Link Add: Invalid body parameter - row id invalid + if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); await ncAxiosLinkAdd({ ...validParams, body: [999, 998, 997], - status: 400, + status: 422, + msg: 'Child record with id 999, 998, 997 not found', }); // Link Add: Invalid body parameter - repeated row id + if (debugMode) + console.log('Link Add: Invalid body parameter - repeated row id'); await ncAxiosLinkAdd({ ...validParams, body: [1, 2, 1, 2], - status: 400, + status: 422, + msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', }); }); @@ -2367,48 +2411,62 @@ function linkBased() { }; // Link Remove: Invalid table ID + if (debugMode) console.log('Link Remove: Invalid table ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, status: 404, + msg: "Table '9999' not found", }); // Link Remove: Invalid link ID + if (debugMode) console.log('Link Remove: Invalid link ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, status: 404, + msg: "Column '9999' not found", }); // Link Remove: Invalid Source row ID + if (debugMode) console.log('Link Remove: Invalid Source row ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, + msg: "Row '9999' not found", }); // Body parameter error // // Link Remove: Invalid body parameter - empty body : ignore + if (debugMode) + console.log('Link Remove: Invalid body parameter - empty body : ignore'); await ncAxiosLinkRemove({ ...validParams, body: [], - status: 404, + status: 200, }); // Link Remove: Invalid body parameter - row id invalid + if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); await ncAxiosLinkRemove({ ...validParams, body: [999, 998], - status: 404, + status: 422, + msg: 'Child record with id 999, 998 not found', }); // Link Remove: Invalid body parameter - repeated row id + if (debugMode) + console.log('Link Remove: Invalid body parameter - repeated row id'); await ncAxiosLinkRemove({ ...validParams, body: [1, 2, 1, 2], - status: 404, + status: 422, + msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', }); }); @@ -2438,30 +2496,38 @@ function linkBased() { }; // Link List: Invalid table ID + if (debugMode) console.log('Link List: Invalid table ID'); await ncAxiosLinkGet({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, status: 404, + msg: "Table '9999' not found", }); // Link List: Invalid link ID + if (debugMode) console.log('Link List: Invalid link ID'); await ncAxiosLinkGet({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, status: 404, + msg: "Column '9999' not found", }); // Link List: Invalid Source row ID + if (debugMode) console.log('Link List: Invalid Source row ID'); await ncAxiosLinkGet({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, + msg: "Row '9999' not found", }); // Query parameter error // // Link List: Invalid query parameter - negative offset + if (debugMode) + console.log('Link List: Invalid query parameter - negative offset'); await ncAxiosLinkGet({ ...validParams, query: { ...validParams.query, offset: -1 }, @@ -2469,6 +2535,8 @@ function linkBased() { }); // Link List: Invalid query parameter - string offset + if (debugMode) + console.log('Link List: Invalid query parameter - string offset'); await ncAxiosLinkGet({ ...validParams, query: { ...validParams.query, offset: 'abcd' }, @@ -2476,6 +2544,8 @@ function linkBased() { }); // Link List: Invalid query parameter - offset > total rows + if (debugMode) + console.log('Link List: Invalid query parameter - offset > total rows'); await ncAxiosLinkGet({ ...validParams, query: { ...validParams.query, offset: 9999 }, @@ -2483,6 +2553,8 @@ function linkBased() { }); // Link List: Invalid query parameter - negative limit + if (debugMode) + console.log('Link List: Invalid query parameter - negative limit'); await ncAxiosLinkGet({ ...validParams, query: { ...validParams.query, limit: -1 }, @@ -2490,6 +2562,8 @@ function linkBased() { }); // Link List: Invalid query parameter - string limit + if (debugMode) + console.log('Link List: Invalid query parameter - string limit'); await ncAxiosLinkGet({ ...validParams, query: { ...validParams.query, limit: 'abcd' }, @@ -2497,6 +2571,8 @@ function linkBased() { }); // Link List: Invalid query parameter - limit > total rows + if (debugMode) + console.log('Link List: Invalid query parameter - limit > total rows'); await ncAxiosLinkGet({ ...validParams, query: { ...validParams.query, limit: 9999 }, From 18273caade9de547d2079988d1d0623d1b80c9c8 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Sat, 17 Jun 2023 14:24:05 +0530 Subject: [PATCH 69/97] refactor: error code corrections Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 32 +++++++++++++++---- .../nocodb/src/services/data-table.service.ts | 10 +++--- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 071cb9b77d..08d1514a30 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -17,8 +17,8 @@ import Validator from 'validator'; import { customAlphabet } from 'nanoid'; import DOMPurify from 'isomorphic-dompurify'; import { v4 as uuidv4 } from 'uuid'; -import { extractLimitAndOffset } from '../helpers'; import { Knex } from 'knex'; +import { extractLimitAndOffset } from '../helpers'; import { NcError } from '../helpers/catchError'; import getAst from '../helpers/getAst'; import { @@ -3628,7 +3628,7 @@ class BaseModelSqlv2 { const column = columns.find((c) => c.id === colId); if (!column || column.uidt !== UITypes.LinkToAnotherRecord) - NcError.notFound('Column not found'); + NcError.notFound(`Link column with id ${colId} not found`); const row = await this.dbDriver(this.tnPath) .where(await this._wherePk(rowId)) @@ -3636,7 +3636,7 @@ class BaseModelSqlv2 { // validate rowId if (!row) { - NcError.notFound('Row not found'); + NcError.notFound(`Row with id ${rowId} not found`); } const colOptions = await column.getColOptions(); @@ -3809,7 +3809,9 @@ class BaseModelSqlv2 { const childRow = await childRowsQb; if (!childRow) { - NcError.unprocessableEntity(`Child record with id ${childIds[0]} not found`); + NcError.unprocessableEntity( + `Child record with id ${childIds[0]} not found`, + ); } } @@ -3849,7 +3851,7 @@ class BaseModelSqlv2 { const column = columns.find((c) => c.id === colId); if (!column || column.uidt !== UITypes.LinkToAnotherRecord) - NcError.notFound('Column not found'); + NcError.notFound(`Link column with id ${colId} not found`); const row = await this.dbDriver(this.tnPath) .where(await this._wherePk(rowId)) @@ -3857,7 +3859,7 @@ class BaseModelSqlv2 { // validate rowId if (!row) { - NcError.notFound('Row not found'); + NcError.notFound(`Row with id ${rowId} not found`); } const colOptions = await column.getColOptions(); @@ -3961,6 +3963,11 @@ class BaseModelSqlv2 { { // validate Ids { + if (childIds.length > 1) + NcError.unprocessableEntity( + 'Request must contain only one parent id', + ); + const childRowsQb = this.dbDriver(parentTn) .select(parentTable.primaryKey.column_name) .where(_wherePk(parentTable.primaryKeys, childIds[0])) @@ -3969,7 +3976,9 @@ class BaseModelSqlv2 { const childRow = await childRowsQb; if (!childRow) { - NcError.unprocessableEntity(`Child record with id ${childIds[0]} not found`); + NcError.unprocessableEntity( + `Child record with id ${childIds[0]} not found`, + ); } } @@ -4004,6 +4013,15 @@ class BaseModelSqlv2 { (c) => c.id === colId, ); + const row = await this.dbDriver(this.tnPath) + .where(await this._wherePk(id)) + .first(); + + // validate rowId + if (!row) { + NcError.notFound(`Row with id ${id} not found`); + } + const parentCol = await ( (await relColumn.getColOptions()) as LinkToAnotherRecordColumn ).getParentColumn(); diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 31f3920441..1060c4310f 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -6,7 +6,6 @@ import getAst from '../helpers/getAst'; import { PagedResponseImpl } from '../helpers/PagedResponse'; import { Base, Column, Model, View } from '../models'; import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; -import { referencedRowIdParam } from './api-docs/swagger/templates/params'; import { DatasService } from './datas.service'; import type { LinkToAnotherRecordColumn } from '../models'; @@ -169,7 +168,7 @@ export class DataTableService { const model = await Model.get(param.modelId); if (!model) { - NcError.notFound(`Table '${param.modelId}' not found`); + NcError.notFound(`Table with id '${param.modelId}' not found`); } if (param.projectId && model.project_id !== param.projectId) { @@ -181,7 +180,7 @@ export class DataTableService { if (param.viewId) { view = await View.get(param.viewId); if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) { - NcError.unprocessableEntity(`View '${param.viewId}' not found`); + NcError.unprocessableEntity(`View with id '${param.viewId}' not found`); } } @@ -322,7 +321,7 @@ export class DataTableService { 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) NcError.badRequest(`Column with id '${param.columnId}' not found`); if (column.fk_model_id !== param.modelId) NcError.badRequest('Column not belong to model'); @@ -344,7 +343,6 @@ export class DataTableService { this.validateIds(param.refRowIds); const { model, view } = await this.getModelAndView(param); - if (!model) NcError.notFound('Table not found'); const base = await Base.get(model.base_id); @@ -380,7 +378,7 @@ export class DataTableService { this.validateIds(param.refRowIds); const { model, view } = await this.getModelAndView(param); - if (!model) NcError.notFound('Table not found'); + if (!model) NcError.notFound('Table with id ' + param.modelId + ' not found'); const base = await Base.get(model.base_id); From e7135e040ff5fb63d1abd501c4e7aab9bfb1690a Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 14:29:18 +0530 Subject: [PATCH 70/97] test: many-many, belongs to error handling Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 372 +++++++++++++----- 1 file changed, 272 insertions(+), 100 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index e7df041c36..b6d743b51b 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -208,7 +208,7 @@ async function ncAxiosLinkGet({ if (debugMode && status !== 200) { console.log('#### ', response.body.msg); } - if (msg) expect(response.body.msg).to.equal(msg); + if (!debugMode && msg) expect(response.body.msg).to.equal(msg); return response; } @@ -234,7 +234,7 @@ async function ncAxiosLinkAdd({ console.log('#### ', response.body.msg); } - if (msg) expect(response.body.msg).to.equal(msg); + if (!debugMode && msg) expect(response.body.msg).to.equal(msg); return response; } @@ -258,7 +258,7 @@ async function ncAxiosLinkRemove({ if (debugMode && status !== 200) { console.log('#### ', response.body.msg); } - if (msg) expect(response.body.msg).to.equal(msg); + if (!debugMode && msg) expect(response.body.msg).to.equal(msg); return response; } @@ -2316,18 +2316,7 @@ function linkBased() { } }); - // Error handling (has-many) - it('Error handling : Nested ADD', async function () { - const validParams = { - urlParams: { - tableId: tblCountry.id, - linkId: getColumnId(columnsCountry, 'Cities'), - rowId: 1, - }, - body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - status: 201, - }; - + async function nestedAddTests(validParams, relationType?) { // Link Add: Invalid table ID if (debugMode) console.log('Link Add: Invalid table ID'); await ncAxiosLinkAdd({ @@ -2367,49 +2356,40 @@ function linkBased() { status: 201, }); - // Link Add: Invalid body parameter - row id invalid - if (debugMode) - console.log('Link Add: Invalid body parameter - row id invalid'); - await ncAxiosLinkAdd({ - ...validParams, - body: [999, 998, 997], - status: 422, - msg: 'Child record with id 999, 998, 997 not found', - }); - - // Link Add: Invalid body parameter - repeated row id - if (debugMode) - console.log('Link Add: Invalid body parameter - repeated row id'); - await ncAxiosLinkAdd({ - ...validParams, - body: [1, 2, 1, 2], - status: 422, - msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', - }); - }); - - it('Error handling : Nested REMOVE', async function () { - // Prepare data - await ncAxiosLinkAdd({ - urlParams: { - tableId: tblCountry.id, - linkId: getColumnId(columnsCountry, 'Cities'), - rowId: 1, - }, - body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - status: 201, - }); + if (relationType === 'bt') { + // Link Add: Invalid body parameter - row id invalid + if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); + await ncAxiosLinkAdd({ + ...validParams, + body: [999, 998], + status: 422, + msg: 'Child record with id 999, 998 invalid for belongs-to relation field. Should contain only one value', + }); + } else { + // Link Add: Invalid body parameter - row id invalid + if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); + await ncAxiosLinkAdd({ + ...validParams, + body: [999, 998, 997], + status: 422, + msg: 'Child record with id 999, 998, 997 not found', + }); - const validParams = { - urlParams: { - tableId: tblCountry.id, - linkId: getColumnId(columnsCountry, 'Cities'), - rowId: 1, - }, - body: [1, 2, 3], - status: 200, - }; + // Link Add: Invalid body parameter - repeated row id + if (debugMode) + console.log('Link Add: Invalid body parameter - repeated row id'); + await ncAxiosLinkAdd({ + ...validParams, + body: [1, 2, 1, 2], + status: 422, + msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', + }); + } + } + async function nestedRemoveTests(validParams, relationType?) { // Link Remove: Invalid table ID if (debugMode) console.log('Link Remove: Invalid table ID'); await ncAxiosLinkRemove({ @@ -2449,52 +2429,40 @@ function linkBased() { status: 200, }); - // Link Remove: Invalid body parameter - row id invalid - if (debugMode) - console.log('Link Remove: Invalid body parameter - row id invalid'); - await ncAxiosLinkRemove({ - ...validParams, - body: [999, 998], - status: 422, - msg: 'Child record with id 999, 998 not found', - }); - - // Link Remove: Invalid body parameter - repeated row id - if (debugMode) - console.log('Link Remove: Invalid body parameter - repeated row id'); - await ncAxiosLinkRemove({ - ...validParams, - body: [1, 2, 1, 2], - status: 422, - msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', - }); - }); - - it('Error handling : Nested List', async function () { - // Prepare data - await ncAxiosLinkAdd({ - urlParams: { - tableId: tblCountry.id, - linkId: getColumnId(columnsCountry, 'Cities'), - rowId: 1, - }, - body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - status: 201, - }); + if (relationType === 'bt') { + // Link Remove: Invalid body parameter - row id invalid + if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); + await ncAxiosLinkRemove({ + ...validParams, + body: [999, 998], + status: 422, + msg: 'Child record with id 999, 998 invalid for belongs-to relation field. Should contain only one value', + }); + } else { + // Link Remove: Invalid body parameter - row id invalid + if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); + await ncAxiosLinkRemove({ + ...validParams, + body: [999, 998], + status: 422, + msg: 'Child record with id 999, 998 not found', + }); - const validParams = { - urlParams: { - tableId: tblCountry.id, - linkId: getColumnId(columnsCountry, 'Cities'), - rowId: 1, - }, - query: { - offset: 0, - limit: 10, - }, - status: 200, - }; + // Link Remove: Invalid body parameter - repeated row id + if (debugMode) + console.log('Link Remove: Invalid body parameter - repeated row id'); + await ncAxiosLinkRemove({ + ...validParams, + body: [1, 2, 1, 2], + status: 422, + msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', + }); + } + } + async function nestedListTests(validParams) { // Link List: Invalid table ID if (debugMode) console.log('Link List: Invalid table ID'); await ncAxiosLinkGet({ @@ -2578,6 +2546,210 @@ function linkBased() { query: { ...validParams.query, limit: 9999 }, status: 200, }); + } + + // Error handling (has-many) + it('Error handling : HM: Nested ADD', async function () { + const validParams = { + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }; + + await nestedAddTests(validParams); + }); + + it('Error handling : HM: Nested REMOVE', async function () { + // Prepare data + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + body: [1, 2, 3], + status: 200, + }; + + await nestedRemoveTests(validParams); + }); + + it('Error handling : HM: Nested List', async function () { + // Prepare data + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + query: { + offset: 0, + limit: 10, + }, + status: 200, + }; + + await nestedListTests(validParams); + }); + + // Error handling (belongs to) + it('Error handling : BT: Nested ADD', async function () { + const validParams = { + urlParams: { + tableId: tblCity.id, + linkId: getColumnId(columnsCity, 'Country'), + rowId: 1, + }, + body: [1], + status: 201, + }; + + await nestedAddTests(validParams, 'bt'); + }); + + it('Error handling : BT: Nested REMOVE', async function () { + // Prepare data + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCity.id, + linkId: getColumnId(columnsCity, 'Country'), + rowId: 1, + }, + body: [1], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblCity.id, + linkId: getColumnId(columnsCity, 'Country'), + rowId: 1, + }, + body: [1], + status: 200, + }; + + await nestedRemoveTests(validParams, 'bt'); + }); + + it('Error handling : BT: Nested List', async function () { + // Prepare data + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblCity.id, + linkId: getColumnId(columnsCity, 'Country'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblCity.id, + linkId: getColumnId(columnsCity, 'Country'), + rowId: 1, + }, + query: { + offset: 0, + limit: 10, + }, + status: 200, + }; + + await nestedListTests(validParams); + }); + + // Error handling (many-many) + it('Error handling : MM: Nested ADD', async function () { + const validParams = { + urlParams: { + tableId: tblActor.id, + linkId: getColumnId(columnsActor, 'Films'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }; + + await nestedAddTests(validParams); + }); + + it('Error handling : MM: Nested REMOVE', async function () { + // Prepare data + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblActor.id, + linkId: getColumnId(columnsActor, 'Films'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblCountry.id, + linkId: getColumnId(columnsCountry, 'Cities'), + rowId: 1, + }, + body: [1, 2, 3], + status: 200, + }; + + await nestedRemoveTests(validParams); + }); + + it('Error handling : MM: Nested List', async function () { + // Prepare data + await ncAxiosLinkAdd({ + urlParams: { + tableId: tblActor.id, + linkId: getColumnId(columnsActor, 'Films'), + rowId: 1, + }, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + status: 201, + }); + + const validParams = { + urlParams: { + tableId: tblActor.id, + linkId: getColumnId(columnsActor, 'Films'), + rowId: 1, + }, + query: { + offset: 0, + limit: 10, + }, + status: 200, + }; + + await nestedListTests(validParams); }); } From 69a394e7ff6243457685a28911b79e47f981278c Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 23:24:36 +0530 Subject: [PATCH 71/97] test: has-many crud Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index b6d743b51b..29f6db5a9a 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -1877,13 +1877,13 @@ function linkBased() { expect(rsp.body.pageInfo).to.deep.equal(pageInfo); // links - let subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - for (let i = 1; i <= 5; i++) { - expect(subResponse[i - 1]).to.deep.equal({ - Id: i, - City: `City ${i}`, - }); - } + expect(rsp.body.list).to.deep.equal([ + { Id: 1, City: 'City 1' }, + { Id: 2, City: 'City 2' }, + { Id: 3, City: 'City 3' }, + { Id: 4, City: 'City 4' }, + { Id: 5, City: 'City 5' }, + ]); /////////////////////////////////////////////////////////////////// @@ -1896,17 +1896,13 @@ function linkBased() { rowId: i, }, }); - subResponse = rsp.body.list.map(({ Id, Country }) => ({ - Id, - Country, - })); if (i <= 5) { - expect(subResponse).to.deep.equal({ - Id: i, + expect(rsp.body).to.deep.equal({ + Id: 1, Country: `Country 1`, }); } else { - expect(subResponse.list.length).to.equal(0); + expect(rsp.body).to.deep.equal({}); } } @@ -1929,34 +1925,32 @@ function linkBased() { rowId: 1, }, }); - subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - for (let i = 1; i <= 7; i++) { - expect(subResponse[i - 1]).to.deep.equal({ - Id: i, - City: `City ${i}`, - }); - } + expect(rsp.body.list).to.deep.equal([ + { Id: 1, City: 'City 1' }, + { Id: 2, City: 'City 2' }, + { Id: 3, City: 'City 3' }, + { Id: 4, City: 'City 4' }, + { Id: 5, City: 'City 5' }, + { Id: 6, City: 'City 6' }, + { Id: 7, City: 'City 7' }, + ]); // verify in City table for (let i = 1; i <= 10; i++) { - const rsp = await ncAxiosLinkGet({ + rsp = await ncAxiosLinkGet({ urlParams: { tableId: tblCity.id, linkId: getColumnId(columnsCity, 'Country'), rowId: i, }, }); - subResponse = rsp.body.list.map(({ Id, Country }) => ({ - Id, - Country, - })); if (i <= 7) { - expect(subResponse).to.deep.equal({ + expect(rsp.body).to.deep.equal({ Id: 1, Country: `Country 1`, }); } else { - expect(subResponse.list.length).to.equal(0); + expect(rsp.body).to.deep.equal({}); } } @@ -1979,14 +1973,11 @@ function linkBased() { rowId: 1, }, }); - subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - expect(subResponse.list.length).to.equal(3); - for (let i = 1; i <= 3; i++) { - expect(subResponse[i - 1]).to.deep.equal({ - Id: i * 2, - City: `City ${i * 2}`, - }); - } + expect(rsp.body.list).to.deep.equal([ + { Id: 2, City: 'City 2' }, + { Id: 4, City: 'City 4' }, + { Id: 6, City: 'City 6' }, + ]); // verify in City table for (let i = 1; i <= 10; i++) { rsp = await ncAxiosLinkGet({ @@ -1996,18 +1987,14 @@ function linkBased() { rowId: i, }, }); - subResponse = rsp.body.list.map(({ Id, Country }) => ({ - Id, - Country, - })); if (i % 2 === 0 && i <= 6) { - expect(subResponse).to.deep.equal({ + expect(rsp.body).to.deep.equal({ Id: 1, Country: `Country 1`, }); } else { - expect(subResponse.list.length).to.equal(0); + expect(rsp.body).to.deep.equal({}); } } }); From e5291bcddc07edc4c366dc947b623513b3ee0e8e Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 23:28:38 +0530 Subject: [PATCH 72/97] test: many-many crud Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../nocodb/tests/unit/rest/tests/newDataApis.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 29f6db5a9a..d614c7471e 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2013,7 +2013,9 @@ function linkBased() { linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, - body: initializeArrayFromSequence(1, 20), + body: [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ], }); await ncAxiosLinkAdd({ urlParams: { @@ -2021,7 +2023,9 @@ function linkBased() { linkId: getColumnId(columnsFilm, 'Actor List'), rowId: 1, }, - body: initializeArrayFromSequence(1, 20), + body: [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + ], }); // verify in Actor table @@ -2090,7 +2094,7 @@ function linkBased() { linkId: getColumnId(columnsActor, 'Films'), rowId: 1, }, - body: initializeArrayFromSequence(21, 10), + body: [21, 22, 23, 24, 25, 26, 27, 28, 29, 30], }); // verify in Actor table From 8a2cfdbfdf2a6cce4bbef12f72d1ca679130fb33 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 23:33:12 +0530 Subject: [PATCH 73/97] test: has-many change existing link to a new cell Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index d614c7471e..d04c7f610c 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2180,7 +2180,7 @@ function linkBased() { // Other scenarios // Has-many : change an existing link to a new one - it('Change an existing link to a new one', async function () { + it('HM: Change an existing link to a new one', async function () { // add a link await ncAxiosLinkAdd({ urlParams: { @@ -2209,12 +2209,8 @@ function linkBased() { rowId: 1, }, }); - let subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - expect(subResponse.length).to.equal(1); - expect(subResponse[0]).to.deep.equal({ - Id: 1, - City: 'City 1', - }); + expect(rsp.body.list.length).to.equal(1); + expect(rsp.body.list).to.deep.equal([{ Id: 1, City: 'City 1' }]); rsp = await ncAxiosLinkGet({ urlParams: { @@ -2224,13 +2220,10 @@ function linkBased() { }, }); expect(rsp.body.list.length).to.equal(2); - subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - for (let i = 2; i <= 3; i++) { - expect(subResponse[i - 2]).to.deep.equal({ - Id: i, - City: `City ${i}`, - }); - } + expect(rsp.body.list).to.deep.equal([ + { Id: 2, City: 'City 2' }, + { Id: 3, City: 'City 3' }, + ]); }); // limit & offset verification From ead82d12e3f9a4156cca78cf8dbfa590b023e4f4 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 23:39:58 +0530 Subject: [PATCH 74/97] test: limit & offset verification Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index d04c7f610c..c6e0942c1c 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2002,7 +2002,7 @@ function linkBased() { // Create mm link between Actor and Film // List them for a record & verify in both tables - function initializeArrayFromSequence(i, count) { + function initArraySeq(i, count) { return Array.from({ length: count }, (_, index) => i + index); } @@ -2235,7 +2235,7 @@ function linkBased() { linkId: getColumnId(columnsCountry, 'Cities'), rowId: 1, }, - body: initializeArrayFromSequence(1, 50), + body: initArraySeq(1, 50), }); // verify record 1 @@ -2251,13 +2251,18 @@ function linkBased() { }, }); expect(rsp.body.list.length).to.equal(10); - let subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - for (let i = 1; i <= 10; i++) { - expect(subResponse[i - 1]).to.deep.equal({ - Id: i, - City: `City ${i}`, - }); - } + expect(rsp.body.list).to.deep.equal([ + { Id: 1, City: 'City 1' }, + { Id: 2, City: 'City 2' }, + { Id: 3, City: 'City 3' }, + { Id: 4, City: 'City 4' }, + { Id: 5, City: 'City 5' }, + { Id: 6, City: 'City 6' }, + { Id: 7, City: 'City 7' }, + { Id: 8, City: 'City 8' }, + { Id: 9, City: 'City 9' }, + { Id: 10, City: 'City 10' }, + ]); rsp = await ncAxiosLinkGet({ urlParams: { @@ -2266,18 +2271,18 @@ function linkBased() { rowId: 1, }, query: { - limit: 10, + limit: 5, offset: 10, }, }); - subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - expect(subResponse.length).to.equal(10); - for (let i = 11; i <= 20; i++) { - expect(subResponse[i - 11]).to.deep.equal({ - Id: i, - City: `City ${i}`, - }); - } + expect(rsp.body.list.length).to.equal(5); + expect(rsp.body.list).to.deep.equal([ + { Id: 11, City: 'City 11' }, + { Id: 12, City: 'City 12' }, + { Id: 13, City: 'City 13' }, + { Id: 14, City: 'City 14' }, + { Id: 15, City: 'City 15' }, + ]); rsp = await ncAxiosLinkGet({ urlParams: { @@ -2287,17 +2292,17 @@ function linkBased() { }, query: { limit: 100, - offset: 40, + offset: 45, }, }); - subResponse = rsp.body.list.map(({ Id, City }) => ({ Id, City })); - expect(subResponse.length).to.equal(10); - for (let i = 41; i <= 50; i++) { - expect(subResponse[i - 41]).to.deep.equal({ - Id: i, - City: `City ${i}`, - }); - } + expect(rsp.body.list.length).to.equal(5); + expect(rsp.body.list).to.deep.equal([ + { Id: 46, City: 'City 46' }, + { Id: 47, City: 'City 47' }, + { Id: 48, City: 'City 48' }, + { Id: 49, City: 'City 49' }, + { Id: 50, City: 'City 50' }, + ]); }); async function nestedAddTests(validParams, relationType?) { From 3a236654ae9190f6062a63e5012563a9625583f4 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sat, 17 Jun 2023 23:53:12 +0530 Subject: [PATCH 75/97] test: error validation Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/src/db/BaseModelSqlv2.ts | 12 ++++---- .../tests/unit/rest/tests/newDataApis.test.ts | 30 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 08d1514a30..d20b689a1c 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3700,7 +3700,7 @@ class BaseModelSqlv2 { ); NcError.unprocessableEntity( - `Child record with id ${missingIds.join(', ')} not found`, + `Child record with id [${missingIds.join(', ')}] not found`, ); } @@ -3778,7 +3778,7 @@ class BaseModelSqlv2 { ); NcError.unprocessableEntity( - `Child record with id ${missingIds.join(', ')} not found`, + `Child record with id [${missingIds.join(', ')}] not found`, ); } } @@ -3810,7 +3810,7 @@ class BaseModelSqlv2 { if (!childRow) { NcError.unprocessableEntity( - `Child record with id ${childIds[0]} not found`, + `Child record with id [${childIds[0]}] not found`, ); } } @@ -3902,7 +3902,7 @@ class BaseModelSqlv2 { ); NcError.unprocessableEntity( - `Child record with id ${missingIds.join(', ')} not found`, + `Child record with id [${missingIds.join(', ')}] not found`, ); } } @@ -3942,7 +3942,7 @@ class BaseModelSqlv2 { ); NcError.unprocessableEntity( - `Child record with id ${missingIds.join(', ')} not found`, + `Child record with id [${missingIds.join(', ')}] not found`, ); } } @@ -3977,7 +3977,7 @@ class BaseModelSqlv2 { if (!childRow) { NcError.unprocessableEntity( - `Child record with id ${childIds[0]} not found`, + `Child record with id [${childIds[0]}] not found`, ); } } diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index c6e0942c1c..a783622caf 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2312,7 +2312,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, status: 404, - msg: "Table '9999' not found", + msg: "Table with id '9999' not found", }); // Link Add: Invalid link ID @@ -2321,7 +2321,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, status: 404, - msg: "Column '9999' not found", + msg: "Column with id '9999' not found", }); // Link Add: Invalid Source row ID @@ -2330,7 +2330,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, - msg: "Row '9999' not found", + msg: "Row with id '9999' not found", }); // Body parameter error @@ -2353,7 +2353,7 @@ function linkBased() { ...validParams, body: [999, 998], status: 422, - msg: 'Child record with id 999, 998 invalid for belongs-to relation field. Should contain only one value', + msg: 'Child record with id [999, 998] invalid for belongs-to relation field. Should contain only one value', }); } else { // Link Add: Invalid body parameter - row id invalid @@ -2363,7 +2363,7 @@ function linkBased() { ...validParams, body: [999, 998, 997], status: 422, - msg: 'Child record with id 999, 998, 997 not found', + msg: 'Child record with id [999, 998, 997] not found', }); // Link Add: Invalid body parameter - repeated row id @@ -2373,7 +2373,7 @@ function linkBased() { ...validParams, body: [1, 2, 1, 2], status: 422, - msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', + msg: 'Child record with id [1, 2, 1, 2] contains duplicate value', }); } } @@ -2385,7 +2385,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, status: 404, - msg: "Table '9999' not found", + msg: "Table with id '9999' not found", }); // Link Remove: Invalid link ID @@ -2394,7 +2394,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, status: 404, - msg: "Column '9999' not found", + msg: "Column with id '9999' not found", }); // Link Remove: Invalid Source row ID @@ -2403,7 +2403,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, - msg: "Row '9999' not found", + msg: "Row with id '9999' not found", }); // Body parameter error @@ -2426,7 +2426,7 @@ function linkBased() { ...validParams, body: [999, 998], status: 422, - msg: 'Child record with id 999, 998 invalid for belongs-to relation field. Should contain only one value', + msg: 'Child record with id [999, 998] invalid for belongs-to relation field. Should contain only one value', }); } else { // Link Remove: Invalid body parameter - row id invalid @@ -2436,7 +2436,7 @@ function linkBased() { ...validParams, body: [999, 998], status: 422, - msg: 'Child record with id 999, 998 not found', + msg: 'Child record with id [999, 998] not found', }); // Link Remove: Invalid body parameter - repeated row id @@ -2446,7 +2446,7 @@ function linkBased() { ...validParams, body: [1, 2, 1, 2], status: 422, - msg: 'Child record with id 1, 2, 1, 2 contains duplicate value', + msg: 'Child record with id [1, 2, 1, 2] contains duplicate value', }); } } @@ -2458,7 +2458,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, status: 404, - msg: "Table '9999' not found", + msg: "Table with id '9999' not found", }); // Link List: Invalid link ID @@ -2467,7 +2467,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, status: 404, - msg: "Column '9999' not found", + msg: "Column with id '9999' not found", }); // Link List: Invalid Source row ID @@ -2476,7 +2476,7 @@ function linkBased() { ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, status: 404, - msg: "Row '9999' not found", + msg: "Row with id '9999' not found", }); // Query parameter error From a8cf1ed822b52fa8397fe57bc4373cdc69dfabf2 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Sun, 18 Jun 2023 21:21:47 +0530 Subject: [PATCH 76/97] test: datetime fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 63 +++---------------- 1 file changed, 8 insertions(+), 55 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index a783622caf..377e430167 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -1568,59 +1568,6 @@ function dateBased() { expect(insertedRecords.length).to.equal(800); }); - const records = [ - { - Id: 1, - Date: '2022-04-25', - DateTime: '2022-04-25T06:30:00.000Z', - }, - { - Id: 2, - Date: '2022-04-26', - DateTime: '2022-04-26T06:30:00.000Z', - }, - { - Id: 3, - Date: '2022-04-27', - DateTime: '2022-04-27T06:30:00.000Z', - }, - { - Id: 4, - Date: '2022-04-28', - DateTime: '2022-04-28T06:30:00.000Z', - }, - { - Id: 5, - Date: '2022-04-29', - DateTime: '2022-04-29T06:30:00.000Z', - }, - { - Id: 6, - Date: '2022-04-30', - DateTime: '2022-04-30T06:30:00.000Z', - }, - { - Id: 7, - Date: '2022-05-01', - DateTime: '2022-05-01T06:30:00.000Z', - }, - { - Id: 8, - Date: '2022-05-02', - DateTime: '2022-05-02T06:30:00.000Z', - }, - { - Id: 9, - Date: '2022-05-03', - DateTime: '2022-05-03T06:30:00.000Z', - }, - { - Id: 10, - Date: '2022-05-04', - DateTime: '2022-05-04T06:30:00.000Z', - }, - ]; - it('Date based- List & CRUD', async function () { // list 10 records let rsp = await ncAxiosGet({ @@ -1635,8 +1582,14 @@ function dateBased() { isFirstPage: true, isLastPage: false, }; + expect(rsp.body.pageInfo).to.deep.equal(pageInfo); - expect(rsp.body.list).to.deep.equal(records); + + // extract first 10 records from inserted records + const records = insertedRecords.slice(0, 10); + expect(JSON.stringify(rsp.body.list)).to.deep.equal( + JSON.stringify(records), + ); /////////////////////////////////////////////////////////////////////////// @@ -1667,7 +1620,7 @@ function dateBased() { // update record with Id 801 to 804 const updatedRecord = { Date: '2022-04-25', - DateTime: '2022-04-25T06:30:00.000Z', + DateTime: '2022-04-25 08:30:00+00:00', }; const updatedRecords = [ { From 8a49da271201adc8a55ea4c759d910ec1e5feef3 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:53:22 +0530 Subject: [PATCH 77/97] test: error msg fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 377e430167..fb1659194f 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -513,7 +513,15 @@ function textBased() { isLastPage: false, }; expect(rsp.body.pageInfo).to.deep.equal(expectedPageInfo); + + // verify if all the columns are present in the response expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true); + + // verify column data + const expectedData = insertedRecords.slice(0, 1); + expect(JSON.stringify(rsp.body.list[0])).to.deep.equal( + JSON.stringify(expectedData[0]), + ); }); it('List: offset, limit', async function () { @@ -883,6 +891,8 @@ function textBased() { }); it('List: invalid sort, filter, fields', async function () { + // expect to ignore invalid sort, filter, fields + await ncAxiosGet({ query: { sort: 'abc', @@ -2326,7 +2336,7 @@ function linkBased() { ...validParams, body: [1, 2, 1, 2], status: 422, - msg: 'Child record with id [1, 2, 1, 2] contains duplicate value', + msg: 'Child record with id [1, 2] contains duplicate value', }); } } @@ -2379,7 +2389,7 @@ function linkBased() { ...validParams, body: [999, 998], status: 422, - msg: 'Child record with id [999, 998] invalid for belongs-to relation field. Should contain only one value', + msg: 'Child record with id [999, 998] invalid. Belongs-to can link to only one record', }); } else { // Link Remove: Invalid body parameter - row id invalid @@ -2399,7 +2409,7 @@ function linkBased() { ...validParams, body: [1, 2, 1, 2], status: 422, - msg: 'Child record with id [1, 2, 1, 2] contains duplicate value', + msg: 'Child record with id [1, 2] contains duplicate value', }); } } @@ -2700,15 +2710,15 @@ function linkBased() { /////////////////////////////////////////////////////////////////////////////// export default function () { - // based out of sakila db, for link based tests - describe('General', generalDb); - // standalone tables describe('Text based', textBased); describe('Numerical', numberBased); describe('Select based', selectBased); describe('Date based', dateBased); describe('Link based', linkBased); + + // based out of sakila db, for link based tests + describe('General', generalDb); } /////////////////////////////////////////////////////////////////////////////// From 73f83a881590cb50373b59f858118d0dde989305 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 19 Jun 2023 12:30:42 +0530 Subject: [PATCH 78/97] fix: error code corrections Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 8 +++--- .../nocodb/src/services/data-table.service.ts | 27 ++++++++++++++----- .../tests/unit/rest/tests/newDataApis.test.ts | 2 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index d20b689a1c..33a61fef2d 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3628,7 +3628,7 @@ class BaseModelSqlv2 { const column = columns.find((c) => c.id === colId); if (!column || column.uidt !== UITypes.LinkToAnotherRecord) - NcError.notFound(`Link column with id ${colId} not found`); + NcError.notFound(`Link column ${colId} not found`); const row = await this.dbDriver(this.tnPath) .where(await this._wherePk(rowId)) @@ -3636,7 +3636,7 @@ class BaseModelSqlv2 { // validate rowId if (!row) { - NcError.notFound(`Row with id ${rowId} not found`); + NcError.notFound(`Row '${rowId}' not found`); } const colOptions = await column.getColOptions(); @@ -3851,7 +3851,7 @@ class BaseModelSqlv2 { const column = columns.find((c) => c.id === colId); if (!column || column.uidt !== UITypes.LinkToAnotherRecord) - NcError.notFound(`Link column with id ${colId} not found`); + NcError.notFound(`Link column ${colId} not found`); const row = await this.dbDriver(this.tnPath) .where(await this._wherePk(rowId)) @@ -3859,7 +3859,7 @@ class BaseModelSqlv2 { // validate rowId if (!row) { - NcError.notFound(`Row with id ${rowId} not found`); + NcError.notFound(`Row '${rowId}' not found`); } const colOptions = await column.getColOptions(); diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 1060c4310f..fb319aafdd 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -168,11 +168,11 @@ export class DataTableService { const model = await Model.get(param.modelId); if (!model) { - NcError.notFound(`Table with id '${param.modelId}' not found`); + NcError.notFound(`Table '${param.modelId}' not found`); } if (param.projectId && model.project_id !== param.projectId) { - throw new Error('Model not belong to project'); + throw new Error('Table not belong to project'); } let view: View; @@ -180,7 +180,7 @@ export class DataTableService { if (param.viewId) { view = await View.get(param.viewId); if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) { - NcError.unprocessableEntity(`View with id '${param.viewId}' not found`); + NcError.unprocessableEntity(`View '${param.viewId}' not found`); } } @@ -252,6 +252,11 @@ export class DataTableService { viewId: view?.id, dbDriver: await NcConnectionMgrv2.get(base), }); + + if (!(await baseModel.exist(param.rowId))) { + NcError.notFound(`Row '${param.rowId}' not found`); + } + const column = await this.getColumn(param); const colOptions = await column.getColOptions(); @@ -321,7 +326,7 @@ export class DataTableService { private async getColumn(param: { modelId: string; columnId: string }) { const column = await Column.get({ colId: param.columnId }); - if (!column) NcError.badRequest(`Column with id '${param.columnId}' not found`); + if (!column) NcError.notFound(`Column '${param.columnId}' not found`); if (column.fk_model_id !== param.modelId) NcError.badRequest('Column not belong to model'); @@ -378,7 +383,8 @@ export class DataTableService { this.validateIds(param.refRowIds); const { model, view } = await this.getModelAndView(param); - if (!model) NcError.notFound('Table with id ' + param.modelId + ' not found'); + if (!model) + NcError.notFound('Table with id ' + param.modelId + ' not found'); const base = await Base.get(model.base_id); @@ -405,14 +411,21 @@ export class DataTableService { private validateIds(rowIds: any[] | any) { if (Array.isArray(rowIds)) { const map = new Map(); + const set = new Set(); for (const rowId of rowIds) { if (rowId === undefined || rowId === null) NcError.unprocessableEntity('Invalid row id ' + rowId); if (map.has(rowId)) { - NcError.unprocessableEntity('Duplicate row with id ' + rowId); + set.add(rowId); + } else { + map.set(rowId, true); } - map.set(rowId, true); } + + if (set.size > 0) + NcError.unprocessableEntity( + 'Child record with id [' + [...set].join(', ') + '] are duplicated', + ); } else if (rowIds === undefined || rowIds === null) { NcError.unprocessableEntity('Invalid row id ' + rowIds); } diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index fb1659194f..3b6bc8f544 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2409,7 +2409,7 @@ function linkBased() { ...validParams, body: [1, 2, 1, 2], status: 422, - msg: 'Child record with id [1, 2] contains duplicate value', + msg: 'Child record with id [1, 2] are duplicated', }); } } From ac26e141ec9907290c5d8ac0988e433d8d655488 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 22 Jun 2023 00:35:23 +0530 Subject: [PATCH 79/97] refactor: update error message Signed-off-by: Pranav C --- packages/nocodb/src/services/data-table.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index fb319aafdd..2144e26d72 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -168,7 +168,7 @@ export class DataTableService { const model = await Model.get(param.modelId); if (!model) { - NcError.notFound(`Table '${param.modelId}' not found`); + NcError.notFound(`Table with id '${param.modelId}' not found`); } if (param.projectId && model.project_id !== param.projectId) { From 2ace61ee770af0c5cb36b00ef02b1ab17c3f1170 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 22 Jun 2023 09:34:31 +0530 Subject: [PATCH 80/97] test: error message correction Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/src/services/data-table.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 2144e26d72..75ca740039 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -326,7 +326,8 @@ export class DataTableService { private async getColumn(param: { modelId: string; columnId: string }) { const column = await Column.get({ colId: param.columnId }); - if (!column) NcError.notFound(`Column '${param.columnId}' not found`); + if (!column) + NcError.notFound(`Column with id '${param.columnId}' not found`); if (column.fk_model_id !== param.modelId) NcError.badRequest('Column not belong to model'); From 3f5066a457feddc5834a2bc541b98895a62f8a82 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 22 Jun 2023 09:51:39 +0530 Subject: [PATCH 81/97] test: error message correction (2) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- packages/nocodb/src/db/BaseModelSqlv2.ts | 4 ++-- packages/nocodb/src/services/data-table.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 33a61fef2d..d56769b8a9 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3636,7 +3636,7 @@ class BaseModelSqlv2 { // validate rowId if (!row) { - NcError.notFound(`Row '${rowId}' not found`); + NcError.notFound(`Row with id '${rowId}' not found`); } const colOptions = await column.getColOptions(); @@ -3859,7 +3859,7 @@ class BaseModelSqlv2 { // validate rowId if (!row) { - NcError.notFound(`Row '${rowId}' not found`); + NcError.notFound(`Row with id '${rowId}' not found`); } const colOptions = await column.getColOptions(); diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 75ca740039..262961abac 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -180,7 +180,7 @@ export class DataTableService { if (param.viewId) { view = await View.get(param.viewId); if (!view || (view.fk_model_id && view.fk_model_id !== param.modelId)) { - NcError.unprocessableEntity(`View '${param.viewId}' not found`); + NcError.unprocessableEntity(`View with id '${param.viewId}' not found`); } } @@ -254,7 +254,7 @@ export class DataTableService { }); if (!(await baseModel.exist(param.rowId))) { - NcError.notFound(`Row '${param.rowId}' not found`); + NcError.notFound(`Row with id '${param.rowId}' not found`); } const column = await this.getColumn(param); From 6a1805e0f48ec9336eaf73d6bdb6ba8744367ec9 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 22 Jun 2023 10:13:30 +0530 Subject: [PATCH 82/97] test: error message correction (3) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 3b6bc8f544..6476af1977 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2270,7 +2270,8 @@ function linkBased() { async function nestedAddTests(validParams, relationType?) { // Link Add: Invalid table ID - if (debugMode) console.log('Link Add: Invalid table ID'); + // if (debugMode) + console.log('Link Add: Invalid table ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, @@ -2279,7 +2280,8 @@ function linkBased() { }); // Link Add: Invalid link ID - if (debugMode) console.log('Link Add: Invalid link ID'); + // if (debugMode) + console.log('Link Add: Invalid link ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, @@ -2288,7 +2290,8 @@ function linkBased() { }); // Link Add: Invalid Source row ID - if (debugMode) console.log('Link Add: Invalid Source row ID'); + // if (debugMode) + console.log('Link Add: Invalid Source row ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, @@ -2300,8 +2303,8 @@ function linkBased() { // // Link Add: Invalid body parameter - empty body : ignore - if (debugMode) - console.log('Link Add: Invalid body parameter - empty body : ignore'); + // if (debugMode) + console.log('Link Add: Invalid body parameter - empty body : ignore'); await ncAxiosLinkAdd({ ...validParams, body: [], @@ -2310,8 +2313,8 @@ function linkBased() { if (relationType === 'bt') { // Link Add: Invalid body parameter - row id invalid - if (debugMode) - console.log('Link Add: Invalid body parameter - row id invalid'); + // if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); await ncAxiosLinkAdd({ ...validParams, body: [999, 998], @@ -2320,8 +2323,8 @@ function linkBased() { }); } else { // Link Add: Invalid body parameter - row id invalid - if (debugMode) - console.log('Link Add: Invalid body parameter - row id invalid'); + // if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); await ncAxiosLinkAdd({ ...validParams, body: [999, 998, 997], @@ -2330,20 +2333,21 @@ function linkBased() { }); // Link Add: Invalid body parameter - repeated row id - if (debugMode) - console.log('Link Add: Invalid body parameter - repeated row id'); + // if (debugMode) + console.log('Link Add: Invalid body parameter - repeated row id'); await ncAxiosLinkAdd({ ...validParams, body: [1, 2, 1, 2], status: 422, - msg: 'Child record with id [1, 2] contains duplicate value', + msg: 'Child record with id [1, 2] are duplicated', }); } } async function nestedRemoveTests(validParams, relationType?) { // Link Remove: Invalid table ID - if (debugMode) console.log('Link Remove: Invalid table ID'); + // if (debugMode) + console.log('Link Remove: Invalid table ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, @@ -2352,7 +2356,8 @@ function linkBased() { }); // Link Remove: Invalid link ID - if (debugMode) console.log('Link Remove: Invalid link ID'); + // if (debugMode) + console.log('Link Remove: Invalid link ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, @@ -2361,7 +2366,8 @@ function linkBased() { }); // Link Remove: Invalid Source row ID - if (debugMode) console.log('Link Remove: Invalid Source row ID'); + // if (debugMode) + console.log('Link Remove: Invalid Source row ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, @@ -2373,8 +2379,8 @@ function linkBased() { // // Link Remove: Invalid body parameter - empty body : ignore - if (debugMode) - console.log('Link Remove: Invalid body parameter - empty body : ignore'); + // if (debugMode) + console.log('Link Remove: Invalid body parameter - empty body : ignore'); await ncAxiosLinkRemove({ ...validParams, body: [], @@ -2383,8 +2389,8 @@ function linkBased() { if (relationType === 'bt') { // Link Remove: Invalid body parameter - row id invalid - if (debugMode) - console.log('Link Remove: Invalid body parameter - row id invalid'); + // if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); await ncAxiosLinkRemove({ ...validParams, body: [999, 998], @@ -2393,8 +2399,8 @@ function linkBased() { }); } else { // Link Remove: Invalid body parameter - row id invalid - if (debugMode) - console.log('Link Remove: Invalid body parameter - row id invalid'); + // if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); await ncAxiosLinkRemove({ ...validParams, body: [999, 998], @@ -2403,8 +2409,8 @@ function linkBased() { }); // Link Remove: Invalid body parameter - repeated row id - if (debugMode) - console.log('Link Remove: Invalid body parameter - repeated row id'); + // if (debugMode) + console.log('Link Remove: Invalid body parameter - repeated row id'); await ncAxiosLinkRemove({ ...validParams, body: [1, 2, 1, 2], From 2806aef2e7f1d78269c72f4e18978345978310c6 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 22 Jun 2023 10:59:19 +0530 Subject: [PATCH 83/97] refactor: if child ids array empty return without throwing error Signed-off-by: Pranav C --- packages/nocodb/src/db/BaseModelSqlv2.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index d56769b8a9..1334fa805f 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -3639,6 +3639,8 @@ class BaseModelSqlv2 { NcError.notFound(`Row with id '${rowId}' not found`); } + if (!childIds.length) return; + const colOptions = await column.getColOptions(); const childColumn = await colOptions.getChildColumn(); @@ -3862,6 +3864,8 @@ class BaseModelSqlv2 { NcError.notFound(`Row with id '${rowId}' not found`); } + if (!childIds.length) return; + const colOptions = await column.getColOptions(); const childColumn = await colOptions.getChildColumn(); From d1999cacebe428feb985e9453fe6181da8e07926 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:58:51 +0530 Subject: [PATCH 84/97] test: error message corrections (4) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index 6476af1977..a06d8ece0d 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -2270,8 +2270,7 @@ function linkBased() { async function nestedAddTests(validParams, relationType?) { // Link Add: Invalid table ID - // if (debugMode) - console.log('Link Add: Invalid table ID'); + if (debugMode) console.log('Link Add: Invalid table ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, @@ -2280,8 +2279,7 @@ function linkBased() { }); // Link Add: Invalid link ID - // if (debugMode) - console.log('Link Add: Invalid link ID'); + if (debugMode) console.log('Link Add: Invalid link ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, @@ -2290,8 +2288,7 @@ function linkBased() { }); // Link Add: Invalid Source row ID - // if (debugMode) - console.log('Link Add: Invalid Source row ID'); + if (debugMode) console.log('Link Add: Invalid Source row ID'); await ncAxiosLinkAdd({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, @@ -2303,8 +2300,8 @@ function linkBased() { // // Link Add: Invalid body parameter - empty body : ignore - // if (debugMode) - console.log('Link Add: Invalid body parameter - empty body : ignore'); + if (debugMode) + console.log('Link Add: Invalid body parameter - empty body : ignore'); await ncAxiosLinkAdd({ ...validParams, body: [], @@ -2313,18 +2310,18 @@ function linkBased() { if (relationType === 'bt') { // Link Add: Invalid body parameter - row id invalid - // if (debugMode) - console.log('Link Add: Invalid body parameter - row id invalid'); + if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); await ncAxiosLinkAdd({ ...validParams, body: [999, 998], status: 422, - msg: 'Child record with id [999, 998] invalid for belongs-to relation field. Should contain only one value', + msg: 'Child record with id [999] not found', }); } else { // Link Add: Invalid body parameter - row id invalid - // if (debugMode) - console.log('Link Add: Invalid body parameter - row id invalid'); + if (debugMode) + console.log('Link Add: Invalid body parameter - row id invalid'); await ncAxiosLinkAdd({ ...validParams, body: [999, 998, 997], @@ -2333,8 +2330,8 @@ function linkBased() { }); // Link Add: Invalid body parameter - repeated row id - // if (debugMode) - console.log('Link Add: Invalid body parameter - repeated row id'); + if (debugMode) + console.log('Link Add: Invalid body parameter - repeated row id'); await ncAxiosLinkAdd({ ...validParams, body: [1, 2, 1, 2], @@ -2346,8 +2343,7 @@ function linkBased() { async function nestedRemoveTests(validParams, relationType?) { // Link Remove: Invalid table ID - // if (debugMode) - console.log('Link Remove: Invalid table ID'); + if (debugMode) console.log('Link Remove: Invalid table ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, tableId: 9999 }, @@ -2356,8 +2352,7 @@ function linkBased() { }); // Link Remove: Invalid link ID - // if (debugMode) - console.log('Link Remove: Invalid link ID'); + if (debugMode) console.log('Link Remove: Invalid link ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, linkId: 9999 }, @@ -2366,8 +2361,7 @@ function linkBased() { }); // Link Remove: Invalid Source row ID - // if (debugMode) - console.log('Link Remove: Invalid Source row ID'); + if (debugMode) console.log('Link Remove: Invalid Source row ID'); await ncAxiosLinkRemove({ ...validParams, urlParams: { ...validParams.urlParams, rowId: 9999 }, @@ -2379,8 +2373,8 @@ function linkBased() { // // Link Remove: Invalid body parameter - empty body : ignore - // if (debugMode) - console.log('Link Remove: Invalid body parameter - empty body : ignore'); + if (debugMode) + console.log('Link Remove: Invalid body parameter - empty body : ignore'); await ncAxiosLinkRemove({ ...validParams, body: [], @@ -2389,18 +2383,18 @@ function linkBased() { if (relationType === 'bt') { // Link Remove: Invalid body parameter - row id invalid - // if (debugMode) - console.log('Link Remove: Invalid body parameter - row id invalid'); + if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); await ncAxiosLinkRemove({ ...validParams, body: [999, 998], status: 422, - msg: 'Child record with id [999, 998] invalid. Belongs-to can link to only one record', + msg: 'Request must contain only one parent id', }); } else { // Link Remove: Invalid body parameter - row id invalid - // if (debugMode) - console.log('Link Remove: Invalid body parameter - row id invalid'); + if (debugMode) + console.log('Link Remove: Invalid body parameter - row id invalid'); await ncAxiosLinkRemove({ ...validParams, body: [999, 998], @@ -2409,8 +2403,8 @@ function linkBased() { }); // Link Remove: Invalid body parameter - repeated row id - // if (debugMode) - console.log('Link Remove: Invalid body parameter - repeated row id'); + if (debugMode) + console.log('Link Remove: Invalid body parameter - repeated row id'); await ncAxiosLinkRemove({ ...validParams, body: [1, 2, 1, 2], From 7832272df69b056d3511277dce2d582c32d3af32 Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:38:57 +0530 Subject: [PATCH 85/97] test: number based corrections for PG Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 140 +++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index a06d8ece0d..d0886dc5aa 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -97,6 +97,7 @@ import { } from '../../factory/column'; import { createView, updateView } from '../../factory/view'; +import { isPg } from '../../init/db'; import type { ColumnType } from 'nocodb-sdk'; import type Project from '../../../../src/models/Project'; import type Model from '../../../../src/models/Model'; @@ -1256,6 +1257,99 @@ function numberBased() { }, ]; + const recordsPg = [ + { + Id: 1, + Number: '33', + Decimal: '33.3', + Currency: '33.3', + Percent: 33, + Duration: '10', + Rating: 0, + }, + { + Id: 2, + Number: null, + Decimal: '456.34', + Currency: '456.34', + Percent: null, + Duration: '20', + Rating: 1, + }, + { + Id: 3, + Number: '456', + Decimal: '333.3', + Currency: '333.3', + Percent: 456, + Duration: '30', + Rating: 2, + }, + { + Id: 4, + Number: '333', + Decimal: null, + Currency: null, + Percent: 333, + Duration: '40', + Rating: 3, + }, + { + Id: 5, + Number: '267', + Decimal: '267.5674', + Currency: '267.5674', + Percent: 267, + Duration: '50', + Rating: null, + }, + { + Id: 6, + Number: '34', + Decimal: '34', + Currency: '34', + Percent: 34, + Duration: '60', + Rating: 0, + }, + { + Id: 7, + Number: '8754', + Decimal: '8754', + Currency: '8754', + Percent: 8754, + Duration: null, + Rating: 4, + }, + { + Id: 8, + Number: '3234', + Decimal: '3234.547', + Currency: '3234.547', + Percent: 3234, + Duration: '70', + Rating: 5, + }, + { + Id: 9, + Number: '44', + Decimal: '44.2647', + Currency: '44.2647', + Percent: 44, + Duration: '80', + Rating: 0, + }, + { + Id: 10, + Number: '33', + Decimal: '33.98', + Currency: '33.98', + Percent: 33, + Duration: '90', + Rating: 1, + }, + ]; + it('Number based- List & CRUD', async function () { // list 10 records let rsp = await ncAxiosGet({ @@ -1271,7 +1365,11 @@ function numberBased() { isLastPage: false, }; expect(rsp.body.pageInfo).to.deep.equal(pageInfo); - expect(rsp.body.list).to.deep.equal(records); + if (isPg(context)) { + expect(rsp.body.list).to.deep.equal(recordsPg); + } else { + expect(rsp.body.list).to.deep.equal(records); + } /////////////////////////////////////////////////////////////////////////// @@ -1295,7 +1393,11 @@ function numberBased() { rsp = await ncAxiosGet({ url: `/api/v1/tables/${table.id}/rows/401`, }); - expect(rsp.body).to.deep.equal({ Id: 401, ...records[0] }); + if (isPg(context)) { + expect(rsp.body).to.deep.equal({ ...recordsPg[0], Id: 401 }); + } else { + expect(rsp.body).to.deep.equal({ ...records[0], Id: 401 }); + } /////////////////////////////////////////////////////////////////////////// @@ -1308,6 +1410,15 @@ function numberBased() { Duration: 55, Rating: 5, }; + const updatedRecordPg = { + Number: '55', + Decimal: '55.5', + Currency: '55.5', + Percent: 55, + Duration: '55', + Rating: 5, + }; + const updatedRecords = [ { Id: 401, @@ -1326,6 +1437,25 @@ function numberBased() { ...updatedRecord, }, ]; + const updatedRecordsPg = [ + { + Id: 401, + ...updatedRecordPg, + }, + { + Id: 402, + ...updatedRecordPg, + }, + { + Id: 403, + ...updatedRecordPg, + }, + { + Id: 404, + ...updatedRecordPg, + }, + ]; + rsp = await ncAxiosPatch({ body: updatedRecords, }); @@ -1340,7 +1470,11 @@ function numberBased() { offset: 400, }, }); - expect(rsp.body.list).to.deep.equal(updatedRecords); + if (isPg(context)) { + expect(rsp.body.list).to.deep.equal(updatedRecordsPg); + } else { + expect(rsp.body.list).to.deep.equal(updatedRecords); + } /////////////////////////////////////////////////////////////////////////// From 0dee98308b7ac925036b877ca3c78854592adf8b Mon Sep 17 00:00:00 2001 From: Raju Udava <86527202+dstala@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:45:52 +0530 Subject: [PATCH 86/97] test: number based corrections for PG (2) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> --- .../tests/unit/rest/tests/newDataApis.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts index d0886dc5aa..7e3ab6dd08 100644 --- a/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts +++ b/packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts @@ -393,6 +393,7 @@ function generalDb() { }); const expectedRecords = [1, 3, 1, 2]; + const expectedRecordsPg = ['1', '3', '1', '2']; // read first 4 records const records = await ncAxiosGet({ @@ -405,7 +406,11 @@ function generalDb() { // extract Lookup column const rollupData = records.body.list.map((record) => record.Rollup); - expect(rollupData).to.deep.equal(expectedRecords); + if (isPg(context)) { + expect(rollupData).to.deep.equal(expectedRecordsPg); + } else { + expect(rollupData).to.deep.equal(expectedRecords); + } }); it('Nested Read - Link to another record', async function () { @@ -450,7 +455,11 @@ function generalDb() { const records = await ncAxiosGet({ url: `/api/v1/tables/${countryTable.id}/rows/1`, }); - expect(records.body.Rollup).to.equal(1); + if (isPg(context)) { + expect(records.body.Rollup).to.equal('1'); + } else { + expect(records.body.Rollup).to.equal(1); + } }); } From dd5bd7c06048d6a194bf3968817dfb3f361757ef Mon Sep 17 00:00:00 2001 From: Pranav C Date: Thu, 22 Jun 2023 15:37:27 +0530 Subject: [PATCH 87/97] refactor: update docs and swagger Signed-off-by: Pranav C --- .../en/developer-resources/rest-apis.md | 6 - packages/nocodb/src/schema/swagger.json | 345 ------------------ 2 files changed, 351 deletions(-) diff --git a/packages/noco-docs/content/en/developer-resources/rest-apis.md b/packages/noco-docs/content/en/developer-resources/rest-apis.md index f1467eea06..b2e8aa693b 100644 --- a/packages/noco-docs/content/en/developer-resources/rest-apis.md +++ b/packages/noco-docs/content/en/developer-resources/rest-apis.md @@ -74,12 +74,6 @@ Currently, the default value for {orgs} is noco. Users will be able to ch | Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} | | Data | Get | dbViewRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count | | Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/group/{columnId} | -| Data | Get | dbTableRow | tableRowList | /api/v1/tables/{tableId} | -| Data | Post | dbTableRow | tableRowCreate | /api/v1/tables/{tableId} | -| Data | Get | dbTableRow | tableRowRead | /api/v1/tables/{tableId}/rows/{rowId} | -| Data | Patch | dbTableRow | tableRowUpdate | /api/v1/tables/{tableId}/rows | -| Data | Delete| dbTableRow | tableRowDelete | /api/v1/tables/{tableId}/rows | -| Data | Get | dbTableRow | tableRowCount | /api/v1/tables/{tableId}/rows/count | ### Meta APIs diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 9269c1ef5c..84d509f0ef 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -13990,351 +13990,6 @@ } ] } - }, - "/api/v1/tables/{tableId}/rows": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "tableId", - "in": "path", - "required": true, - "description": "Table Id" - }, - { - "schema": { - "type": "string" - }, - "name": "viewId", - "in": "query" - } - ], - "get": { - "summary": "List Table View Rows", - "operationId": "table-row-list", - "description": "List all table view rows", - "tags": ["DB Table Row"], - "parameters": [ - { - "schema": { - "type": "array" - }, - "in": "query", - "name": "fields" - }, - { - "schema": { - "type": "array" - }, - "in": "query", - "name": "sort" - }, - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "where" - }, - { - "schema": {}, - "in": "query", - "name": "nested", - "description": "Query params for nested data" - }, - { - "schema": { - "type": "number" - }, - "in": "query", - "name": "offset" - }, - { - "$ref": "#/components/parameters/xc-auth" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "list": { - "type": "array", - "x-stoplight": { - "id": "okd8utzm9xqet" - }, - "description": "List of table view rows", - "items": { - "x-stoplight": { - "id": "j758lsjv53o4q" - }, - "type": "object" - } - }, - "pageInfo": { - "$ref": "#/components/schemas/Paginated", - "x-stoplight": { - "id": "hylgqzgm8yhye" - }, - "description": "Paginated Info" - } - }, - "required": ["list", "pageInfo"] - }, - "examples": { - "Example 1": { - "value": { - "list": [ - { - "Id": 1, - "Title": "baz", - "SingleSelect": null, - "Sheet-1 List": [ - { - "Id": 1, - "Title": "baz" - } - ], - "LTAR": [ - { - "Id": 1, - "Title": "baz" - } - ] - }, - { - "Id": 2, - "Title": "foo", - "SingleSelect": "a", - "Sheet-1 List": [ - { - "Id": 2, - "Title": "foo" - } - ], - "LTAR": [ - { - "Id": 2, - "Title": "foo" - } - ] - }, - { - "Id": 3, - "Title": "bar", - "SingleSelect": "b", - "Sheet-1 List": [], - "LTAR": [] - } - ], - "pageInfo": { - "totalRows": 3, - "page": 1, - "pageSize": 25, - "isFirstPage": true, - "isLastPage": true - } - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - } - }, - "post": { - "summary": "Create Table View Row", - "operationId": "table-row-create", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object" - }, - "examples": { - "Example 1": { - "value": { - "Id": 1, - "col1": "foo", - "col2": "bar", - "CreatedAt": "2023-03-11T08:48:25.598Z", - "UpdatedAt": "2023-03-11T08:48:25.598Z" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - }, - "tags": ["DB Table Row"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - }, - "examples": { - "Example 1": { - "value": { - "col1": "foo", - "col2": "bar" - } - } - } - } - } - }, - "description": "Create a new row in the given Table View" - } - }, - "/api/v1/tables/{tableId}/rows/count": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "tableId", - "in": "path", - "required": true, - "description": "Table Id" - }, - { - "schema": { - "type": "string" - }, - "name": "viewId", - "in": "query" - } - ], - "get": { - "summary": "Count Table View Rows", - "operationId": "table-row-count", - "description": "Count how many rows in the given Table View", - "tags": ["DB Table Row"], - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "where" - }, - { - "schema": {}, - "in": "query", - "name": "nested", - "description": "Query params for nested data" - }, - { - "$ref": "#/components/parameters/xc-auth" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { - "type": "number", - "x-stoplight": { - "id": "hwq29x70rcipi" - } - } - } - }, - "examples": { - "Example 1": { - "value": { - "count": 25 - } - } - } - } - } - } - } - } - }, - "/api/v1/tables/{tableId}/rows/{rowId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "tableId", - "in": "path", - "required": true, - "description": "Table Id" - }, - { - "schema": { - "type": "string" - }, - "name": "viewId", - "in": "query" - }, - { - "schema": { - "example": "1" - }, - "name": "rowId", - "in": "path", - "required": true, - "description": "Unique Row ID" - } - ], - "get": { - "summary": "Get Table View Row", - "operationId": "db-view-row-read", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - }, - "examples": { - "Example 1": { - "value": { - "Id": 1, - "Title": "foo", - "CreatedAt": "2023-03-11T09:11:47.437Z", - "UpdatedAt": "2023-03-11T09:11:47.784Z" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - }, - "description": "Get the target Table View Row", - "tags": ["DB Table Row"], - "parameters": [ - { - "$ref": "#/components/parameters/xc-auth" - } - ] - } } }, "components": { From 6e90e2258e537b8e19fb532c79028025629263eb Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 31 Jul 2023 18:11:12 +0530 Subject: [PATCH 88/97] fix: file read - allow only accessing files from intended folder Signed-off-by: Pranav C --- packages/nocodb/src/plugins/storage/Local.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/nocodb/src/plugins/storage/Local.ts b/packages/nocodb/src/plugins/storage/Local.ts index f0dbebf2a4..a6232ed32f 100644 --- a/packages/nocodb/src/plugins/storage/Local.ts +++ b/packages/nocodb/src/plugins/storage/Local.ts @@ -3,6 +3,7 @@ import path from 'path'; import { promisify } from 'util'; import mkdirp from 'mkdirp'; import axios from 'axios'; +import { NcError } from '../../helpers/catchError'; import { getToolDir } from '../../utils/nc-config'; import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; import type { Readable } from 'stream'; @@ -102,9 +103,20 @@ export default class Local implements IStorageAdapterV2 { public async fileRead(filePath: string): Promise { try { - const fileData = await fs.promises.readFile( + // Get the absolute path to the base directory + const absoluteBasePath = path.resolve(getToolDir()); + + // Get the absolute path to the file + const absolutePath = path.resolve( path.join(getToolDir(), ...filePath.split('/')), ); + + // Check if the resolved path is within the intended directory + if (!absolutePath.startsWith(absoluteBasePath)) { + NcError.notFound('Invalid path'); + } + + const fileData = await fs.promises.readFile(absolutePath); return fileData; } catch (e) { throw e; From 1e303a5cbc7c7bf533a15528ec4bf73b32bcf387 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Mon, 31 Jul 2023 22:35:30 +0530 Subject: [PATCH 89/97] fix: add validation and normalisation for all methods since write/list operations also vulnerable Signed-off-by: Pranav C --- packages/nocodb/src/plugins/storage/Local.ts | 50 +++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/nocodb/src/plugins/storage/Local.ts b/packages/nocodb/src/plugins/storage/Local.ts index a6232ed32f..e7bff7fdce 100644 --- a/packages/nocodb/src/plugins/storage/Local.ts +++ b/packages/nocodb/src/plugins/storage/Local.ts @@ -12,7 +12,7 @@ export default class Local implements IStorageAdapterV2 { constructor() {} public async fileCreate(key: string, file: XcFile): Promise { - const destPath = path.join(getToolDir(), ...key.split('/')); + const destPath = this.validateAndNormalisePath(key); try { await mkdirp(path.dirname(destPath)); const data = await promisify(fs.readFile)(file.path); @@ -25,7 +25,7 @@ export default class Local implements IStorageAdapterV2 { } async fileCreateByUrl(key: string, url: string): Promise { - const destPath = path.join(getToolDir(), ...key.split('/')); + const destPath = this.validateAndNormalisePath(key); return new Promise((resolve, reject) => { axios .get(url, { @@ -72,7 +72,7 @@ export default class Local implements IStorageAdapterV2 { stream: Readable, ): Promise { return new Promise((resolve, reject) => { - const destPath = path.join(getToolDir(), ...key.split('/')); + const destPath = this.validateAndNormalisePath(key); try { mkdirp(path.dirname(destPath)).then(() => { const writableStream = fs.createWriteStream(destPath); @@ -87,12 +87,12 @@ export default class Local implements IStorageAdapterV2 { } public async fileReadByStream(key: string): Promise { - const srcPath = path.join(getToolDir(), ...key.split('/')); + const srcPath = this.validateAndNormalisePath(key); return fs.createReadStream(srcPath, { encoding: 'utf8' }); } public async getDirectoryList(key: string): Promise { - const destDir = path.join(getToolDir(), ...key.split('/')); + const destDir = this.validateAndNormalisePath(key); return fs.promises.readdir(destDir); } @@ -103,20 +103,9 @@ export default class Local implements IStorageAdapterV2 { public async fileRead(filePath: string): Promise { try { - // Get the absolute path to the base directory - const absoluteBasePath = path.resolve(getToolDir()); - - // Get the absolute path to the file - const absolutePath = path.resolve( - path.join(getToolDir(), ...filePath.split('/')), + const fileData = await fs.promises.readFile( + this.validateAndNormalisePath(filePath, true), ); - - // Check if the resolved path is within the intended directory - if (!absolutePath.startsWith(absoluteBasePath)) { - NcError.notFound('Invalid path'); - } - - const fileData = await fs.promises.readFile(absolutePath); return fileData; } catch (e) { throw e; @@ -130,4 +119,29 @@ export default class Local implements IStorageAdapterV2 { test(): Promise { return Promise.resolve(false); } + + // method for validate/normalise the path for avoid path traversal attack + protected validateAndNormalisePath( + fileOrFolderPath: string, + throw404 = false, + ): string { + // Get the absolute path to the base directory + const absoluteBasePath = path.resolve(getToolDir()); + + // Get the absolute path to the file + const absolutePath = path.resolve( + path.join(getToolDir(), ...fileOrFolderPath.split('/')), + ); + + // Check if the resolved path is within the intended directory + if (!absolutePath.startsWith(absoluteBasePath)) { + if (throw404) { + NcError.notFound(); + } else { + NcError.badRequest('Invalid path'); + } + } + + return absolutePath; + } } From 5fcb7c88acc661fed282dff06c51396e454a93ec Mon Sep 17 00:00:00 2001 From: Pranav C Date: Tue, 1 Aug 2023 12:36:24 +0530 Subject: [PATCH 90/97] fix: include `nc` along with base path since we are always uploading under that folder - base path contain sqlite file as well Signed-off-by: Pranav C --- packages/nocodb/src/plugins/storage/Local.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/plugins/storage/Local.ts b/packages/nocodb/src/plugins/storage/Local.ts index e7bff7fdce..7d49e54f70 100644 --- a/packages/nocodb/src/plugins/storage/Local.ts +++ b/packages/nocodb/src/plugins/storage/Local.ts @@ -126,7 +126,7 @@ export default class Local implements IStorageAdapterV2 { throw404 = false, ): string { // Get the absolute path to the base directory - const absoluteBasePath = path.resolve(getToolDir()); + const absoluteBasePath = path.resolve(getToolDir(), 'nc'); // Get the absolute path to the file const absolutePath = path.resolve( From 19e6075f54096940c77d3e4a1c7c55d9f410c8be Mon Sep 17 00:00:00 2001 From: Anbarasu Date: Wed, 2 Aug 2023 14:57:36 +0530 Subject: [PATCH 91/97] fix: test webhook on condition --- packages/nocodb/src/helpers/webhookHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/helpers/webhookHelpers.ts b/packages/nocodb/src/helpers/webhookHelpers.ts index c0fc56a448..400db86b71 100644 --- a/packages/nocodb/src/helpers/webhookHelpers.ts +++ b/packages/nocodb/src/helpers/webhookHelpers.ts @@ -271,7 +271,7 @@ export async function invokeWebhook( return; } - if (hook.condition) { + if (hook.condition && !testHook) { if (isBulkOperation) { const filteredData = []; for (const data of newData) { From 7e743945717993a44351ad9658d9d03c6019d21f Mon Sep 17 00:00:00 2001 From: mertmit Date: Wed, 2 Aug 2023 13:27:29 +0300 Subject: [PATCH 92/97] fix: parse error messages properly for at-import Signed-off-by: mertmit --- .../modules/jobs/jobs/at-import/at-import.processor.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts b/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts index bc9a247dad..dc4afc8b64 100644 --- a/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts +++ b/packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts @@ -1879,9 +1879,7 @@ export class AtImportProcessor { req: { user: syncDB.user, clientIp: '' }, }) .catch((e) => - e.response?.data?.msg - ? logBasic(`NOTICE: ${e.response.data.msg}`) - : console.log(e), + e.message ? logBasic(`NOTICE: ${e.message}`) : console.log(e), ), ); recordPerfStats(_perfStart, 'auth.projectUserAdd'); @@ -2469,12 +2467,12 @@ export class AtImportProcessor { await generateMigrationStats(aTblSchema); } } catch (e) { - if (e.response?.data?.msg) { + if (e.message) { T.event({ event: 'a:airtable-import:error', - data: { error: e.response.data.msg }, + data: { error: e.message }, }); - throw new Error(e.response.data.msg); + throw new Error(e.message); } throw e; } From 90413ddfa28bc1c6daef906ffd0315e4feae5c4d Mon Sep 17 00:00:00 2001 From: pranavxc Date: Wed, 2 Aug 2023 10:53:54 +0000 Subject: [PATCH 93/97] [create-pull-request] automated change Signed-off-by: GitHub --- packages/nc-gui/package-lock.json | 54 +++++++++++++++++++------------ packages/nc-gui/package.json | 2 +- packages/nc-lib-gui/package.json | 2 +- packages/nocodb-sdk/package.json | 2 +- packages/nocodb/package-lock.json | 50 +++++++++++++--------------- packages/nocodb/package.json | 8 ++--- 6 files changed, 63 insertions(+), 55 deletions(-) diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index 7ccbf09e52..e4c6379d78 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -30,7 +30,7 @@ "leaflet.markercluster": "^1.5.3", "locale-codes": "^1.3.1", "monaco-editor": "^0.33.0", - "nocodb-sdk": "file:../nocodb-sdk", + "nocodb-sdk": "0.109.6", "papaparse": "^5.3.2", "pinia": "^2.0.33", "qrcode": "^1.5.1", @@ -110,7 +110,8 @@ } }, "../nocodb-sdk": { - "version": "0.109.5", + "version": "0.109.6", + "extraneous": true, "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^0.21.1", @@ -8719,7 +8720,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "devOptional": true, "funding": [ { "type": "individual", @@ -12238,8 +12238,21 @@ } }, "node_modules/nocodb-sdk": { - "resolved": "../nocodb-sdk", - "link": true + "version": "0.109.6", + "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", + "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", + "dependencies": { + "axios": "^0.21.1", + "jsep": "^1.3.6" + } + }, + "node_modules/nocodb-sdk/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } }, "node_modules/node-abi": { "version": "3.23.0", @@ -24716,8 +24729,7 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "devOptional": true + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "form-data": { "version": "4.0.0", @@ -27267,22 +27279,22 @@ } }, "nocodb-sdk": { - "version": "file:../nocodb-sdk", + "version": "0.109.6", + "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", + "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", "requires": { - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.1", - "cspell": "^4.1.0", - "eslint": "^7.8.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-functional": "^3.0.2", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-prettier": "^4.0.0", - "jsep": "^1.3.6", - "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "typescript": "^4.0.2" + "jsep": "^1.3.6" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + } } }, "node-abi": { diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index 44f0905daf..89e44c49b4 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -54,7 +54,7 @@ "leaflet.markercluster": "^1.5.3", "locale-codes": "^1.3.1", "monaco-editor": "^0.33.0", - "nocodb-sdk": "file:../nocodb-sdk", + "nocodb-sdk": "0.109.6", "papaparse": "^5.3.2", "pinia": "^2.0.33", "qrcode": "^1.5.1", diff --git a/packages/nc-lib-gui/package.json b/packages/nc-lib-gui/package.json index ca7291370c..bc6b9e6e18 100644 --- a/packages/nc-lib-gui/package.json +++ b/packages/nc-lib-gui/package.json @@ -1,6 +1,6 @@ { "name": "nc-lib-gui", - "version": "0.109.5", + "version": "0.109.6", "description": "NocoDB GUI", "author": { "name": "NocoDB", diff --git a/packages/nocodb-sdk/package.json b/packages/nocodb-sdk/package.json index 2fc6ecc0a5..ce941f5c5e 100644 --- a/packages/nocodb-sdk/package.json +++ b/packages/nocodb-sdk/package.json @@ -1,6 +1,6 @@ { "name": "nocodb-sdk", - "version": "0.109.5", + "version": "0.109.6", "description": "NocoDB SDK", "main": "build/main/index.js", "typings": "build/main/index.d.ts", diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index b8da539dec..daedc983b1 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -1,12 +1,12 @@ { "name": "nocodb", - "version": "0.109.5", + "version": "0.109.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nocodb", - "version": "0.109.5", + "version": "0.109.6", "license": "AGPL-3.0-or-later", "dependencies": { "@google-cloud/storage": "^5.7.2", @@ -80,10 +80,10 @@ "mysql2": "^3.2.0", "nanoid": "^3.1.20", "nc-help": "^0.2.87", - "nc-lib-gui": "0.109.5", + "nc-lib-gui": "0.109.6", "nc-plugin": "^0.1.3", "ncp": "^2.0.0", - "nocodb-sdk": "file:../nocodb-sdk", + "nocodb-sdk": "0.109.6", "nodemailer": "^6.4.10", "object-hash": "^3.0.0", "object-sizeof": "^2.6.1", @@ -191,7 +191,8 @@ } }, "../nocodb-sdk": { - "version": "0.109.5", + "version": "0.109.6", + "extraneous": true, "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^0.21.1", @@ -13158,9 +13159,9 @@ } }, "node_modules/nc-lib-gui": { - "version": "0.109.5", - "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.109.5.tgz", - "integrity": "sha512-A6za8yFO167OVd2MnGrzEFAOQ5Nx9UJ5zpk1iH7FC427TEHJOAbvlOYMVa3s7Pb8wjYKhZ5Mr4sBldtY0cKaHA==", + "version": "0.109.6", + "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.109.6.tgz", + "integrity": "sha512-HX0JuimDTgpliJowNDctjHGvsbkO8SZ1dq/i+WEIce1nlgxdBrOM+/79UlGMaeLWMb3RuU7zNLxpxYvvdoFs4w==", "dependencies": { "express": "^4.17.1" } @@ -13207,8 +13208,13 @@ } }, "node_modules/nocodb-sdk": { - "resolved": "../nocodb-sdk", - "link": true + "version": "0.109.6", + "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", + "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", + "dependencies": { + "axios": "^0.21.1", + "jsep": "^1.3.6" + } }, "node_modules/node-abort-controller": { "version": "3.1.1", @@ -28474,9 +28480,9 @@ } }, "nc-lib-gui": { - "version": "0.109.5", - "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.109.5.tgz", - "integrity": "sha512-A6za8yFO167OVd2MnGrzEFAOQ5Nx9UJ5zpk1iH7FC427TEHJOAbvlOYMVa3s7Pb8wjYKhZ5Mr4sBldtY0cKaHA==", + "version": "0.109.6", + "resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.109.6.tgz", + "integrity": "sha512-HX0JuimDTgpliJowNDctjHGvsbkO8SZ1dq/i+WEIce1nlgxdBrOM+/79UlGMaeLWMb3RuU7zNLxpxYvvdoFs4w==", "requires": { "express": "^4.17.1" } @@ -28511,22 +28517,12 @@ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" }, "nocodb-sdk": { - "version": "file:../nocodb-sdk", + "version": "0.109.6", + "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", + "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", "requires": { - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.1", - "cspell": "^4.1.0", - "eslint": "^7.8.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-functional": "^3.0.2", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-prettier": "^4.0.0", - "jsep": "^1.3.6", - "npm-run-all": "^4.1.5", - "prettier": "^2.1.1", - "typescript": "^4.0.2" + "jsep": "^1.3.6" } }, "node-abort-controller": { diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json index 2a1c30f8ca..6fc9bd6749 100644 --- a/packages/nocodb/package.json +++ b/packages/nocodb/package.json @@ -1,6 +1,6 @@ { "name": "nocodb", - "version": "0.109.5", + "version": "0.109.6", "description": "NocoDB Backend", "main": "dist/bundle.js", "author": { @@ -113,10 +113,10 @@ "mysql2": "^3.2.0", "nanoid": "^3.1.20", "nc-help": "^0.2.87", - "nc-lib-gui": "0.109.5", + "nc-lib-gui": "0.109.6", "nc-plugin": "^0.1.3", "ncp": "^2.0.0", - "nocodb-sdk": "file:../nocodb-sdk", + "nocodb-sdk": "0.109.6", "nodemailer": "^6.4.10", "object-hash": "^3.0.0", "object-sizeof": "^2.6.1", @@ -204,4 +204,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file From 397e0854ed13d90d1d24c27a7944baa449520f82 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 2 Aug 2023 17:10:31 +0530 Subject: [PATCH 94/97] chore: update sdk path Signed-off-by: Pranav C --- packages/nc-gui/package-lock.json | 52 +++++++++++---------------- packages/nc-gui/package.json | 2 +- packages/nocodb-sdk/package-lock.json | 4 +-- packages/nocodb/package-lock.json | 30 +++++++++------- packages/nocodb/package.json | 4 +-- 5 files changed, 42 insertions(+), 50 deletions(-) diff --git a/packages/nc-gui/package-lock.json b/packages/nc-gui/package-lock.json index e4c6379d78..dc0198bb4f 100644 --- a/packages/nc-gui/package-lock.json +++ b/packages/nc-gui/package-lock.json @@ -30,7 +30,7 @@ "leaflet.markercluster": "^1.5.3", "locale-codes": "^1.3.1", "monaco-editor": "^0.33.0", - "nocodb-sdk": "0.109.6", + "nocodb-sdk": "file:../nocodb-sdk", "papaparse": "^5.3.2", "pinia": "^2.0.33", "qrcode": "^1.5.1", @@ -111,7 +111,6 @@ }, "../nocodb-sdk": { "version": "0.109.6", - "extraneous": true, "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^0.21.1", @@ -8720,6 +8719,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "devOptional": true, "funding": [ { "type": "individual", @@ -12238,21 +12238,8 @@ } }, "node_modules/nocodb-sdk": { - "version": "0.109.6", - "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", - "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", - "dependencies": { - "axios": "^0.21.1", - "jsep": "^1.3.6" - } - }, - "node_modules/nocodb-sdk/node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } + "resolved": "../nocodb-sdk", + "link": true }, "node_modules/node-abi": { "version": "3.23.0", @@ -24729,7 +24716,8 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "devOptional": true }, "form-data": { "version": "4.0.0", @@ -27279,22 +27267,22 @@ } }, "nocodb-sdk": { - "version": "0.109.6", - "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", - "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", + "version": "file:../nocodb-sdk", "requires": { + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.1", - "jsep": "^1.3.6" - }, - "dependencies": { - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - } + "cspell": "^4.1.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^3.0.2", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^4.0.0", + "jsep": "^1.3.6", + "npm-run-all": "^4.1.5", + "prettier": "^2.1.1", + "typescript": "^4.0.2" } }, "node-abi": { diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index 89e44c49b4..44f0905daf 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -54,7 +54,7 @@ "leaflet.markercluster": "^1.5.3", "locale-codes": "^1.3.1", "monaco-editor": "^0.33.0", - "nocodb-sdk": "0.109.6", + "nocodb-sdk": "file:../nocodb-sdk", "papaparse": "^5.3.2", "pinia": "^2.0.33", "qrcode": "^1.5.1", diff --git a/packages/nocodb-sdk/package-lock.json b/packages/nocodb-sdk/package-lock.json index dda8da70f5..c647526d52 100644 --- a/packages/nocodb-sdk/package-lock.json +++ b/packages/nocodb-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "nocodb-sdk", - "version": "0.109.5", + "version": "0.109.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nocodb-sdk", - "version": "0.109.5", + "version": "0.109.6", "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^0.21.1", diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index daedc983b1..6774504e4d 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -83,7 +83,7 @@ "nc-lib-gui": "0.109.6", "nc-plugin": "^0.1.3", "ncp": "^2.0.0", - "nocodb-sdk": "0.109.6", + "nocodb-sdk": "file:../nocodb-sdk", "nodemailer": "^6.4.10", "object-hash": "^3.0.0", "object-sizeof": "^2.6.1", @@ -192,7 +192,6 @@ }, "../nocodb-sdk": { "version": "0.109.6", - "extraneous": true, "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^0.21.1", @@ -13208,13 +13207,8 @@ } }, "node_modules/nocodb-sdk": { - "version": "0.109.6", - "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", - "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", - "dependencies": { - "axios": "^0.21.1", - "jsep": "^1.3.6" - } + "resolved": "../nocodb-sdk", + "link": true }, "node_modules/node-abort-controller": { "version": "3.1.1", @@ -28517,12 +28511,22 @@ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" }, "nocodb-sdk": { - "version": "0.109.6", - "resolved": "https://registry.npmjs.org/nocodb-sdk/-/nocodb-sdk-0.109.6.tgz", - "integrity": "sha512-Zh4MjkurCPYl/eJWt9ChvoikMpDujI/F6IaQYb9bJBss6Ns4grdhKizCdvywb2dRYaGqXquzwrGg2jmpw/Xyxg==", + "version": "file:../nocodb-sdk", "requires": { + "@typescript-eslint/eslint-plugin": "^4.0.1", + "@typescript-eslint/parser": "^4.0.1", "axios": "^0.21.1", - "jsep": "^1.3.6" + "cspell": "^4.1.0", + "eslint": "^7.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^3.0.2", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^4.0.0", + "jsep": "^1.3.6", + "npm-run-all": "^4.1.5", + "prettier": "^2.1.1", + "typescript": "^4.0.2" } }, "node-abort-controller": { diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json index 6fc9bd6749..51e37a6a4e 100644 --- a/packages/nocodb/package.json +++ b/packages/nocodb/package.json @@ -116,7 +116,7 @@ "nc-lib-gui": "0.109.6", "nc-plugin": "^0.1.3", "ncp": "^2.0.0", - "nocodb-sdk": "0.109.6", + "nocodb-sdk": "file:../nocodb-sdk", "nodemailer": "^6.4.10", "object-hash": "^3.0.0", "object-sizeof": "^2.6.1", @@ -204,4 +204,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} From 8bf7c4faaf5021c7e3f417f7220a059babbc3207 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 2 Aug 2023 20:51:06 +0800 Subject: [PATCH 95/97] fix(nc-gui): lint errors --- packages/nc-gui/components/smartsheet/Pagination.vue | 6 ++++-- packages/nc-gui/components/smartsheet/header/Icon.vue | 4 ++-- packages/nc-gui/components/tabs/Smartsheet.vue | 2 +- .../nc-gui/components/virtual-cell/components/ItemChip.vue | 1 - .../components/virtual-cell/components/ListChildItems.vue | 1 - .../nc-gui/components/virtual-cell/components/ListItems.vue | 1 - packages/nc-gui/composables/useMultiSelect/index.ts | 4 ---- 7 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/nc-gui/components/smartsheet/Pagination.vue b/packages/nc-gui/components/smartsheet/Pagination.vue index 7cc08d7d3e..4bd45cd171 100644 --- a/packages/nc-gui/components/smartsheet/Pagination.vue +++ b/packages/nc-gui/components/smartsheet/Pagination.vue @@ -2,9 +2,11 @@ import { ChangePageInj, PaginationDataInj, computed, iconMap, inject, isRtlLang, useI18n } from '#imports' import type { Language } from '~/lib' -const props = defineProps<{ +interface Props { alignCountOnRight?: boolean -}>() +} + +const { alignCountOnRight } = defineProps() const { locale } = useI18n() diff --git a/packages/nc-gui/components/smartsheet/header/Icon.vue b/packages/nc-gui/components/smartsheet/header/Icon.vue index ff56b943ea..159ef471b0 100644 --- a/packages/nc-gui/components/smartsheet/header/Icon.vue +++ b/packages/nc-gui/components/smartsheet/header/Icon.vue @@ -1,8 +1,8 @@