Skip to content

Commit 2bb7ee8

Browse files
authored
fix: filters on nested fields now works correctly (#763)
1 parent a1d6a91 commit 2bb7ee8

File tree

4 files changed

+296
-30
lines changed

4 files changed

+296
-30
lines changed

src/services/filters-parser.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ function FiltersParser(model, timezone, options) {
2727

2828
return value;
2929
};
30-
const parseArray = (value) => ({ $size: value });
3130
const parseOther = (value) => value;
3231

3332
this.operatorDateParser = new BaseOperatorDateParser({
@@ -64,37 +63,42 @@ function FiltersParser(model, timezone, options) {
6463
};
6564

6665
this.getParserForType = (type) => {
66+
const mongooseTypes = options.Mongoose.Schema.Types;
67+
6768
switch (type) {
68-
case 'Number': return parseInteger;
69-
case 'Date': return parseDate;
70-
case 'Boolean': return parseBoolean;
71-
case 'ObjectId': return parseObjectId;
72-
case Array.isArray(type): return parseArray;
69+
case 'Number':
70+
case Number:
71+
case mongooseTypes.Number:
72+
return parseInteger;
73+
case 'Date':
74+
case Date:
75+
case mongooseTypes.Date:
76+
return parseDate;
77+
case 'Boolean':
78+
case Boolean:
79+
case mongooseTypes.Boolean:
80+
return parseBoolean;
81+
case 'ObjectId':
82+
case mongooseTypes.ObjectId:
83+
return parseObjectId;
7384
default: return parseOther;
7485
}
7586
};
7687

7788
this.getParserForField = async (key) => {
7889
const [fieldName, subfieldName] = key.split(':');
7990

80-
// NOTICE: Mongoose Aggregate don't parse the value automatically.
81-
let field = SchemaUtils.getField(modelSchema, fieldName);
82-
let fieldDefinition = model.schema.paths[fieldName];
91+
const field = SchemaUtils.getField(modelSchema, fieldName);
8392

8493
if (!field) {
8594
throw new InvalidFiltersFormatError(`Field '${fieldName}' not found on collection '${modelSchema.name}'`);
8695
}
8796

88-
const isEmbeddedField = !!field.type.fields;
89-
if (isEmbeddedField) {
90-
field = SchemaUtils.getField(field.type, subfieldName);
91-
fieldDefinition = fieldDefinition[subfieldName];
92-
}
97+
const fieldPath = subfieldName ? `${fieldName}.${subfieldName}` : fieldName;
98+
const fieldType = utils.getNestedFieldType(model.schema, fieldPath);
9399

94-
if (!field) return (val) => val;
100+
if (!fieldType) return (val) => val;
95101

96-
const { ObjectId } = options.Mongoose.Schema.Types;
97-
const fieldType = fieldDefinition instanceof ObjectId ? 'ObjectId' : field.type;
98102
const parse = this.getParserForType(fieldType);
99103

100104
return (value) => {

src/utils/schema.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,29 @@ exports.getModelName = (model) => model.modelName;
1313

1414
// TODO: Remove nameOld attribute once the lianas versions older than 2.0.0 are minority
1515
exports.getModelNameOld = (model) => model.collection.name.replace(' ', '');
16+
17+
const getNestedFieldType = (mongooseSchema, nestedFieldPath) => {
18+
if (!mongooseSchema || !nestedFieldPath) return undefined;
19+
20+
const [currentFieldName, ...deepNestedFieldPath] = nestedFieldPath.split('.');
21+
22+
let nestedFieldDeclaration;
23+
24+
if (mongooseSchema.tree) {
25+
nestedFieldDeclaration = mongooseSchema.tree[currentFieldName];
26+
} else if (mongooseSchema.type) {
27+
nestedFieldDeclaration = mongooseSchema.type[currentFieldName];
28+
} else {
29+
nestedFieldDeclaration = mongooseSchema[currentFieldName];
30+
}
31+
32+
if (!nestedFieldDeclaration) return undefined;
33+
34+
if (!deepNestedFieldPath.length) {
35+
return nestedFieldDeclaration.type || nestedFieldDeclaration;
36+
}
37+
38+
return getNestedFieldType(nestedFieldDeclaration, deepNestedFieldPath?.join('.'));
39+
};
40+
41+
exports.getNestedFieldType = getNestedFieldType;

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

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('service > filters-parser', () => {
1515
Mongoose: mongoose,
1616
connections: { mongoose },
1717
};
18+
const { Types: schemaTypes } = mongoose.Schema;
1819
const { ObjectId } = mongoose.Types;
1920

2021
beforeAll(async () => {
@@ -65,7 +66,7 @@ describe('service > filters-parser', () => {
6566
await mongooseConnect();
6667
const IslandSchema = new mongoose.Schema({
6768
id: { type: Number },
68-
myObjectId: { type: ObjectId },
69+
myObjectId: { type: schemaTypes.ObjectId },
6970
name: { type: String },
7071
size: { type: Number },
7172
isBig: { type: Boolean },
@@ -84,8 +85,8 @@ describe('service > filters-parser', () => {
8485
},
8586
ships: {
8687
type: {
87-
weapon: { type: String },
88-
_id: { type: ObjectId },
88+
weapon: { type: 'String' },
89+
_id: { type: schemaTypes.ObjectId },
8990
},
9091
},
9192
});
@@ -118,27 +119,132 @@ describe('service > filters-parser', () => {
118119

119120
describe('getParserForType', () => {
120121
describe('with a String type', () => {
121-
it('should not try to cast it to ObjectId', () => {
122+
it('should return the default parser', () => {
122123
expect.assertions(3);
123124

125+
expect(defaultParser.getParserForType(String)('a string'))
126+
.toStrictEqual('a string');
127+
expect(defaultParser.getParserForType('String')('a string'))
128+
.toStrictEqual('a string');
129+
expect(defaultParser.getParserForType(schemaTypes.String)('a string'))
130+
.toStrictEqual('a string');
131+
});
132+
133+
it('should not try to cast it to ObjectId', () => {
134+
expect.assertions(1);
135+
124136
const parser = defaultParser.getParserForType('String');
125137

126138
expect(parser('53c2ae8528d75d572c06adbc')).toStrictEqual('53c2ae8528d75d572c06adbc');
127-
expect(parser('Star Wars')).toStrictEqual('Star Wars');
128-
expect(parser('20')).toStrictEqual('20');
129139
});
130140
});
131141

132142
describe('with an ObjectId type', () => {
143+
it('should return the objectId parser', () => {
144+
expect.assertions(2);
145+
146+
expect(defaultParser.getParserForType(schemaTypes.ObjectId)('53c2ae8528d75d572c06adbc'))
147+
.toBeInstanceOf(mongoose.Types.ObjectId);
148+
expect(defaultParser.getParserForType('ObjectId')('53c2ae8528d75d572c06adbc'))
149+
.toBeInstanceOf(mongoose.Types.ObjectId);
150+
});
151+
133152
it('should try to cast it to ObjectId', () => {
134153
expect.assertions(4);
135154

136155
const parser = defaultParser.getParserForType('ObjectId');
137156

138-
expect(parser('53c2ae8528d75d572c06adbc')).toStrictEqual(ObjectId('53c2ae8528d75d572c06adbc'));
139-
expect(parser('star wars with the same ')).toStrictEqual('star wars with the same ');
140-
expect(parser('Star Wars')).toStrictEqual('Star Wars');
141-
expect(parser('20')).toStrictEqual('20');
157+
expect(parser('53c2ae8528d75d572c06adbc')).toBeInstanceOf(mongoose.Types.ObjectId);
158+
expect(parser(ObjectId('53c2ae8528d75d572c06adbc'))).toBeInstanceOf(mongoose.Types.ObjectId);
159+
expect(parser('53c2ae8528d75d572c06adbcc')).not.toBeInstanceOf(mongoose.Types.ObjectId);
160+
expect(parser('star wars with the same ')).not.toBeInstanceOf(mongoose.Types.ObjectId);
161+
});
162+
});
163+
164+
describe('with an Number type', () => {
165+
it('should return the Number parser', () => {
166+
expect.assertions(3);
167+
168+
expect(defaultParser.getParserForType(Number)('10'))
169+
.toBeNumber();
170+
expect(defaultParser.getParserForType('Number')('10'))
171+
.toBeNumber();
172+
expect(defaultParser.getParserForType(schemaTypes.Number)('10'))
173+
.toBeNumber();
174+
});
175+
176+
it('should parse the input to its Number', () => {
177+
expect.assertions(1);
178+
179+
expect(defaultParser.getParserForType(Number)('10'))
180+
.toBeNumber();
181+
});
182+
});
183+
184+
describe('with a Date type', () => {
185+
it('should return the date parser', () => {
186+
expect.assertions(3);
187+
188+
expect(defaultParser.getParserForType(Date)(Date.now()))
189+
.toBeDate();
190+
expect(defaultParser.getParserForType('Date')(Date.now()))
191+
.toBeDate();
192+
expect(defaultParser.getParserForType(schemaTypes.Date)(Date.now()))
193+
.toBeDate();
194+
});
195+
196+
it('should parse the date', () => {
197+
expect.assertions(1);
198+
199+
const date = Date.now();
200+
expect(defaultParser.getParserForType(Date)(date))
201+
.toStrictEqual(new Date(date));
202+
});
203+
});
204+
205+
describe('with a Boolean type', () => {
206+
it('should return the Boolean parser', () => {
207+
expect.assertions(3);
208+
209+
expect(defaultParser.getParserForType(Boolean)('true'))
210+
.toBeBoolean();
211+
expect(defaultParser.getParserForType('Boolean')('true'))
212+
.toBeBoolean();
213+
expect(defaultParser.getParserForType(schemaTypes.Boolean)('true'))
214+
.toBeBoolean();
215+
});
216+
217+
it('should cast values to boolean', () => {
218+
expect.assertions(5);
219+
220+
expect(defaultParser.getParserForType(Boolean)('true'))
221+
.toBeBoolean();
222+
expect(defaultParser.getParserForType(Boolean)('false'))
223+
.toBeBoolean();
224+
expect(defaultParser.getParserForType(Boolean)(true))
225+
.toBeBoolean();
226+
expect(defaultParser.getParserForType(Boolean)(false))
227+
.toBeBoolean();
228+
expect(defaultParser.getParserForType(Boolean)('not a boolean'))
229+
.toBeNull();
230+
});
231+
});
232+
233+
describe('with any other type', () => {
234+
it('should return the default parser', () => {
235+
expect.assertions(5);
236+
237+
const map = new Map();
238+
expect(defaultParser.getParserForType(Map)(map))
239+
.toStrictEqual(map);
240+
expect(defaultParser.getParserForType(Buffer)('test'))
241+
.toStrictEqual('test');
242+
expect(defaultParser.getParserForType(schemaTypes.Mixed)('test'))
243+
.toStrictEqual('test');
244+
expect(defaultParser.getParserForType(schemaTypes.Decimal128)('test'))
245+
.toStrictEqual('test');
246+
expect(defaultParser.getParserForType(['String'])('test'))
247+
.toStrictEqual('test');
142248
});
143249
});
144250
});
@@ -176,7 +282,7 @@ describe('service > filters-parser', () => {
176282
const parserForField = await defaultParser.getParserForField('ships:_id');
177283

178284
expect(defaultParser.getParserForType).toHaveBeenCalledTimes(1);
179-
expect(defaultParser.getParserForType).toHaveBeenCalledWith('ObjectId');
285+
expect(defaultParser.getParserForType).toHaveBeenCalledWith(schemaTypes.ObjectId);
180286

181287
expect(fakeParser).not.toHaveBeenCalled();
182288

@@ -200,7 +306,7 @@ describe('service > filters-parser', () => {
200306
const parserForField = await defaultParser.getParserForField('name');
201307

202308
expect(defaultParser.getParserForType).toHaveBeenCalledTimes(1);
203-
expect(defaultParser.getParserForType).toHaveBeenCalledWith('String');
309+
expect(defaultParser.getParserForType).toHaveBeenCalledWith(String);
204310

205311
expect(fakeParser).not.toHaveBeenCalled();
206312

@@ -223,7 +329,7 @@ describe('service > filters-parser', () => {
223329
const parserForField = await defaultParser.getParserForField('myObjectId');
224330

225331
expect(defaultParser.getParserForType).toHaveBeenCalledTimes(1);
226-
expect(defaultParser.getParserForType).toHaveBeenCalledWith('ObjectId');
332+
expect(defaultParser.getParserForType).toHaveBeenCalledWith(schemaTypes.ObjectId);
227333

228334
expect(fakeParser).not.toHaveBeenCalled();
229335

0 commit comments

Comments
 (0)