Grapeoff
@Grapeoff
В чём концепция...?

Существует ли более удобный способ построить aggregation pipeline для фильтрации?

У в нашей CRM системе есть возможность сортировать учеников по различным параметрам, таким как: возраст, пол, имя, фамилия, отчество, учителя, группы.

И на данный момент у всего этого такая реализация: клиент отправляет на сервер параметры фильтрации в формате такого DTO

export class FilterDTO {
    @IsOptional()
    @IsString({ each: true })
    names?: string[];

    @IsOptional()
    @IsString({ each: true })
    surnames?: string[];

    @IsOptional()
    @IsString({ each: true })
    midnames?: string[];

    @IsOptional()
    @IsArray()
    ages?: number[];

    @IsOptional()
    @IsString({ each: true })
    gender?: string[];

    @IsOptional()
    @IsString({ each: true })
    groups?: string[];

    @IsOptional()
    @IsString({ each: true })
    tutors?: string[];

    @IsOptional()
    @IsObject()
    balance?: { $gte?: number; $lte?: number; $lt?: number };

    @IsOptional()
    @IsBoolean()
    emptyAge?: boolean;

    @IsOptional()
    @IsString({ each: true })
    @IsMongoId({ each: true })
    statuses?: string[];
}

И когда все эти параметры прилетают на сервер, отдельная огромная функция строит aggregation pipeline

private createFilterPipeline(filters: FilterDTO): any[] {
        if (!filters) return;

        const pipeline = [];

        if (filters.names) {
            const nameFilter = {
                $match: {
                    $or: []
                }
            };

            filters.names.forEach(name => {
                nameFilter.$match.$or.push({
                    name: { $regex: new RegExp(`${name}`, 'i') }
                });
            });

            pipeline.push(nameFilter);
        }

        if (filters.surnames) {
            const surnameFilter = {
                $match: {
                    $or: []
                }
            };

            filters.surnames.forEach(surname => {
                surnameFilter.$match.$or.push({
                    surname: { $regex: new RegExp(`${surname}`, 'i') }
                });
            });

            pipeline.push(surnameFilter);
        }

        if (filters.midnames) {
            const midnameFilter = {
                $match: {
                    $or: []
                }
            };

            filters.midnames.forEach(midname => {
                midnameFilter.$match.$or.push({
                    midname: { $regex: new RegExp(`${midname}`, 'i') }
                });
            });

            pipeline.push(midnameFilter);
        }

        if (filters.balance) {
            if (filters.balance.$lte) {
                pipeline.push({
                    $match: {
                        $and: [
                            {
                                balance: {
                                    $gte: filters.balance.$gte
                                }
                            },
                            {
                                balance: {
                                    $lte: filters.balance.$lte
                                }
                            }
                        ]
                    }
                });
            } else {
                pipeline.push({
                    $match: {
                        $or: [
                            {
                                balance: {
                                    $gte: filters.balance.$gte
                                }
                            },
                            {
                                balance: {
                                    $lt: filters.balance.$lt
                                }
                            }
                        ]
                    }
                });
            }
        }

        if (filters.gender) {
            pipeline.push({
                $match: {
                    gender: {
                        $in: filters.gender
                    }
                }
            });
        }

        if (filters.groups) {
            pipeline.push({
                $match: {
                    groups: {
                        $all: filters.groups
                    }
                }
            });
        }

        if (filters.ages) {
            const agesFilter = {
                $match: {
                    $or: []
                }
            };

            if (filters.emptyAge === true) {
                agesFilter.$match.$or.push({ dateOfBirth: null });
            }

            filters?.ages?.forEach(age => {
                agesFilter.$match.$or.push({
                    dateOfBirth: {
                        $gte: new Date(
                            moment()
                                .subtract(age + 1, 'years')
                                .add(1, 'day')
                                .toISOString()
                        ),
                        $lt: new Date(
                            moment().subtract(age, 'years').toISOString()
                        )
                    }
                });
            });

            pipeline.push(agesFilter);
        } else if (!filters.ages && filters.emptyAge === true) {
            pipeline.push({ $match: { dateOfBirth: null } });
        }

        if (filters.tutors) {
            pipeline.push({
                $match: {
                    'tutors.tutor': { $in: filters.tutors }
                }
            });
        }

        if (filters.statuses) {
            pipeline.push({
                $match: {
                    statuses: {
                        $all: filters.statuses.map(status =>
                            Types.ObjectId(status)
                        )
                    }
                }
            });
        }

        return pipeline;
    }
}

Такой подход крайне не удобен, но на момент написания этого метода, ничего лучше я не придумал/не нашёл.

Сейчас проект находится в состоянии рефакторинга, и я очень хочу убрать эту функцию и заменить её на что-нибудь более лаконичное. Какие варианты правильной реализации подобного поведения существуют? Может быть есть какие-то паттерны проектирования? Тут, если я правильно понимаю, очень напрашивается builder, но как тогда организовать приём данных для сортировки с клиента?
  • Вопрос задан
  • 232 просмотра
Пригласить эксперта
Ответы на вопрос 1
Zraza
@Zraza
Помог ответ? Отметь решением!
Ну, можно это все немного обобщить - уберем тьму if'ов
Т.е. у нас есть ​набор фильтров с произвольными параметрами (для каждого фильтра свой)
А на выходе мы ходим получить итоговый запрос к БД
Можно правила обработки фильтров описать в виде объекта, где ключ - это имя фильтра, а значение - коллбэк, который возвращает элемент этого pipeline.

Что-то типа
const filters = {
  groups: (groups) => ({
                $match: {
                    groups: {
                        $all: groups
                    }
                }
            })
...
}

Далее проходимся циклом по всем заданным фильтрам, последовательно применяя коллбэки из объекта.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы