Skip to content

Commit a746050

Browse files
authored
[+] Performance optimization - Do not return fields that are hidden from the UI except if a smart field is displayed. (#274)
1 parent 9ee264e commit a746050

File tree

6 files changed

+235
-3
lines changed

6 files changed

+235
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44
### Changed
5+
- Performance optimization - In a request with no smart fields, do not return fields that are hidden from the UI.
56
- Technical - Upgrade `mongoose` devDependency to the latest version.
67

78
### Fixed

src/services/projection-builder.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class ProjectionBuilder {
2+
constructor(schema) {
3+
this.schemaSmartFields = schema
4+
&& schema.fields
5+
&& schema.fields.filter((field) => field.get).map((field) => field.field);
6+
}
7+
8+
// NOTICE: Convert a list of field names into a mongo $project structure.
9+
static convertToProjection(fieldsNames) {
10+
if (fieldsNames) {
11+
const fieldsObject = fieldsNames.reduce((fields, fieldName) => {
12+
fields[fieldName] = 1;
13+
return fields;
14+
}, {});
15+
return { $project: fieldsObject };
16+
}
17+
return null;
18+
}
19+
20+
// NOTICE: Perform the intersection between schema and request smart fields.
21+
findRequestSmartField(requestFieldsNames) {
22+
if (this.schemaSmartFields && requestFieldsNames) {
23+
return this.schemaSmartFields
24+
.filter((fieldName) => requestFieldsNames.includes(fieldName));
25+
}
26+
return [];
27+
}
28+
29+
getProjection(fieldNames) {
30+
const requestSmartFields = this.findRequestSmartField(fieldNames);
31+
if (requestSmartFields.length) return null;
32+
return ProjectionBuilder.convertToProjection(fieldNames);
33+
}
34+
}
35+
36+
module.exports = ProjectionBuilder;

src/services/query-builder.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import utils from '../utils/schema';
44
import Orm from '../utils/orm';
55
import SearchBuilder from './search-builder';
66
import FiltersParser from './filters-parser';
7+
import ProjectionBuilder from './projection-builder';
78

89
function QueryBuilder(model, params, opts) {
910
const schema = Interface.Schemas.schemas[utils.getModelName(model)];
1011
const searchBuilder = new SearchBuilder(model, opts, params, schema.searchFields);
1112
const filterParser = new FiltersParser(model, params.timezone, opts);
13+
const projectionBuilder = new ProjectionBuilder(schema);
1214

1315
const { filters } = params;
1416

@@ -35,6 +37,10 @@ function QueryBuilder(model, params, opts) {
3537
);
3638
};
3739

40+
this.addProjection = (jsonQuery) => this.getFieldNamesRequested()
41+
.then((fieldNames) => projectionBuilder.getProjection(fieldNames))
42+
.then((projection) => projection && jsonQuery.push(projection));
43+
3844
this.addJoinToQuery = (field, joinQuery) => {
3945
if (field.reference && !field.isVirtual && !field.integration) {
4046
if (this.joinAlreadyExists(field, joinQuery)) {
@@ -128,6 +134,11 @@ function QueryBuilder(model, params, opts) {
128134

129135
this.getQueryWithFiltersAndJoins = async (segment) => {
130136
const jsonQuery = [];
137+
await this.addFiltersAndJoins(jsonQuery, segment);
138+
return jsonQuery;
139+
};
140+
141+
this.addFiltersAndJoins = async (jsonQuery, segment) => {
131142
const conditions = [];
132143

133144
if (filters) {
@@ -150,7 +161,7 @@ function QueryBuilder(model, params, opts) {
150161
});
151162
}
152163

153-
return jsonQuery;
164+
return this;
154165
};
155166
}
156167

src/services/resources-getter.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ function ResourcesGetter(model, opts, params) {
2727

2828
this.perform = () => getSegmentCondition()
2929
.then(async (segment) => {
30-
const jsonQuery = await queryBuilder.getQueryWithFiltersAndJoins(segment);
30+
const jsonQuery = [];
31+
await queryBuilder.addProjection(jsonQuery);
32+
await queryBuilder.addFiltersAndJoins(jsonQuery, segment);
3133

3234
if (params.search) {
3335
fieldsSearched = queryBuilder.getFieldsSearched();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import ProjectionBuilder from '../../../src/services/projection-builder';
2+
3+
describe('service > projection-builder', () => {
4+
describe('findRequestSmartField', () => {
5+
it('should return the intersect between request fields and schema fields', () => {
6+
expect.assertions(1);
7+
8+
const schemaWithSmartFields = {
9+
fields: [
10+
{
11+
field: 'firstname',
12+
type: 'String',
13+
},
14+
{
15+
field: 'lastname',
16+
type: 'String',
17+
},
18+
{
19+
field: 'fullname',
20+
type: 'String',
21+
get: (doc) => `${doc.firstname} ${doc.lastname}`,
22+
},
23+
{
24+
field: 'otherSmartField',
25+
type: 'String',
26+
get: (doc) => `${doc.firstname} ${doc.lastname}`,
27+
},
28+
],
29+
};
30+
const projectionBuilder = new ProjectionBuilder(schemaWithSmartFields);
31+
const requestFields = ['fullname', 'firstname'];
32+
const expectedRequestSmartFields = ['fullname'];
33+
const actualRequestSmartFields = projectionBuilder.findRequestSmartField(requestFields);
34+
expect(actualRequestSmartFields).toStrictEqual(expectedRequestSmartFields);
35+
});
36+
});
37+
describe('convertToProjection', () => {
38+
it('should return a valid projection', () => {
39+
expect.assertions(1);
40+
41+
const expectedProjection = { $project: { one: 1, two: 1 } };
42+
const actualProjection = ProjectionBuilder.convertToProjection(['one', 'two']);
43+
44+
expect(actualProjection).toStrictEqual(expectedProjection);
45+
});
46+
});
47+
48+
describe('getProjection without requested smart fields', () => {
49+
it('should returns a valid projection', () => {
50+
expect.assertions(1);
51+
52+
const schemaWithSmartFields = {
53+
fields: [
54+
{
55+
field: 'firstname',
56+
type: 'String',
57+
},
58+
{
59+
field: 'lastname',
60+
type: 'String',
61+
},
62+
{
63+
field: 'fullname',
64+
type: 'String',
65+
get: (doc) => `${doc.firstname} ${doc.lastname}`,
66+
},
67+
],
68+
};
69+
const projectionBuilder = new ProjectionBuilder(schemaWithSmartFields);
70+
const expectedProjection = { $project: { firstname: 1, lastname: 1 } };
71+
const actualProjection = projectionBuilder.getProjection(['firstname', 'lastname']);
72+
73+
expect(actualProjection).toStrictEqual(expectedProjection);
74+
});
75+
});
76+
77+
describe('getProjection with requested smart fields', () => {
78+
it('should returns null', () => {
79+
expect.assertions(1);
80+
81+
const schemaWithSmartFields = {
82+
fields: [
83+
{
84+
field: 'firstname',
85+
type: 'String',
86+
},
87+
{
88+
field: 'lastname',
89+
type: 'String',
90+
},
91+
{
92+
field: 'fullname',
93+
type: 'String',
94+
get: (doc) => `${doc.firstname} ${doc.lastname}`,
95+
},
96+
],
97+
};
98+
const projectionBuilder = new ProjectionBuilder(schemaWithSmartFields);
99+
const expectedProjection = null;
100+
const actualProjection = projectionBuilder.getProjection(['fullname']);
101+
102+
expect(actualProjection).toStrictEqual(expectedProjection);
103+
});
104+
});
105+
});

test/tests/services/resources-getter.test.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import mongooseConnect from '../../utils/mongoose-connect';
77
describe('service > resources-getter', () => {
88
let OrderModel;
99
let UserModel;
10+
let FilmModel;
1011

1112
const options = {
1213
mongoose,
@@ -43,6 +44,19 @@ describe('service > resources-getter', () => {
4344
{ field: 'age', type: 'Number' },
4445
],
4546
},
47+
Film: {
48+
name: 'Film',
49+
fields: [
50+
{ field: '_id', type: 'String' },
51+
{ field: 'title', type: 'String' },
52+
{ field: 'duration', type: 'Number' },
53+
{
54+
field: 'description',
55+
type: 'String',
56+
get: (film) => `${film.title} ${film.duration}`,
57+
},
58+
],
59+
},
4660
},
4761
};
4862

@@ -60,11 +74,17 @@ describe('service > resources-getter', () => {
6074
name: { type: String },
6175
age: { type: Number },
6276
});
77+
const FilmSchema = mongoose.Schema({
78+
_id: { type: 'ObjectId' },
79+
title: { type: String },
80+
duration: { type: Number },
81+
});
6382

6483
OrderModel = mongoose.model('Order', OrderSchema);
6584
UserModel = mongoose.model('User', UserSchema);
85+
FilmModel = mongoose.model('Film', FilmSchema);
6686

67-
return Promise.all([OrderModel.remove({}), UserModel.remove({})]);
87+
return Promise.all([OrderModel.remove({}), UserModel.remove({}), FilmModel.remove({})]);
6888
})
6989
.then(() =>
7090
Promise.all([
@@ -95,6 +115,23 @@ describe('service > resources-getter', () => {
95115
name: 'Jacco Gardner',
96116
},
97117
]),
118+
loadFixture(FilmModel, [
119+
{
120+
_id: '41224d776a326fb40f000011',
121+
duration: 149,
122+
title: 'Terminator',
123+
},
124+
{
125+
_id: '41224d776a326fb40f000012',
126+
duration: 360,
127+
title: 'Titanic',
128+
},
129+
{
130+
_id: '41224d776a326fb40f000013',
131+
duration: 125,
132+
title: 'Matrix',
133+
},
134+
]),
98135
]));
99136
});
100137

@@ -273,4 +310,44 @@ describe('service > resources-getter', () => {
273310
});
274311
});
275312
});
313+
314+
describe('projection feature', () => {
315+
describe('with selected smartfield', () => {
316+
it('should return all fields', async () => {
317+
expect.assertions(3);
318+
const parameters = {
319+
fields: { films: 'description' },
320+
page: { number: '1', size: '15' },
321+
searchExtended: '0',
322+
timezone: '+01:00',
323+
};
324+
325+
const [result] = await new ResourcesGetter(FilmModel, options, parameters).perform();
326+
expect(result).toHaveLength(3);
327+
const titles = result.filter((film) => !!film.title);
328+
expect(titles).toHaveLength(3);
329+
const durations = result.filter((film) => !!film.duration);
330+
expect(durations).toHaveLength(3);
331+
});
332+
});
333+
334+
describe('without selected smartfield', () => {
335+
it('should return only selected fields', async () => {
336+
expect.assertions(3);
337+
const parameters = {
338+
fields: { films: 'title' },
339+
page: { number: '1', size: '15' },
340+
searchExtended: '0',
341+
timezone: '+01:00',
342+
};
343+
344+
const [result] = await new ResourcesGetter(FilmModel, options, parameters).perform();
345+
expect(result).toHaveLength(3);
346+
const titles = result.filter((film) => !!film.title);
347+
expect(titles).toHaveLength(3);
348+
const durations = result.filter((film) => !!film.duration);
349+
expect(durations).toHaveLength(0);
350+
});
351+
});
352+
});
276353
});

0 commit comments

Comments
 (0)