mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(API): Create schema and dto types for insights dashboard query param and api responses (#14163)
This commit is contained in:
committed by
GitHub
parent
1ff3049ffb
commit
6eee081cf3
@@ -59,3 +59,5 @@ export { CreateFolderDto } from './folders/create-folder.dto';
|
||||
export { UpdateFolderDto } from './folders/update-folder.dto';
|
||||
export { DeleteFolderDto } from './folders/delete-folder.dto';
|
||||
export { ListFolderQueryDto } from './folders/list-folder-query.dto';
|
||||
|
||||
export { ListInsightsWorkflowQueryDto } from './insights/list-workflow-query.dto';
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ListInsightsWorkflowQueryDto } from '../list-workflow-query.dto';
|
||||
|
||||
const DEFAULT_PAGINATION = { skip: 0, take: 10 };
|
||||
|
||||
describe('ListInsightsWorkflowQueryDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'empty object (no filters)',
|
||||
request: {},
|
||||
parsedResult: DEFAULT_PAGINATION,
|
||||
},
|
||||
{
|
||||
name: 'valid sortBy',
|
||||
request: {
|
||||
sortBy: 'total:asc',
|
||||
},
|
||||
parsedResult: {
|
||||
...DEFAULT_PAGINATION,
|
||||
sortBy: 'total:asc',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'valid skip and take',
|
||||
request: {
|
||||
skip: '0',
|
||||
take: '20',
|
||||
},
|
||||
parsedResult: {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'full query parameters',
|
||||
request: {
|
||||
skip: '0',
|
||||
take: '10',
|
||||
sortBy: 'total:desc',
|
||||
},
|
||||
parsedResult: {
|
||||
skip: 0,
|
||||
take: 10,
|
||||
sortBy: 'total:desc',
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request, parsedResult }) => {
|
||||
const result = ListInsightsWorkflowQueryDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
if (parsedResult) {
|
||||
expect(result.data).toMatchObject(parsedResult);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid skip format',
|
||||
request: {
|
||||
skip: 'not-a-number',
|
||||
take: '10',
|
||||
},
|
||||
expectedErrorPath: ['skip'],
|
||||
},
|
||||
{
|
||||
name: 'invalid take format',
|
||||
request: {
|
||||
skip: '0',
|
||||
take: 'not-a-number',
|
||||
},
|
||||
expectedErrorPath: ['take'],
|
||||
},
|
||||
{
|
||||
name: 'invalid sortBy value',
|
||||
request: {
|
||||
sortBy: 'invalid-value',
|
||||
},
|
||||
expectedErrorPath: ['sortBy'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = ListInsightsWorkflowQueryDto.safeParse(request);
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
const VALID_SORT_OPTIONS = [
|
||||
'total:asc',
|
||||
'total:desc',
|
||||
'succeeded:asc',
|
||||
'succeeded:desc',
|
||||
'failed:asc',
|
||||
'failed:desc',
|
||||
'timeSaved:asc',
|
||||
'timeSaved:desc',
|
||||
'runTime:asc',
|
||||
'runTime:desc',
|
||||
'averageRunTime:asc',
|
||||
'averageRunTime:desc',
|
||||
] as const;
|
||||
|
||||
// ---------------------
|
||||
// Parameter Validators
|
||||
// ---------------------
|
||||
|
||||
// Skip parameter validation
|
||||
const skipValidator = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? parseInt(val, 10) : 0))
|
||||
.refine((val) => !isNaN(val), {
|
||||
message: 'Skip must be a valid number',
|
||||
});
|
||||
|
||||
// Take parameter validation
|
||||
const takeValidator = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? parseInt(val, 10) : 10))
|
||||
.refine((val) => !isNaN(val), {
|
||||
message: 'Take must be a valid number',
|
||||
});
|
||||
|
||||
// SortBy parameter validation
|
||||
const sortByValidator = z
|
||||
.enum(VALID_SORT_OPTIONS, { message: `sortBy must be one of: ${VALID_SORT_OPTIONS.join(', ')}` })
|
||||
.optional();
|
||||
|
||||
export class ListInsightsWorkflowQueryDto extends Z.class({
|
||||
skip: skipValidator,
|
||||
take: takeValidator,
|
||||
sortBy: sortByValidator,
|
||||
}) {}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { insightsSummarySchema } from '../insights.schema';
|
||||
import {
|
||||
insightsByTimeSchema,
|
||||
insightsByWorkflowSchema,
|
||||
insightsSummarySchema,
|
||||
} from '../insights.schema';
|
||||
|
||||
describe('insightsSummarySchema', () => {
|
||||
test.each([
|
||||
@@ -62,3 +66,162 @@ describe('insightsSummarySchema', () => {
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insightsByWorkflowSchema', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'valid workflow insights',
|
||||
value: {
|
||||
count: 2,
|
||||
data: [
|
||||
{
|
||||
workflowId: 'w1',
|
||||
workflowName: 'Test Workflow',
|
||||
projectId: 'p1',
|
||||
projectName: 'Test Project',
|
||||
total: 100,
|
||||
succeeded: 90,
|
||||
failed: 10,
|
||||
failureRate: 0.56,
|
||||
runTime: 300,
|
||||
averageRunTime: 30.5,
|
||||
timeSaved: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'wrong data type',
|
||||
value: {
|
||||
count: 2,
|
||||
data: [
|
||||
{
|
||||
workflowId: 'w1',
|
||||
total: '100',
|
||||
succeeded: 90,
|
||||
failed: 10,
|
||||
failureRate: 10,
|
||||
runTime: 300,
|
||||
averageRunTime: 30,
|
||||
timeSaved: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'missing required field',
|
||||
value: {
|
||||
count: 2,
|
||||
data: [
|
||||
{
|
||||
workflowId: 'w1',
|
||||
total: 100,
|
||||
succeeded: 90,
|
||||
failed: 10,
|
||||
failureRate: 10,
|
||||
runTime: 300,
|
||||
averageRunTime: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'unexpected key',
|
||||
value: {
|
||||
count: 2,
|
||||
data: [
|
||||
{
|
||||
workflowId: 'w1',
|
||||
total: 100,
|
||||
succeeded: 90,
|
||||
failed: 10,
|
||||
failureRate: 10,
|
||||
runTime: 300,
|
||||
averageRunTime: 30,
|
||||
timeSaved: 50,
|
||||
extraKey: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = insightsByWorkflowSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insightsByTimeSchema', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'valid insights by time',
|
||||
value: {
|
||||
date: '2025-03-25T10:34:36.484Z',
|
||||
values: {
|
||||
total: 200,
|
||||
failureRate: 10,
|
||||
averageRunTime: 40,
|
||||
timeSaved: 100,
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'invalid date format',
|
||||
value: {
|
||||
date: '20240325', // Should be a string
|
||||
values: {
|
||||
total: 200,
|
||||
failureRate: 10,
|
||||
averageRunTime: 40,
|
||||
timeSaved: 100,
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'invalid field type',
|
||||
value: {
|
||||
date: 20240325, // Should be a string
|
||||
values: {
|
||||
total: 200,
|
||||
failureRate: 10,
|
||||
averageRunTime: 40,
|
||||
timeSaved: 100,
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'missing required key',
|
||||
value: {
|
||||
date: '2025-03-25T10:34:36.484Z',
|
||||
values: {
|
||||
total: 200,
|
||||
failureRate: 10,
|
||||
averageRunTime: 40,
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'unexpected key',
|
||||
value: {
|
||||
date: '2025-03-25T10:34:36.484Z',
|
||||
values: {
|
||||
total: 200,
|
||||
failureRate: 10,
|
||||
averageRunTime: 40,
|
||||
extraKey: 'value',
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = insightsByTimeSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,3 +42,44 @@ export const insightsSummaryDataSchemas = {
|
||||
|
||||
export const insightsSummarySchema = z.object(insightsSummaryDataSchemas).strict();
|
||||
export type InsightsSummary = z.infer<typeof insightsSummarySchema>;
|
||||
|
||||
export const insightsByWorkflowDataSchemas = {
|
||||
count: z.number(),
|
||||
data: z.array(
|
||||
z
|
||||
.object({
|
||||
workflowId: z.string(),
|
||||
workflowName: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
projectName: z.string().optional(),
|
||||
total: z.number(),
|
||||
succeeded: z.number(),
|
||||
failed: z.number(),
|
||||
failureRate: z.number(),
|
||||
runTime: z.number(),
|
||||
averageRunTime: z.number(),
|
||||
timeSaved: z.number(),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
} as const;
|
||||
|
||||
export const insightsByWorkflowSchema = z.object(insightsByWorkflowDataSchemas).strict();
|
||||
export type InsightsByWorkflow = z.infer<typeof insightsByWorkflowSchema>;
|
||||
|
||||
export const insightsByTimeDataSchemas = {
|
||||
date: z.string().refine((val) => !isNaN(Date.parse(val)) && new Date(val).toISOString() === val, {
|
||||
message: 'Invalid date format, must be ISO 8601 format',
|
||||
}),
|
||||
values: z
|
||||
.object({
|
||||
total: z.number(),
|
||||
failureRate: z.number(),
|
||||
averageRunTime: z.number(),
|
||||
timeSaved: z.number(),
|
||||
})
|
||||
.strict(),
|
||||
} as const;
|
||||
|
||||
export const insightsByTimeSchema = z.object(insightsByTimeDataSchemas).strict();
|
||||
export type InsightsByTime = z.infer<typeof insightsByTimeSchema>;
|
||||
|
||||
Reference in New Issue
Block a user