Browse Source

Merge pull request #7716 from nocodb/nc-feat/new-cal-api

feat(nocodb): calendar data apis
pull/7788/head
Raju Udava 10 months ago committed by GitHub
parent
commit
bc575f26b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/nc-gui/components/smartsheet/calendar/index.vue
  2. 273
      packages/nc-gui/composables/useCalendarViewStore.ts
  3. 8
      packages/nc-gui/composables/useExpandedFormStore.ts
  4. 71
      packages/nc-gui/composables/useSharedView.ts
  5. 103
      packages/nocodb/src/controllers/calendars-datas.controller.ts
  6. 25
      packages/nocodb/src/controllers/data-alias.controller.ts
  7. 1
      packages/nocodb/src/controllers/data-table.controller.ts
  8. 15
      packages/nocodb/src/controllers/public-datas.controller.ts
  9. 12
      packages/nocodb/src/db/BaseModelSqlv2.ts
  10. 5
      packages/nocodb/src/modules/datas/datas.module.ts
  11. 176
      packages/nocodb/src/schema/swagger-v2.json
  12. 757
      packages/nocodb/src/schema/swagger.json
  13. 230
      packages/nocodb/src/services/calendar-datas.service.ts
  14. 121
      packages/nocodb/src/services/datas.service.ts
  15. 79
      packages/nocodb/src/services/public-datas.service.ts
  16. 45
      packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

1
packages/nc-gui/components/smartsheet/calendar/index.vue

@ -86,7 +86,6 @@ const expandedFormRowState = ref<Record<string, any>>()
const expandRecord = (row: RowType, state?: Record<string, any>) => {
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) {
router.push({
query: {

273
packages/nc-gui/composables/useCalendarViewStore.ts

@ -97,7 +97,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { sharedView, fetchSharedViewData, fetchSharedViewActiveDate } = useSharedView()
const { sharedView, fetchSharedViewData, fetchSharedViewActiveDate, fetchSharedCalendarViewData } = useSharedView()
const calendarMetaData = ref<CalendarType>({})
@ -219,7 +219,6 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
fromDate = fromDate!.format('YYYY-MM-DD HH:mm:ssZ')
toDate = toDate!.format('YYYY-MM-DD HH:mm:ssZ')
prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ')
nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ')
@ -337,112 +336,6 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
}
const filterJSON = computed(() => {
if (!calendarRange.value) return []
const combinedFilters: any = {
is_group: true,
logical_op: 'and',
children: [],
}
let prevDate: string | null | dayjs.Dayjs = null
let fromDate: dayjs.Dayjs | null | string = null
let toDate: dayjs.Dayjs | null | string = null
let nextDate: string | null | dayjs.Dayjs = null
switch (activeCalendarView.value) {
case 'week':
fromDate = selectedDateRange.value.start.startOf('day')
toDate = selectedDateRange.value.end.endOf('day')
prevDate = selectedDateRange.value.start.subtract(1, 'day').endOf('day')
nextDate = selectedDateRange.value.end.add(1, 'day').startOf('day')
break
case 'month': {
const startOfMonth = selectedMonth.value.startOf('month')
const endOfMonth = selectedMonth.value.endOf('month')
const daysToDisplay = Math.max(endOfMonth.diff(startOfMonth, 'day') + 1, 35)
fromDate = startOfMonth.subtract((startOfMonth.day() + 7) % 7, 'day')
toDate = fromDate.add(daysToDisplay, 'day')
prevDate = fromDate.subtract(1, 'day').endOf('day')
nextDate = toDate.add(1, 'day').startOf('day')
break
}
case 'year':
fromDate = selectedDate.value.startOf('year')
toDate = selectedDate.value.endOf('year')
prevDate = fromDate.subtract(1, 'day').endOf('day')
nextDate = toDate.add(1, 'day').startOf('day')
break
case 'day':
fromDate = selectedDate.value.startOf('day')
toDate = selectedDate.value.endOf('day')
prevDate = selectedDate.value.subtract(1, 'day').endOf('day')
nextDate = selectedDate.value.add(1, 'day').startOf('day')
break
}
fromDate = fromDate!.format('YYYY-MM-DD HH:mm:ssZ')
toDate = toDate!.format('YYYY-MM-DD HH:mm:ssZ')
prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ')
nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ')
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
const toCol = range.fk_to_col
let rangeFilter: any = []
if (fromCol && toCol) {
rangeFilter = [
{
is_group: true,
logical_op: 'and',
children: [
{
fk_column_id: fromCol.id,
comparison_op: 'lt',
comparison_sub_op: 'exactDate',
value: nextDate,
},
{
fk_column_id: toCol.id,
comparison_op: 'gt',
comparison_sub_op: 'exactDate',
value: prevDate,
},
],
},
{
fk_column_id: fromCol.id,
comparison_op: 'eq',
logical_op: 'or',
comparison_sub_op: 'exactDate',
value: fromDate,
},
]
} else if (fromCol) {
rangeFilter = [
{
fk_column_id: fromCol.id,
comparison_op: 'lt',
comparison_sub_op: 'exactDate',
value: nextDate,
},
{
fk_column_id: fromCol.id,
comparison_op: 'gt',
comparison_sub_op: 'exactDate',
value: prevDate,
},
]
}
if (rangeFilter.length > 0) {
combinedFilters.children.push(rangeFilter)
}
})
return combinedFilters.children.length > 0 ? [combinedFilters] : []
})
const fetchActiveDates = async () => {
if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !calendarRange.value) return
let prevDate: dayjs.Dayjs | string | null = null
@ -459,85 +352,37 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
prevDate = fromDate.subtract(1, 'day').endOf('day')
nextDate = toDate.add(1, 'day').startOf('day')
} else if (activeCalendarView.value === 'year') {
fromDate = selectedDate.value.startOf('year')
prevDate = selectedDate.value.startOf('year').subtract(1, 'day').endOf('day')
nextDate = selectedDate.value.endOf('year').add(1, 'day').startOf('day')
}
prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ')
nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ')
fromDate = fromDate!.format('YYYY-MM-DD HH:mm:ssZ')
const activeDateFilter: Array<any> = []
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
const toCol = range.fk_to_col
let rangeFilter: any = []
if (fromCol && toCol) {
rangeFilter = [
{
is_group: true,
logical_op: 'and',
children: [
{
fk_column_id: fromCol.id,
comparison_op: 'lt',
comparison_sub_op: 'exactDate',
value: nextDate,
},
{
fk_column_id: toCol.id,
comparison_op: 'gt',
comparison_sub_op: 'exactDate',
value: prevDate,
},
],
},
{
fk_column_id: fromCol.id,
comparison_op: 'eq',
logical_op: 'or',
comparison_sub_op: 'exactDate',
value: fromDate,
},
]
} else if (fromCol) {
rangeFilter = [
{
fk_column_id: fromCol.id,
comparison_op: 'lt',
comparison_sub_op: 'exactDate',
value: nextDate,
},
{
fk_column_id: fromCol.id,
comparison_op: 'gt',
comparison_sub_op: 'exactDate',
value: prevDate,
},
]
}
activeDateFilter.push(rangeFilter)
})
if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) return
try {
const res = !isPublic.value
? await api.dbViewRow.calendarCount('noco', base.value.id!, meta.value!.id!, viewMeta.value.id, {
? await api.dbCalendarViewRowCount.dbCalendarViewRowCount('noco', base.value.id!, meta.value!.id!, viewMeta.value.id, {
...queryParams.value,
...{},
...{},
...{ filterArrJson: JSON.stringify([...activeDateFilter]) },
from_date: prevDate,
to_date: nextDate,
})
: await fetchSharedViewActiveDate({
from_date: prevDate,
to_date: nextDate,
sortsArr: sorts.value,
filtersArr: activeDateFilter,
})
activeDates.value = res.map((dateObj: unknown) => dayjs(dateObj))
activeDates.value = res.dates.map((dateObj: unknown) => dayjs(dateObj))
if (res.count > 3000 && activeCalendarView.value !== 'year') {
message.warning(
'This current date range has more than 3000 records. Some records may not be displayed. To get complete records, contact support',
)
}
} catch (e) {
activeDates.value = []
message.error(`${t('msg.error.fetchingActiveDates')} ${await extractSdkResponseErrorMsg(e)}`)
@ -585,30 +430,78 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
async function loadCalendarData() {
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !filterJSON.value) && !isPublic?.value) return
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return
if (activeCalendarView.value === 'year') {
return
}
let prevDate: string | null | dayjs.Dayjs = null
let fromDate: dayjs.Dayjs | null | string = null
let toDate: dayjs.Dayjs | null | string = null
let nextDate: string | null | dayjs.Dayjs = null
switch (activeCalendarView.value) {
case 'week':
fromDate = selectedDateRange.value.start.startOf('day')
toDate = selectedDateRange.value.end.endOf('day')
prevDate = selectedDateRange.value.start.subtract(1, 'day').endOf('day')
nextDate = selectedDateRange.value.end.add(1, 'day').startOf('day')
break
case 'month': {
const startOfMonth = selectedMonth.value.startOf('month')
const endOfMonth = selectedMonth.value.endOf('month')
const daysToDisplay = Math.max(endOfMonth.diff(startOfMonth, 'day') + 1, 35)
fromDate = startOfMonth.subtract((startOfMonth.day() + 7) % 7, 'day')
toDate = fromDate.add(daysToDisplay, 'day')
prevDate = fromDate.subtract(1, 'day').endOf('day')
nextDate = toDate.add(1, 'day').startOf('day')
break
}
case 'year':
fromDate = selectedDate.value.startOf('year')
toDate = selectedDate.value.endOf('year')
prevDate = fromDate.subtract(1, 'day').endOf('day')
nextDate = toDate.add(1, 'day').startOf('day')
break
case 'day':
fromDate = selectedDate.value.startOf('day')
toDate = selectedDate.value.endOf('day')
prevDate = selectedDate.value.subtract(1, 'day').endOf('day')
nextDate = selectedDate.value.add(1, 'day').startOf('day')
break
}
prevDate = prevDate!.format('YYYY-MM-DD HH:mm:ssZ')
nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ')
try {
isCalendarDataLoading.value = true
const res = !isPublic.value
? await api.dbViewRow.list(
? await api.dbCalendarViewRow.list(
'noco',
base.value.id!,
meta.value!.id!,
viewMeta.value!.id!,
{
...queryParams.value,
...(isUIAllowed('filterSync')
? { filterArrJson: JSON.stringify([...filterJSON.value]) }
: { filterArrJson: JSON.stringify([nestedFilters.value, ...filterJSON.value]) }),
where: where?.value ?? '',
from_date: prevDate,
to_date: nextDate,
},
{
headers: {
'xc-ignore-pagination': true,
},
...queryParams.value,
...(isUIAllowed('filterSync') ? { filterArrJson: [] } : { filterArrJson: JSON.stringify([nestedFilters.value]) }),
where: where?.value ?? '',
filterArrJson: JSON.stringify([...nestedFilters.value]),
},
)
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: filterJSON.value })
: await fetchSharedCalendarViewData({
sortsArr: sorts.value,
from_date: prevDate,
to_date: nextDate,
filtersArr: nestedFilters.value,
where: where?.value ?? '',
})
formattedData.value = formatData(res!.list)
} catch (e) {
message.error(`${t('msg.error.fetchingCalendarData')} ${await extractSdkResponseErrorMsg(e)}`)
@ -782,17 +675,21 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
watch(selectedDate, async (value, oldValue) => {
if (activeCalendarView.value === 'month' || activeCalendarView.value === 'week') {
if (sideBarFilterOption.value === 'selectedDate') {
if (sideBarFilterOption.value === 'selectedDate' && showSideMenu.value) {
await loadSidebarData()
}
} else if (activeCalendarView.value === 'year') {
if (value.year() !== oldValue.year()) {
await Promise.all([loadCalendarData(), loadSidebarData(), await fetchActiveDates()])
} else if (sideBarFilterOption.value === 'selectedDate') {
} else if (sideBarFilterOption.value === 'selectedDate' && showSideMenu.value) {
await loadSidebarData()
}
} else {
await Promise.all([loadSidebarData(), loadCalendarData()])
if (showSideMenu.value) {
await Promise.all([loadSidebarData(), loadCalendarData()])
} else {
await Promise.all([loadCalendarData()])
}
}
if (activeCalendarView.value === 'year' && value.year() !== oldValue.year()) {
@ -801,7 +698,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
})
watch(selectedTime, async () => {
if (calDataType.value !== UITypes.Date) {
if (calDataType.value !== UITypes.Date && showSideMenu.value) {
await loadSidebarData()
}
})
@ -850,7 +747,15 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
}
sideBarFilterOption.value = activeCalendarView.value ?? 'allRecords'
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
if (activeCalendarView.value === 'year') {
await Promise.all([loadSidebarData(), fetchActiveDates()])
} else {
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
}
})
watch(showSideMenu, async (val) => {
if (val) await loadSidebarData()
})
watch(sideBarFilterOption, async () => {

8
packages/nc-gui/composables/useExpandedFormStore.ts

@ -1,9 +1,11 @@
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import type { AuditType, ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import dayjs from 'dayjs'
import {
IsPublicInj,
NOCO,
type Row,
computed,
extractPkFromRow,
extractSdkResponseErrorMsg,
@ -22,7 +24,6 @@ import {
useSharedView,
useUndoRedo,
} from '#imports'
import type { Row } from '#imports'
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, _row: Ref<Row>) => {
const { $e, $state, $api } = useNuxtApp()
@ -31,6 +32,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const { t } = useI18n()
const isPublic = inject(IsPublicInj, ref(false))
const commentsOnly = ref(false)
const commentsAndLogs = ref<any[]>([])
@ -301,6 +304,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
const loadRow = async (rowId?: string, onlyVirtual = false) => {
if (isPublic.value) return
let record = await $api.dbTableRow.read(
NOCO,
// todo: base_id missing on view type

71
packages/nc-gui/composables/useSharedView.ts

@ -110,21 +110,16 @@ export function useSharedView() {
}
}
const fetchSharedViewData = async (
param: {
sortsArr: SortType[]
filtersArr: FilterType[]
fields?: any[]
sort?: any[]
where?: string
/** Query params for nested data */
nested?: any
offset?: number
},
headers?: {
ignorePagination?: boolean
},
) => {
const fetchSharedViewData = async (param: {
sortsArr: SortType[]
filtersArr: FilterType[]
fields?: any[]
sort?: any[]
where?: string
/** Query params for nested data */
nested?: any
offset?: number
}) => {
if (!sharedView.value)
return {
list: [],
@ -148,13 +143,54 @@ export function useSharedView() {
{
headers: {
'xc-password': password.value,
'xc-ignore-pagination': headers?.ignorePagination ? 'true' : 'false',
},
},
)
}
const fetchSharedCalendarViewData = async (param: {
from_date: string
to_date: string
sortsArr: SortType[]
filtersArr: FilterType[]
fields?: any[]
sort?: any[]
where?: string
/** Query params for nested data */
nested?: any
offset?: number
}) => {
if (!sharedView.value)
return {
list: [],
pageInfo: {},
}
if (!param.offset) {
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit
param.offset = (page - 1) * pageSize
}
return await $api.dbCalendarViewRow.publicDataCalendarRowList(
sharedView.value.uuid!,
{
limit: sharedView.value?.type === ViewTypes.CALENDAR ? 3000 : undefined,
...param,
filterArrJson: JSON.stringify(param.filtersArr ?? nestedFilters.value),
sortArrJson: JSON.stringify(param.sortsArr ?? sorts.value),
} as any,
{
headers: {
'xc-password': password.value,
},
},
)
}
const fetchSharedViewActiveDate = async (param: {
from_date: string
to_date: string
sortsArr: SortType[]
filtersArr: FilterType[]
sort?: any[]
@ -166,7 +202,7 @@ export function useSharedView() {
pageInfo: {},
}
return await $api.public.calendarCount(
return await $api.public.dataCalendarRowCount(
sharedView.value.uuid!,
{
...param,
@ -234,6 +270,7 @@ export function useSharedView() {
nestedFilters,
fetchSharedViewData,
fetchSharedViewActiveDate,
fetchSharedCalendarViewData,
fetchSharedViewGroupedData,
paginationData,
sorts,

103
packages/nocodb/src/controllers/calendars-datas.controller.ts

@ -0,0 +1,103 @@
import {
Controller,
Get,
Param,
Query,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { GlobalGuard } from '~/guards/global/global.guard';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
import { CalendarDatasService } from '~/services/calendar-datas.service';
import { parseHrtimeToMilliSeconds } from '~/helpers';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
@Controller()
@UseGuards(DataApiLimiterGuard, GlobalGuard)
export class CalendarDatasController {
constructor(private readonly calendarDatasService: CalendarDatasService) {}
@Get(['/api/v1/db/calendar-data/:orgs/:baseName/:tableName/views/:viewName'])
@Acl('dataList')
async dataList(
@Req() req: Request,
@Param('viewName') viewId: string,
@Query('from_date') fromDate: string,
@Query('to_date') toDate: string,
) {
return await this.calendarDatasService.getCalendarDataList({
viewId: viewId,
query: req.query,
from_date: fromDate,
to_date: toDate,
});
}
@Get([
'/api/v1/db/calendar-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,
@Query('from_date') fromDate: string,
@Query('to_date') toDate: string,
) {
const startTime = process.hrtime();
const data = await this.calendarDatasService.getCalendarRecordCount({
query: req.query,
viewId: viewName,
from_date: fromDate,
to_date: toDate,
});
const elapsedSeconds = parseHrtimeToMilliSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(data);
}
@Get([
'/api/v1/db/public/calendar-view/:sharedViewUuid/countByDate',
'/api/v2/public/calendar-view/:sharedViewUuid/countByDate',
])
async countByDate(
@Req() req: Request,
@Param('sharedViewUuid') sharedViewUuid: string,
@Query('from_date') fromDate: string,
@Query('to_date') toDate: string,
) {
return await this.calendarDatasService.getPublicCalendarRecordCount({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid,
from_date: fromDate,
to_date: toDate,
});
}
@Get([
'/api/v1/db/public/calendar-view/:sharedViewUuid',
'/api/v2/public/calendar-view/:sharedViewUuid',
])
async getPublicCalendarDataList(
@Req() req: Request,
@Param('sharedViewUuid') sharedViewUuid: string,
@Query('from_date') fromDate: string,
@Query('to_date') toDate: string,
) {
return await this.calendarDatasService.getPublicCalendarDataList({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid,
from_date: fromDate,
to_date: toDate,
});
}
}

25
packages/nocodb/src/controllers/data-alias.controller.ts

@ -45,7 +45,6 @@ export class DataAliasController {
tableName: tableName,
viewName: viewName,
disableOptimization: opt === 'false',
ignorePagination: req.headers?.['xc-ignore-pagination'] === 'true',
});
const elapsedMilliSeconds = parseHrtimeToMilliSeconds(
process.hrtime(startTime),
@ -118,30 +117,6 @@ 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',

1
packages/nocodb/src/controllers/data-table.controller.ts

@ -38,7 +38,6 @@ export class DataTableController {
query: req.query,
modelId: modelId,
viewId: viewId,
ignorePagination: req.headers?.['xc-ignore-pagination'] === 'true',
});
const elapsedSeconds = parseHrtimeToMilliSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);

15
packages/nocodb/src/controllers/public-datas.controller.ts

@ -34,21 +34,6 @@ export class PublicDatasController {
return pagedResponse;
}
@Get([
'/api/v1/db/public/shared-view/:sharedViewUuid/countByDate',
'/api/v2/public/shared-view/:sharedViewUuid/countByDate',
])
async countByDate(
@Req() req: Request,
@Param('sharedViewUuid') sharedViewUuid: string,
) {
return await this.publicDatasService.getCalendarRecordCount({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid,
});
}
@Get([
'/api/v1/db/public/shared-view/:sharedViewUuid/groupby',
'/api/v2/public/shared-view/:sharedViewUuid/groupby',

12
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -318,12 +318,14 @@ class BaseModelSqlv2 {
sortArr?: Sort[];
sort?: string | string[];
fieldsSet?: Set<string>;
calendarLimitOverride?: number;
} = {},
options: {
ignoreViewFilterAndSort?: boolean;
ignorePagination?: boolean;
validateFormula?: boolean;
throwErrorIfInvalidParams?: boolean;
calendarLimitOverride?: number;
} = {},
): Promise<any> {
const {
@ -331,6 +333,7 @@ class BaseModelSqlv2 {
ignorePagination = false,
validateFormula = false,
throwErrorIfInvalidParams = false,
calendarLimitOverride,
} = options;
const { where, fields, ...rest } = this._getListArgs(args as any);
@ -426,7 +429,14 @@ class BaseModelSqlv2 {
if (createdCol) qb.orderBy(createdCol.column_name);
}
if (!ignorePagination) applyPaginate(qb, rest);
// For calendar View, if calendarLimitOverride is provided, use it as limit for the query
if (!ignorePagination) {
if (!calendarLimitOverride) {
applyPaginate(qb, rest);
} else {
applyPaginate(qb, { ...rest, limit: calendarLimitOverride });
}
}
const proto = await this.getProto();
let data;

5
packages/nocodb/src/modules/datas/datas.module.ts

@ -18,6 +18,8 @@ import { DataAliasNestedService } from '~/services/data-alias-nested.service';
import { OldDatasService } from '~/controllers/old-datas/old-datas.service';
import { PublicDatasExportService } from '~/services/public-datas-export.service';
import { PublicDatasService } from '~/services/public-datas.service';
import { CalendarDatasController } from '~/controllers/calendars-datas.controller';
import { CalendarDatasService } from '~/services/calendar-datas.service';
export const dataModuleMetadata = {
imports: [
@ -33,6 +35,7 @@ export const dataModuleMetadata = {
? [
DataTableController,
DatasController,
CalendarDatasController,
BulkDataAliasController,
DataAliasController,
DataAliasNestedController,
@ -48,6 +51,7 @@ export const dataModuleMetadata = {
DatasService,
BulkDataAliasService,
DataAliasNestedService,
CalendarDatasService,
OldDatasService,
PublicDatasService,
PublicDatasExportService,
@ -55,6 +59,7 @@ export const dataModuleMetadata = {
exports: [
DatasService,
BulkDataAliasService,
CalendarDatasService,
DataAliasNestedService,
OldDatasService,
PublicDatasService,

176
packages/nocodb/src/schema/swagger-v2.json

@ -7284,7 +7284,7 @@
"description": "List Shared View Grouped Data"
}
},
"/api/v2/public/shared-view/{sharedViewUuid}/countByDate": {
"/api/v2/public/calendar-view/{sharedViewUuid}/countByDate": {
"parameters": [
{
"schema": {
@ -7361,6 +7361,180 @@
]
}
},
"/api/v2/public/calendar-view/{sharedViewUuid}/": {
"parameters": [
{
"schema": {
"type": "string",
"example": "24a6d0bb-e45d-4b1a-bfef-f492d870de9f"
},
"name": "sharedViewUuid",
"in": "path",
"required": true,
"description": "Shared View UUID"
},
{
"schema": {
"type": "string"
},
"in": "header",
"name": "xc-password",
"description": "Shared view password"
}
],
"get": {
"summary": "List Shared View Rows",
"operationId": "public-calendar-data-list",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedViewList"
},
"examples": {
"Example 1": {
"value": {
"list": [
{
"source_id": "ds_g4ccx6e77h1dmi",
"created_at": "2023-03-02 17:46:31",
"fk_model_id": "md_mhs9z4r2ak98x0",
"id": "vw_lg052cnc1c26kf",
"is_default": 1,
"lock_type": "collaborative",
"meta": {},
"order": 1,
"password": null,
"base_id": "p_xm3thidrblw4n7",
"show": 1,
"show_system_fields": null,
"title": "Sheet-1",
"type": 3,
"updated_at": "2023-03-02 17:46:31",
"uuid": null,
"view": {
"source_id": "ds_g4ccx6e77h1dmi",
"created_at": "2023-03-02 17:46:31",
"fk_view_id": "vw_lg052cnc1c26kf",
"meta": null,
"base_id": "p_xm3thidrblw4n7",
"row_height": null,
"updated_at": "2023-03-02 17:46:31",
"uuid": null
}
}
],
"pageInfo": {
"isFirstPage": true,
"isLastPage": true,
"page": 1,
"pageSize": 10,
"totalRows": 1
}
}
}
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
}
},
"tags": [
"Public"
],
"parameters": [
{
"schema": {
"type": "array"
},
"in": "query",
"name": "fields",
"description": "Which fields to be shown"
},
{
"schema": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
},
"in": "query",
"name": "sort",
"description": "The result will be sorted based on `sort` query"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "from_date",
"description": "From Date"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "to_date",
"description": "To Date"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "where",
"description": "Extra filtering"
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"in": "query",
"name": "offset",
"description": "Offset in rows"
},
{
"schema": {
"type": "integer",
"minimum": 1
},
"in": "query",
"name": "limit",
"description": "Limit in rows"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "sortArrJson",
"description": "Used for multiple sort queries"
},
{
"schema": {
"type": "string"
},
"in": "query",
"name": "filterArrJson",
"description": "Used for multiple filter queries"
}
],
"description": "List all shared view rows"
},
},
"/api/v2/public/shared-view/{sharedViewUuid}/rows": {
"parameters": [
{

757
packages/nocodb/src/schema/swagger.json

@ -9727,6 +9727,591 @@
}
}
},
"/api/v1/db/calendar-data/{orgs}/{baseName}/{tableName}/views/{viewName}": {
"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": "List rows in Calendar View of a Table",
"operationId": "db-calendar-view-row-list",
"description": "List all rows in Calendar View of a Table",
"tags": [
"DB Calendar View Row"
],
"parameters": [
{
"schema": {
"type": "string"
},
"name": "from_date",
"in": "query",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "to_date",
"in": "query",
"required": true
},
{
"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 calendar 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"
}
}
},
"/api/v1/db/calendar-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-calendar-view-row-count",
"description": "Get the count of table view rows grouped by the dates",
"tags": [
"DB Calendar View Row Count"
],
"parameters": [
{
"schema": {
"type": "string"
},
"name": "from_date",
"in": "query",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "to_date",
"in": "query",
"required": true
},
{
"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/public/calendar-view/{sharedViewUuid}": {
"parameters": [
{
"schema": {
"type": "string",
"example": "24a6d0bb-e45d-4b1a-bfef-f492d870de9f"
},
"name": "sharedViewUuid",
"in": "path",
"required": true,
"description": "Shared View UUID"
},
{
"schema": {
"type": "string"
},
"in": "header",
"name": "xc-password",
"description": "Shared view password"
}
],
"get": {
"summary": "List rows in Calendar View of a Table",
"operationId": "public-data-calendar-row-list",
"description": "List all rows in Calendar View of a Table",
"tags": [
"DB Calendar View Row"
],
"parameters": [
{
"schema": {
"type": "string"
},
"name": "from_date",
"in": "query",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "to_date",
"in": "query",
"required": true
},
{
"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 calendar 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"
}
}
},
"/api/v1/db/public/calendar-view/{sharedViewUuid}/countByDate": {
"parameters": [
{
"schema": {
"type": "string",
"example": "24a6d0bb-e45d-4b1a-bfef-f492d870de9f"
},
"name": "sharedViewUuid",
"in": "path",
"required": true,
"description": "Shared View UUID"
},
{
"schema": {
"type": "string"
},
"in": "header",
"name": "xc-password",
"description": "Shared view password"
}
],
"get": {
"summary": "Count of Records in Dates in Calendar View",
"operationId": "public-data-calendar-row-count",
"parameters": [
{
"schema": {
"type": "string"
},
"name": "from_date",
"in": "query",
"required": true
},
{
"schema": {
"type": "string"
},
"name": "to_date",
"in": "query",
"required": true
},
{
"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"
}
},
"tags": [
"Public"
]
}
},
"/api/v1/db/data/{orgs}/{baseName}/{tableName}/views/{viewName}": {
"parameters": [
{
@ -10065,101 +10650,6 @@
}
}
},
"/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": [
{
@ -12129,83 +12619,6 @@
"description": "List Shared View Grouped Data"
}
},
"/api/v1/db/public/shared-view/{sharedViewUuid}/countByDate": {
"parameters": [
{
"schema": {
"type": "string",
"example": "24a6d0bb-e45d-4b1a-bfef-f492d870de9f"
},
"name": "sharedViewUuid",
"in": "path",
"required": true,
"description": "Shared View UUID"
},
{
"schema": {
"type": "string"
},
"in": "header",
"name": "xc-password",
"description": "Shared view password"
}
],
"get": {
"summary": "Count of Records in Dates in Calendar View",
"operationId": "public-calendar-count",
"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"
}
},
"tags": [
"Public"
]
}
},
"/api/v1/db/public/shared-view/{sharedViewUuid}/rows": {
"parameters": [
{

230
packages/nocodb/src/services/calendar-datas.service.ts

@ -0,0 +1,230 @@
import { Injectable, Logger } from '@nestjs/common';
import { ErrorMessages, ViewTypes } from 'nocodb-sdk';
import dayjs from 'dayjs';
import type { CalendarRangeType, FilterType } from 'nocodb-sdk';
import { CalendarRange, Model, View } from '~/models';
import { NcError } from '~/helpers/catchError';
import { DatasService } from '~/services/datas.service';
@Injectable()
export class CalendarDatasService {
protected logger = new Logger(CalendarDatasService.name);
constructor(protected datasService: DatasService) {}
async getCalendarDataList(param: {
viewId: string;
query: any;
from_date: string;
to_date: string;
}) {
const { viewId, query, from_date, to_date } = param;
if (!from_date || !to_date)
NcError.badRequest('from_date and to_date are required');
if (dayjs(to_date).diff(dayjs(from_date), 'days') > 42) {
NcError.badRequest('Date range should not exceed 42 days');
}
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 calendarRange = await CalendarRange.read(view.id);
if (!calendarRange?.ranges?.length) NcError.badRequest('No ranges found');
const filterArr = await this.buildFilterArr({
viewId,
from_date,
to_date,
});
query.filterArr = [...(query.filterArr ? query.filterArr : []), filterArr];
const model = await Model.getByIdOrName({
id: view.fk_model_id,
});
return await this.datasService.dataList({
...param,
...query,
baseName: model.base_id,
tableName: model.id,
calendarLimitOverride: 3000, // TODO: make this configurable in env
});
}
async getPublicCalendarRecordCount(param: {
password: string;
query: any;
sharedViewUuid: string;
from_date: string;
to_date: string;
}) {
const { sharedViewUuid, password, query = {} } = param;
const view = await View.getByUUID(sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (view.type !== ViewTypes.CALENDAR) {
NcError.notFound('Not found');
}
if (view.password && view.password !== password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
}
return this.getCalendarRecordCount({
viewId: view.id,
query,
from_date: param.from_date,
to_date: param.to_date,
});
}
async getPublicCalendarDataList(param: {
password: string;
query: any;
sharedViewUuid: string;
from_date: string;
to_date: string;
}) {
const { sharedViewUuid, password, query = {} } = param;
const view = await View.getByUUID(sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (view.type !== ViewTypes.CALENDAR) {
NcError.notFound('Not found');
}
if (view.password && view.password !== password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
}
return this.getCalendarDataList({
viewId: view.id,
query,
from_date: param.from_date,
to_date: param.to_date,
});
}
async getCalendarRecordCount(param: {
viewId: string;
query: any;
from_date: string;
to_date: string;
}) {
const { viewId, query, from_date, to_date } = param;
if (!from_date || !to_date)
NcError.badRequest('from_date and to_date are required');
if (dayjs(to_date).diff(dayjs(from_date), 'days') > 395) {
NcError.badRequest('Date range should not exceed 395 days');
}
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 { ranges } = await CalendarRange.read(view.id);
if (!ranges.length) NcError.badRequest('No ranges found');
const filterArr = await this.buildFilterArr({
viewId,
from_date,
to_date,
});
query.filterArr = [...(query.filterArr ? query.filterArr : []), filterArr];
const model = await Model.getByIdOrName({
id: view.fk_model_id,
});
const data = await this.datasService.dataList({
...param,
baseName: model.base_id,
tableName: model.id,
ignorePagination: true,
});
if (!data) NcError.notFound('Data not found');
const dates: Array<string> = [];
const columns = await model.getColumns();
ranges.forEach((range: CalendarRangeType) => {
const fromCol = columns.find(
(c) => c.id === range.fk_from_column_id,
)?.title;
data.list.forEach((date) => {
const fromDt = dayjs(date[fromCol]);
if (fromCol && fromDt.isValid()) {
dates.push(fromDt.format('YYYY-MM-DD HH:mm:ssZ'));
}
});
});
return {
count: dates.length,
dates: Array.from(new Set(dates)),
};
}
async buildFilterArr({
viewId,
from_date,
to_date,
}: {
viewId: string;
from_date: string;
to_date: string;
}) {
const calendarRange = await CalendarRange.read(viewId);
if (!calendarRange?.ranges?.length) NcError.badRequest('No ranges found');
const filterArr: FilterType = {
is_group: true,
logical_op: 'and',
children: [],
};
calendarRange.ranges.forEach((range: CalendarRange) => {
const fromColumn = range.fk_from_column_id;
let rangeFilter: any = [];
if (fromColumn) {
rangeFilter = [
{
fk_column_id: fromColumn,
comparison_op: 'lt',
comparison_sub_op: 'exactDate',
value: to_date as string,
},
{
fk_column_id: fromColumn,
comparison_op: 'gt',
comparison_sub_op: 'exactDate',
value: from_date as string,
},
];
}
if (rangeFilter.length > 0) filterArr.children.push(rangeFilter);
});
return filterArr;
}
}

121
packages/nocodb/src/services/datas.service.ts

@ -1,13 +1,12 @@
import { Injectable, Logger } from '@nestjs/common';
import { isSystemColumn, ViewTypes } from 'nocodb-sdk';
import { isSystemColumn } from 'nocodb-sdk';
import * as XLSX from 'xlsx';
import papaparse from 'papaparse';
import { nocoExecute } from 'nc-help';
import dayjs from 'dayjs';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type { PathParams } from '~/modules/datas/helpers';
import { getDbRows, getViewAndModelByAliasOrId } from '~/modules/datas/helpers';
import { Base, CalendarRange, Column, Model, Source, View } from '~/models';
import { Base, Column, Model, Source, View } from '~/models';
import { NcBaseError, NcError } from '~/helpers/catchError';
import getAst from '~/helpers/getAst';
import { PagedResponseImpl } from '~/helpers/PagedResponse';
@ -24,6 +23,7 @@ export class DatasService {
query: any;
disableOptimization?: boolean;
ignorePagination?: boolean;
calendarLimitOverride?: number;
throwErrorIfInvalidParams?: boolean;
},
) {
@ -43,6 +43,7 @@ export class DatasService {
query: param.query,
throwErrorIfInvalidParams: true,
ignorePagination: param.ignorePagination,
calendarLimitOverride: param.calendarLimitOverride,
});
}
@ -152,6 +153,7 @@ export class DatasService {
throwErrorIfInvalidParams?: boolean;
ignoreViewFilterAndSort?: boolean;
ignorePagination?: boolean;
calendarLimitOverride?: number;
}) {
const { model, view, query = {}, ignoreViewFilterAndSort = false } = param;
@ -180,16 +182,6 @@ export class DatasService {
listArgs.sortArr = JSON.parse(listArgs.sortArrJson);
} catch (e) {}
let options = {};
if (view && view.type === ViewTypes.CALENDAR && param.ignorePagination) {
{
options = {
ignorePagination: true,
};
}
}
const [count, data] = await Promise.all([
baseModel.count(listArgs, false, param.throwErrorIfInvalidParams),
(async () => {
@ -200,7 +192,8 @@ export class DatasService {
await baseModel.list(listArgs, {
ignoreViewFilterAndSort,
throwErrorIfInvalidParams: param.throwErrorIfInvalidParams,
...options,
ignorePagination: param.ignorePagination,
calendarLimitOverride: param.calendarLimitOverride,
}),
{},
listArgs,
@ -221,67 +214,6 @@ 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 calendarRange = await CalendarRange.read(view.id);
if (!calendarRange?.ranges?.length) NcError.badRequest('No ranges found');
const model = await Model.getByIdOrName({
id: view.fk_model_id,
});
const data = await this.getDataList({
model,
view,
query,
});
if (!data) NcError.notFound('Data not found');
const dates: Array<string> = [];
calendarRange.ranges.forEach((range: any) => {
data.list.forEach((date) => {
const from =
date[
model.columns.find((c) => c.id === range.fk_from_column_id).title
];
let to;
if (range.fk_to_column_id) {
to =
date[
model.columns.find((c) => c.id === range.fk_to_column_id).title
];
}
if (from && to) {
const fromDt = dayjs(from);
const toDt = dayjs(to);
let current = fromDt;
while (current.isSameOrBefore(toDt)) {
dates.push(current.format('YYYY-MM-DD HH:mm:ssZ'));
current = current.add(1, 'day');
}
} else if (from) {
dates.push(dayjs(from).format('YYYY-MM-DD HH:mm:ssZ'));
}
});
});
return dates;
}
async getFindOne(param: { model: Model; view: View; query: any }) {
const { model, view, query = {} } = param;
@ -1054,43 +986,4 @@ export class DatasService {
return column;
}
async getDataAggregateBy(param: {
viewId: string;
query?: any;
aggregateColumnName: string;
aggregateFunction: string;
groupByColumnName?: string;
ignoreFilters?: boolean;
sort?: {
column_name: string;
direction: 'asc' | 'desc';
};
}) {
const { viewId, query = {} } = param;
const view = await View.get(viewId);
const source = await Source.get(view.source_id);
const baseModel = await Model.getBaseModelSQL({
id: view.fk_model_id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(source),
});
const data = await baseModel.groupByAndAggregate(
param.aggregateColumnName,
param.aggregateFunction,
{
groupByColumnName: param.groupByColumnName,
sortBy: param.sort,
...query,
},
);
return new PagedResponseImpl(data, {
...query,
});
}
}

79
packages/nocodb/src/services/public-datas.service.ts

@ -10,9 +10,8 @@ import {
import slash from 'slash';
import { nocoExecute } from 'nc-help';
import dayjs from 'dayjs';
import type { LinkToAnotherRecordColumn } from '~/models';
import { CalendarRange, Column, Model, Source, View } from '~/models';
import { Column, Model, Source, View } from '~/models';
import { NcError } from '~/helpers/catchError';
import getAst from '~/helpers/getAst';
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
@ -80,17 +79,10 @@ export class PublicDatasService {
let data = [];
let count = 0;
let option = {};
if (view && view.type === ViewTypes.CALENDAR) {
option = {
ignorePagination: true,
};
}
try {
data = await nocoExecute(
ast,
await baseModel.list(listArgs, option),
await baseModel.list(listArgs),
{},
listArgs,
);
@ -103,73 +95,6 @@ export class PublicDatasService {
return new PagedResponseImpl(data, { ...param.query, count });
}
async getCalendarRecordCount(param: {
sharedViewUuid: string;
password?: string;
query: any;
}) {
const { sharedViewUuid, password, query = {} } = param;
const view = await View.getByUUID(sharedViewUuid);
if (!view) NcError.notFound('Not found');
if (view.password && view.password !== password) {
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD);
}
if (view.type !== ViewTypes.CALENDAR)
NcError.badRequest('View is not a calendar view');
const calendarRange = await CalendarRange.read(view.id);
if (!calendarRange?.ranges?.length)
NcError.notFound('Calendar ranges are required in a calendar view');
const model = await Model.getByIdOrName({
id: view.fk_model_id,
});
const columns = await model.getColumns();
const data: any = await this.dataList({
sharedViewUuid,
password,
query,
});
if (!data) NcError.notFound('Data not found');
const dates: Array<string> = [];
calendarRange.ranges.forEach((range: any) => {
data.list.forEach((date) => {
const from =
date[columns.find((c) => c.id === range.fk_from_column_id).title];
let to;
if (range.fk_to_column_id) {
to = date[columns.find((c) => c.id === range.fk_to_column_id).title];
}
if (from && to) {
const fromDt = dayjs(from);
const toDt = dayjs(to);
let current = fromDt;
while (current.isSameOrBefore(toDt)) {
dates.push(current.format('YYYY-MM-DD HH:mm:ssZ'));
current = current.add(1, 'day');
}
} else if (from) {
dates.push(dayjs(from).format('YYYY-MM-DD HH:mm:ssZ'));
}
});
});
return dates;
}
// todo: Handle the error case where view doesnt belong to model
async groupedDataList(param: {
sharedViewUuid: string;

45
packages/nocodb/tests/unit/rest/tests/viewRow.test.ts

@ -1554,6 +1554,39 @@ function viewRowTests() {
await testViewRowNotExists(ViewTypes.CALENDAR);
});
const testCalendarDataApi = async () => {
const table = rentalTable;
const calendar_range = {
fk_from_column_id: rentalColumns.find((c) => c.title === 'RentalDate').id,
};
const view = await createView(context, {
title: 'View',
table: table,
type: ViewTypes.CALENDAR,
range: calendar_range,
});
const response = await request(context.app)
.get(
`/api/v1/db/calendar-data/noco/${sakilaProject.id}/${table.id}/views/${view.id}`,
)
.query({
from_date: '2005-05-25',
to_date: '2005-05-26',
})
.set('xc-auth', context.token)
.expect(200);
if (response.body.list.length !== 137) {
throw new Error('Wrong calendar data');
}
};
it('Calendar data', async function () {
await testCalendarDataApi();
});
const testCountDatesByRange = async (viewType: ViewTypes) => {
let calendar_range = {};
let expectStatus = 400;
@ -1575,12 +1608,20 @@ function viewRowTests() {
const response = await request(context.app)
.get(
`/api/v1/db/data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}/countByDate/`,
`/api/v1/db/calendar-data/noco/${sakilaProject.id}/${rentalTable.id}/views/${view.id}/countByDate/`,
)
.query({
from_date: '2005-05-25',
to_date: '2005-05-26',
})
.set('xc-auth', context.token)
.expect(expectStatus);
if (expectStatus === 200 && response.body.length !== 25) {
if (
expectStatus === 200 &&
response.body.count !== 137 &&
response.body.dates.length !== 137
) {
throw new Error('Wrong count');
} else if (
expectStatus === 400 &&

Loading…
Cancel
Save