Skip to content

Commit e27feca

Browse files
feat(filter): add possibility to filter on smart field (#478)
2 parents 7be71d4 + e0b9e36 commit e27feca

File tree

5 files changed

+341
-22
lines changed

5 files changed

+341
-22
lines changed

jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
module.exports = {
22
testEnvironment: 'node',
3+
collectCoverageFrom: [
4+
'src/**/*.{ts,js}',
5+
],
6+
setupFilesAfterEnv: [
7+
'jest-extended',
8+
],
39
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"eslint-plugin-sonarjs": "0.5.0",
5555
"husky": "4.2.5",
5656
"jest": "26.0.1",
57+
"jest-extended": "0.11.5",
5758
"mongoose": "5.8.1",
5859
"mongoose-fixture-loader": "1.0.2",
5960
"onchange": "6.0.0",

src/services/filters-parser.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import utils from '../utils/schema';
66
const AGGREGATOR_OPERATORS = ['and', 'or'];
77

88
function FiltersParser(model, timezone, options) {
9-
const schema = Interface.Schemas.schemas[utils.getModelName(model)];
9+
const modelSchema = Interface.Schemas.schemas[utils.getModelName(model)];
1010

1111
const parseInteger = (value) => Number.parseInt(value, 10);
1212
const parseDate = (value) => new Date(value);
@@ -40,7 +40,7 @@ function FiltersParser(model, timezone, options) {
4040
return { [aggregatorOperator]: formatedConditions };
4141
};
4242

43-
this.formatCondition = async (condition) => {
43+
this._ensureIsValidCondition = (condition) => {
4444
if (_.isEmpty(condition)) {
4545
throw new InvalidFiltersFormatError('Empty condition in filter');
4646
}
@@ -55,6 +55,37 @@ function FiltersParser(model, timezone, options) {
5555
|| _.isUndefined(condition.value)) {
5656
throw new InvalidFiltersFormatError('Invalid condition format');
5757
}
58+
};
59+
60+
this.getSmartFieldCondition = async (condition) => {
61+
const fieldFound = modelSchema.fields.find((field) => field.field === condition.field);
62+
63+
if (!fieldFound.filter) {
64+
throw new Error(`"filter" method missing on smart field "${fieldFound.field}"`);
65+
}
66+
67+
const formattedCondition = await Promise.resolve(fieldFound
68+
.filter({
69+
where: await this.formatOperatorValue(
70+
condition.field,
71+
condition.operator,
72+
condition.value,
73+
),
74+
condition,
75+
}));
76+
if (!formattedCondition) {
77+
throw new Error(`"filter" method on smart field "${fieldFound.field}" must return a condition`);
78+
}
79+
return formattedCondition;
80+
};
81+
82+
this.formatCondition = async (condition) => {
83+
this._ensureIsValidCondition(condition);
84+
85+
if (this.isSmartField(modelSchema, condition.field)) {
86+
return this.getSmartFieldCondition(condition);
87+
}
88+
5889
const formatedField = this.formatField(condition.field);
5990

6091
return {
@@ -81,10 +112,10 @@ function FiltersParser(model, timezone, options) {
81112
const [fieldName, subfieldName] = key.split(':');
82113

83114
// NOTICE: Mongoose Aggregate don't parse the value automatically.
84-
let field = _.find(schema.fields, { field: fieldName });
115+
let field = _.find(modelSchema.fields, { field: fieldName });
85116

86117
if (!field) {
87-
throw new InvalidFiltersFormatError(`Field '${fieldName}' not found on collection '${schema.name}'`);
118+
throw new InvalidFiltersFormatError(`Field '${fieldName}' not found on collection '${modelSchema.name}'`);
88119
}
89120

90121
const isEmbeddedField = !!field.type.fields;
@@ -149,6 +180,11 @@ function FiltersParser(model, timezone, options) {
149180

150181
this.formatField = (field) => field.replace(':', '.');
151182

183+
this.isSmartField = (schema, fieldName) => {
184+
const fieldFound = schema.fields.find((field) => field.field === fieldName);
185+
return !!fieldFound && !!fieldFound.isVirtual;
186+
};
187+
152188
this.getAssociations = async (filtersString) => BaseFiltersParser.getAssociations(filtersString);
153189

154190
this.formatAggregationForReferences = (aggregator, conditions) => ({ aggregator, conditions });
@@ -175,10 +211,10 @@ function FiltersParser(model, timezone, options) {
175211
}
176212

177213
// Mongoose Aggregate don't parse the value automatically.
178-
const field = _.find(schema.fields, { field: fieldName });
214+
const field = _.find(modelSchema.fields, { field: fieldName });
179215

180216
if (!field) {
181-
throw new InvalidFiltersFormatError(`Field '${fieldName}' not found on collection '${schema.name}'`);
217+
throw new InvalidFiltersFormatError(`Field '${fieldName}' not found on collection '${modelSchema.name}'`);
182218
}
183219

184220
if (!field.reference) {

test/tests/services/filters-parser.test.js

Lines changed: 138 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { InvalidFiltersFormatError, NoMatchingOperatorError } from '../../../src
77

88
describe('service > filters-parser', () => {
99
let IslandModel;
10+
let islandForestSchema;
1011
let defaultParser;
1112
const timezone = 'Europe/Paris';
1213

@@ -16,22 +17,24 @@ describe('service > filters-parser', () => {
1617
};
1718

1819
beforeAll(() => {
20+
islandForestSchema = {
21+
name: 'Island',
22+
idField: 'id',
23+
primaryKeys: ['id'],
24+
isCompositePrimary: false,
25+
searchFields: ['name'],
26+
fields: [
27+
{ field: 'id', type: 'Number' },
28+
{ field: 'name', type: 'String' },
29+
{ field: 'size', type: 'Number' },
30+
{ field: 'isBig', type: 'Boolean' },
31+
{ field: 'inhabitedOn', type: 'Date' },
32+
],
33+
};
34+
1935
Interface.Schemas = {
2036
schemas: {
21-
Island: {
22-
name: 'Island',
23-
idField: 'id',
24-
primaryKeys: ['id'],
25-
isCompositePrimary: false,
26-
searchFields: ['name'],
27-
fields: [
28-
{ field: 'id', type: 'Number' },
29-
{ field: 'name', type: 'String' },
30-
{ field: 'size', type: 'Number' },
31-
{ field: 'isBig', type: 'Boolean' },
32-
{ field: 'inhabitedOn', type: 'Date' },
33-
],
34-
},
37+
Island: islandForestSchema,
3538
},
3639
};
3740

@@ -132,6 +135,88 @@ describe('service > filters-parser', () => {
132135
await expect(defaultParser.formatCondition({ field: 'toto', operator: 'contains', value: 'it' })).rejects.toThrow(InvalidFiltersFormatError);
133136
});
134137
});
138+
139+
describe('on a smart field', () => {
140+
describe('with filter method not defined', () => {
141+
it('should throw an error', async () => {
142+
expect.assertions(1);
143+
144+
const oldFields = islandForestSchema.fields;
145+
islandForestSchema.fields = [{
146+
field: 'smart name',
147+
type: 'String',
148+
isVirtual: true,
149+
get() {},
150+
}];
151+
152+
await expect(defaultParser.formatCondition({
153+
field: 'smart name',
154+
operator: 'present',
155+
value: null,
156+
})).rejects.toThrow('"filter" method missing on smart field "smart name"');
157+
158+
islandForestSchema.fields = oldFields;
159+
});
160+
});
161+
162+
describe('with filter method defined', () => {
163+
describe('when filter method return null or undefined', () => {
164+
it('should throw an error', async () => {
165+
expect.assertions(1);
166+
167+
const oldFields = islandForestSchema.fields;
168+
islandForestSchema.fields = [{
169+
field: 'smart name',
170+
type: 'String',
171+
isVirtual: true,
172+
get() {},
173+
filter() {},
174+
}];
175+
176+
await expect(defaultParser.formatCondition({
177+
field: 'smart name',
178+
operator: 'present',
179+
value: null,
180+
})).rejects.toThrow('"filter" method on smart field "smart name" must return a condition');
181+
182+
islandForestSchema.fields = oldFields;
183+
});
184+
});
185+
186+
describe('when filter method return a condition', () => {
187+
it('should return the condition', async () => {
188+
expect.assertions(4);
189+
190+
const where = { id: 1 };
191+
const oldFields = islandForestSchema.fields;
192+
islandForestSchema.fields = [{
193+
field: 'smart name',
194+
type: 'String',
195+
isVirtual: true,
196+
get() {},
197+
filter: jest.fn(() => where),
198+
}];
199+
200+
const condition = {
201+
field: 'smart name',
202+
operator: 'present',
203+
value: null,
204+
};
205+
expect(await defaultParser.formatCondition(condition)).toStrictEqual(where);
206+
expect(islandForestSchema.fields[0].filter.mock.calls).toHaveLength(1);
207+
expect(islandForestSchema.fields[0].filter.mock.calls[0]).toHaveLength(1);
208+
expect(islandForestSchema.fields[0].filter.mock.calls[0][0]).toStrictEqual({
209+
where: {
210+
$exists: true,
211+
$ne: null,
212+
},
213+
condition,
214+
});
215+
islandForestSchema.fields = oldFields;
216+
});
217+
});
218+
});
219+
});
135220
});
136221

137222
describe('formatAggregatorOperator function', () => {
@@ -182,6 +267,45 @@ describe('service > filters-parser', () => {
182267
});
183268
});
184269

270+
describe('isSmartField', () => {
271+
describe('on a unknown field', () => {
272+
it('should return false', () => {
273+
expect.assertions(1);
274+
const schemaToTest = { fields: [] };
275+
276+
expect(defaultParser.isSmartField(schemaToTest, 'unknown')).toBeFalse();
277+
});
278+
});
279+
280+
describe('on a non smart field', () => {
281+
it('should return false', () => {
282+
expect.assertions(1);
283+
const schemaToTest = {
284+
fields: [{
285+
field: 'name',
286+
isVirtual: false,
287+
}],
288+
};
289+
290+
expect(defaultParser.isSmartField(schemaToTest, 'name')).toBeFalse();
291+
});
292+
});
293+
294+
describe('on a smart field', () => {
295+
it('should return true', () => {
296+
expect.assertions(1);
297+
const schemaToTest = {
298+
fields: [{
299+
field: 'name',
300+
isVirtual: true,
301+
}],
302+
};
303+
304+
expect(defaultParser.isSmartField(schemaToTest, 'name')).toBeTrue();
305+
});
306+
});
307+
});
308+
185309
describe('formatField function', () => {
186310
it('should format default field correctly', () => {
187311
expect.assertions(1);

0 commit comments

Comments
 (0)