@mee4dy/crud-nestjs 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +5 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/crud.controller.d.ts +22 -0
- package/dist/crud.controller.js +134 -0
- package/dist/crud.controller.js.map +1 -0
- package/dist/crud.service.d.ts +15 -0
- package/dist/crud.service.js +55 -0
- package/dist/crud.service.js.map +1 -0
- package/dist/decorators/crud-params.decorator.d.ts +5 -0
- package/dist/decorators/crud-params.decorator.js +53 -0
- package/dist/decorators/crud-params.decorator.js.map +1 -0
- package/dist/decorators/crud.decorator.d.ts +3 -0
- package/dist/decorators/crud.decorator.js +77 -0
- package/dist/decorators/crud.decorator.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +19 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/exceptions/endpoint-disabled.exception.d.ts +4 -0
- package/dist/exceptions/endpoint-disabled.exception.js +14 -0
- package/dist/exceptions/endpoint-disabled.exception.js.map +1 -0
- package/dist/filters/crud-exception.filter.d.ts +4 -0
- package/dist/filters/crud-exception.filter.js +48 -0
- package/dist/filters/crud-exception.filter.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/services/crud-params-adapter.service.d.ts +14 -0
- package/dist/services/crud-params-adapter.service.js +147 -0
- package/dist/services/crud-params-adapter.service.js.map +1 -0
- package/dist/services/query-params-extractor.service.d.ts +14 -0
- package/dist/services/query-params-extractor.service.js +112 -0
- package/dist/services/query-params-extractor.service.js.map +1 -0
- package/dist/types/crud-config.types.d.ts +25 -0
- package/dist/types/crud-config.types.js +3 -0
- package/dist/types/crud-config.types.js.map +1 -0
- package/dist/types/crud-params.types.d.ts +40 -0
- package/dist/types/crud-params.types.js +3 -0
- package/dist/types/crud-params.types.js.map +1 -0
- package/dist/types/crud-query.types.d.ts +16 -0
- package/dist/types/crud-query.types.js +3 -0
- package/dist/types/crud-query.types.js.map +1 -0
- package/dist/types/crud-response.types.d.ts +9 -0
- package/dist/types/crud-response.types.js +3 -0
- package/dist/types/crud-response.types.js.map +1 -0
- package/dist/utils/merge.util.d.ts +1 -0
- package/dist/utils/merge.util.js +45 -0
- package/dist/utils/merge.util.js.map +1 -0
- package/dist/utils/replace-pk.util.d.ts +1 -0
- package/dist/utils/replace-pk.util.js +25 -0
- package/dist/utils/replace-pk.util.js.map +1 -0
- package/package.json +34 -0
- package/readme.md +426 -0
package/readme.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# @mee4dy/crud-nestjs
|
|
2
|
+
|
|
3
|
+
Модуль для NestJS, предоставляющий декларативный и расширяемый CRUD-контроллер с минимальной конфигурацией. Позволяет быстро создавать REST API для любых моделей с поддержкой фильтрации, сортировки, пагинации, скопов и гибкой настройки.
|
|
4
|
+
|
|
5
|
+
## Преимущества
|
|
6
|
+
|
|
7
|
+
- Декларативный подход через декоратор `@Crud`
|
|
8
|
+
- Не требует создания сервисов и наследования
|
|
9
|
+
- Гибкая настройка разрешённых параметров (allowedParams)
|
|
10
|
+
- Поддержка scopes (динамические параметры по каждому эндпоинту)
|
|
11
|
+
- Единый формат ответа и ошибок
|
|
12
|
+
- Лёгкая интеграция с клиентом (@mee4dy/crud-client)
|
|
13
|
+
- Расширяемость и чистый код
|
|
14
|
+
|
|
15
|
+
## Установка
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @mee4dy/crud-nestjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Быстрый старт
|
|
22
|
+
|
|
23
|
+
1. Импортируйте пакет и используйте декоратор `@Crud` в контроллере:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { Controller } from '@nestjs/common';
|
|
27
|
+
import { Crud } from '@mee4dy/crud-nestjs';
|
|
28
|
+
import { Post } from './posts.model';
|
|
29
|
+
|
|
30
|
+
@Crud({
|
|
31
|
+
repository: () => Posts,
|
|
32
|
+
})
|
|
33
|
+
@Controller('posts')
|
|
34
|
+
export class PostsController {}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. Подключите модель к SequelizeModule в модуле:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { Module } from '@nestjs/common';
|
|
41
|
+
import { SequelizeModule } from '@nestjs/sequelize';
|
|
42
|
+
import { PostsController } from './posts.controller';
|
|
43
|
+
import { Posts } from './posts.model';
|
|
44
|
+
|
|
45
|
+
@Module({
|
|
46
|
+
imports: [SequelizeModule.forFeature([Posts])],
|
|
47
|
+
controllers: [PostsController],
|
|
48
|
+
})
|
|
49
|
+
export class PostsModule {}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Структура опций для декоратора @Crud
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
type CrudDecoratorConfig = {
|
|
56
|
+
/** Функция, возвращающая модель Sequelize для работы с БД */
|
|
57
|
+
repository: () => any;
|
|
58
|
+
|
|
59
|
+
/** Имя поля первичного ключа (по умолчанию 'id') */
|
|
60
|
+
pk?: string;
|
|
61
|
+
|
|
62
|
+
/** Включение/отключение CRUD-методов */
|
|
63
|
+
endpoints?: {
|
|
64
|
+
items?: boolean; // GET /items - получение списка
|
|
65
|
+
item?: boolean; // GET /items/:pk - получение одного элемента
|
|
66
|
+
create?: boolean; // POST /items - создание
|
|
67
|
+
update?: boolean; // PUT /items/:pk - обновление
|
|
68
|
+
delete?: boolean; // DELETE /items/:pk - удаление
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Настройка разрешённых параметров из query string */
|
|
72
|
+
query?: {
|
|
73
|
+
allowedParams?: {
|
|
74
|
+
filters?: string[] | boolean; // Разрешённые поля для фильтрации
|
|
75
|
+
orders?: string[] | boolean; // Разрешённые поля для сортировки
|
|
76
|
+
groups?: string[] | boolean; // Разрешённые поля для группировки
|
|
77
|
+
limit?: boolean; // Разрешить пагинацию (limit)
|
|
78
|
+
offset?: boolean; // Разрешить пагинацию (offset)
|
|
79
|
+
fields?: string[]; // Разрешённые поля для выборки
|
|
80
|
+
fieldsExclude?: string[]; // Поля для исключения из выборки
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Параметры по умолчанию для всех запросов */
|
|
85
|
+
params?: {
|
|
86
|
+
default?: Partial<CrudParams>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Динамические параметры для каждого эндпоинта */
|
|
90
|
+
scopes?: {
|
|
91
|
+
global?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Применяется ко всем эндпоинтам
|
|
92
|
+
items?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для получения списка
|
|
93
|
+
item?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для получения одного элемента
|
|
94
|
+
create?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для создания
|
|
95
|
+
update?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для обновления
|
|
96
|
+
delete?: (req: any) => Partial<{ params: Partial<CrudParams> }>; // Только для удаления
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Тип CrudParams
|
|
102
|
+
|
|
103
|
+
`CrudParams` — это основной тип для описания параметров запроса к базе данных.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
interface CrudParams {
|
|
107
|
+
filters?: CrudParamsFilter; // Фильтры для WHERE условий
|
|
108
|
+
join?: CrudJoin[]; // Join связи
|
|
109
|
+
fields?: CrudFields; // Настройка выборки полей
|
|
110
|
+
orders?: CrudOrder[]; // Сортировка (ORDER BY)
|
|
111
|
+
groups?: string[]; // Группировка (GROUP BY)
|
|
112
|
+
limit?: number; // Лимит записей
|
|
113
|
+
offset?: number; // Смещение для пагинации
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Фильтры (filters)
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
type CrudParamsFilter = {
|
|
121
|
+
[field: string]: CrudFieldFilter;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
type CrudFieldFilter =
|
|
125
|
+
| string // Простое равенство: { user_id: "123" }
|
|
126
|
+
| number // Простое равенство: { status: 1 }
|
|
127
|
+
| boolean // Простое равенство: { active: true }
|
|
128
|
+
| { op: 'eq'; value: string | number | boolean } // Явное равенство
|
|
129
|
+
| { op: 'like'; value: string } // LIKE поиск
|
|
130
|
+
| { op: 'range' | 'period'; from: number | string; to: number | string }; // Диапазон значений
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Примеры фильтров:**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// В коде:
|
|
137
|
+
const filters = {
|
|
138
|
+
user_id: 1, // Простое равенство
|
|
139
|
+
title: { op: 'like', value: '%test%' }, // LIKE поиск
|
|
140
|
+
created_at: { op: 'range', from: '2024-01-01', to: '2024-12-31' }, // Диапазон дат
|
|
141
|
+
status: { op: 'eq', value: 'active' } // Явное равенство
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// HTTP запрос:
|
|
145
|
+
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
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Сортировка (orders)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
type CrudOrder = [string, 'asc' | 'desc']; // [поле, направление]
|
|
152
|
+
|
|
153
|
+
// В коде:
|
|
154
|
+
const orders = [
|
|
155
|
+
['created_at', 'desc'], // Сначала новые
|
|
156
|
+
['title', 'asc'], // По алфавиту
|
|
157
|
+
['id', 'desc'] // По ID убывание
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
// HTTP запрос:
|
|
161
|
+
GET /posts?orders[0][0]=created_at&orders[0][1]=desc&orders[1][0]=title&orders[1][1]=asc
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Группировка (groups)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// В коде:
|
|
168
|
+
const groups = ['user_id', 'status'];
|
|
169
|
+
|
|
170
|
+
// HTTP запрос:
|
|
171
|
+
GET /posts?groups[0]=user_id&groups[1]=status
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Пагинация (limit, offset)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// В коде:
|
|
178
|
+
const limit = 20; // Количество записей на странице
|
|
179
|
+
const offset = 40; // Пропустить первые 40 записей (для 3-й страницы)
|
|
180
|
+
|
|
181
|
+
// HTTP запрос:
|
|
182
|
+
GET /posts?limit=20&offset=40
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Выборка полей (fields)
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
type CrudFields = {
|
|
189
|
+
include?: [Literal, CrudField][]; // Дополнительные SQL выражения
|
|
190
|
+
exclude?: CrudField[]; // Исключаемые поля
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// В коде:
|
|
194
|
+
const fields = {
|
|
195
|
+
include: [
|
|
196
|
+
[Sequelize.literal('CONCAT(name, " (", email, ")")'), 'full_info'],
|
|
197
|
+
[Sequelize.literal('COUNT(*)'), 'total_count'],
|
|
198
|
+
],
|
|
199
|
+
exclude: ['password', 'deleted_at'],
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Связи (join)
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
type CrudJoin = {
|
|
207
|
+
repository: () => any; // Функция, возвращающая модель Sequelize
|
|
208
|
+
attributes?: CrudFields; // Настройка полей для связанной модели
|
|
209
|
+
join?: CrudJoin[]; // Вложенные join для связанной модели
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Примеры join связей:**
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Простой join с пользователем
|
|
217
|
+
const join = [
|
|
218
|
+
{
|
|
219
|
+
repository: () => User,
|
|
220
|
+
attributes: {
|
|
221
|
+
include: [[Sequelize.literal('CONCAT(name, " (", email, ")")'), 'full_info']],
|
|
222
|
+
exclude: ['password', 'deleted_at'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
// Вложенный join: посты с комментариями и пользователями комментариев
|
|
228
|
+
const join = [
|
|
229
|
+
{
|
|
230
|
+
repository: () => Comment,
|
|
231
|
+
attributes: {
|
|
232
|
+
include: [[Sequelize.literal('CONCAT("[", id, "] ", text)'), 'formatted_text']],
|
|
233
|
+
},
|
|
234
|
+
join: [
|
|
235
|
+
{
|
|
236
|
+
repository: () => User,
|
|
237
|
+
attributes: {
|
|
238
|
+
exclude: ['password', 'deleted_at'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
// Множественные join
|
|
246
|
+
const join = [
|
|
247
|
+
{
|
|
248
|
+
repository: () => User,
|
|
249
|
+
attributes: {
|
|
250
|
+
exclude: ['password'],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
repository: () => Category,
|
|
255
|
+
attributes: {
|
|
256
|
+
include: [[Sequelize.literal('UPPER(name)'), 'upper_name']],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Примеры контроллеров
|
|
263
|
+
|
|
264
|
+
### Минимальный контроллер
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
@Crud({ repository: () => Users })
|
|
268
|
+
@Controller('users')
|
|
269
|
+
export class UsersController {}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Контроллер с ограничением параметров
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
@Crud({
|
|
276
|
+
repository: () => Posts,
|
|
277
|
+
query: {
|
|
278
|
+
allowedParams: {
|
|
279
|
+
filters: ['id', 'user_id'],
|
|
280
|
+
orders: ['id', 'created_at'],
|
|
281
|
+
limit: true,
|
|
282
|
+
offset: true,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
@Controller('posts')
|
|
287
|
+
export class PostsController {}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Контроллер с default params и scopes по эндпоинтам
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
@Crud({
|
|
294
|
+
repository: () => Comments,
|
|
295
|
+
params: {
|
|
296
|
+
default: {
|
|
297
|
+
orders: [['id', 'desc']],
|
|
298
|
+
limit: 10,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
scopes: {
|
|
302
|
+
global: (req) => ({
|
|
303
|
+
params: {
|
|
304
|
+
filters: {
|
|
305
|
+
deleted_at: null,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
items: (req) => ({
|
|
310
|
+
params: {
|
|
311
|
+
filters: {
|
|
312
|
+
user_id: req.user.id,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
create: (req) => ({
|
|
317
|
+
params: {
|
|
318
|
+
filters: {
|
|
319
|
+
user_id: req.user.id,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
@Controller('comments')
|
|
326
|
+
export class CommentsController {}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Контроллер с join связями
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
@Crud({
|
|
333
|
+
repository: () => Posts,
|
|
334
|
+
params: {
|
|
335
|
+
default: {
|
|
336
|
+
join: [
|
|
337
|
+
{
|
|
338
|
+
repository: () => Users,
|
|
339
|
+
attributes: {
|
|
340
|
+
exclude: [
|
|
341
|
+
'password',
|
|
342
|
+
'deleted_at'
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
scopes: {
|
|
350
|
+
item: (req) => ({
|
|
351
|
+
params: {
|
|
352
|
+
join: [
|
|
353
|
+
{
|
|
354
|
+
repository: () => Comments,
|
|
355
|
+
attributes: {
|
|
356
|
+
include: [
|
|
357
|
+
[
|
|
358
|
+
Sequelize.literal('CONCAT("[", id, "] ", text)'),
|
|
359
|
+
'formatted_text'
|
|
360
|
+
]
|
|
361
|
+
]
|
|
362
|
+
},
|
|
363
|
+
join: [
|
|
364
|
+
{
|
|
365
|
+
repository: () => Users,
|
|
366
|
+
attributes: {
|
|
367
|
+
exclude: [
|
|
368
|
+
'password'
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
@Controller('posts')
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Формат ответа
|
|
383
|
+
|
|
384
|
+
Все ответы возвращаются в едином формате:
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"status": true,
|
|
389
|
+
"data": {
|
|
390
|
+
"items": [ ... ],
|
|
391
|
+
"item": { ... }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
В случае ошибки:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"status": false,
|
|
401
|
+
"error": {
|
|
402
|
+
"message": "...",
|
|
403
|
+
"statusCode": 400,
|
|
404
|
+
"errorType": "..."
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Обработка ошибок
|
|
410
|
+
|
|
411
|
+
- Все ошибки автоматически форматируются через фильтр `CrudExceptionFilter`
|
|
412
|
+
- Для отключённых эндпоинтов выбрасывается `EndpointDisabledException`
|
|
413
|
+
|
|
414
|
+
## Интеграция с клиентом
|
|
415
|
+
|
|
416
|
+
Пакет полностью совместим с [@mee4dy/crud-client](../@mee4dy/crud-client) для фронтенда.
|
|
417
|
+
|
|
418
|
+
## Совместимость
|
|
419
|
+
|
|
420
|
+
- NestJS >= 9
|
|
421
|
+
- Sequelize >= 6
|
|
422
|
+
- Node.js >= 16
|
|
423
|
+
|
|
424
|
+
## Лицензия
|
|
425
|
+
|
|
426
|
+
MIT
|