@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.
@@ -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 {IDraxPaginateOptions, IDraxPaginateResult, IDraxFindOptions, IDraxCrud, IDraxFieldFilter} from "@drax/crud-share";
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: string[] | PopulateOptions[] = []
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 => this.assertId(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 = 0): Promise<T[]> {
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
- search = '',
229
- filters = []
230
- }: IDraxFindOptions): Promise<T> {
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 = this._model
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 = await this._model
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 = [],