feat(core): Add filtering, selection and pagination to users (#6994)
https://linear.app/n8n/issue/PAY-646
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user