@drax/crud-back 3.17.0 → 3.17.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/repository/AbstractMongoRepository.js +80 -17
- package/package.json +3 -3
- package/src/repository/AbstractMongoRepository.ts +91 -17
- package/test/controllers/PersonController.test.ts +116 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/types/repository/AbstractMongoRepository.d.ts +1 -0
- package/types/repository/AbstractMongoRepository.d.ts.map +1 -1
|
@@ -9,6 +9,9 @@ class AbstractMongoRepository {
|
|
|
9
9
|
this._populateFields = [];
|
|
10
10
|
this._lean = true;
|
|
11
11
|
}
|
|
12
|
+
getNestedValue(source, path) {
|
|
13
|
+
return path.split('.').reduce((value, key) => value == null ? undefined : value[key], source);
|
|
14
|
+
}
|
|
12
15
|
assertId(id) {
|
|
13
16
|
if (!mongoose.Types.ObjectId.isValid(id)) {
|
|
14
17
|
console.log(`Invalid ID: ${id} is not a valid ObjectId.`);
|
|
@@ -228,17 +231,29 @@ class AbstractMongoRepository {
|
|
|
228
231
|
const schema = this._model.schema;
|
|
229
232
|
// Construir el objeto de agrupación dinámicamente
|
|
230
233
|
const groupId = {};
|
|
231
|
-
const
|
|
234
|
+
const preGroupStages = [];
|
|
235
|
+
const postGroupStages = [];
|
|
232
236
|
const finalProjectFields = { count: 1, _id: 0 };
|
|
233
237
|
const refFields = new Set();
|
|
234
238
|
const dateFields = new Set();
|
|
235
239
|
const numericFields = new Set();
|
|
236
240
|
const groupFields = [];
|
|
241
|
+
const groupFieldAliases = new Map();
|
|
242
|
+
const preGroupLookupAliases = new Map();
|
|
237
243
|
const numericInstances = new Set(['Number', 'Decimal128', 'Double', 'Int32', 'Long', 'BigInt']);
|
|
238
244
|
const totalGroupFields = fields.filter(field => {
|
|
239
245
|
const schemaPath = schema.path(field);
|
|
240
246
|
return !(schemaPath && numericInstances.has(schemaPath.instance));
|
|
241
247
|
}).length;
|
|
248
|
+
const getGroupFieldAlias = (field) => {
|
|
249
|
+
const existingAlias = groupFieldAliases.get(field);
|
|
250
|
+
if (existingAlias) {
|
|
251
|
+
return existingAlias;
|
|
252
|
+
}
|
|
253
|
+
const newAlias = `field_${groupFieldAliases.size}`;
|
|
254
|
+
groupFieldAliases.set(field, newAlias);
|
|
255
|
+
return newAlias;
|
|
256
|
+
};
|
|
242
257
|
// Función para obtener el formato de fecha según el nivel de granularidad
|
|
243
258
|
const getDateFormat = (field, format) => {
|
|
244
259
|
const formats = {
|
|
@@ -293,8 +308,38 @@ class AbstractMongoRepository {
|
|
|
293
308
|
};
|
|
294
309
|
return formats[format] || formats['day'];
|
|
295
310
|
};
|
|
311
|
+
const ensureLookupForReferencedField = (field, refModel) => {
|
|
312
|
+
const existingAlias = preGroupLookupAliases.get(field);
|
|
313
|
+
if (existingAlias) {
|
|
314
|
+
return existingAlias;
|
|
315
|
+
}
|
|
316
|
+
const refModelInstance = mongoose.model(refModel);
|
|
317
|
+
const collectionName = refModelInstance.collection.name;
|
|
318
|
+
const lookupAlias = `${field}_groupByRef`;
|
|
319
|
+
preGroupStages.push({
|
|
320
|
+
$lookup: {
|
|
321
|
+
from: collectionName,
|
|
322
|
+
localField: field,
|
|
323
|
+
foreignField: '_id',
|
|
324
|
+
as: lookupAlias
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
preGroupStages.push({
|
|
328
|
+
$unwind: {
|
|
329
|
+
path: `$${lookupAlias}`,
|
|
330
|
+
preserveNullAndEmptyArrays: true
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
preGroupLookupAliases.set(field, lookupAlias);
|
|
334
|
+
return lookupAlias;
|
|
335
|
+
};
|
|
296
336
|
fields.forEach(field => {
|
|
297
337
|
const schemaPath = schema.path(field);
|
|
338
|
+
const fieldAlias = getGroupFieldAlias(field);
|
|
339
|
+
const fieldParts = field.split('.');
|
|
340
|
+
const rootField = fieldParts[0];
|
|
341
|
+
const nestedFieldPath = fieldParts.slice(1).join('.');
|
|
342
|
+
const rootSchemaPath = rootField === field ? schemaPath : schema.path(rootField);
|
|
298
343
|
// Verificar si el campo es numérico: se agregará con $sum y no formará parte de la clave de agrupación
|
|
299
344
|
if (schemaPath && numericInstances.has(schemaPath.instance)) {
|
|
300
345
|
numericFields.add(field);
|
|
@@ -305,50 +350,54 @@ class AbstractMongoRepository {
|
|
|
305
350
|
// Verificar si el campo es de tipo Date
|
|
306
351
|
if (schemaPath && schemaPath.instance === 'Date') {
|
|
307
352
|
dateFields.add(field);
|
|
308
|
-
groupId[
|
|
353
|
+
groupId[fieldAlias] = getDateFormat(field, dateFormat);
|
|
309
354
|
}
|
|
310
355
|
// Verificar si el campo es una referencia
|
|
311
356
|
else if (schemaPath && schemaPath.options && schemaPath.options.ref) {
|
|
312
357
|
const refModel = schemaPath.options.ref;
|
|
313
|
-
const fieldName = field;
|
|
314
358
|
refFields.add(field);
|
|
315
359
|
// Obtener el modelo referenciado y su nombre de colección real
|
|
316
360
|
const refModelInstance = mongoose.model(refModel);
|
|
317
361
|
const collectionName = refModelInstance.collection.name;
|
|
318
362
|
// Determinar el campo local correcto según si es un solo campo o múltiples
|
|
319
|
-
const localField = totalGroupFields === 1 ? '_id' : `_id.${
|
|
320
|
-
|
|
363
|
+
const localField = totalGroupFields === 1 ? '_id' : `_id.${fieldAlias}`;
|
|
364
|
+
postGroupStages.push({
|
|
321
365
|
$lookup: {
|
|
322
366
|
from: collectionName,
|
|
323
367
|
localField: localField,
|
|
324
368
|
foreignField: '_id',
|
|
325
|
-
as: `${
|
|
369
|
+
as: `${fieldAlias}_populated`
|
|
326
370
|
}
|
|
327
371
|
});
|
|
328
372
|
// Unwind para convertir el array en objeto único
|
|
329
|
-
|
|
373
|
+
postGroupStages.push({
|
|
330
374
|
$unwind: {
|
|
331
|
-
path: `$${
|
|
375
|
+
path: `$${fieldAlias}_populated`,
|
|
332
376
|
preserveNullAndEmptyArrays: true
|
|
333
377
|
}
|
|
334
378
|
});
|
|
335
379
|
// En la proyección final, usar el objeto poblado
|
|
336
|
-
finalProjectFields[field] = `$${
|
|
337
|
-
groupId[
|
|
380
|
+
finalProjectFields[field] = `$${fieldAlias}_populated`;
|
|
381
|
+
groupId[fieldAlias] = `$${field}`;
|
|
382
|
+
}
|
|
383
|
+
else if (nestedFieldPath && rootSchemaPath && rootSchemaPath.options && rootSchemaPath.options.ref) {
|
|
384
|
+
const lookupAlias = ensureLookupForReferencedField(rootField, rootSchemaPath.options.ref);
|
|
385
|
+
groupId[fieldAlias] = `$${lookupAlias}.${nestedFieldPath}`;
|
|
338
386
|
}
|
|
339
387
|
else {
|
|
340
388
|
// Si no es una referencia ni fecha, usar el valor directo
|
|
341
|
-
groupId[
|
|
389
|
+
groupId[fieldAlias] = `$${field}`;
|
|
342
390
|
}
|
|
343
391
|
});
|
|
344
392
|
// Construir la proyección final para campos de fecha
|
|
345
393
|
groupFields.forEach(field => {
|
|
394
|
+
const fieldAlias = groupFieldAliases.get(field);
|
|
346
395
|
if (dateFields.has(field)) {
|
|
347
396
|
if (groupFields.length === 1) {
|
|
348
397
|
finalProjectFields[field] = `$_id`;
|
|
349
398
|
}
|
|
350
399
|
else {
|
|
351
|
-
finalProjectFields[field] = `$_id.${
|
|
400
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`;
|
|
352
401
|
}
|
|
353
402
|
}
|
|
354
403
|
else if (!refFields.has(field)) {
|
|
@@ -356,7 +405,7 @@ class AbstractMongoRepository {
|
|
|
356
405
|
finalProjectFields[field] = `$_id`;
|
|
357
406
|
}
|
|
358
407
|
else {
|
|
359
|
-
finalProjectFields[field] = `$_id.${
|
|
408
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`;
|
|
360
409
|
}
|
|
361
410
|
}
|
|
362
411
|
});
|
|
@@ -369,27 +418,41 @@ class AbstractMongoRepository {
|
|
|
369
418
|
});
|
|
370
419
|
if (groupFields.length === 1) {
|
|
371
420
|
const field = groupFields[0];
|
|
372
|
-
|
|
421
|
+
const fieldAlias = groupFieldAliases.get(field);
|
|
422
|
+
groupStage._id = dateFields.has(field) ? getDateFormat(field, dateFormat) : groupId[fieldAlias];
|
|
373
423
|
}
|
|
374
424
|
else if (groupFields.length > 1) {
|
|
375
425
|
groupStage._id = groupId;
|
|
376
426
|
}
|
|
377
427
|
const pipeline = [
|
|
378
428
|
{ $match: query },
|
|
429
|
+
...preGroupStages,
|
|
379
430
|
{
|
|
380
431
|
$group: groupStage
|
|
381
432
|
}
|
|
382
433
|
];
|
|
383
434
|
// Solo agregar lookups si hay campos de referencia
|
|
384
|
-
if (
|
|
385
|
-
pipeline.push(...
|
|
435
|
+
if (postGroupStages.length > 0) {
|
|
436
|
+
pipeline.push(...postGroupStages);
|
|
386
437
|
}
|
|
387
438
|
pipeline.push({
|
|
388
439
|
$project: finalProjectFields
|
|
389
440
|
}, { $sort: { count: -1 } });
|
|
390
441
|
// console.log("pipeline", JSON.stringify(pipeline, null, 2))
|
|
391
442
|
const result = await this._model.aggregate(pipeline).exec();
|
|
392
|
-
return result
|
|
443
|
+
return result.map((item) => {
|
|
444
|
+
const normalizedItem = { ...item };
|
|
445
|
+
groupFields.forEach(field => {
|
|
446
|
+
if (!field.includes('.') || normalizedItem[field] !== undefined) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const nestedValue = this.getNestedValue(normalizedItem, field);
|
|
450
|
+
if (nestedValue !== undefined) {
|
|
451
|
+
normalizedItem[field] = nestedValue;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
return normalizedItem;
|
|
455
|
+
});
|
|
393
456
|
}
|
|
394
457
|
}
|
|
395
458
|
export default AbstractMongoRepository;
|
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.3",
|
|
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.3",
|
|
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": "f3cc4a70c5f96cb4a5de6da037f7dd0d3eed6202"
|
|
51
51
|
}
|
|
@@ -30,6 +30,10 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
30
30
|
protected _populateFields: string[] | PopulateOptions[] = []
|
|
31
31
|
protected _lean: boolean = true
|
|
32
32
|
|
|
33
|
+
private getNestedValue(source: any, path: string): any {
|
|
34
|
+
return path.split('.').reduce((value, key) => value == null ? undefined : value[key], source)
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
assertId(id: string): void {
|
|
34
38
|
if (!mongoose.Types.ObjectId.isValid(id)) {
|
|
35
39
|
console.log(`Invalid ID: ${id} is not a valid ObjectId.`)
|
|
@@ -337,18 +341,32 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
337
341
|
|
|
338
342
|
// Construir el objeto de agrupación dinámicamente
|
|
339
343
|
const groupId: any = {}
|
|
340
|
-
const
|
|
344
|
+
const preGroupStages: any[] = []
|
|
345
|
+
const postGroupStages: any[] = []
|
|
341
346
|
const finalProjectFields: any = {count: 1, _id: 0}
|
|
342
347
|
const refFields = new Set<string>()
|
|
343
348
|
const dateFields = new Set<string>()
|
|
344
349
|
const numericFields = new Set<string>()
|
|
345
350
|
const groupFields: string[] = []
|
|
351
|
+
const groupFieldAliases = new Map<string, string>()
|
|
352
|
+
const preGroupLookupAliases = new Map<string, string>()
|
|
346
353
|
const numericInstances = new Set(['Number', 'Decimal128', 'Double', 'Int32', 'Long', 'BigInt'])
|
|
347
354
|
const totalGroupFields = fields.filter(field => {
|
|
348
355
|
const schemaPath = schema.path(field)
|
|
349
356
|
return !(schemaPath && numericInstances.has(schemaPath.instance))
|
|
350
357
|
}).length
|
|
351
358
|
|
|
359
|
+
const getGroupFieldAlias = (field: string): string => {
|
|
360
|
+
const existingAlias = groupFieldAliases.get(field)
|
|
361
|
+
if (existingAlias) {
|
|
362
|
+
return existingAlias
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const newAlias = `field_${groupFieldAliases.size}`
|
|
366
|
+
groupFieldAliases.set(field, newAlias)
|
|
367
|
+
return newAlias
|
|
368
|
+
}
|
|
369
|
+
|
|
352
370
|
// Función para obtener el formato de fecha según el nivel de granularidad
|
|
353
371
|
const getDateFormat = (field: string, format: string) => {
|
|
354
372
|
const formats = {
|
|
@@ -404,8 +422,43 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
404
422
|
return formats[format] || formats['day']
|
|
405
423
|
}
|
|
406
424
|
|
|
425
|
+
const ensureLookupForReferencedField = (field: string, refModel: string): string => {
|
|
426
|
+
const existingAlias = preGroupLookupAliases.get(field)
|
|
427
|
+
if (existingAlias) {
|
|
428
|
+
return existingAlias
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const refModelInstance = mongoose.model(refModel)
|
|
432
|
+
const collectionName = refModelInstance.collection.name
|
|
433
|
+
const lookupAlias = `${field}_groupByRef`
|
|
434
|
+
|
|
435
|
+
preGroupStages.push({
|
|
436
|
+
$lookup: {
|
|
437
|
+
from: collectionName,
|
|
438
|
+
localField: field,
|
|
439
|
+
foreignField: '_id',
|
|
440
|
+
as: lookupAlias
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
preGroupStages.push({
|
|
445
|
+
$unwind: {
|
|
446
|
+
path: `$${lookupAlias}`,
|
|
447
|
+
preserveNullAndEmptyArrays: true
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
preGroupLookupAliases.set(field, lookupAlias)
|
|
452
|
+
return lookupAlias
|
|
453
|
+
}
|
|
454
|
+
|
|
407
455
|
fields.forEach(field => {
|
|
408
456
|
const schemaPath = schema.path(field)
|
|
457
|
+
const fieldAlias = getGroupFieldAlias(field)
|
|
458
|
+
const fieldParts = field.split('.')
|
|
459
|
+
const rootField = fieldParts[0]
|
|
460
|
+
const nestedFieldPath = fieldParts.slice(1).join('.')
|
|
461
|
+
const rootSchemaPath = rootField === field ? schemaPath : schema.path(rootField)
|
|
409
462
|
|
|
410
463
|
// Verificar si el campo es numérico: se agregará con $sum y no formará parte de la clave de agrupación
|
|
411
464
|
if (schemaPath && numericInstances.has(schemaPath.instance)) {
|
|
@@ -419,12 +472,11 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
419
472
|
// Verificar si el campo es de tipo Date
|
|
420
473
|
if (schemaPath && schemaPath.instance === 'Date') {
|
|
421
474
|
dateFields.add(field)
|
|
422
|
-
groupId[
|
|
475
|
+
groupId[fieldAlias] = getDateFormat(field, dateFormat)
|
|
423
476
|
}
|
|
424
477
|
// Verificar si el campo es una referencia
|
|
425
478
|
else if (schemaPath && schemaPath.options && schemaPath.options.ref) {
|
|
426
479
|
const refModel = schemaPath.options.ref
|
|
427
|
-
const fieldName = field
|
|
428
480
|
|
|
429
481
|
refFields.add(field)
|
|
430
482
|
|
|
@@ -433,47 +485,51 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
433
485
|
const collectionName = refModelInstance.collection.name
|
|
434
486
|
|
|
435
487
|
// Determinar el campo local correcto según si es un solo campo o múltiples
|
|
436
|
-
const localField = totalGroupFields === 1 ? '_id' : `_id.${
|
|
488
|
+
const localField = totalGroupFields === 1 ? '_id' : `_id.${fieldAlias}`
|
|
437
489
|
|
|
438
|
-
|
|
490
|
+
postGroupStages.push({
|
|
439
491
|
$lookup: {
|
|
440
492
|
from: collectionName,
|
|
441
493
|
localField: localField,
|
|
442
494
|
foreignField: '_id',
|
|
443
|
-
as: `${
|
|
495
|
+
as: `${fieldAlias}_populated`
|
|
444
496
|
}
|
|
445
497
|
})
|
|
446
498
|
|
|
447
499
|
// Unwind para convertir el array en objeto único
|
|
448
|
-
|
|
500
|
+
postGroupStages.push({
|
|
449
501
|
$unwind: {
|
|
450
|
-
path: `$${
|
|
502
|
+
path: `$${fieldAlias}_populated`,
|
|
451
503
|
preserveNullAndEmptyArrays: true
|
|
452
504
|
}
|
|
453
505
|
})
|
|
454
506
|
|
|
455
507
|
// En la proyección final, usar el objeto poblado
|
|
456
|
-
finalProjectFields[field] = `$${
|
|
457
|
-
groupId[
|
|
508
|
+
finalProjectFields[field] = `$${fieldAlias}_populated`
|
|
509
|
+
groupId[fieldAlias] = `$${field}`
|
|
510
|
+
} else if (nestedFieldPath && rootSchemaPath && rootSchemaPath.options && rootSchemaPath.options.ref) {
|
|
511
|
+
const lookupAlias = ensureLookupForReferencedField(rootField, rootSchemaPath.options.ref)
|
|
512
|
+
groupId[fieldAlias] = `$${lookupAlias}.${nestedFieldPath}`
|
|
458
513
|
} else {
|
|
459
514
|
// Si no es una referencia ni fecha, usar el valor directo
|
|
460
|
-
groupId[
|
|
515
|
+
groupId[fieldAlias] = `$${field}`
|
|
461
516
|
}
|
|
462
517
|
})
|
|
463
518
|
|
|
464
519
|
// Construir la proyección final para campos de fecha
|
|
465
520
|
groupFields.forEach(field => {
|
|
521
|
+
const fieldAlias = groupFieldAliases.get(field) as string
|
|
466
522
|
if (dateFields.has(field)) {
|
|
467
523
|
if (groupFields.length === 1) {
|
|
468
524
|
finalProjectFields[field] = `$_id`
|
|
469
525
|
} else {
|
|
470
|
-
finalProjectFields[field] = `$_id.${
|
|
526
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`
|
|
471
527
|
}
|
|
472
528
|
} else if (!refFields.has(field)) {
|
|
473
529
|
if (groupFields.length === 1) {
|
|
474
530
|
finalProjectFields[field] = `$_id`
|
|
475
531
|
} else {
|
|
476
|
-
finalProjectFields[field] = `$_id.${
|
|
532
|
+
finalProjectFields[field] = `$_id.${fieldAlias}`
|
|
477
533
|
}
|
|
478
534
|
}
|
|
479
535
|
})
|
|
@@ -489,21 +545,23 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
489
545
|
|
|
490
546
|
if (groupFields.length === 1) {
|
|
491
547
|
const field = groupFields[0]
|
|
492
|
-
|
|
548
|
+
const fieldAlias = groupFieldAliases.get(field) as string
|
|
549
|
+
groupStage._id = dateFields.has(field) ? getDateFormat(field, dateFormat) : groupId[fieldAlias]
|
|
493
550
|
} else if (groupFields.length > 1) {
|
|
494
551
|
groupStage._id = groupId
|
|
495
552
|
}
|
|
496
553
|
|
|
497
554
|
const pipeline: any[] = [
|
|
498
555
|
{$match: query},
|
|
556
|
+
...preGroupStages,
|
|
499
557
|
{
|
|
500
558
|
$group: groupStage
|
|
501
559
|
}
|
|
502
560
|
]
|
|
503
561
|
|
|
504
562
|
// Solo agregar lookups si hay campos de referencia
|
|
505
|
-
if (
|
|
506
|
-
pipeline.push(...
|
|
563
|
+
if (postGroupStages.length > 0) {
|
|
564
|
+
pipeline.push(...postGroupStages)
|
|
507
565
|
}
|
|
508
566
|
|
|
509
567
|
pipeline.push(
|
|
@@ -514,7 +572,23 @@ class AbstractMongoRepository<T, C, U> implements IDraxCrud<T, C, U> {
|
|
|
514
572
|
)
|
|
515
573
|
// console.log("pipeline", JSON.stringify(pipeline, null, 2))
|
|
516
574
|
const result = await this._model.aggregate(pipeline).exec()
|
|
517
|
-
|
|
575
|
+
|
|
576
|
+
return result.map((item: any) => {
|
|
577
|
+
const normalizedItem = {...item}
|
|
578
|
+
|
|
579
|
+
groupFields.forEach(field => {
|
|
580
|
+
if (!field.includes('.') || normalizedItem[field] !== undefined) {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const nestedValue = this.getNestedValue(normalizedItem, field)
|
|
585
|
+
if (nestedValue !== undefined) {
|
|
586
|
+
normalizedItem[field] = nestedValue
|
|
587
|
+
}
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
return normalizedItem
|
|
591
|
+
})
|
|
518
592
|
}
|
|
519
593
|
}
|
|
520
594
|
|
|
@@ -561,6 +561,122 @@ 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
|
+
|
|
630
|
+
it("should groupBy a single embedded nested field", async () => {
|
|
631
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
632
|
+
await testSetup.dropCollection('Person')
|
|
633
|
+
|
|
634
|
+
const entityData: IPersonBase[] = [
|
|
635
|
+
{
|
|
636
|
+
fullname: "Argentina One",
|
|
637
|
+
address: { street: "Main St", country: "Argentina", city: "Buenos Aires" }
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
fullname: "Argentina Two",
|
|
641
|
+
address: { street: "Second St", country: "Argentina", city: "Cordoba" }
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
fullname: "Chile One",
|
|
645
|
+
address: { street: "Third St", country: "Chile", city: "Santiago" }
|
|
646
|
+
}
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
for (const data of entityData) {
|
|
650
|
+
const createResp = await testSetup.fastifyInstance.inject({
|
|
651
|
+
method: 'POST',
|
|
652
|
+
url: '/api/person',
|
|
653
|
+
payload: data,
|
|
654
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
expect(createResp.statusCode).toBe(200)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const groupResp = await testSetup.fastifyInstance.inject({
|
|
661
|
+
method: 'GET',
|
|
662
|
+
url: '/api/person/group-by?fields=address.country',
|
|
663
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
const groupResult = await groupResp.json()
|
|
667
|
+
expect(groupResp.statusCode).toBe(200)
|
|
668
|
+
expect(groupResult).toEqual(expect.arrayContaining([
|
|
669
|
+
expect.objectContaining({
|
|
670
|
+
'address.country': 'Argentina',
|
|
671
|
+
count: 2
|
|
672
|
+
}),
|
|
673
|
+
expect.objectContaining({
|
|
674
|
+
'address.country': 'Chile',
|
|
675
|
+
count: 1
|
|
676
|
+
})
|
|
677
|
+
]))
|
|
678
|
+
})
|
|
679
|
+
|
|
564
680
|
// 9. Error Handling - Not Found
|
|
565
681
|
it("should handle error responses correctly when person is not found", async () => {
|
|
566
682
|
const { accessToken } = await testSetup.rootUserLogin()
|