feat(core): Add filtering, selection and pagination to users (#6994)

https://linear.app/n8n/issue/PAY-646
This commit is contained in:
Iván Ovejero
2023-08-28 16:13:17 +02:00
committed by GitHub
parent a7785b2c5d
commit b716241b42
23 changed files with 535 additions and 211 deletions

View File

@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { isObjectLiteral } from '@/utils';
import { plainToInstance, instanceToPlain } from 'class-transformer';
import { validate } from 'class-validator';
import { jsonParse } from 'n8n-workflow';
export class BaseFilter {
protected static async toFilter(rawFilter: string, Filter: typeof BaseFilter) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isObjectLiteral(dto)) throw new Error('Filter must be an object literal');
const instance = plainToInstance(Filter, dto, {
excludeExtraneousValues: true, // remove fields not in class
});
await instance.validate();
return instanceToPlain(instance, {
exposeUnsetFields: false, // remove in-class undefined fields
});
}
private async validate() {
const result = await validate(this);
if (result.length > 0) throw new Error('Parsed filter does not fit the schema');
}
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { isStringArray } from '@/utils';
import { jsonParse } from 'n8n-workflow';
export class BaseSelect {
static selectableFields: Set<string>;
protected static toSelect(rawFilter: string, Select: typeof BaseSelect) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isStringArray(dto)) throw new Error('Parsed select is not a string array');
return dto.reduce<Record<string, true>>((acc, field) => {
if (!Select.selectableFields.has(field)) return acc;
return (acc[field] = true), acc;
}, {});
}
}

View File

@@ -0,0 +1,29 @@
import { IsOptional, IsString, IsBoolean } from 'class-validator';
import { Expose } from 'class-transformer';
import { BaseFilter } from './base.filter.dto';
export class UserFilter extends BaseFilter {
@IsString()
@IsOptional()
@Expose()
email?: string;
@IsString()
@IsOptional()
@Expose()
firstName?: string;
@IsString()
@IsOptional()
@Expose()
lastName?: string;
@IsBoolean()
@IsOptional()
@Expose()
isOwner?: boolean;
static async fromString(rawFilter: string) {
return this.toFilter(rawFilter, UserFilter);
}
}

View File

@@ -0,0 +1,11 @@
import { BaseSelect } from './base.select.dto';
export class UserSelect extends BaseSelect {
static get selectableFields() {
return new Set(['id', 'email', 'firstName', 'lastName']);
}
static fromString(rawFilter: string) {
return this.toSelect(rawFilter, UserSelect);
}
}

View File

@@ -1,9 +1,9 @@
import { IsOptional, IsString, IsBoolean, IsArray, validate } from 'class-validator';
import { Expose, instanceToPlain, plainToInstance } from 'class-transformer';
import { jsonParse } from 'n8n-workflow';
import { isObjectLiteral } from '@/utils';
import { IsOptional, IsString, IsBoolean, IsArray } from 'class-validator';
import { Expose } from 'class-transformer';
export class WorkflowFilter {
import { BaseFilter } from './base.filter.dto';
export class WorkflowFilter extends BaseFilter {
@IsString()
@IsOptional()
@Expose()
@@ -21,23 +21,6 @@ export class WorkflowFilter {
tags?: string[];
static async fromString(rawFilter: string) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isObjectLiteral(dto)) throw new Error('Filter must be an object literal');
const instance = plainToInstance(WorkflowFilter, dto, {
excludeExtraneousValues: true, // remove fields not in class
exposeUnsetFields: false, // remove in-class undefined fields
});
await instance.validate();
return instanceToPlain(instance);
}
private async validate() {
const result = await validate(this);
if (result.length > 0) throw new Error('Parsed filter does not fit the schema');
return this.toFilter(rawFilter, WorkflowFilter);
}
}

View File

@@ -1,9 +1,6 @@
import { isStringArray } from '@/utils';
import { jsonParse } from 'n8n-workflow';
export class WorkflowSelect {
fields: string[];
import { BaseSelect } from './base.select.dto';
export class WorkflowSelect extends BaseSelect {
static get selectableFields() {
return new Set([
'id', // always included downstream
@@ -18,13 +15,6 @@ export class WorkflowSelect {
}
static fromString(rawFilter: string) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isStringArray(dto)) throw new Error('Parsed select is not a string array');
return dto.reduce<Record<string, true>>((acc, field) => {
if (!WorkflowSelect.selectableFields.has(field)) return acc;
return (acc[field] = true), acc;
}, {});
return this.toSelect(rawFilter, WorkflowSelect);
}
}

View File

@@ -2,6 +2,7 @@
import * as ResponseHelper from '@/ResponseHelper';
import { WorkflowFilter } from './dtos/workflow.filter.dto';
import { UserFilter } from './dtos/user.filter.dto';
import { toError } from '@/utils';
import type { NextFunction, Response } from 'express';
@@ -20,6 +21,8 @@ export const filterListQueryMiddleware = async (
if (req.baseUrl.endsWith('workflows')) {
Filter = WorkflowFilter;
} else if (req.baseUrl.endsWith('users')) {
Filter = UserFilter;
} else {
return next();
}

View File

@@ -11,9 +11,13 @@ export const paginationListQueryMiddleware: RequestHandler = (
) => {
const { take: rawTake, skip: rawSkip = '0' } = req.query;
if (!rawTake) return next();
try {
if (!rawTake && req.query.skip) {
throw new Error('Please specify `take` when using `skip`');
}
if (!rawTake) return next();
const { take, skip } = Pagination.fromString(rawTake, rawSkip);
req.listQueryOptions = { ...req.listQueryOptions, skip, take };

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { WorkflowSelect } from './dtos/workflow.select.dto';
import { UserSelect } from './dtos/user.select.dto';
import * as ResponseHelper from '@/ResponseHelper';
import { toError } from '@/utils';
@@ -16,6 +17,8 @@ export const selectListQueryMiddleware: RequestHandler = (req: ListQuery.Request
if (req.baseUrl.endsWith('workflows')) {
Select = WorkflowSelect;
} else if (req.baseUrl.endsWith('users')) {
Select = UserSelect;
} else {
return next();
}