diff --git a/packages/@n8n/api-types/src/dto/insights/__tests__/date-filter.dto.test.ts b/packages/@n8n/api-types/src/dto/insights/__tests__/date-filter.dto.test.ts index ae95c4ec8d3..63d0f0af7c0 100644 --- a/packages/@n8n/api-types/src/dto/insights/__tests__/date-filter.dto.test.ts +++ b/packages/@n8n/api-types/src/dto/insights/__tests__/date-filter.dto.test.ts @@ -13,8 +13,39 @@ describe('InsightsDateFilterDto', () => { request: { dateRange: 'week', // Using a valid option from the provided list }, + parsedResult: {}, + }, + { + name: 'valid startDate and endDate (as strings)', + request: { + startDate: '2025-01-01', + endDate: '2025-01-31', + }, parsedResult: { - dateRange: 'week', + startDate: new Date('2025-01-01'), + endDate: new Date('2025-01-31'), + }, + }, + { + name: 'valid startDate and endDate (as ISO strings)', + request: { + startDate: '2025-01-01T00:00:00Z', + endDate: '2025-01-31T23:59:59Z', + }, + parsedResult: { + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + }, + }, + { + name: 'valid startDate and endDate (as timestamps)', + request: { + startDate: new Date('2025-01-01').getTime(), + endDate: new Date('2025-01-31').getTime(), + }, + parsedResult: { + startDate: new Date('2025-01-01'), + endDate: new Date('2025-01-31'), }, }, { @@ -37,31 +68,74 @@ describe('InsightsDateFilterDto', () => { describe('Invalid requests', () => { test.each([ + { + name: 'invalid startDate format', + request: { + startDate: '2025-13-01', // Invalid month + endDate: '2025-13-31', // Invalid month + }, + expectedErrorPaths: ['startDate', 'endDate'], + }, + { + name: 'startDate is an invalid timestamp', + request: { + startDate: NaN, + }, + expectedErrorPaths: ['startDate'], + }, + { + name: 'endDate is an invalid timestamp', + request: { + endDate: NaN, + projectId: 'validProjectId', + }, + expectedErrorPaths: ['endDate'], + }, + { + name: 'startDate is an invalid ISO string', + request: { + startDate: 'invalid--date', + }, + expectedErrorPaths: ['startDate'], + }, + { + name: 'endDate is an invalid ISO string', + request: { + startDate: '2025-01-01', + endDate: 'not-a-date', + }, + expectedErrorPaths: ['endDate'], + }, { name: 'invalid dateRange value', request: { dateRange: 'invalid-value', }, - expectedErrorPath: ['dateRange'], + expectedErrorPaths: ['dateRange'], }, { name: 'invalid projectId value', request: { projectId: 10, }, - expectedErrorPath: ['projectId'], + expectedErrorPaths: ['projectId'], }, - ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + { + name: 'all fields invalid', + request: { + dateRange: 'invalid-value', + startDate: '2025-13-01', // Invalid month + endDate: 'not-a-date', + projectId: 10, + }, + expectedErrorPaths: ['dateRange', 'startDate', 'endDate', 'projectId'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPaths }) => { const result = InsightsDateFilterDto.safeParse(request); + const issuesPaths = new Set(result.error?.issues.map((issue) => issue.path[0])); expect(result.success).toBe(false); - - if (expectedErrorPath && !result.success) { - if (Array.isArray(expectedErrorPath)) { - const errorPaths = result.error.issues[0].path; - expect(errorPaths).toContain(expectedErrorPath[0]); - } - } + expect(new Set(issuesPaths)).toEqual(new Set(expectedErrorPaths)); }); }); }); diff --git a/packages/@n8n/api-types/src/dto/insights/__tests__/list-workflow-query.dto.test.ts b/packages/@n8n/api-types/src/dto/insights/__tests__/list-workflow-query.dto.test.ts index 585e8916610..2679040da51 100644 --- a/packages/@n8n/api-types/src/dto/insights/__tests__/list-workflow-query.dto.test.ts +++ b/packages/@n8n/api-types/src/dto/insights/__tests__/list-workflow-query.dto.test.ts @@ -86,6 +86,39 @@ describe('ListInsightsWorkflowQueryDto', () => { projectId: '2gQLpmP5V4wOY627', }, }, + { + name: 'valid startDate and endDate (as strings)', + request: { + startDate: '2025-01-01', + endDate: '2025-01-31', + }, + parsedResult: { + startDate: new Date('2025-01-01'), + endDate: new Date('2025-01-31'), + }, + }, + { + name: 'valid startDate and endDate (as ISO strings)', + request: { + startDate: '2025-01-01T00:00:00Z', + endDate: '2025-01-31T23:59:59Z', + }, + parsedResult: { + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: new Date('2025-01-31T23:59:59Z'), + }, + }, + { + name: 'valid startDate and endDate (as timestamps)', + request: { + startDate: new Date('2025-01-01').getTime(), + endDate: new Date('2025-01-31').getTime(), + }, + parsedResult: { + startDate: new Date('2025-01-01'), + endDate: new Date('2025-01-31'), + }, + }, ])('should validate $name', ({ request, parsedResult }) => { const result = ListInsightsWorkflowQueryDto.safeParse(request); expect(result.success).toBe(true); @@ -103,7 +136,7 @@ describe('ListInsightsWorkflowQueryDto', () => { skip: 'not-a-number', take: '10', }, - expectedErrorPath: ['skip'], + expectedErrorPaths: ['skip'], }, { name: 'invalid take format', @@ -111,33 +144,77 @@ describe('ListInsightsWorkflowQueryDto', () => { skip: '0', take: 'not-a-number', }, - expectedErrorPath: ['take'], + expectedErrorPaths: ['take'], }, { name: 'invalid sortBy value', request: { sortBy: 'invalid-value', }, - expectedErrorPath: ['sortBy'], + expectedErrorPaths: ['sortBy'], }, { name: 'invalid projectId value', request: { projectId: 10, }, - expectedErrorPath: ['projectId'], + expectedErrorPaths: ['projectId'], }, - ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + { + name: 'invalid startDate format', + request: { + startDate: '2025-13-01', // Invalid month + endDate: '2025-13-31', // Invalid month + }, + expectedErrorPaths: ['startDate', 'endDate'], + }, + { + name: 'startDate is an invalid timestamp', + request: { + startDate: NaN, + }, + expectedErrorPaths: ['startDate'], + }, + { + name: 'endDate is an invalid timestamp', + request: { + endDate: NaN, + projectId: 'validProjectId', + }, + expectedErrorPaths: ['endDate'], + }, + { + name: 'startDate is an invalid ISO string', + request: { + startDate: 'invalid--date', + }, + expectedErrorPaths: ['startDate'], + }, + { + name: 'endDate is an invalid ISO string', + request: { + startDate: '2025-01-01', + endDate: 'not-a-date', + }, + expectedErrorPaths: ['endDate'], + }, + { + name: 'all fields invalid', + request: { + sortBy: 'invalid-value', + startDate: '2025-13-01', // Invalid month + endDate: 'not-a-date', + projectId: 10, + }, + expectedErrorPaths: ['sortBy', 'startDate', 'endDate', 'projectId'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPaths }) => { const result = ListInsightsWorkflowQueryDto.safeParse(request); - expect(result.success).toBe(false); + const issuesPaths = new Set(result.error?.issues.map((issue) => issue.path[0])); - if (expectedErrorPath && !result.success) { - if (Array.isArray(expectedErrorPath)) { - const errorPaths = result.error.issues[0].path; - expect(errorPaths).toContain(expectedErrorPath[0]); - } - } + expect(result.success).toBe(false); + expect(new Set(issuesPaths)).toEqual(new Set(expectedErrorPaths)); }); }); }); diff --git a/packages/@n8n/api-types/src/dto/insights/date-filter.dto.ts b/packages/@n8n/api-types/src/dto/insights/date-filter.dto.ts index bc11018ff14..2519e778f22 100644 --- a/packages/@n8n/api-types/src/dto/insights/date-filter.dto.ts +++ b/packages/@n8n/api-types/src/dto/insights/date-filter.dto.ts @@ -10,5 +10,7 @@ const dateRange = z.enum(VALID_DATE_RANGE_OPTIONS).optional(); export class InsightsDateFilterDto extends Z.class({ dateRange, + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), projectId: z.string().optional(), }) {} diff --git a/packages/@n8n/api-types/src/dto/insights/list-workflow-query.dto.ts b/packages/@n8n/api-types/src/dto/insights/list-workflow-query.dto.ts index 5fd40f2b33f..b5baee91dbb 100644 --- a/packages/@n8n/api-types/src/dto/insights/list-workflow-query.dto.ts +++ b/packages/@n8n/api-types/src/dto/insights/list-workflow-query.dto.ts @@ -37,6 +37,8 @@ export class ListInsightsWorkflowQueryDto extends Z.class({ ...paginationSchema, take: createTakeValidator(MAX_ITEMS_PER_PAGE), dateRange: InsightsDateFilterDto.shape.dateRange, + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), sortBy: sortByValidator, projectId: z.string().optional(), }) {}