refactor(insights): extract date filter logic into base controller

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fabian8n 2026-04-07 17:10:35 +02:00
parent 60168d9e99
commit fba4af6fce
2 changed files with 86 additions and 76 deletions

View File

@ -0,0 +1,81 @@
import type { InsightsDateFilterDto, ListInsightsWorkflowQueryDto } from '@n8n/api-types';
import { DateTime } from 'luxon';
import { UserError } from 'n8n-workflow';
import { z } from 'zod';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { InsightsService } from './insights.service';
export abstract class InsightsBaseController {
protected abstract readonly insightsService: InsightsService;
protected prepareDateFilters(query: InsightsDateFilterDto | ListInsightsWorkflowQueryDto): {
startDate: Date;
endDate: Date;
} {
this.validateQueryDates(query);
const { startDate, endDate } = this.getSanitizedDateFilters(query);
this.checkDatesFiltersAgainstLicense({ startDate, endDate });
return { startDate, endDate };
}
private validateQueryDates(query: InsightsDateFilterDto | ListInsightsWorkflowQueryDto) {
const schema = z
.object({
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
})
.refine(
(data) => {
if (data.startDate && data.endDate) {
return data.startDate <= data.endDate;
}
return true;
},
{
message: 'endDate must be the same as or after startDate',
path: ['endDate'],
},
);
const result = schema.safeParse(query);
if (!result.success) {
throw new BadRequestError(result.error.errors.map(({ message }) => message).join(' '));
}
}
/**
* When the `startDate` is not provided, we default to the last 7 days.
* When the `endDate` is not provided, we default to today.
*/
private getSanitizedDateFilters(query: InsightsDateFilterDto | ListInsightsWorkflowQueryDto): {
startDate: Date;
endDate: Date;
} {
const today = new Date();
if (!query.startDate) {
return {
startDate: DateTime.now().minus({ days: 7 }).toJSDate(),
endDate: today,
};
}
return { startDate: query.startDate, endDate: query.endDate ?? today };
}
private checkDatesFiltersAgainstLicense(dateFilters: { startDate: Date; endDate: Date }) {
try {
this.insightsService.validateDateFiltersLicense(dateFilters);
} catch (error: unknown) {
if (error instanceof UserError) {
throw new ForbiddenError(error.message);
}
throw new InternalServerError();
}
}
}

View File

@ -7,19 +7,15 @@ import type {
import { InsightsDateFilterDto, ListInsightsWorkflowQueryDto } from '@n8n/api-types';
import { AuthenticatedRequest } from '@n8n/db';
import { Get, GlobalScope, Licensed, Query, RestController } from '@n8n/decorators';
import { DateTime } from 'luxon';
import { UserError } from 'n8n-workflow';
import { z } from 'zod';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { InsightsBaseController } from './insights-base.controller';
import { InsightsService } from './insights.service';
@RestController('/insights')
export class InsightsController {
constructor(private readonly insightsService: InsightsService) {}
export class InsightsController extends InsightsBaseController {
constructor(protected readonly insightsService: InsightsService) {
super();
}
@Get('/summary')
@GlobalScope('insights:list')
@ -98,71 +94,4 @@ export class InsightsController {
endDate,
})) as RestrictedInsightsByTime[];
}
private validateQueryDates(query: InsightsDateFilterDto | ListInsightsWorkflowQueryDto) {
const schema = z
.object({
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
})
.refine(
(data) => {
if (data.startDate && data.endDate) {
return data.startDate <= data.endDate;
}
return true;
},
{
message: 'endDate must be the same as or after startDate',
path: ['endDate'],
},
);
const result = schema.safeParse(query);
if (!result.success) {
throw new BadRequestError(result.error.errors.map(({ message }) => message).join(' '));
}
}
private prepareDateFilters(query: InsightsDateFilterDto | ListInsightsWorkflowQueryDto): {
startDate: Date;
endDate: Date;
} {
this.validateQueryDates(query);
const { startDate, endDate } = this.getSanitizedDateFilters(query);
this.checkDatesFiltersAgainstLicense({ startDate, endDate });
return { startDate, endDate };
}
/**
* When the `startDate` is not provided, we default to the last 7 days.
* When the `endDate` is not provided, we default to today.
*/
private getSanitizedDateFilters(query: InsightsDateFilterDto | ListInsightsWorkflowQueryDto): {
startDate: Date;
endDate: Date;
} {
const today = new Date();
if (!query.startDate) {
return {
startDate: DateTime.now().minus({ days: 7 }).toJSDate(),
endDate: today,
};
}
return { startDate: query.startDate, endDate: query.endDate ?? today };
}
private checkDatesFiltersAgainstLicense(dateFilters: { startDate: Date; endDate: Date }) {
try {
this.insightsService.validateDateFiltersLicense(dateFilters);
} catch (error: unknown) {
if (error instanceof UserError) {
throw new ForbiddenError(error.message);
}
throw new InternalServerError();
}
}
}