@mee4dy/crud-nestjs
v1.0.24
Published
Модуль для NestJS, предоставляющий декларативный и расширяемый CRUD-контроллер с минимальной конфигурацией. Позволяет быстро создавать REST API для любых моделей с поддержкой фильтрации, сортировки, пагинации, скопов и гибкой настройки.
Downloads
148
Readme
@mee4dy/crud-nestjs
Модуль для NestJS, предоставляющий декларативный и расширяемый CRUD-контроллер с минимальной конфигурацией. Позволяет быстро создавать REST API для любых моделей с поддержкой фильтрации, сортировки, пагинации, скопов и гибкой настройки.
Преимущества
- Декларативный подход через декоратор
@Crud - Не требует создания сервисов и наследования
- Гибкая настройка разрешённых параметров (allowedParams)
- Поддержка scopes (динамические параметры по каждому эндпоинту)
- Единый формат ответа и ошибок
- Лёгкая интеграция с клиентом (@mee4dy/crud-client)
- Расширяемость и чистый код
Установка
npm install @mee4dy/crud-nestjsБыстрый старт
- Импортируйте пакет и используйте декоратор
@Crudв контроллере:
import { Controller } from '@nestjs/common';
import { Crud } from '@mee4dy/crud-nestjs';
import { Post } from './posts.model';
@Crud({
repository: () => Posts,
})
@Controller('posts')
export class PostsController {}- Подключите модель к SequelizeModule в модуле:
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { PostsController } from './posts.controller';
import { Posts } from './posts.model';
@Module({
imports: [SequelizeModule.forFeature([Posts])],
controllers: [PostsController],
})
export class PostsModule {}Структура опций для декоратора @Crud
type CrudDecoratorConfig = {
/** Функция, возвращающая модель Sequelize для работы с БД */
repository: () => any;
/** Имя поля первичного ключа (по умолчанию 'id') */
pk?: string;
/** Включение/отключение CRUD-методов */
endpoints?: {
items?: boolean; // GET /items - получение списка
item?: boolean; // GET /items/:pk - получение одного элемента
create?: boolean; // POST /items - создание
update?: boolean; // PUT /items/:pk - обновление
delete?: boolean; // DELETE /items/:pk - удаление
};
/** Настройка разрешённых параметров из query string */
query?: {
allowedParams?: {
filters?: string[] | boolean; // Разрешённые поля для фильтрации
orders?: string[] | boolean; // Разрешённые поля для сортировки
groups?: string[] | boolean; // Разрешённые поля для группировки
limit?: boolean; // Разрешить пагинацию (limit)
offset?: boolean; // Разрешить пагинацию (offset)
fields?: string[]; // Разрешённые поля для выборки
fieldsExclude?: string[]; // Поля для исключения из выборки
};
};
/** Параметры по умолчанию для всех запросов */
params?: {
default?: Partial<CrudParams>;
};
/** Динамические параметры для каждого эндпоинта */
scopes?: {
global?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Применяется ко всем эндпоинтам
items?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для получения списка
item?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для получения одного элемента
create?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для создания
update?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для обновления
delete?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для удаления
};
};Тип CrudParams
CrudParams — это основной тип для описания параметров запроса к базе данных.
interface CrudParams {
filters?: CrudParamsFilter; // Фильтры для WHERE условий
join?: CrudJoin[]; // Join связи
fields?: CrudFields; // Настройка выборки полей
orders?: CrudOrder[]; // Сортировка (ORDER BY)
groups?: string[]; // Группировка (GROUP BY)
limit?: number; // Лимит записей
offset?: number; // Смещение для пагинации
repositoryParams?: Partial<FindOptions>; // Прямые параметры Sequelize
repository?: () => any; // Динамическая смена репозитория со scopes
}Фильтры (filters)
type CrudParamsFilter = {
[field: string]: CrudFieldFilter;
};
type CrudFieldFilter =
| string // Простое равенство: { user_id: "123" }
| number // Простое равенство: { status: 1 }
| boolean // Простое равенство: { active: true }
| { op: 'eq'; value: string | number | boolean } // Явное равенство
| { op: 'like'; value: string } // LIKE поиск
| { op: 'range' | 'period'; from: number | string; to: number | string }; // Диапазон значенийПримеры фильтров:
// В коде:
const filters = {
user_id: 1, // Простое равенство
title: { op: 'like', value: '%test%' }, // LIKE поиск
created_at: { op: 'range', from: '2024-01-01', to: '2024-12-31' }, // Диапазон дат
status: { op: 'eq', value: 'active' } // Явное равенство
};
// HTTP запрос:
GET /posts?filters[user_id]=1&filters[title][op]=like&filters[title][value]=test&filters[created_at][op]=range&filters[created_at][from]=2024-01-01&filters[created_at][to]=2024-12-31Пример объединения условий:
// CRUD фильтры + repositoryParams
const params: CrudParams = {
filters: { user_id: 1, status: 'active' },
repositoryParams: {
where: {
created_at: {
[Op.lte]: '2025-07-28 12:00:00',
},
},
},
};
// Результат объединения:
// WHERE user_id = 1 AND status = 'active' AND created_at <= '2025-07-28 12:00:00'Сортировка (orders)
type CrudOrder = [string, 'asc' | 'desc']; // [поле, направление]
// В коде:
const orders = [
['created_at', 'desc'], // Сначала новые
['title', 'asc'], // По алфавиту
['id', 'desc'] // По ID убывание
];
// HTTP запрос:
GET /posts?orders[0][0]=created_at&orders[0][1]=desc&orders[1][0]=title&orders[1][1]=ascГруппировка (groups)
// В коде:
const groups = ['user_id', 'status'];
// HTTP запрос:
GET /posts?groups[0]=user_id&groups[1]=statusПагинация (limit, offset)
// В коде:
const limit = 20; // Количество записей на странице
const offset = 40; // Пропустить первые 40 записей (для 3-й страницы)
// HTTP запрос:
GET /posts?limit=20&offset=40Выборка полей (fields)
type CrudFields = {
include?: [Literal, CrudField][]; // Дополнительные SQL выражения
exclude?: CrudField[]; // Исключаемые поля
aggregate?: [Literal, CrudField][]; // Агрегационные поля (для группировки)
};Примеры использования:
// Обычная выборка полей
const fields = {
include: [
[Sequelize.literal('CONCAT(name, " (", email, ")")'), 'full_info'],
[Sequelize.literal('COUNT(*)'), 'total_count'],
],
exclude: ['password', 'deleted_at'],
};
// Агрегационные поля для группировки
const fields = {
aggregate: [
[Sequelize.col('name'), 'name'],
[Sequelize.col('type'), 'type'],
],
include: [
[Sequelize.literal('COUNT(*)'), 'total_count'],
[Sequelize.literal('MAX(created_at)'), 'last_created'],
],
};
// Автоматическое исключение при группировке
const params: CrudParams = {
groups: ['name', 'type'],
// Автоматически исключатся все поля кроме name и type
};Особенности работы с группировкой:
- Автоматическое исключение - при наличии
groupsавтоматически исключаются все поля модели, которые не участвуют в группировке - Агрегационные поля - поле
aggregateпозволяет явно указать поля для группировки, игнорируя автоматическое исключение - Приоритет aggregate - если указано
aggregate, автоматическое исключение не применяется
Примеры SQL запросов:
// С группировкой без override
const params: CrudParams = {
groups: ['name', 'type'],
fields: {
include: [[Sequelize.literal('COUNT(*)'), 'total']],
},
};
// SQL: SELECT name, type, COUNT(*) as total FROM table GROUP BY name, type
// С группировкой и aggregate
const params: CrudParams = {
groups: ['name', 'type'],
fields: {
aggregate: [
[Sequelize.col('name'), 'name'],
[Sequelize.col('type'), 'type'],
],
include: [
[Sequelize.literal('COUNT(*)'), 'total'],
[Sequelize.literal('MAX(date)'), 'last_date'],
],
},
};
// SQL: SELECT name, type, COUNT(*) as total, MAX(date) as last_date FROM table GROUP BY name, typeСвязи (join)
type CrudJoin = {
repository: () => any; // Функция, возвращающая модель Sequelize
fields?: CrudFields; // Настройка полей для связанной модели
join?: CrudJoin[]; // Вложенные join для связанной модели
};Примеры join связей:
Особенности настройки полей в join:
fields: CrudFields- настройка полей для связанной моделиfields: false- отключить все поля из join (только для фильтрации)fields: undefined- использовать все поля по умолчанию
// Простой join с пользователем
const join = [
{
repository: () => User,
fields: {
include: [[Sequelize.literal('CONCAT(name, " (", email, ")")'), 'full_info']],
exclude: ['password', 'deleted_at'],
},
},
];
// Join без полей (только для фильтрации)
const join = [
{
repository: () => User,
fields: false, // Отключить все поля из join
},
];
// Вложенный join: посты с комментариями и пользователями комментариев
const join = [
{
repository: () => Comment,
fields: {
include: [[Sequelize.literal('CONCAT("[", id, "] ", text)'), 'formatted_text']],
},
join: [
{
repository: () => User,
fields: {
exclude: ['password', 'deleted_at'],
},
},
],
},
];
// Множественные join
const join = [
{
repository: () => User,
fields: {
exclude: ['password'],
},
},
{
repository: () => Category,
fields: {
include: [[Sequelize.literal('UPPER(name)'), 'upper_name']],
},
},
];
// Join только для фильтрации (без полей в результате)
const join = [
{
repository: () => User,
fields: false, // Не включать поля пользователя в результат
},
{
repository: () => Category,
fields: {
include: [[Sequelize.literal('UPPER(name)'), 'category_name']],
},
},
];
// Результат: только поля поста + category_name, без полей пользователяПрямые параметры Sequelize (repositoryParams)
Поле repositoryParams позволяет передавать любые параметры напрямую в Sequelize для формирования сложных кастомных запросов.
// В коде:
const repositoryParams = {
subQuery: false,
distinct: true,
lock: true,
paranoid: false,
// любые другие параметры Sequelize FindOptions
};Примеры использования:
// Отключение soft delete для конкретного запроса
const params: CrudParams = {
filters: { user_id: 1 },
repositoryParams: {
paranoid: false,
},
};
// Использование блокировки для критических операций
const params: CrudParams = {
filters: { id: 123 },
repositoryParams: {
lock: true,
},
};
// Сложные запросы с подзапросами
const params: CrudParams = {
repositoryParams: {
subQuery: false,
distinct: true,
attributes: {
include: [
[Sequelize.literal('(SELECT COUNT(*) FROM comments WHERE comments.post_id = Posts.id)'), 'comments_count'],
],
},
},
};
// Результат: Объединяется с любыми существующими фильтрами и атрибутамиВажно: Параметры из repositoryParams объединяются с параметрами, сгенерированными из других полей CrudParams, используя глубокое слияние объектов. Это позволяет комбинировать CRUD фильтры с прямыми Sequelize условиями.
Примеры контроллеров
Минимальный контроллер
@Crud({ repository: () => Users })
@Controller('users')
export class UsersController {}Контроллер с ограничением параметров
@Crud({
repository: () => Posts,
query: {
allowedParams: {
filters: ['id', 'user_id'],
orders: ['id', 'created_at'],
limit: true,
offset: true,
},
},
})
@Controller('posts')
export class PostsController {}Контроллер с default params и scopes по эндпоинтам
@Crud({
repository: () => Comments,
params: {
default: {
orders: [['id', 'desc']],
limit: 10,
},
},
scopes: {
global: (req) => ({
params: {
filters: {
deleted_at: null,
},
},
}),
items: (req) => ({
params: {
filters: {
user_id: req.user.id,
},
},
}),
create: (req) => ({
params: {
filters: {
user_id: req.user.id,
},
},
}),
},
})
@Controller('comments')
export class CommentsController {}Контроллер с join связями
@Crud({
repository: () => Posts,
params: {
default: {
join: [
{
repository: () => Users,
fields: {
exclude: ['password', 'deleted_at'],
},
},
],
},
},
scopes: {
item: (req) => ({
params: {
join: [
{
repository: () => Comments,
fields: {
include: [[Sequelize.literal('CONCAT("[", id, "] ", text)'), 'formatted_text']],
},
join: [
{
repository: () => Users,
fields: {
exclude: ['password'],
},
},
],
},
],
},
}),
},
})
@Controller('posts')
export class PostsController {}Контроллер с Sequelize scopes
@Crud({
repository: () => Posts.scope('active'),
})
@Controller('posts')
export class PostsController {}
// Несколько scopes
@Crud({
repository: () => Posts.scope(['active', 'withUser', 'withComments']),
})
@Controller('posts')
export class PostsController {}
// Динамические scopes через params
@Crud({
repository: () => Posts.scope('active'),
scopes: {
items: (req) => ({
params: {
repository: () => Posts.scope(['published', 'withComments']),
},
}),
item: (req) => ({
params: {
repository: () => Posts.scope(['withUser', 'withComments', 'withLikes']),
},
}),
},
})
@Controller('posts')
export class PostsController {}Формат ответа
Все ответы возвращаются в едином формате:
{
"status": true,
"data": {
"items": [ ... ],
"item": { ... }
}
}В случае ошибки:
{
"status": false,
"error": {
"message": "...",
"statusCode": 400,
"errorType": "..."
}
}Обработка ошибок
- Все ошибки автоматически форматируются через фильтр
CrudExceptionFilter - Для отключённых эндпоинтов выбрасывается
EndpointDisabledException
Интеграция с клиентом
Пакет полностью совместим с @mee4dy/crud-client для фронтенда.
Совместимость
- NestJS >= 9
- Sequelize >= 6
- Node.js >= 16
Лицензия
MIT
