@drax/crud-back 0.36.0 → 0.37.0
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/builders/CrudSchemaBuilder.js +25 -1
- package/dist/controllers/AbstractFastifyController.js +24 -1
- package/dist/index.js +2 -1
- package/dist/repository/AbstractMongoRepository.js +145 -0
- package/dist/schemas/GroupBySchema.js +7 -0
- package/dist/services/AbstractService.js +3 -0
- package/package.json +6 -6
- package/src/builders/CrudSchemaBuilder.ts +34 -1
- package/src/controllers/AbstractFastifyController.ts +32 -1
- package/src/index.ts +2 -0
- package/src/repository/AbstractMongoRepository.ts +196 -26
- package/src/schemas/GroupBySchema.ts +10 -0
- package/src/services/AbstractService.ts +5 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/types/builders/CrudSchemaBuilder.d.ts +177 -0
- package/types/builders/CrudSchemaBuilder.d.ts.map +1 -1
- package/types/controllers/AbstractFastifyController.d.ts +3 -0
- package/types/controllers/AbstractFastifyController.d.ts.map +1 -1
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/repository/AbstractMongoRepository.d.ts +2 -1
- package/types/repository/AbstractMongoRepository.d.ts.map +1 -1
- package/types/schemas/GroupBySchema.d.ts +13 -0
- package/types/schemas/GroupBySchema.d.ts.map +1 -0
- package/types/services/AbstractService.d.ts +2 -1
- package/types/services/AbstractService.d.ts.map +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
import "mongoose-paginate-v2";
|
|
2
3
|
import mongoose from "mongoose";
|
|
3
4
|
import type {Cursor, PopulateOptions} from "mongoose";
|
|
@@ -9,7 +10,14 @@ import {
|
|
|
9
10
|
MongoServerErrorToValidationError
|
|
10
11
|
} from "@drax/common-back";
|
|
11
12
|
import type {DeleteResult} from "mongodb";
|
|
12
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
IDraxPaginateOptions,
|
|
15
|
+
IDraxPaginateResult,
|
|
16
|
+
IDraxFindOptions,
|
|
17
|
+
IDraxCrud,
|
|
18
|
+
IDraxFieldFilter,
|
|
19
|
+
IDraxGroupByOptions
|
|
20
|
+
} from "@drax/crud-share";
|
|
13
21
|
import type {PaginateModel, PaginateOptions, PaginateResult} from "mongoose";
|
|
14
22
|
import {InvalidIdError} from "@drax/common-back";
|
|
15
23
|
import {MongoServerError} from "mongodb";
|
|
@@ -19,11 +27,11 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
19
27
|
|
|
20
28
|
protected _model: mongoose.Model<T> & PaginateModel<T>
|
|
21
29
|
protected _searchFields: string[] = []
|
|
22
|
-
protected _populateFields:
|
|
30
|
+
protected _populateFields: string[] | PopulateOptions[] = []
|
|
23
31
|
protected _lean: boolean = true
|
|
24
32
|
|
|
25
33
|
assertId(id: string): void {
|
|
26
|
-
if(!mongoose.Types.ObjectId.isValid(id)){
|
|
34
|
+
if (!mongoose.Types.ObjectId.isValid(id)) {
|
|
27
35
|
console.log(`Invalid ID: ${id} is not a valid ObjectId.`)
|
|
28
36
|
throw new InvalidIdError(id)
|
|
29
37
|
}
|
|
@@ -34,7 +42,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
34
42
|
try {
|
|
35
43
|
const item: mongoose.HydratedDocument<T> = await this._model.create(data)
|
|
36
44
|
|
|
37
|
-
if(this._populateFields && this._populateFields.length > 0){
|
|
45
|
+
if (this._populateFields && this._populateFields.length > 0) {
|
|
38
46
|
//@ts-ignore
|
|
39
47
|
await item.populate(this._populateFields)
|
|
40
48
|
}
|
|
@@ -46,7 +54,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
46
54
|
if (e instanceof mongoose.Error.CastError) {
|
|
47
55
|
throw MongooseCastErrorToValidationError(e)
|
|
48
56
|
}
|
|
49
|
-
if(e instanceof MongoServerError || e.name === 'MongoServerError'){
|
|
57
|
+
if (e instanceof MongoServerError || e.name === 'MongoServerError') {
|
|
50
58
|
throw MongoServerErrorToValidationError(e)
|
|
51
59
|
}
|
|
52
60
|
throw e
|
|
@@ -68,7 +76,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
68
76
|
if (e instanceof mongoose.Error.CastError) {
|
|
69
77
|
throw MongooseCastErrorToValidationError(e)
|
|
70
78
|
}
|
|
71
|
-
if(e instanceof MongoServerError || e.name === 'MongoServerError'){
|
|
79
|
+
if (e instanceof MongoServerError || e.name === 'MongoServerError') {
|
|
72
80
|
throw MongoServerErrorToValidationError(e)
|
|
73
81
|
}
|
|
74
82
|
throw e
|
|
@@ -90,7 +98,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
90
98
|
if (e instanceof mongoose.Error.CastError) {
|
|
91
99
|
throw MongooseCastErrorToValidationError(e)
|
|
92
100
|
}
|
|
93
|
-
if(e instanceof MongoServerError || e.name === 'MongoServerError'){
|
|
101
|
+
if (e instanceof MongoServerError || e.name === 'MongoServerError') {
|
|
94
102
|
throw MongoServerErrorToValidationError(e)
|
|
95
103
|
}
|
|
96
104
|
throw e
|
|
@@ -116,7 +124,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
116
124
|
|
|
117
125
|
async findByIds(ids: Array<string>): Promise<T[]> {
|
|
118
126
|
|
|
119
|
-
ids.map(id =>
|
|
127
|
+
ids.map(id => this.assertId(id))
|
|
120
128
|
|
|
121
129
|
const items = await this._model
|
|
122
130
|
.find({_id: {$in: ids}})
|
|
@@ -141,7 +149,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
141
149
|
return item as T
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
async findBy(field: string, value: any, limit: number
|
|
152
|
+
async findBy(field: string, value: any, limit: number = 0): Promise<T[]> {
|
|
145
153
|
const filter: any = {[field]: value}
|
|
146
154
|
const items = await this._model
|
|
147
155
|
.find(filter)
|
|
@@ -166,13 +174,13 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
166
174
|
return items as T[]
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
async search(value: string, limit: number = 1000, filters: IDraxFieldFilter[] =[]): Promise<T[]> {
|
|
177
|
+
async search(value: string, limit: number = 1000, filters: IDraxFieldFilter[] = []): Promise<T[]> {
|
|
170
178
|
|
|
171
179
|
const query = {}
|
|
172
180
|
|
|
173
|
-
if(mongoose.Types.ObjectId.isValid(value)) {
|
|
181
|
+
if (mongoose.Types.ObjectId.isValid(value)) {
|
|
174
182
|
query['_id'] = new mongoose.Types.ObjectId(value)
|
|
175
|
-
}else if (value) {
|
|
183
|
+
} else if (value) {
|
|
176
184
|
query['$or'] = this._searchFields.map(field => ({[field]: new RegExp(value.toString(), 'i')}))
|
|
177
185
|
}
|
|
178
186
|
|
|
@@ -201,10 +209,10 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
201
209
|
|
|
202
210
|
const query = {}
|
|
203
211
|
|
|
204
|
-
if(search){
|
|
205
|
-
if(mongoose.Types.ObjectId.isValid(search)) {
|
|
212
|
+
if (search) {
|
|
213
|
+
if (mongoose.Types.ObjectId.isValid(search)) {
|
|
206
214
|
query['_id'] = new mongoose.Types.ObjectId(search)
|
|
207
|
-
}else{
|
|
215
|
+
} else {
|
|
208
216
|
query['$or'] = this._searchFields.map(field => ({[field]: new RegExp(search.toString(), 'i')}))
|
|
209
217
|
}
|
|
210
218
|
}
|
|
@@ -225,23 +233,23 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
async findOne({
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
search = '',
|
|
237
|
+
filters = []
|
|
238
|
+
}: IDraxFindOptions): Promise<T> {
|
|
231
239
|
|
|
232
240
|
const query = {}
|
|
233
241
|
|
|
234
|
-
if(search){
|
|
235
|
-
if(mongoose.Types.ObjectId.isValid(search)) {
|
|
242
|
+
if (search) {
|
|
243
|
+
if (mongoose.Types.ObjectId.isValid(search)) {
|
|
236
244
|
query['_id'] = new mongoose.Types.ObjectId(search)
|
|
237
|
-
}else{
|
|
245
|
+
} else {
|
|
238
246
|
query['$or'] = this._searchFields.map(field => ({[field]: new RegExp(search.toString(), 'i')}))
|
|
239
247
|
}
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
MongooseQueryFilter.applyFilters(query, filters)
|
|
243
251
|
|
|
244
|
-
const item =
|
|
252
|
+
const item = this._model
|
|
245
253
|
.findOne(query)
|
|
246
254
|
.populate(this._populateFields)
|
|
247
255
|
.lean(this._lean)
|
|
@@ -260,10 +268,10 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
260
268
|
|
|
261
269
|
const query = {}
|
|
262
270
|
|
|
263
|
-
if(search){
|
|
264
|
-
if(mongoose.Types.ObjectId.isValid(search)) {
|
|
271
|
+
if (search) {
|
|
272
|
+
if (mongoose.Types.ObjectId.isValid(search)) {
|
|
265
273
|
query['_id'] = new mongoose.Types.ObjectId(search)
|
|
266
|
-
}else{
|
|
274
|
+
} else {
|
|
267
275
|
query['$or'] = this._searchFields.map(field => ({[field]: new RegExp(search.toString(), 'i')}))
|
|
268
276
|
}
|
|
269
277
|
}
|
|
@@ -271,7 +279,7 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
271
279
|
MongooseQueryFilter.applyFilters(query, filters)
|
|
272
280
|
|
|
273
281
|
const sort = MongooseSort.applySort(orderBy, order)
|
|
274
|
-
const items =
|
|
282
|
+
const items = await this._model
|
|
275
283
|
.find(query)
|
|
276
284
|
.limit(limit)
|
|
277
285
|
.sort(sort)
|
|
@@ -305,6 +313,168 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
305
313
|
|
|
306
314
|
return this._model.find(query).limit(limit).sort(sort).cursor() as Cursor<T>;
|
|
307
315
|
}
|
|
316
|
+
|
|
317
|
+
async groupBy({fields = [], filters = [], dateFormat = 'day'}: IDraxGroupByOptions): Promise<Array<any>> {
|
|
318
|
+
|
|
319
|
+
const query = {}
|
|
320
|
+
|
|
321
|
+
MongooseQueryFilter.applyFilters(query, filters)
|
|
322
|
+
|
|
323
|
+
// Obtener el schema para identificar campos de referencia y fechas
|
|
324
|
+
const schema = this._model.schema
|
|
325
|
+
|
|
326
|
+
// Construir el objeto de agrupación dinámicamente
|
|
327
|
+
const groupId: any = {}
|
|
328
|
+
const lookupStages: any[] = []
|
|
329
|
+
const finalProjectFields: any = {count: 1, _id: 0}
|
|
330
|
+
const refFields = new Set<string>()
|
|
331
|
+
const dateFields = new Set<string>()
|
|
332
|
+
|
|
333
|
+
// Función para obtener el formato de fecha según el nivel de granularidad
|
|
334
|
+
const getDateFormat = (field: string, format: string) => {
|
|
335
|
+
const formats = {
|
|
336
|
+
'year': {
|
|
337
|
+
$dateFromParts: {
|
|
338
|
+
year: {$year: `$${field}`},
|
|
339
|
+
month: 1,
|
|
340
|
+
day: 1
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
'month': {
|
|
344
|
+
$dateFromParts: {
|
|
345
|
+
year: {$year: `$${field}`},
|
|
346
|
+
month: {$month: `$${field}`},
|
|
347
|
+
day: 1
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
'day': {
|
|
351
|
+
$dateFromParts: {
|
|
352
|
+
year: {$year: `$${field}`},
|
|
353
|
+
month: {$month: `$${field}`},
|
|
354
|
+
day: {$dayOfMonth: `$${field}`}
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
'hour': {
|
|
358
|
+
$dateFromParts: {
|
|
359
|
+
year: {$year: `$${field}`},
|
|
360
|
+
month: {$month: `$${field}`},
|
|
361
|
+
day: {$dayOfMonth: `$${field}`},
|
|
362
|
+
hour: {$hour: `$${field}`}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
'minute': {
|
|
366
|
+
$dateFromParts: {
|
|
367
|
+
year: {$year: `$${field}`},
|
|
368
|
+
month: {$month: `$${field}`},
|
|
369
|
+
day: {$dayOfMonth: `$${field}`},
|
|
370
|
+
hour: {$hour: `$${field}`},
|
|
371
|
+
minute: {$minute: `$${field}`}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
'second': {
|
|
375
|
+
$dateFromParts: {
|
|
376
|
+
year: {$year: `$${field}`},
|
|
377
|
+
month: {$month: `$${field}`},
|
|
378
|
+
day: {$dayOfMonth: `$${field}`},
|
|
379
|
+
hour: {$hour: `$${field}`},
|
|
380
|
+
minute: {$minute: `$${field}`},
|
|
381
|
+
second: {$second: `$${field}`}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return formats[format] || formats['day']
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fields.forEach(field => {
|
|
389
|
+
const schemaPath = schema.path(field)
|
|
390
|
+
|
|
391
|
+
// Verificar si el campo es de tipo Date
|
|
392
|
+
if (schemaPath && schemaPath.instance === 'Date') {
|
|
393
|
+
dateFields.add(field)
|
|
394
|
+
groupId[field] = getDateFormat(field, dateFormat)
|
|
395
|
+
}
|
|
396
|
+
// Verificar si el campo es una referencia
|
|
397
|
+
else if (schemaPath && schemaPath.options && schemaPath.options.ref) {
|
|
398
|
+
const refModel = schemaPath.options.ref
|
|
399
|
+
const fieldName = field
|
|
400
|
+
|
|
401
|
+
refFields.add(field)
|
|
402
|
+
|
|
403
|
+
// Obtener el modelo referenciado y su nombre de colección real
|
|
404
|
+
const refModelInstance = mongoose.model(refModel)
|
|
405
|
+
const collectionName = refModelInstance.collection.name
|
|
406
|
+
|
|
407
|
+
// Determinar el campo local correcto según si es un solo campo o múltiples
|
|
408
|
+
const localField = fields.length === 1 ? '_id' : `_id.${fieldName}`
|
|
409
|
+
|
|
410
|
+
lookupStages.push({
|
|
411
|
+
$lookup: {
|
|
412
|
+
from: collectionName,
|
|
413
|
+
localField: localField,
|
|
414
|
+
foreignField: '_id',
|
|
415
|
+
as: `${fieldName}_populated`
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// Unwind para convertir el array en objeto único
|
|
420
|
+
lookupStages.push({
|
|
421
|
+
$unwind: {
|
|
422
|
+
path: `$${fieldName}_populated`,
|
|
423
|
+
preserveNullAndEmptyArrays: true
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// En la proyección final, usar el objeto poblado
|
|
428
|
+
finalProjectFields[field] = `$${fieldName}_populated`
|
|
429
|
+
groupId[field] = `$${field}`
|
|
430
|
+
} else {
|
|
431
|
+
// Si no es una referencia ni fecha, usar el valor directo
|
|
432
|
+
groupId[field] = `$${field}`
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// Construir la proyección final para campos de fecha
|
|
437
|
+
fields.forEach(field => {
|
|
438
|
+
if (dateFields.has(field)) {
|
|
439
|
+
if (fields.length === 1) {
|
|
440
|
+
finalProjectFields[field] = `$_id`
|
|
441
|
+
} else {
|
|
442
|
+
finalProjectFields[field] = `$_id.${field}`
|
|
443
|
+
}
|
|
444
|
+
} else if (!refFields.has(field)) {
|
|
445
|
+
if (fields.length === 1) {
|
|
446
|
+
finalProjectFields[field] = `$_id`
|
|
447
|
+
} else {
|
|
448
|
+
finalProjectFields[field] = `$_id.${field}`
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const pipeline: any[] = [
|
|
454
|
+
{$match: query},
|
|
455
|
+
{
|
|
456
|
+
$group: {
|
|
457
|
+
_id: fields.length === 1 ? (dateFields.has(fields[0]) ? getDateFormat(fields[0], dateFormat) : `$${fields[0]}`) : groupId,
|
|
458
|
+
count: {$sum: 1}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
// Solo agregar lookups si hay campos de referencia
|
|
464
|
+
if (lookupStages.length > 0) {
|
|
465
|
+
pipeline.push(...lookupStages)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
pipeline.push(
|
|
469
|
+
{
|
|
470
|
+
$project: finalProjectFields
|
|
471
|
+
},
|
|
472
|
+
{$sort: {count: -1}}
|
|
473
|
+
)
|
|
474
|
+
console.log("pipeline", JSON.stringify(pipeline, null, 2))
|
|
475
|
+
const result = await this._model.aggregate(pipeline).exec()
|
|
476
|
+
return result
|
|
477
|
+
}
|
|
308
478
|
}
|
|
309
479
|
|
|
310
480
|
export default AbstractMongoRepository
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
import QueryFilterRegex from "../regexs/QueryFilterRegex.js";
|
|
3
|
+
|
|
4
|
+
const GroupByQuerySchema = z.object({
|
|
5
|
+
fields: z.array(z.string()).min(1).max(10),
|
|
6
|
+
filters: z.string().regex(QueryFilterRegex).optional().describe("Format: field;operator;value|field;operator;value|..."),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export {GroupByQuerySchema}
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
IDraxPaginateResult,
|
|
7
7
|
IDraxFindOptions,
|
|
8
8
|
IDraxExportOptions,
|
|
9
|
-
IDraxCrudRepository, IDraxFieldFilter
|
|
9
|
+
IDraxCrudRepository, IDraxFieldFilter, IDraxGroupByOptions
|
|
10
10
|
} from "@drax/crud-share";
|
|
11
11
|
import {IDraxCrudService} from "@drax/crud-share";
|
|
12
12
|
import ExportCsv from "../exports/ExportCsv.js";
|
|
@@ -262,6 +262,10 @@ abstract class AbstractService<T, C, U> implements IDraxCrudService<T, C, U> {
|
|
|
262
262
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
async groupBy({fields= [], filters= [], dateFormat= 'day'}: IDraxGroupByOptions): Promise<Array<any>> {
|
|
266
|
+
return await this._repository.groupBy({fields, filters, dateFormat})
|
|
267
|
+
}
|
|
268
|
+
|
|
265
269
|
async export({
|
|
266
270
|
format = 'JSON',
|
|
267
271
|
headers = [],
|