Merge pull request #294 from bhunjadi/support-projection-operators

Support for query projection operators
This commit is contained in:
Theodor Diaconu 2018-10-23 09:12:39 +03:00 committed by GitHub
commit 77702acd84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 10 deletions

0
lib/query/lib/applyProps.js Normal file → Executable file
View file

21
lib/query/lib/createGraph.js Normal file → Executable file
View file

@ -81,6 +81,14 @@ export function createNodes(root) {
}
}
function isProjectionOperatorExpression(body) {
if (_.isObject(body)) {
const keys = _.keys(body);
return keys.length === 1 && _.contains(['$elemMatch', '$meta', '$slice'], keys[0]);
}
return false;
}
/**
* @param body
* @param fieldName
@ -89,10 +97,15 @@ export function createNodes(root) {
export function addFieldNode(body, fieldName, root) {
// it's not a link and not a special variable => we assume it's a field
if (_.isObject(body)) {
let dotted = dotize.convert({[fieldName]: body});
_.each(dotted, (value, key) => {
root.add(new FieldNode(key, value));
});
if (!isProjectionOperatorExpression(body)) {
let dotted = dotize.convert({[fieldName]: body});
_.each(dotted, (value, key) => {
root.add(new FieldNode(key, value));
});
}
else {
root.add(new FieldNode(fieldName, body, true));
}
} else {
let fieldNode = new FieldNode(fieldName, body);
root.add(fieldNode);

22
lib/query/nodes/collectionNode.js Normal file → Executable file
View file

@ -87,7 +87,17 @@ export default class CollectionNode {
let hasAddedAnyField = false;
_.each(this.fieldNodes, n => {
hasAddedAnyField = true;
/**
* $meta field should be added to the options.fields, but MongoDB does not exclude other fields.
* Therefore, we do not count this as a field addition.
*
* See: https://docs.mongodb.com/manual/reference/operator/projection/meta/
* The $meta expression specifies the inclusion of the field to the result set
* and does not specify the exclusion of the other fields.
*/
if (n.projectionOperator !== '$meta') {
hasAddedAnyField = true;
}
n.applyFields(options.fields)
});
@ -103,8 +113,8 @@ export default class CollectionNode {
// if he selected filters, we should automatically add those fields
_.each(filters, (value, field) => {
// special handling for the $meta filter and conditional operators
if (!_.contains(['$or', '$nor', '$not', '$and', '$meta'], field)) {
// special handling for the $meta filter, conditional operators and text search
if (!_.contains(['$or', '$nor', '$not', '$and', '$meta', '$text'], field)) {
// if the field or the parent of the field already exists, don't add it
if (!_.has(options.fields, field.split('.')[0])){
hasAddedAnyField = true;
@ -114,7 +124,11 @@ export default class CollectionNode {
});
if (!hasAddedAnyField) {
options.fields = {_id: 1};
options.fields = {
_id: 1,
// fields might contain $meta expression, so it should be added here,
...options.fields,
};
}
}

5
lib/query/nodes/fieldNode.js Normal file → Executable file
View file

@ -1,7 +1,8 @@
export default class FieldNode {
constructor(name, body) {
constructor(name, body, isProjectionOperator = false) {
this.name = name;
this.body = _.isObject(body) ? 1 : body;
this.projectionOperator = isProjectionOperator ? _.keys(body)[0] : null;
this.body = !_.isObject(body) || isProjectionOperator ? body : 1;
this.scheduledForDeletion = false;
}

0
lib/query/testing/client.test.js Normal file → Executable file
View file

View file

@ -10,8 +10,66 @@ import './link-cache/server.test';
// Used in some tests below
const Users = new Mongo.Collection('__many_inversed_users');
const Restaurants = new Mongo.Collection('__many_inversed_restaurants');
const ShoppingCart = new Mongo.Collection('__projection_operators_cart');
const Clients = new Mongo.Collection('__text_search_clients');
Clients._ensureIndex({name: 'text'});
describe('Hypernova', function() {
it('Should support projection operators', () => {
ShoppingCart.remove({});
ShoppingCart.insert({
date: new Date(),
items: [{
title: 'Item 1',
price: 30,
}, {
title: 'Item 2',
price: 50,
}],
})
const data = ShoppingCart.createQuery({
items: {$elemMatch: {price: {$gt: 40}}},
}).fetch();
assert.lengthOf(data, 1);
assert.lengthOf(data[0].items, 1);
});
it('Should properly handle text search with sorting and score value projection', () => {
Clients.remove({});
Clients.insert({name: 'John Doe', age: 23});
Clients.insert({name: 'John F McNull', age: 23});
Clients.insert({name: 'Mary Smith', age: 40});
const data = Clients.createQuery({
$filters: {
$text: {$search: 'john'},
},
$options: {
sort: {
score: {$meta: 'textScore'}
}
},
score: {$meta: 'textScore'},
}).fetch();
assert.lengthOf(data, 2);
data.forEach(client => {
// unspecified fields must be excluded
assert.isUndefined(client.name);
assert.isUndefined(client.age);
// _id and score should be included
assert.isString(client._id);
assert.isNumber(client.score);
});
// sort check
const [client1, client2] = data;
assert.isTrue(client1.score > client2.score);
});
it('Should fetch One links correctly', function() {
const data = createQuery({
comments: {