mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +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 { UpdateFolderDto } from './folders/update-folder.dto';
|
||||||
export { DeleteFolderDto } from './folders/delete-folder.dto';
|
export { DeleteFolderDto } from './folders/delete-folder.dto';
|
||||||
export { ListFolderQueryDto } from './folders/list-folder-query.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', () => {
|
describe('insightsSummarySchema', () => {
|
||||||
test.each([
|
test.each([
|
||||||
@@ -62,3 +66,162 @@ describe('insightsSummarySchema', () => {
|
|||||||
expect(result.success).toBe(expected);
|
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 const insightsSummarySchema = z.object(insightsSummaryDataSchemas).strict();
|
||||||
export type InsightsSummary = z.infer<typeof insightsSummarySchema>;
|
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