Skip to content

Commit 9be668c

Browse files
authored
fix(fields-flattener): flattened nested references are now correctly fetched (#749)
1 parent 1e75ed6 commit 9be668c

File tree

4 files changed

+234
-8
lines changed

4 files changed

+234
-8
lines changed

src/services/flattener.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,19 @@ module.exports = class Flattener {
234234
.filter((field) => Flattener._isFieldFlattened(field.field))
235235
.map((field) => field.field);
236236
}
237+
238+
static getFlattenedReferenceFieldsFromParams(collectionName, fields) {
239+
if (!collectionName || !fields) {
240+
return [];
241+
}
242+
243+
const flattenedReferences = Object.keys(fields)
244+
.filter((field) => Flattener._isFieldFlattened(field));
245+
246+
const collectionReferenceFields = (Interface.Schemas.schemas[collectionName]?.fields || [])
247+
.filter(({ reference }) => reference);
248+
249+
return flattenedReferences.filter((flattenedReference) =>
250+
collectionReferenceFields.some(({ field }) => field === flattenedReference));
251+
}
237252
};

src/services/query-builder.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,27 @@ class QueryBuilder {
5555

5656
addJoinToQuery(field, joinQuery) {
5757
if (field.reference && !field.isVirtual && !field.integration) {
58-
if (QueryBuilder._joinAlreadyExists(field, joinQuery)) {
59-
return this;
60-
}
58+
if (QueryBuilder._joinAlreadyExists(field, joinQuery)) return this;
6159

6260
const referencedKey = utils.getReferenceField(field.reference);
6361
const subModel = utils.getReferenceModel(this._opts, field.reference);
62+
const unflattenedFieldName = Flattener.unflattenFieldName(field.field);
63+
6464
joinQuery.push({
6565
$lookup: {
6666
from: subModel.collection.name,
67-
localField: field.field,
67+
localField: unflattenedFieldName,
6868
foreignField: referencedKey,
69-
as: field.field,
69+
as: unflattenedFieldName,
7070
},
7171
});
7272

73-
const fieldPath = field.field && this._model.schema.path(field.field);
73+
const fieldPath = unflattenedFieldName && this._model.schema.path(unflattenedFieldName);
74+
7475
if (fieldPath && fieldPath.instance !== 'Array') {
7576
joinQuery.push({
7677
$unwind: {
77-
path: `$${field.field}`,
78+
path: `$${unflattenedFieldName}`,
7879
preserveNullAndEmptyArrays: true,
7980
},
8081
});
@@ -85,7 +86,12 @@ class QueryBuilder {
8586
}
8687

8788
async joinAllReferences(jsonQuery, alreadyJoinedQuery) {
88-
const fieldNames = await this.getFieldNamesRequested();
89+
let fieldNames = await this.getFieldNamesRequested();
90+
const flattenReferenceNames = Flattener
91+
.getFlattenedReferenceFieldsFromParams(this._model.collection.name, this._params.fields);
92+
93+
fieldNames = flattenReferenceNames.concat(fieldNames);
94+
8995
this._schema.fields.forEach((field) => {
9096
if ((fieldNames && !fieldNames.includes(field.field))
9197
|| QueryBuilder._joinAlreadyExists(field, alreadyJoinedQuery)) {

test/tests/services/flattener.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,4 +680,159 @@ describe('service > Flattener', () => {
680680
expect(result).toStrictEqual([`engine${FLATTEN_SEPARATOR}horsePower`, `engine${FLATTEN_SEPARATOR}identification${FLATTEN_SEPARATOR}manufacturer`]);
681681
});
682682
});
683+
684+
describe('getFlattenedReferenceFieldsFromFieldNames', () => {
685+
beforeEach(() => {
686+
Interface.Schemas.schemas = {
687+
cars: {
688+
fields: [{
689+
field: 'name',
690+
type: 'String',
691+
defaultValue: null,
692+
enums: null,
693+
integration: null,
694+
isFilterable: true,
695+
isPrimaryKey: false,
696+
isReadOnly: false,
697+
isRequired: false,
698+
isSortable: true,
699+
isVirtual: false,
700+
reference: null,
701+
inverseOf: null,
702+
validations: [],
703+
}, {
704+
field: 'engine@@@horsePower',
705+
type: 'String',
706+
defaultValue: null,
707+
enums: null,
708+
integration: null,
709+
isFilterable: true,
710+
isPrimaryKey: false,
711+
isReadOnly: false,
712+
isRequired: false,
713+
isSortable: true,
714+
isVirtual: false,
715+
reference: null,
716+
inverseOf: null,
717+
validations: [],
718+
}, {
719+
field: 'engine@@@identification@@@manufacturer',
720+
type: 'String',
721+
defaultValue: null,
722+
enums: null,
723+
integration: null,
724+
isFilterable: true,
725+
isPrimaryKey: false,
726+
isReadOnly: false,
727+
isRequired: false,
728+
isSortable: true,
729+
isVirtual: false,
730+
reference: 'companies._id',
731+
inverseOf: null,
732+
validations: [],
733+
}, {
734+
field: 'engine@@@owner',
735+
type: 'String',
736+
defaultValue: null,
737+
enums: null,
738+
integration: null,
739+
isFilterable: true,
740+
isPrimaryKey: false,
741+
isReadOnly: false,
742+
isRequired: false,
743+
isSortable: true,
744+
isVirtual: false,
745+
reference: 'companies._id',
746+
inverseOf: null,
747+
validations: [],
748+
}],
749+
},
750+
};
751+
});
752+
753+
afterEach(() => {
754+
Interface.Schemas = {};
755+
});
756+
757+
describe('when not fields are actually present on the collection', () => {
758+
it('should return an empty array', () => {
759+
expect.assertions(1);
760+
761+
Interface.Schemas.schemas.cars.fields = [];
762+
763+
const referenceNestedFields = Flattener
764+
.getFlattenedReferenceFieldsFromParams('cars', {});
765+
766+
expect(referenceNestedFields).toStrictEqual([]);
767+
});
768+
});
769+
770+
describe('when no references has been requested in the fields', () => {
771+
it('should return an empty array', () => {
772+
expect.assertions(1);
773+
774+
const fields = {
775+
cars: ['name'],
776+
};
777+
778+
const referenceNestedFields = Flattener
779+
.getFlattenedReferenceFieldsFromParams('cars', fields);
780+
781+
expect(referenceNestedFields).toStrictEqual([]);
782+
});
783+
});
784+
785+
describe('when requested reference is not part of the collection', () => {
786+
it('should not include the wrong reference', () => {
787+
expect.assertions(1);
788+
789+
const fields = {
790+
cars: ['name'],
791+
'cars@@@notexisting': ['name'],
792+
};
793+
794+
const referenceNestedFields = Flattener
795+
.getFlattenedReferenceFieldsFromParams('cars', fields);
796+
797+
expect(referenceNestedFields).toStrictEqual([]);
798+
});
799+
});
800+
801+
describe('when requested references belongs to the collection', () => {
802+
it('should include the references', () => {
803+
expect.assertions(1);
804+
805+
const fields = {
806+
cars: ['name'],
807+
'engine@@@identification@@@manufacturer': ['name'],
808+
'engine@@@owner': ['name'],
809+
};
810+
811+
const referenceNestedFields = Flattener
812+
.getFlattenedReferenceFieldsFromParams('cars', fields);
813+
814+
expect(referenceNestedFields).toStrictEqual([
815+
'engine@@@identification@@@manufacturer',
816+
'engine@@@owner',
817+
]);
818+
});
819+
});
820+
821+
describe('when requested references are actually not references', () => {
822+
it('should not include non reference fields', () => {
823+
expect.assertions(1);
824+
825+
const fields = {
826+
cars: ['name'],
827+
'engine@@@horsePower': ['name'],
828+
'engine@@@owner': ['name'],
829+
};
830+
831+
const referenceNestedFields = Flattener
832+
.getFlattenedReferenceFieldsFromParams('cars', fields);
833+
834+
expect(referenceNestedFields).toStrictEqual(['engine@@@owner']);
835+
});
836+
});
837+
});
683838
});

test/tests/services/query-builder.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const FLATTEN_SEPARATOR = '@@@';
99
describe('service > query-builder', () => {
1010
let TreeModel;
1111
let LumberJackModel;
12+
let CarsModel;
1213

1314
const options = {
1415
Mongoose: mongoose,
@@ -18,6 +19,17 @@ describe('service > query-builder', () => {
1819
beforeAll(async () => {
1920
Interface.Schemas = {
2021
schemas: {
22+
Cars: {
23+
name: 'Cars',
24+
idField: 'id',
25+
primaryKeys: ['id'],
26+
isCompositePrimary: false,
27+
searchFields: ['name'],
28+
fields: [
29+
{ field: 'engine@@@owner', type: 'String', reference: 'LumberJack._id' },
30+
{ field: 'name', type: 'String' },
31+
],
32+
},
2133
LumberJack: {
2234
name: 'LumberJack',
2335
idField: 'id',
@@ -58,9 +70,17 @@ describe('service > query-builder', () => {
5870
age: { type: String },
5971
owner: { type: 'ObjectId' },
6072
});
73+
const CarsSchema = new mongoose.Schema({
74+
id: { type: Number },
75+
name: { type: String },
76+
engine: {
77+
owner: { type: 'ObjectId' },
78+
},
79+
});
6180

6281
LumberJackModel = mongoose.model('LumberJack', LumberJackSchema);
6382
TreeModel = mongoose.model('Tree', TreeSchema);
83+
CarsModel = mongoose.model('Cars', CarsSchema);
6484

6585
await Promise.all([LumberJackModel.deleteMany({}), TreeModel.deleteMany({})]);
6686
await Promise.all([
@@ -151,6 +171,36 @@ describe('service > query-builder', () => {
151171
expect(joins).toHaveLength(0);
152172
});
153173
});
174+
175+
describe('on flattened reference field', () => {
176+
it('should unflatten the field and add the join correctly', () => {
177+
expect.assertions(1);
178+
const queryBuilder = new QueryBuilder(CarsModel, {
179+
timezone: 'Europe/Paris',
180+
}, options);
181+
182+
const field = {
183+
field: 'engine@@@owner',
184+
displayName: 'engine@@@owner',
185+
type: 'String',
186+
reference: 'LumberJack._id',
187+
};
188+
189+
const expectedJoin = {
190+
$lookup: {
191+
from: 'lumberjacks',
192+
localField: 'engine.owner',
193+
foreignField: '_id',
194+
as: 'engine.owner',
195+
},
196+
};
197+
198+
const jsonQuery = [];
199+
queryBuilder.addJoinToQuery(field, jsonQuery);
200+
expect(jsonQuery[0])
201+
.toStrictEqual(expectedJoin);
202+
});
203+
});
154204
});
155205

156206
describe('addSortToQuery function', () => {

0 commit comments

Comments
 (0)