diff --git a/packages/nocodb/src/controllers/data-alias.controller.ts b/packages/nocodb/src/controllers/data-alias.controller.ts index a1228605d1..fb8c32243d 100644 --- a/packages/nocodb/src/controllers/data-alias.controller.ts +++ b/packages/nocodb/src/controllers/data-alias.controller.ts @@ -117,6 +117,30 @@ export class DataAliasController { res.json(countResult); } + @Get([ + '/api/v1/db/data/:orgs/:baseName/:tableName/countByDate/', + '/api/v1/db/data/:orgs/:baseName/:tableName/views/:viewName/countByDate/', + ]) + @Acl('dataList') + async calendarDataCount( + @Req() req: Request, + @Res() res: Response, + @Param('baseName') baseName: string, + @Param('tableName') tableName: string, + @Param('viewName') viewName: string, + ) { + const startTime = process.hrtime(); + + const data = await this.datasService.getCalendarRecordCount({ + query: req.query, + viewId: viewName, + }); + + const elapsedSeconds = parseHrtimeToMilliSeconds(process.hrtime(startTime)); + res.setHeader('xc-db-response', elapsedSeconds); + res.json(data); + } + @Post([ '/api/v1/db/data/:orgs/:baseName/:tableName', '/api/v1/db/data/:orgs/:baseName/:tableName/views/:viewName', diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 6bac3a9673..a19b1c21b3 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -21,7 +21,7 @@ import { customAlphabet } from 'nanoid'; import DOMPurify from 'isomorphic-dompurify'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from '@nestjs/common'; -import type { SortType } from 'nocodb-sdk'; +import type { CalendarRangeType, SortType } from 'nocodb-sdk'; import type { Knex } from 'knex'; import type LookupColumn from '~/models/LookupColumn'; import type { XKnex } from '~/db/CustomKnex'; @@ -251,6 +251,71 @@ class BaseModelSqlv2 { return !!(await this.execAndParse(qb, null, { raw: true, first: true })); } + public async countByRanges({ + ranges, + model, + filterArr, + }: { + ranges: Partial[]; + model: Model; + filterArr?: Filter[]; + }) { + const columns = await model.getColumns(); + + const queryRanges = []; + + for (const range of ranges) { + let query; + if (range?.fk_from_column_id && range?.fk_to_column_id) { + query = this.dbDriver( + this.dbDriver.raw( + `SELECT generate_series( + ??, + ??, + '1 day' + )::date AS date FROM ??`, + [ + columns.find((c) => c.id === range.fk_from_column_id).column_name, + columns.find((c) => c.id === range.fk_to_column_id).column_name, + this.tnPath, + ], + ), + ); + } else if (range.fk_from_column_id) { + query = this.dbDriver( + this.dbDriver.raw(`SELECT ??::date AS date FROM ??`, [ + columns.find((c) => c.id === range.fk_from_column_id).column_name, + this.tnPath, + ]), + ); + } + + if (query) { + await conditionV2(this, filterArr, query); + queryRanges.push(query); + } + } + + const unionQuery = this.dbDriver.raw( + queryRanges.reduce( + (acc, range) => + acc ? `${acc} UNION ALL ${range.toQuery()}` : range.toQuery(), + '', + ), + ); + + const qb = this.dbDriver( + this.dbDriver.raw(`(${unionQuery.toQuery()}) AS ??`, ['nc']), + ) + .select('date') + .count('* as count') + .groupBy('date') + .orderBy('date'); + + console.log(qb.toQuery()); + return await this.execAndParse(qb); + } + // todo: add support for sortArrJson public async findOne( args: { diff --git a/packages/nocodb/src/helpers/getAst.ts b/packages/nocodb/src/helpers/getAst.ts index a13eb0f363..93b4ae2b76 100644 --- a/packages/nocodb/src/helpers/getAst.ts +++ b/packages/nocodb/src/helpers/getAst.ts @@ -34,6 +34,7 @@ const getAst = async ({ }, getHiddenColumn = query?.['getHiddenColumn'], throwErrorIfInvalidParams = false, + extractOnlyRangeFields = false, }: { query?: RequestQuery; extractOnlyPrimaries?: boolean; @@ -43,6 +44,8 @@ const getAst = async ({ dependencyFields?: DependantFields; getHiddenColumn?: boolean; throwErrorIfInvalidParams?: boolean; + // Used for calendar view + extractOnlyRangeFields?: boolean; }) => { // set default values of dependencyFields and nested dependencyFields.nested = dependencyFields.nested || {}; @@ -88,6 +91,26 @@ const getAst = async ({ return { ast, dependencyFields, parsedQuery: dependencyFields }; } + if (extractOnlyRangeFields) { + const ast = { + ...(dependencyFieldsForCalenderView || []).reduce((o, f) => { + const col = model.columns.find((c) => c.id === f); + return { ...o, [col.title]: 1 }; + }, {}), + }; + + await Promise.all( + (dependencyFieldsForCalenderView || []).map((f) => + extractDependencies( + model.columns.find((c) => c.id === f), + dependencyFields, + ), + ), + ); + + return { ast, dependencyFields, parsedQuery: dependencyFields }; + } + let fields = query?.fields || query?.f; if (fields && fields !== '*') { fields = Array.isArray(fields) ? fields : fields.split(','); diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json index 14123abbbe..a46bd8a9da 100644 --- a/packages/nocodb/src/schema/swagger.json +++ b/packages/nocodb/src/schema/swagger.json @@ -10054,6 +10054,101 @@ } } }, + "/api/v1/db/data/{orgs}/{baseName}/{tableName}/views/{viewName}/countByDate/" : { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "orgs", + "in": "path", + "required": true, + "description": "Organisation Name. Currently `noco` will be used." + }, + { + "schema": { + "type": "string" + }, + "name": "baseName", + "in": "path", + "required": true, + "description": "Base Name" + }, + { + "schema": { + "type": "string" + }, + "name": "tableName", + "in": "path", + "required": true, + "description": "Table Name" + }, + { + "schema": { + "type": "string" + }, + "name": "viewName", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Count of Records in Dates in Calendar View", + "operationId": "db-view-row-calendar-count", + "description": "Get the count of table view rows grouped by the dates", + "tags": [ + "DB View Row" + ], + "parameters": [ + { + "schema": { + "type": "array" + }, + "in": "query", + "name": "sort" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "where" + }, + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "in": "query", + "name": "limit" + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "in": "query", + "name": "offset" + }, + { + "$ref": "#/components/parameters/xc-auth" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + } + }, "/api/v1/db/data/{orgs}/{baseName}/{tableName}/views/{viewName}/groupby": { "parameters": [ { diff --git a/packages/nocodb/src/services/datas.service.ts b/packages/nocodb/src/services/datas.service.ts index b2355eee30..a4499933b2 100644 --- a/packages/nocodb/src/services/datas.service.ts +++ b/packages/nocodb/src/services/datas.service.ts @@ -6,7 +6,7 @@ import { nocoExecute } from 'nc-help'; import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; import type { PathParams } from '~/modules/datas/helpers'; import { getDbRows, getViewAndModelByAliasOrId } from '~/modules/datas/helpers'; -import { Base, Column, Model, Source, View } from '~/models'; +import { Base, CalendarRange, Column, Model, Source, View } from '~/models'; import { NcBaseError, NcError } from '~/helpers/catchError'; import getAst from '~/helpers/getAst'; import { PagedResponseImpl } from '~/helpers/PagedResponse'; @@ -205,6 +205,53 @@ export class DatasService { }); } + async getCalendarRecordCount(param: { viewId: string; query: any }) { + const { viewId, query = {} } = param; + + const view = await View.get(viewId); + + if (!view) NcError.notFound('View not found'); + if (view.type !== ViewTypes.CALENDAR) + NcError.badRequest('View is not a calendar view'); + + const source = await Source.get(view.source_id); + + const { ranges } = await CalendarRange.read(view.id); + + if (!ranges.length) NcError.badRequest('No ranges found'); + + const model = await Model.getByIdOrName({ + id: view.fk_model_id, + }); + + const baseModel = await Model.getBaseModelSQL({ + id: view.fk_model_id, + viewId: view?.id, + dbDriver: await NcConnectionMgrv2.get(source), + }); + + const { dependencyFields } = await getAst({ + model, + query, + view, + extractOnlyRangeFields: true, + }); + + const listArgs: any = dependencyFields; + try { + listArgs.filterArr = JSON.parse(listArgs.filterArrJson); + } catch (e) {} + try { + listArgs.sortArr = JSON.parse(listArgs.sortArrJson); + } catch (e) {} + + return await baseModel.countByRanges({ + model, + ranges, + ...listArgs, + }); + } + async getFindOne(param: { model: Model; view: View; query: any }) { const { model, view, query = {} } = param;