Skip to content

Commit f803868

Browse files
Merge pull request #726 from ForestAdmin/fix/prevent-auto-cast-to-objectid
2 parents 3dc6c00 + cad38c9 commit f803868

File tree

2 files changed

+122
-8
lines changed

2 files changed

+122
-8
lines changed

src/services/filters-parser.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ function FiltersParser(model, timezone, options) {
1616
if (value === 'false') { return false; }
1717
return typeof value === 'boolean' ? value : null;
1818
};
19-
const parseString = (value) => {
20-
// NOTICE: Check if the value is a real ObjectID. By default, the isValid method returns true
21-
// for a random string with length 12 (example: 'Black Friday').
19+
const parseObjectId = (value) => {
20+
// This fix issue where using aggregation pipeline, mongoose does not
21+
// automatically cast 'looking like' string value to ObjectId
22+
// CF Github Issue https://github.com/Automattic/mongoose/issues/1399
2223
const { ObjectId } = options.Mongoose.Types;
2324
if (ObjectId.isValid(value) && ObjectId(value).toString() === value) {
2425
return ObjectId(value);
2526
}
27+
2628
return value;
2729
};
2830
const parseArray = (value) => ({ $size: value });
@@ -66,8 +68,8 @@ function FiltersParser(model, timezone, options) {
6668
case 'Number': return parseInteger;
6769
case 'Date': return parseDate;
6870
case 'Boolean': return parseBoolean;
69-
case 'String': return parseString;
70-
case _.isArray(type): return parseArray;
71+
case 'ObjectId': return parseObjectId;
72+
case Array.isArray(type): return parseArray;
7173
default: return parseOther;
7274
}
7375
};
@@ -77,6 +79,7 @@ function FiltersParser(model, timezone, options) {
7779

7880
// NOTICE: Mongoose Aggregate don't parse the value automatically.
7981
let field = SchemaUtils.getField(modelSchema, fieldName);
82+
let fieldDefinition = model.schema.paths[fieldName];
8083

8184
if (!field) {
8285
throw new InvalidFiltersFormatError(`Field '${fieldName}' not found on collection '${modelSchema.name}'`);
@@ -85,14 +88,17 @@ function FiltersParser(model, timezone, options) {
8588
const isEmbeddedField = !!field.type.fields;
8689
if (isEmbeddedField) {
8790
field = SchemaUtils.getField(field.type, subfieldName);
91+
fieldDefinition = fieldDefinition[subfieldName];
8892
}
8993

9094
if (!field) return (val) => val;
9195

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

94100
return (value) => {
95-
if (value && _.isArray(value)) {
101+
if (value && Array.isArray(value)) {
96102
return value.map(parse);
97103
}
98104
return parse(value);
@@ -157,7 +163,7 @@ function FiltersParser(model, timezone, options) {
157163
if (!_.isObject(condition)) {
158164
throw new InvalidFiltersFormatError('Condition cannot be a raw value');
159165
}
160-
if (_.isArray(condition)) {
166+
if (Array.isArray(condition)) {
161167
throw new InvalidFiltersFormatError('Filters cannot be a raw array');
162168
}
163169
if (!_.isString(condition.field)

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

Lines changed: 108 additions & 0 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 { ObjectId } = mongoose.Types;
1819

1920
beforeAll(async () => {
2021
islandForestSchema = {
@@ -25,6 +26,7 @@ describe('service > filters-parser', () => {
2526
searchFields: ['name'],
2627
fields: [
2728
{ field: 'id', type: 'Number' },
29+
{ field: 'myObjectId', type: 'String' },
2830
{ field: 'name', type: 'String' },
2931
{ field: 'size', type: 'Number' },
3032
{ field: 'isBig', type: 'Boolean' },
@@ -44,6 +46,9 @@ describe('service > filters-parser', () => {
4446
fields: [{
4547
field: 'weapon',
4648
type: 'String',
49+
}, {
50+
field: '_id',
51+
type: 'ObjectId',
4752
}],
4853
},
4954
},
@@ -60,6 +65,7 @@ describe('service > filters-parser', () => {
6065
await mongooseConnect();
6166
const IslandSchema = new mongoose.Schema({
6267
id: { type: Number },
68+
myObjectId: { type: ObjectId },
6369
name: { type: String },
6470
size: { type: Number },
6571
isBig: { type: Boolean },
@@ -76,6 +82,12 @@ describe('service > filters-parser', () => {
7682
},
7783
},
7884
},
85+
ships: {
86+
type: {
87+
weapon: { type: String },
88+
_id: { type: ObjectId },
89+
},
90+
},
7991
});
8092

8193
IslandModel = mongoose.model('Island', IslandSchema);
@@ -104,6 +116,33 @@ describe('service > filters-parser', () => {
104116

105117
afterEach(() => jest.restoreAllMocks());
106118

119+
describe('getParserForType', () => {
120+
describe('with a String type', () => {
121+
it('should not try to cast it to ObjectId', () => {
122+
expect.assertions(3);
123+
124+
const parser = defaultParser.getParserForType('String');
125+
126+
expect(parser('53c2ae8528d75d572c06adbc')).toStrictEqual('53c2ae8528d75d572c06adbc');
127+
expect(parser('Star Wars')).toStrictEqual('Star Wars');
128+
expect(parser('20')).toStrictEqual('20');
129+
});
130+
});
131+
132+
describe('with an ObjectId type', () => {
133+
it('should try to cast it to ObjectId', () => {
134+
expect.assertions(4);
135+
136+
const parser = defaultParser.getParserForType('ObjectId');
137+
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');
142+
});
143+
});
144+
});
145+
107146
describe('getParserForField', () => {
108147
describe('with an embedded field', () => {
109148
it('should return the parser for the nested field', async () => {
@@ -126,6 +165,75 @@ describe('service > filters-parser', () => {
126165

127166
spy.mockRestore();
128167
});
168+
169+
describe('with a nested ObjectId', () => {
170+
it('should return the parser for the nested field', async () => {
171+
expect.assertions(6);
172+
173+
const fakeParser = jest.fn().mockReturnValue('parsedValue');
174+
const spy = jest.spyOn(defaultParser, 'getParserForType').mockReturnValue(fakeParser);
175+
176+
const parserForField = await defaultParser.getParserForField('ships:_id');
177+
178+
expect(defaultParser.getParserForType).toHaveBeenCalledTimes(1);
179+
expect(defaultParser.getParserForType).toHaveBeenCalledWith('ObjectId');
180+
181+
expect(fakeParser).not.toHaveBeenCalled();
182+
183+
expect(parserForField('myValue')).toStrictEqual('parsedValue');
184+
185+
expect(fakeParser).toHaveBeenCalledTimes(1);
186+
expect(fakeParser).toHaveBeenCalledWith('myValue');
187+
188+
spy.mockRestore();
189+
});
190+
});
191+
});
192+
193+
describe('with a String', () => {
194+
it('should return the string parser', async () => {
195+
expect.assertions(6);
196+
197+
const fakeParser = jest.fn().mockReturnValue('parsedValue');
198+
const spy = jest.spyOn(defaultParser, 'getParserForType').mockReturnValue(fakeParser);
199+
200+
const parserForField = await defaultParser.getParserForField('name');
201+
202+
expect(defaultParser.getParserForType).toHaveBeenCalledTimes(1);
203+
expect(defaultParser.getParserForType).toHaveBeenCalledWith('String');
204+
205+
expect(fakeParser).not.toHaveBeenCalled();
206+
207+
expect(parserForField('myValue')).toStrictEqual('parsedValue');
208+
209+
expect(fakeParser).toHaveBeenCalledTimes(1);
210+
expect(fakeParser).toHaveBeenCalledWith('myValue');
211+
212+
spy.mockRestore();
213+
});
214+
});
215+
216+
describe('with an ObjectId', () => {
217+
it('should return the ObjectId parser', async () => {
218+
expect.assertions(6);
219+
220+
const fakeParser = jest.fn().mockReturnValue('parsedValue');
221+
const spy = jest.spyOn(defaultParser, 'getParserForType').mockReturnValue(fakeParser);
222+
223+
const parserForField = await defaultParser.getParserForField('myObjectId');
224+
225+
expect(defaultParser.getParserForType).toHaveBeenCalledTimes(1);
226+
expect(defaultParser.getParserForType).toHaveBeenCalledWith('ObjectId');
227+
228+
expect(fakeParser).not.toHaveBeenCalled();
229+
230+
expect(parserForField('myValue')).toStrictEqual('parsedValue');
231+
232+
expect(fakeParser).toHaveBeenCalledTimes(1);
233+
expect(fakeParser).toHaveBeenCalledWith('myValue');
234+
235+
spy.mockRestore();
236+
});
129237
});
130238
});
131239

0 commit comments

Comments
 (0)