mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat(core): Implement granularity and date range filtering on insights (#14841)
This commit is contained in:
committed by
GitHub
parent
5ff073bd7b
commit
28596a633e
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Z } from 'zod-class';
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
|
import { InsightsDateFilterDto } from './date-filter.dto';
|
||||||
import { paginationSchema } from '../pagination/pagination.dto';
|
import { paginationSchema } from '../pagination/pagination.dto';
|
||||||
|
|
||||||
const VALID_SORT_OPTIONS = [
|
const VALID_SORT_OPTIONS = [
|
||||||
@@ -30,5 +31,6 @@ const sortByValidator = z
|
|||||||
|
|
||||||
export class ListInsightsWorkflowQueryDto extends Z.class({
|
export class ListInsightsWorkflowQueryDto extends Z.class({
|
||||||
...paginationSchema,
|
...paginationSchema,
|
||||||
|
dateRange: InsightsDateFilterDto.shape.dateRange,
|
||||||
sortBy: sortByValidator,
|
sortBy: sortByValidator,
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ export {
|
|||||||
SOURCE_CONTROL_FILE_TYPE,
|
SOURCE_CONTROL_FILE_TYPE,
|
||||||
} from './schemas/source-controlled-file.schema';
|
} from './schemas/source-controlled-file.schema';
|
||||||
|
|
||||||
export type {
|
export {
|
||||||
InsightsSummaryType,
|
type InsightsSummaryType,
|
||||||
InsightsSummaryUnit,
|
type InsightsSummaryUnit,
|
||||||
InsightsSummary,
|
type InsightsSummary,
|
||||||
InsightsByWorkflow,
|
type InsightsByWorkflow,
|
||||||
InsightsByTime,
|
type InsightsByTime,
|
||||||
InsightsDateRange,
|
type InsightsDateRange,
|
||||||
|
INSIGHTS_DATE_RANGE_KEYS,
|
||||||
} from './schemas/insights.schema';
|
} from './schemas/insights.schema';
|
||||||
|
|||||||
@@ -82,13 +82,21 @@ export const insightsByTimeDataSchemas = {
|
|||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const insightsByTimeSchema = z.object(insightsByTimeDataSchemas).strict();
|
export const insightsByTimeSchema = z.object(insightsByTimeDataSchemas).strict();
|
||||||
export type InsightsByTime = z.infer<typeof insightsByTimeSchema>;
|
export type InsightsByTime = z.infer<typeof insightsByTimeSchema>;
|
||||||
|
|
||||||
|
export const INSIGHTS_DATE_RANGE_KEYS = [
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'2weeks',
|
||||||
|
'month',
|
||||||
|
'quarter',
|
||||||
|
'6months',
|
||||||
|
'year',
|
||||||
|
] as const;
|
||||||
export const insightsDateRangeSchema = z
|
export const insightsDateRangeSchema = z
|
||||||
.object({
|
.object({
|
||||||
key: z.enum(['day', 'week', '2weeks', 'month', 'quarter', '6months', 'year']),
|
key: z.enum(INSIGHTS_DATE_RANGE_KEYS),
|
||||||
licensed: z.boolean(),
|
licensed: z.boolean(),
|
||||||
granularity: z.enum(['hour', 'day', 'week']),
|
granularity: z.enum(['hour', 'day', 'week']),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
import * as testDb from '@test-integration/test-db';
|
import * as testDb from '@test-integration/test-db';
|
||||||
|
|
||||||
@@ -30,7 +32,10 @@ describe('InsightsController', () => {
|
|||||||
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates.mockResolvedValue([]);
|
insightsByPeriodRepository.getPreviousAndCurrentPeriodTypeAggregates.mockResolvedValue([]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const response = await controller.getInsightsSummary();
|
const response = await controller.getInsightsSummary(
|
||||||
|
mock<AuthenticatedRequest>(),
|
||||||
|
mock<Response>(),
|
||||||
|
);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
@@ -52,7 +57,10 @@ describe('InsightsController', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const response = await controller.getInsightsSummary();
|
const response = await controller.getInsightsSummary(
|
||||||
|
mock<AuthenticatedRequest>(),
|
||||||
|
mock<Response>(),
|
||||||
|
);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
@@ -78,7 +86,10 @@ describe('InsightsController', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const response = await controller.getInsightsSummary();
|
const response = await controller.getInsightsSummary(
|
||||||
|
mock<AuthenticatedRequest>(),
|
||||||
|
mock<Response>(),
|
||||||
|
);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { InsightsDateRange } from '@n8n/api-types';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@@ -517,6 +518,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
{ key: '2weeks', licensed: true, granularity: 'day' },
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
||||||
{ key: 'month', licensed: true, granularity: 'day' },
|
{ key: 'month', licensed: true, granularity: 'day' },
|
||||||
{ key: 'quarter', licensed: true, granularity: 'week' },
|
{ key: 'quarter', licensed: true, granularity: 'week' },
|
||||||
|
{ key: '6months', licensed: true, granularity: 'week' },
|
||||||
{ key: 'year', licensed: true, granularity: 'week' },
|
{ key: 'year', licensed: true, granularity: 'week' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -533,6 +535,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
{ key: '2weeks', licensed: true, granularity: 'day' },
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
||||||
{ key: 'month', licensed: true, granularity: 'day' },
|
{ key: 'month', licensed: true, granularity: 'day' },
|
||||||
{ key: 'quarter', licensed: true, granularity: 'week' },
|
{ key: 'quarter', licensed: true, granularity: 'week' },
|
||||||
|
{ key: '6months', licensed: true, granularity: 'week' },
|
||||||
{ key: 'year', licensed: true, granularity: 'week' },
|
{ key: 'year', licensed: true, granularity: 'week' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -549,6 +552,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
{ key: '2weeks', licensed: true, granularity: 'day' },
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
||||||
{ key: 'month', licensed: true, granularity: 'day' },
|
{ key: 'month', licensed: true, granularity: 'day' },
|
||||||
{ key: 'quarter', licensed: false, granularity: 'week' },
|
{ key: 'quarter', licensed: false, granularity: 'week' },
|
||||||
|
{ key: '6months', licensed: false, granularity: 'week' },
|
||||||
{ key: 'year', licensed: false, granularity: 'week' },
|
{ key: 'year', licensed: false, granularity: 'week' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -565,6 +569,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
{ key: '2weeks', licensed: false, granularity: 'day' },
|
{ key: '2weeks', licensed: false, granularity: 'day' },
|
||||||
{ key: 'month', licensed: false, granularity: 'day' },
|
{ key: 'month', licensed: false, granularity: 'day' },
|
||||||
{ key: 'quarter', licensed: false, granularity: 'week' },
|
{ key: 'quarter', licensed: false, granularity: 'week' },
|
||||||
|
{ key: '6months', licensed: false, granularity: 'week' },
|
||||||
{ key: 'year', licensed: false, granularity: 'week' },
|
{ key: 'year', licensed: false, granularity: 'week' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -581,7 +586,84 @@ describe('getAvailableDateRanges', () => {
|
|||||||
{ key: '2weeks', licensed: true, granularity: 'day' },
|
{ key: '2weeks', licensed: true, granularity: 'day' },
|
||||||
{ key: 'month', licensed: true, granularity: 'day' },
|
{ key: 'month', licensed: true, granularity: 'day' },
|
||||||
{ key: 'quarter', licensed: true, granularity: 'week' },
|
{ key: 'quarter', licensed: true, granularity: 'week' },
|
||||||
|
{ key: '6months', licensed: false, granularity: 'week' },
|
||||||
{ key: 'year', licensed: false, granularity: 'week' },
|
{ key: 'year', licensed: false, granularity: 'week' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMaxAgeInDaysAndGranularity', () => {
|
||||||
|
let insightsService: InsightsService;
|
||||||
|
let licenseMock: jest.Mocked<License>;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
licenseMock = mock<License>();
|
||||||
|
insightsService = new InsightsService(
|
||||||
|
mock<InsightsByPeriodRepository>(),
|
||||||
|
mock<InsightsCompactionService>(),
|
||||||
|
mock<InsightsCollectionService>(),
|
||||||
|
licenseMock,
|
||||||
|
mock<Logger>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns correct maxAgeInDays and granularity for a valid licensed date range', () => {
|
||||||
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
||||||
|
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
||||||
|
|
||||||
|
const result = insightsService.getMaxAgeInDaysAndGranularity('month');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: 'month',
|
||||||
|
licensed: true,
|
||||||
|
granularity: 'day',
|
||||||
|
maxAgeInDays: 30,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws an error if the date range is not available', () => {
|
||||||
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
||||||
|
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
insightsService.getMaxAgeInDaysAndGranularity('invalidKey' as InsightsDateRange['key']);
|
||||||
|
}).toThrowError('The selected date range is not available');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws an error if the date range is not licensed', () => {
|
||||||
|
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
||||||
|
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
insightsService.getMaxAgeInDaysAndGranularity('year');
|
||||||
|
}).toThrowError('The selected date range exceeds the maximum history allowed by your license.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns correct maxAgeInDays and granularity for a valid date range with hourly data disabled', () => {
|
||||||
|
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
||||||
|
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = insightsService.getMaxAgeInDaysAndGranularity('quarter');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: 'quarter',
|
||||||
|
licensed: true,
|
||||||
|
granularity: 'week',
|
||||||
|
maxAgeInDays: 90,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns correct maxAgeInDays and granularity for a valid date range with unlimited history', () => {
|
||||||
|
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
||||||
|
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
||||||
|
|
||||||
|
const result = insightsService.getMaxAgeInDaysAndGranularity('day');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: 'day',
|
||||||
|
licensed: true,
|
||||||
|
granularity: 'hour',
|
||||||
|
maxAgeInDays: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
import { ListInsightsWorkflowQueryDto } from '@n8n/api-types';
|
import { InsightsDateFilterDto, ListInsightsWorkflowQueryDto } from '@n8n/api-types';
|
||||||
import type { InsightsSummary, InsightsByTime, InsightsByWorkflow } from '@n8n/api-types';
|
import type { InsightsSummary, InsightsByTime, InsightsByWorkflow } from '@n8n/api-types';
|
||||||
import { Get, GlobalScope, Licensed, Query, RestController } from '@n8n/decorators';
|
import { Get, GlobalScope, Licensed, Query, RestController } from '@n8n/decorators';
|
||||||
|
import type { UserError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
import { AuthenticatedRequest } from '@/requests';
|
import { AuthenticatedRequest } from '@/requests';
|
||||||
|
|
||||||
import { InsightsService } from './insights.service';
|
import { InsightsService } from './insights.service';
|
||||||
|
|
||||||
@RestController('/insights')
|
@RestController('/insights')
|
||||||
export class InsightsController {
|
export class InsightsController {
|
||||||
private readonly maxAgeInDaysFilteredInsights = 7;
|
|
||||||
|
|
||||||
constructor(private readonly insightsService: InsightsService) {}
|
constructor(private readonly insightsService: InsightsService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used to transform the date range from the request payload into a maximum age in days.
|
||||||
|
* It throws a ForbiddenError if the date range does not match the license insights max history
|
||||||
|
*/
|
||||||
|
private getMaxAgeInDaysAndGranularity(payload: InsightsDateFilterDto) {
|
||||||
|
try {
|
||||||
|
return this.insightsService.getMaxAgeInDaysAndGranularity(payload.dateRange ?? 'week');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new ForbiddenError((error as UserError).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/summary')
|
@Get('/summary')
|
||||||
@GlobalScope('insights:list')
|
@GlobalScope('insights:list')
|
||||||
async getInsightsSummary(): Promise<InsightsSummary> {
|
async getInsightsSummary(
|
||||||
|
_req: AuthenticatedRequest,
|
||||||
|
_res: Response,
|
||||||
|
@Query payload: InsightsDateFilterDto = { dateRange: 'week' },
|
||||||
|
): Promise<InsightsSummary> {
|
||||||
|
const dateRangeAndMaxAgeInDays = this.getMaxAgeInDaysAndGranularity(payload);
|
||||||
return await this.insightsService.getInsightsSummary({
|
return await this.insightsService.getInsightsSummary({
|
||||||
periodLengthInDays: this.maxAgeInDaysFilteredInsights,
|
periodLengthInDays: dateRangeAndMaxAgeInDays.maxAgeInDays,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,8 +45,11 @@ export class InsightsController {
|
|||||||
_res: Response,
|
_res: Response,
|
||||||
@Query payload: ListInsightsWorkflowQueryDto,
|
@Query payload: ListInsightsWorkflowQueryDto,
|
||||||
): Promise<InsightsByWorkflow> {
|
): Promise<InsightsByWorkflow> {
|
||||||
|
const dateRangeAndMaxAgeInDays = this.getMaxAgeInDaysAndGranularity({
|
||||||
|
dateRange: payload.dateRange ?? 'week',
|
||||||
|
});
|
||||||
return await this.insightsService.getInsightsByWorkflow({
|
return await this.insightsService.getInsightsByWorkflow({
|
||||||
maxAgeInDays: this.maxAgeInDaysFilteredInsights,
|
maxAgeInDays: dateRangeAndMaxAgeInDays.maxAgeInDays,
|
||||||
skip: payload.skip,
|
skip: payload.skip,
|
||||||
take: payload.take,
|
take: payload.take,
|
||||||
sortBy: payload.sortBy,
|
sortBy: payload.sortBy,
|
||||||
@@ -39,10 +59,15 @@ export class InsightsController {
|
|||||||
@Get('/by-time')
|
@Get('/by-time')
|
||||||
@GlobalScope('insights:list')
|
@GlobalScope('insights:list')
|
||||||
@Licensed('feat:insights:viewDashboard')
|
@Licensed('feat:insights:viewDashboard')
|
||||||
async getInsightsByTime(): Promise<InsightsByTime[]> {
|
async getInsightsByTime(
|
||||||
|
_req: AuthenticatedRequest,
|
||||||
|
_res: Response,
|
||||||
|
@Query payload: InsightsDateFilterDto,
|
||||||
|
): Promise<InsightsByTime[]> {
|
||||||
|
const dateRangeAndMaxAgeInDays = this.getMaxAgeInDaysAndGranularity(payload);
|
||||||
return await this.insightsService.getInsightsByTime({
|
return await this.insightsService.getInsightsByTime({
|
||||||
maxAgeInDays: this.maxAgeInDaysFilteredInsights,
|
maxAgeInDays: dateRangeAndMaxAgeInDays.maxAgeInDays,
|
||||||
periodUnit: 'day',
|
periodUnit: dateRangeAndMaxAgeInDays.granularity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { InsightsSummary } from '@n8n/api-types';
|
import {
|
||||||
import type { InsightsDateRange } from '@n8n/api-types/src/schemas/insights.schema';
|
type InsightsSummary,
|
||||||
|
type InsightsDateRange,
|
||||||
|
INSIGHTS_DATE_RANGE_KEYS,
|
||||||
|
} from '@n8n/api-types';
|
||||||
import { OnShutdown } from '@n8n/decorators';
|
import { OnShutdown } from '@n8n/decorators';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { Logger } from 'n8n-core';
|
import { Logger } from 'n8n-core';
|
||||||
import type { ExecutionLifecycleHooks } from 'n8n-core';
|
import type { ExecutionLifecycleHooks } from 'n8n-core';
|
||||||
import type { IRun } from 'n8n-workflow';
|
import { UserError, type IRun } from 'n8n-workflow';
|
||||||
|
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
|
|
||||||
@@ -14,6 +17,16 @@ import { InsightsByPeriodRepository } from './database/repositories/insights-by-
|
|||||||
import { InsightsCollectionService } from './insights-collection.service';
|
import { InsightsCollectionService } from './insights-collection.service';
|
||||||
import { InsightsCompactionService } from './insights-compaction.service';
|
import { InsightsCompactionService } from './insights-compaction.service';
|
||||||
|
|
||||||
|
const keyRangeToDays: Record<InsightsDateRange['key'], number> = {
|
||||||
|
day: 1,
|
||||||
|
week: 7,
|
||||||
|
'2weeks': 14,
|
||||||
|
month: 30,
|
||||||
|
quarter: 90,
|
||||||
|
'6months': 180,
|
||||||
|
year: 365,
|
||||||
|
};
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class InsightsService {
|
export class InsightsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -181,6 +194,10 @@ export class InsightsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the available date ranges with their license authorization and time granularity
|
||||||
|
* when grouped by time.
|
||||||
|
*/
|
||||||
getAvailableDateRanges(): InsightsDateRange[] {
|
getAvailableDateRanges(): InsightsDateRange[] {
|
||||||
const maxHistoryInDays =
|
const maxHistoryInDays =
|
||||||
this.license.getInsightsMaxHistory() === -1
|
this.license.getInsightsMaxHistory() === -1
|
||||||
@@ -188,13 +205,31 @@ export class InsightsService {
|
|||||||
: this.license.getInsightsMaxHistory();
|
: this.license.getInsightsMaxHistory();
|
||||||
const isHourlyDateEnabled = this.license.isInsightsHourlyDataEnabled();
|
const isHourlyDateEnabled = this.license.isInsightsHourlyDataEnabled();
|
||||||
|
|
||||||
return [
|
return INSIGHTS_DATE_RANGE_KEYS.map((key) => ({
|
||||||
{ key: 'day', licensed: isHourlyDateEnabled ?? false, granularity: 'hour' },
|
key,
|
||||||
{ key: 'week', licensed: maxHistoryInDays >= 7, granularity: 'day' },
|
licensed:
|
||||||
{ key: '2weeks', licensed: maxHistoryInDays >= 14, granularity: 'day' },
|
key === 'day' ? (isHourlyDateEnabled ?? false) : maxHistoryInDays >= keyRangeToDays[key],
|
||||||
{ key: 'month', licensed: maxHistoryInDays >= 30, granularity: 'day' },
|
granularity: key === 'day' ? 'hour' : keyRangeToDays[key] <= 30 ? 'day' : 'week',
|
||||||
{ key: 'quarter', licensed: maxHistoryInDays >= 90, granularity: 'week' },
|
}));
|
||||||
{ key: 'year', licensed: maxHistoryInDays >= 365, granularity: 'week' },
|
}
|
||||||
];
|
|
||||||
|
getMaxAgeInDaysAndGranularity(
|
||||||
|
dateRangeKey: InsightsDateRange['key'],
|
||||||
|
): InsightsDateRange & { maxAgeInDays: number } {
|
||||||
|
const availableDateRanges = this.getAvailableDateRanges();
|
||||||
|
|
||||||
|
const dateRange = availableDateRanges.find((range) => range.key === dateRangeKey);
|
||||||
|
if (!dateRange) {
|
||||||
|
// Not supposed to happen if we trust the dateRangeKey type
|
||||||
|
throw new UserError('The selected date range is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dateRange.licensed) {
|
||||||
|
throw new UserError(
|
||||||
|
'The selected date range exceeds the maximum history allowed by your license.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...dateRange, maxAgeInDays: keyRangeToDays[dateRangeKey] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { InsightsDateRange } from '@n8n/api-types';
|
||||||
|
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ let agents: Record<string, SuperAgentTest> = {};
|
|||||||
const testServer = utils.setupTestServer({
|
const testServer = utils.setupTestServer({
|
||||||
endpointGroups: ['insights', 'license', 'auth'],
|
endpointGroups: ['insights', 'license', 'auth'],
|
||||||
enabledFeatures: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
enabledFeatures: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||||
|
quotas: { 'quota:insights:maxHistoryDays': 365 },
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -49,6 +52,54 @@ describe('GET /insights routes return 403 for dashboard routes when summary lice
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /insights routes return 403 if date range outside license limits', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
testServer.license.setDefaults({ quotas: { 'quota:insights:maxHistoryDays': 3 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Call should throw forbidden for default week insights', async () => {
|
||||||
|
const authAgent = agents.admin;
|
||||||
|
await authAgent.get('/insights/summary').expect(403);
|
||||||
|
await authAgent.get('/insights/by-time').expect(403);
|
||||||
|
await authAgent.get('/insights/by-workflow').expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Call should throw forbidden for daily data without viewHourlyData enabled', async () => {
|
||||||
|
const authAgent = agents.admin;
|
||||||
|
await authAgent.get('/insights/summary?dateRange=day').expect(403);
|
||||||
|
await authAgent.get('/insights/by-time?dateRange=day').expect(403);
|
||||||
|
await authAgent.get('/insights/by-workflow?dateRange=day').expect(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /insights routes return 200 if date range inside license limits', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
testServer.license.setDefaults({
|
||||||
|
features: [
|
||||||
|
'feat:insights:viewSummary',
|
||||||
|
'feat:insights:viewDashboard',
|
||||||
|
'feat:insights:viewHourlyData',
|
||||||
|
],
|
||||||
|
quotas: { 'quota:insights:maxHistoryDays': 365 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each<InsightsDateRange['key']>([
|
||||||
|
'day',
|
||||||
|
'week',
|
||||||
|
'2weeks',
|
||||||
|
'month',
|
||||||
|
'quarter',
|
||||||
|
'6months',
|
||||||
|
'year',
|
||||||
|
])('Call should work for date range %s', async (dateRange) => {
|
||||||
|
const authAgent = agents.admin;
|
||||||
|
await authAgent.get(`/insights/summary?dateRange=${dateRange}`).expect(200);
|
||||||
|
await authAgent.get(`/insights/by-time?dateRange=${dateRange}`).expect(200);
|
||||||
|
await authAgent.get(`/insights/by-workflow?dateRange=${dateRange}`).expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET /insights/by-workflow', () => {
|
describe('GET /insights/by-workflow', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
testServer.license.setDefaults({
|
testServer.license.setDefaults({
|
||||||
|
|||||||
Reference in New Issue
Block a user