@drax/crud-back 3.17.0 → 3.17.2
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.
|
@@ -228,17 +228,29 @@ class AbstractMongoRepository {
|
|
|
228
228
|
const schema = this._model.schema;
|
|
229
229
|
// Construir el objeto de agrupación dinámicamente
|
|
230
230
|
const groupId = {};
|
|
231
|
-
const
|
|
231
|
+
const preGroupStages = [];
|
|
232
|
+
const postGroupStages = [];
|
|
232
233
|
const finalProjectFields = { count: 1, _id: 0 };
|
|
233
234
|
const refFields = new Set();
|
|
234
235
|
const dateFields = new Set();
|
|
235
236
|
const numericFields = new Set();
|
|
236
237
|
const groupFields = [];
|
|
238
|
+
const groupFieldAliases = new Map();
|
|
239
|
+
const preGroupLookupAliases = new Map();
|
|
237
240
|
const numericInstances = new Set(['Number', 'Decimal128', 'Double', 'Int32', 'Long', 'BigInt']);
|
|
238
241
|
const totalGroupFields = fields.filter(field => {
|
|
239
242
|
const schemaPath = schema.path(field);
|
|
240
243
|
return !(schemaPath && numericInstances.has(schemaPath.instance));
|
|
241
244
|
}).length;
|
|
245
|
+
const getGroupFieldAlias = (field) => {
|
|
246
|
+
const existingAlias = groupFieldAliases.get(field);
|
|
247
|
+
if (existingAlias) {
|
|
248
|
+
return existingAlias;
|
|
249
|
+
}
|
|
250
|
+
const newAlias = `field_${groupFieldAliases.size}`;
|
|
251
|
+
groupFieldAliases.set(field, newAlias);
|
|
252
|
+
return newAlias;
|
|
253
|
+
};
|
|
242
254
|
// Función para obtener el formato de fecha según el nivel de granularidad
|
|
243
255
|
const getDateFormat = (field, format) => {
|
|
244
256
|
const formats = {
|
|
@@ -293,8 +305,38 @@ class AbstractMongoRepository {
|
|
|
293
305
|
};
|
|
294
306
|
return formats[format] || formats['day'];
|
|
295
307
|
};
|
|
308
|
+
const ensureLookupForReferencedField = (field, refModel) => {
|
|
309
|
+
const existingAlias = preGroupLookupAliases.get(field);
|
|
310
|
+
if (existingAlias) {
|
|
311
|
+
return existingAlias;
|
|
312
|
+
}
|
|
313
|
+
const refModelInstance = mongoose.model(refModel);
|
|
314
|
+
const collectionName = refModelInstance.collection.name;
|
|
315
|
+
const lookupAlias = `${field}_groupByRef`;
|
|
316
|
+
preGroupStages.push({
|
|
317
|
+
$lookup: {
|
|
318
|
+
from: collectionName,
|
|
319
|
+
localField: field,
|
|
320
|
+
foreignField: '_id',
|
|
321
|
+
as: lookupAlias
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
preGroupStages.push({
|
|
325
|
+
$unwind: {
|
|
326
|
+
path: `$${lookupAlias}`,
|
|
327
|
+
preserveNullAndEmptyArrays: true
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
preGroupLookupAliases.set(field, lookupAlias);
|
|
331
|
+
return lookupAlias;
|
|
332
|
+
};
|
|
296
333
|
fields.forEach(field => {
|
|
297
334
|
const schemaPath = schema.path(field);
|
|
335
|
+
const fieldAlias = getGroupFieldAlias(field);
|
|
336
|
+
const fieldParts = field.split('.');
|
|
337
|
+
const rootField = fieldParts[0];
|
|
338
|
+
const nestedFieldPath = fieldParts.slice(1).join('.');
|
|
339
|
+
const rootSchemaPath = rootField === field ? schemaPath : schema.path(rootField);
|
|
298
340
|
// Verificar si el campo es numérico: se agregará con $sum y no formará parte de la clave de agrupación
|
|
299
341
|
if (schemaPath && numericInstances.has(schemaPath.instance)) {
|
|
300
342
|
numericFields.add(field);
|
|
@@ -305,50 +347,54 @@ class AbstractMongoRepository {
|
|
|
305
347
|
// Verificar si el campo es de tipo Date
|
|
306
348
|
if (schemaPath && schemaPath.instance === 'Date') {
|
|
307
349
|
dateFields.add(field);
|
|
308
|
-
groupId[
|
|
350
|
+
groupId[fieldAlias] = getDateFormat(field, dateFormat);
|
|
309
351
|
}
|
|
310
352
|
// Verificar si el campo es una referencia
|
|
311
353
|
else if (schemaPath && schemaPath.options && schemaPath.options.ref) {
|
|
312
354
|
const refModel = schemaPath.options.ref;
|
|
313
|
-
const fieldName = field;
|
|
314
355
|
refFields.add(field);
|
|
315
356
|
// Obtener el modelo referenciado y su nombre de colección real
|
|
316
357
|
const refModelInstance = mongoose.model(refModel);
|
|
317
358
|
const collectionName = refModelInstance.collection.name;
|
|
318
359
|
// Determinar el campo local correcto según si es un solo campo o múltiples
|
|
319
|
-
const localField = totalGroupFields === 1 ? '_id' : `_id.${
|
|
320
|
-
|
|
360
|
+
const localField = totalGroupFields === 1 ? '_id' : `_id.${fieldAlias}`;
|
|
361
|
+
postGroupStages.push({
|
|
321
362
|
$lookup: {
|
|
322
363
|
from: collectionName,
|
|
323
364
|
localField: localField,
|
|
324
365
|
foreignField: '_id',
|
|
325
|
-
as: `${
|
|
366
|
+
as: `${fieldAlias}_populated`
|
|
326
367
|
}
|
|
327
368
|
});
|
|
328
369
|
// Unwind para convertir el array en objeto único
|
|
329
|
-
|
|
370
|
+
postGroupStages.push({
|
|
330
371
|
$unwind: {
|
|
331
|
-
path: `$${
|
|
372
|
+
path: `$${fieldAlias}_populated`,
|
|
332
373
|
preserveNullAndEmptyArrays: true
|
|
333
374
|
}
|
|
334
375
|
});
|
|
335
376
|
// En la proyección final, usar el objeto poblado
|
|
336
|
-
finalProjectFields[field] = `$${
|
|
337
|
-
groupId[
|
|
377
|
+
finalProjectFields[field] = `$${fieldAlias}_populated`;
|
|
378
|
+
groupId[fieldAlias] = `$${field}`;
|
|
379
|
+
}
|
|
380
|
+
else if (nestedFieldPath && rootSchemaPath && rootSchemaPath.options && rootSchemaPath.options.ref) {
|
|
381
|
+
const lookupAlias = ensureLookupForReferencedField(rootField, rootSchemaPath.options.ref);
|
|
382
|
+
groupId[fieldAlias] = `$${lookupAlias}.${nestedFieldPath}`;
|
|
338
383
|
}
|
|
339
384
|
else {
|
|
340
385
|
// Si no es una referencia ni fecha, usar el valor directo
|
|
341
|
-
groupId[
|
|
386
|
+
groupId[fieldAlias] = `$${field}`;
|
|
342
387
|
}
|
|
343
388
|
});
|
|
344
389
|
// Construir la proyección final para campos de fecha
|
|
345
390
|
groupFields.forEach(field => {
|
|
391
|
+
const fieldAlias = groupFieldAliases.get(field);
|
|
346
392
|
if (dateFields.has(field)) {
|
|
347
393
|
if (groupFields.length === 1) {
|
|
348
394
|
finalProjectFields[field] = `$_id`;
|
|
349
395
|
}
|
|
350
396
|
else {
|
|
351
|
-
finalProjectFields[field] = `$_id.${
|
|
397
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`;
|
|
352
398
|
}
|
|
353
399
|
}
|
|
354
400
|
else if (!refFields.has(field)) {
|
|
@@ -356,7 +402,7 @@ class AbstractMongoRepository {
|
|
|
356
402
|
finalProjectFields[field] = `$_id`;
|
|
357
403
|
}
|
|
358
404
|
else {
|
|
359
|
-
finalProjectFields[field] = `$_id.${
|
|
405
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`;
|
|
360
406
|
}
|
|
361
407
|
}
|
|
362
408
|
});
|
|
@@ -376,13 +422,14 @@ class AbstractMongoRepository {
|
|
|
376
422
|
}
|
|
377
423
|
const pipeline = [
|
|
378
424
|
{ $match: query },
|
|
425
|
+
...preGroupStages,
|
|
379
426
|
{
|
|
380
427
|
$group: groupStage
|
|
381
428
|
}
|
|
382
429
|
];
|
|
383
430
|
// Solo agregar lookups si hay campos de referencia
|
|
384
|
-
if (
|
|
385
|
-
pipeline.push(...
|
|
431
|
+
if (postGroupStages.length > 0) {
|
|
432
|
+
pipeline.push(...postGroupStages);
|
|
386
433
|
}
|
|
387
434
|
pipeline.push({
|
|
388
435
|
$project: finalProjectFields
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.17.
|
|
6
|
+
"version": "3.17.2",
|
|
7
7
|
"description": "Crud utils across modules",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@drax/common-back": "^3.14.0",
|
|
26
26
|
"@drax/common-share": "^3.0.0",
|
|
27
27
|
"@drax/identity-share": "^3.15.0",
|
|
28
|
-
"@drax/media-back": "^3.17.
|
|
28
|
+
"@drax/media-back": "^3.17.2",
|
|
29
29
|
"@graphql-tools/load-files": "^7.0.0",
|
|
30
30
|
"@graphql-tools/merge": "^9.0.4",
|
|
31
31
|
"mongoose": "^8.23.0",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"typescript": "^5.9.3",
|
|
48
48
|
"vitest": "^3.2.4"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "832f9362009d55b03e0a51c3f558c439f158d9e6"
|
|
51
51
|
}
|
|
@@ -337,18 +337,32 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
337
337
|
|
|
338
338
|
// Construir el objeto de agrupación dinámicamente
|
|
339
339
|
const groupId: any = {}
|
|
340
|
-
const
|
|
340
|
+
const preGroupStages: any[] = []
|
|
341
|
+
const postGroupStages: any[] = []
|
|
341
342
|
const finalProjectFields: any = {count: 1, _id: 0}
|
|
342
343
|
const refFields = new Set<string>()
|
|
343
344
|
const dateFields = new Set<string>()
|
|
344
345
|
const numericFields = new Set<string>()
|
|
345
346
|
const groupFields: string[] = []
|
|
347
|
+
const groupFieldAliases = new Map<string, string>()
|
|
348
|
+
const preGroupLookupAliases = new Map<string, string>()
|
|
346
349
|
const numericInstances = new Set(['Number', 'Decimal128', 'Double', 'Int32', 'Long', 'BigInt'])
|
|
347
350
|
const totalGroupFields = fields.filter(field => {
|
|
348
351
|
const schemaPath = schema.path(field)
|
|
349
352
|
return !(schemaPath && numericInstances.has(schemaPath.instance))
|
|
350
353
|
}).length
|
|
351
354
|
|
|
355
|
+
const getGroupFieldAlias = (field: string): string => {
|
|
356
|
+
const existingAlias = groupFieldAliases.get(field)
|
|
357
|
+
if (existingAlias) {
|
|
358
|
+
return existingAlias
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const newAlias = `field_${groupFieldAliases.size}`
|
|
362
|
+
groupFieldAliases.set(field, newAlias)
|
|
363
|
+
return newAlias
|
|
364
|
+
}
|
|
365
|
+
|
|
352
366
|
// Función para obtener el formato de fecha según el nivel de granularidad
|
|
353
367
|
const getDateFormat = (field: string, format: string) => {
|
|
354
368
|
const formats = {
|
|
@@ -404,8 +418,43 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
404
418
|
return formats[format] || formats['day']
|
|
405
419
|
}
|
|
406
420
|
|
|
421
|
+
const ensureLookupForReferencedField = (field: string, refModel: string): string => {
|
|
422
|
+
const existingAlias = preGroupLookupAliases.get(field)
|
|
423
|
+
if (existingAlias) {
|
|
424
|
+
return existingAlias
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const refModelInstance = mongoose.model(refModel)
|
|
428
|
+
const collectionName = refModelInstance.collection.name
|
|
429
|
+
const lookupAlias = `${field}_groupByRef`
|
|
430
|
+
|
|
431
|
+
preGroupStages.push({
|
|
432
|
+
$lookup: {
|
|
433
|
+
from: collectionName,
|
|
434
|
+
localField: field,
|
|
435
|
+
foreignField: '_id',
|
|
436
|
+
as: lookupAlias
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
preGroupStages.push({
|
|
441
|
+
$unwind: {
|
|
442
|
+
path: `$${lookupAlias}`,
|
|
443
|
+
preserveNullAndEmptyArrays: true
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
preGroupLookupAliases.set(field, lookupAlias)
|
|
448
|
+
return lookupAlias
|
|
449
|
+
}
|
|
450
|
+
|
|
407
451
|
fields.forEach(field => {
|
|
408
452
|
const schemaPath = schema.path(field)
|
|
453
|
+
const fieldAlias = getGroupFieldAlias(field)
|
|
454
|
+
const fieldParts = field.split('.')
|
|
455
|
+
const rootField = fieldParts[0]
|
|
456
|
+
const nestedFieldPath = fieldParts.slice(1).join('.')
|
|
457
|
+
const rootSchemaPath = rootField === field ? schemaPath : schema.path(rootField)
|
|
409
458
|
|
|
410
459
|
// Verificar si el campo es numérico: se agregará con $sum y no formará parte de la clave de agrupación
|
|
411
460
|
if (schemaPath && numericInstances.has(schemaPath.instance)) {
|
|
@@ -419,12 +468,11 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
419
468
|
// Verificar si el campo es de tipo Date
|
|
420
469
|
if (schemaPath && schemaPath.instance === 'Date') {
|
|
421
470
|
dateFields.add(field)
|
|
422
|
-
groupId[
|
|
471
|
+
groupId[fieldAlias] = getDateFormat(field, dateFormat)
|
|
423
472
|
}
|
|
424
473
|
// Verificar si el campo es una referencia
|
|
425
474
|
else if (schemaPath && schemaPath.options && schemaPath.options.ref) {
|
|
426
475
|
const refModel = schemaPath.options.ref
|
|
427
|
-
const fieldName = field
|
|
428
476
|
|
|
429
477
|
refFields.add(field)
|
|
430
478
|
|
|
@@ -433,47 +481,51 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
433
481
|
const collectionName = refModelInstance.collection.name
|
|
434
482
|
|
|
435
483
|
// Determinar el campo local correcto según si es un solo campo o múltiples
|
|
436
|
-
const localField = totalGroupFields === 1 ? '_id' : `_id.${
|
|
484
|
+
const localField = totalGroupFields === 1 ? '_id' : `_id.${fieldAlias}`
|
|
437
485
|
|
|
438
|
-
|
|
486
|
+
postGroupStages.push({
|
|
439
487
|
$lookup: {
|
|
440
488
|
from: collectionName,
|
|
441
489
|
localField: localField,
|
|
442
490
|
foreignField: '_id',
|
|
443
|
-
as: `${
|
|
491
|
+
as: `${fieldAlias}_populated`
|
|
444
492
|
}
|
|
445
493
|
})
|
|
446
494
|
|
|
447
495
|
// Unwind para convertir el array en objeto único
|
|
448
|
-
|
|
496
|
+
postGroupStages.push({
|
|
449
497
|
$unwind: {
|
|
450
|
-
path: `$${
|
|
498
|
+
path: `$${fieldAlias}_populated`,
|
|
451
499
|
preserveNullAndEmptyArrays: true
|
|
452
500
|
}
|
|
453
501
|
})
|
|
454
502
|
|
|
455
503
|
// En la proyección final, usar el objeto poblado
|
|
456
|
-
finalProjectFields[field] = `$${
|
|
457
|
-
groupId[
|
|
504
|
+
finalProjectFields[field] = `$${fieldAlias}_populated`
|
|
505
|
+
groupId[fieldAlias] = `$${field}`
|
|
506
|
+
} else if (nestedFieldPath && rootSchemaPath && rootSchemaPath.options && rootSchemaPath.options.ref) {
|
|
507
|
+
const lookupAlias = ensureLookupForReferencedField(rootField, rootSchemaPath.options.ref)
|
|
508
|
+
groupId[fieldAlias] = `$${lookupAlias}.${nestedFieldPath}`
|
|
458
509
|
} else {
|
|
459
510
|
// Si no es una referencia ni fecha, usar el valor directo
|
|
460
|
-
groupId[
|
|
511
|
+
groupId[fieldAlias] = `$${field}`
|
|
461
512
|
}
|
|
462
513
|
})
|
|
463
514
|
|
|
464
515
|
// Construir la proyección final para campos de fecha
|
|
465
516
|
groupFields.forEach(field => {
|
|
517
|
+
const fieldAlias = groupFieldAliases.get(field) as string
|
|
466
518
|
if (dateFields.has(field)) {
|
|
467
519
|
if (groupFields.length === 1) {
|
|
468
520
|
finalProjectFields[field] = `$_id`
|
|
469
521
|
} else {
|
|
470
|
-
finalProjectFields[field] = `$_id.${
|
|
522
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`
|
|
471
523
|
}
|
|
472
524
|
} else if (!refFields.has(field)) {
|
|
473
525
|
if (groupFields.length === 1) {
|
|
474
526
|
finalProjectFields[field] = `$_id`
|
|
475
527
|
} else {
|
|
476
|
-
finalProjectFields[field] = `$_id.${
|
|
528
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`
|
|
477
529
|
}
|
|
478
530
|
}
|
|
479
531
|
})
|
|
@@ -496,14 +548,15 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
496
548
|
|
|
497
549
|
const pipeline: any[] = [
|
|
498
550
|
{$match: query},
|
|
551
|
+
...preGroupStages,
|
|
499
552
|
{
|
|
500
553
|
$group: groupStage
|
|
501
554
|
}
|
|
502
555
|
]
|
|
503
556
|
|
|
504
557
|
// Solo agregar lookups si hay campos de referencia
|
|
505
|
-
if (
|
|
506
|
-
pipeline.push(...
|
|
558
|
+
if (postGroupStages.length > 0) {
|
|
559
|
+
pipeline.push(...postGroupStages)
|
|
507
560
|
}
|
|
508
561
|
|
|
509
562
|
pipeline.push(
|
|
@@ -561,6 +561,72 @@ describe("Person Controller Test", function () {
|
|
|
561
561
|
expect(groupResult.length).toBeGreaterThan(0)
|
|
562
562
|
})
|
|
563
563
|
|
|
564
|
+
it("should groupBy nested reference and embedded fields together", async () => {
|
|
565
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
566
|
+
await testSetup.dropCollection('Person')
|
|
567
|
+
await testSetup.dropCollection('Country')
|
|
568
|
+
|
|
569
|
+
const argentina = await CountryModel.create({
|
|
570
|
+
name: "Argentina",
|
|
571
|
+
createdBy: testSetup.rootUser._id
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
const chile = await CountryModel.create({
|
|
575
|
+
name: "Chile",
|
|
576
|
+
createdBy: testSetup.rootUser._id
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
const entityData: IPersonBase[] = [
|
|
580
|
+
{
|
|
581
|
+
fullname: "Buenos Aires One",
|
|
582
|
+
nationality: argentina._id.toString(),
|
|
583
|
+
address: { street: "Main St", city: "Buenos Aires" }
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
fullname: "Buenos Aires Two",
|
|
587
|
+
nationality: argentina._id.toString(),
|
|
588
|
+
address: { street: "Second St", city: "Buenos Aires" }
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
fullname: "Santiago One",
|
|
592
|
+
nationality: chile._id.toString(),
|
|
593
|
+
address: { street: "Main St", city: "Santiago" }
|
|
594
|
+
}
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
for (const data of entityData) {
|
|
598
|
+
const createResp = await testSetup.fastifyInstance.inject({
|
|
599
|
+
method: 'POST',
|
|
600
|
+
url: '/api/person',
|
|
601
|
+
payload: data,
|
|
602
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
expect(createResp.statusCode).toBe(200)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const groupResp = await testSetup.fastifyInstance.inject({
|
|
609
|
+
method: 'GET',
|
|
610
|
+
url: '/api/person/group-by?fields=nationality.name,address.city',
|
|
611
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
const groupResult = await groupResp.json()
|
|
615
|
+
expect(groupResp.statusCode).toBe(200)
|
|
616
|
+
expect(groupResult).toEqual(expect.arrayContaining([
|
|
617
|
+
expect.objectContaining({
|
|
618
|
+
'nationality.name': 'Argentina',
|
|
619
|
+
'address.city': 'Buenos Aires',
|
|
620
|
+
count: 2
|
|
621
|
+
}),
|
|
622
|
+
expect.objectContaining({
|
|
623
|
+
'nationality.name': 'Chile',
|
|
624
|
+
'address.city': 'Santiago',
|
|
625
|
+
count: 1
|
|
626
|
+
})
|
|
627
|
+
]))
|
|
628
|
+
})
|
|
629
|
+
|
|
564
630
|
// 9. Error Handling - Not Found
|
|
565
631
|
it("should handle error responses correctly when person is not found", async () => {
|
|
566
632
|
const { accessToken } = await testSetup.rootUserLogin()
|