Changeset View
Changeset View
Standalone View
Standalone View
lib/datastore/query.test.js
- This file was added.
const Query = require("./query.js"); | |||||
const assert = require("assert"); | |||||
const QueryStep = require("./query-step.js"); | |||||
describe("Query", () => { | |||||
describe("Query general", () => { | |||||
it("Creates correct query from custom pipeline", () => { | |||||
const pipeline = [ | |||||
{ $match: { title: { $ne: "The Joy of PHP" }, edition: 1 } }, | |||||
kuba-orlik: Aby uniknąć powtórzeń, tutaj możemy korzystać z przeniesionej do `Query` metody statycznej | |||||
{ | |||||
$lookup: { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
as: "author_item", | |||||
Done Inline Actionsbeka :D kuba-orlik: beka :D | |||||
}, | |||||
}, | |||||
{ | |||||
$unwind: "$author_item", | |||||
}, | |||||
{ $match: { "author_item.name": { $regex: "some_regex" } } }, | |||||
{ | |||||
$lookup: { | |||||
from: "states", | |||||
localField: "author.state", | |||||
foreignField: "_id", | |||||
as: "state_item", | |||||
}, | |||||
}, | |||||
{ $unwind: "$state_item" }, | |||||
{ | |||||
$match: { | |||||
$or: [ | |||||
{ "author_item.age": { $le: 30 } }, | |||||
{ edition: { $gt: 3 } }, | |||||
], | |||||
"state_item.abbrevation": { $eq: "PL" }, | |||||
}, | |||||
}, | |||||
]; | |||||
const query = Query.fromCustomPipeline(pipeline); | |||||
const authors_hash = hashLookup(pipeline[1]); | |||||
const states_hash = hashLookup(pipeline[4]); | |||||
const expected_pipeline = [ | |||||
{ $match: { title: { $ne: "The Joy of PHP" } } }, | |||||
{ $match: { edition: 1 } }, | |||||
{ | |||||
$lookup: { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
as: authors_hash, | |||||
}, | |||||
}, | |||||
{ | |||||
$unwind: "$" + authors_hash, | |||||
}, | |||||
{ | |||||
$match: { | |||||
[`${authors_hash}.name`]: { | |||||
Done Inline ActionsHm, myślę i myślę i nie mogę zrozumieć, co tu się dzieje :< Jakbyś to opisał własnymi słowami? kuba-orlik: Hm, myślę i myślę i nie mogę zrozumieć, co tu się dzieje :< Jakbyś to opisał własnymi słowami? | |||||
Done Inline ActionsTak jak mówi komentarz - transformujemy pipeline z którego stworzyliśmy Query, aby był tym czego oczekujemy - czyli pola as przyjmą wartość hashy, a matche złożone, które mają więcej niż 1 krok rozbijamy na takie z pojedynczym krokiem piotr-ptaszynski: Tak jak mówi komentarz - transformujemy pipeline z którego stworzyliśmy `Query`, aby był tym… | |||||
Done Inline ActionsMyślę, że w tym wypadku lepiej podać bezpośrednio obiekt w takiej formie, w jakiej go oczekujemy - w postaci zwykłego, statycznego obiektu expected_json = JSON.stringify([{$match: ...}, ...]); Coprawda będzie to trochę redundantne, ale pomoże czytającemu zrozumieć, co się dzieje (widzimy efekt przed i po) kuba-orlik: Myślę, że w tym wypadku lepiej podać bezpośrednio obiekt w takiej formie, w jakiej go… | |||||
$regex: "some_regex", | |||||
}, | |||||
}, | |||||
}, | |||||
{ | |||||
$lookup: { | |||||
from: "states", | |||||
localField: "author.state", | |||||
foreignField: "_id", | |||||
as: states_hash, | |||||
}, | |||||
}, | |||||
{ $unwind: "$" + states_hash }, | |||||
{ | |||||
$match: { | |||||
$or: [ | |||||
{ [`${authors_hash}.age`]: { $le: 30 } }, | |||||
{ edition: { $gt: 3 } }, | |||||
], | |||||
}, | |||||
}, | |||||
{ $match: { [`${states_hash}.abbrevation`]: { $eq: "PL" } } }, | |||||
]; | |||||
assert.deepEqual(query.toPipeline(), expected_pipeline); | |||||
}); | |||||
}); | |||||
describe("Query.Or", () => { | |||||
it("Returns correct pipeline stages for simple case", () => { | |||||
const queries = []; | |||||
const M1 = { | |||||
title: { $ne: "The Joy of PHP" }, | |||||
}; | |||||
queries.push(Query.fromSingleMatch(M1)); | |||||
let query = new Query(); | |||||
const L2 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
unwind: true, | |||||
}; | |||||
const L2_id = query.lookup(L2); | |||||
const M3 = { | |||||
[`${L2_id}.last_name`]: { $in: ["Scott", "Dostoyevsky"] }, | |||||
}; | |||||
query.match(M3); | |||||
queries.push(query); | |||||
const or = new Query.Or(...queries); | |||||
const expected_pipeline = [ | |||||
{ | |||||
$lookup: { | |||||
from: L2.from, | |||||
localField: L2.localField, | |||||
foreignField: L2.foreignField, | |||||
as: L2_id, | |||||
}, | |||||
}, | |||||
{ $unwind: `$${L2_id}` }, | |||||
{ $match: { $or: [M1, M3] } }, | |||||
]; | |||||
assert.deepEqual(or.toPipeline(), expected_pipeline); | |||||
}); | |||||
it("Returns correct pipeline stages when And query is provided", () => { | |||||
let queries = []; | |||||
let subquery = new Query(); | |||||
const L1 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
unwind: true, | |||||
}; | |||||
const L1_id = subquery.lookup(L1); | |||||
const M2 = { | |||||
[`${L1_id}.last_name`]: { $in: ["Christie", "Rowling"] }, | |||||
}; | |||||
subquery.match(M2); | |||||
queries.push(subquery); | |||||
const M3 = { | |||||
title: { $ne: "The Joy of PHP" }, | |||||
}; | |||||
queries.push(Query.fromSingleMatch(M3)); | |||||
const and_1 = new Query.And(...queries); | |||||
queries = []; | |||||
subquery = new Query(); | |||||
const L4 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
unwind: true, | |||||
}; | |||||
const L4_id = subquery.lookup(L4); | |||||
const M4 = { | |||||
[`${L4_id}.middle_name`]: { $in: ["Brown", "Black"] }, | |||||
}; | |||||
subquery.match(M4); | |||||
queries.push(subquery); | |||||
subquery = new Query(); | |||||
subquery.lookup(L4); | |||||
const L5 = { | |||||
from: "publisher", | |||||
localField: `${L4_id}.publisher`, | |||||
foreignField: "publisher_id", | |||||
unwind: true, | |||||
}; | |||||
const L5_id = subquery.lookup(L5); | |||||
const M6 = { | |||||
$or: [ | |||||
{ [`${L4_id}.first_name`]: "Ann" }, | |||||
{ [`${L5_id}.income`]: { $gt: 1000 } }, | |||||
], | |||||
}; | |||||
subquery.match(M6); | |||||
const M7 = { | |||||
price: { $lte: 100 }, | |||||
}; | |||||
subquery.match(M7); | |||||
queries.push(subquery); | |||||
const and_2 = new Query.And(...queries); | |||||
const query = new Query.Or(and_1, and_2); | |||||
const expected_pipeline = makeQueryFromStageBodies([ | |||||
L1, | |||||
L4, | |||||
L5, | |||||
{ | |||||
$or: [{ $and: [M3, M2] }, { $and: [M7, M4, M6] }], | |||||
}, | |||||
]).toPipeline(); | |||||
assert.deepEqual(expected_pipeline, query.toPipeline()); | |||||
}); | |||||
}); | |||||
describe("Query.And", () => { | |||||
it("Returns pipeline stages in correct order for simple case", () => { | |||||
const queries = []; | |||||
let query = new Query(); | |||||
const L1 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
unwind: true, | |||||
}; | |||||
const L1_id = query.lookup(L1); | |||||
const M2 = { | |||||
[`${L1_id}.last_name`]: { $in: ["Christie", "Rowling"] }, | |||||
}; | |||||
query.match(M2); | |||||
queries.push(query); | |||||
const M3 = { | |||||
title: { $ne: "The Joy of PHP" }, | |||||
}; | |||||
queries.push(Query.fromSingleMatch(M3)); | |||||
const and = new Query.And(...queries); | |||||
const stageBodies = [M3, L1, M2]; | |||||
assertStagesAreCorrectlyOrdered(stageBodies, and.toPipeline()); | |||||
assert.deepEqual(makeSteps(stageBodies), and.dump()); | |||||
}); | |||||
function assertStagesAreCorrectlyOrdered( | |||||
expectedRawPipeline, | |||||
actualPipeline | |||||
) { | |||||
const query = makeQueryFromStageBodies(expectedRawPipeline); | |||||
assert.deepEqual(actualPipeline, query.toPipeline()); | |||||
} | |||||
function makeSteps(stageBodies) { | |||||
return stageBodies.reduce((acc, stageBody) => { | |||||
if (stageBody instanceof Query.Or) { | |||||
return acc.concat(stageBody.dump()); | |||||
} | |||||
if (stageBody.from) { | |||||
return acc.concat(new QueryStep.Lookup(stageBody)); | |||||
} | |||||
return acc.concat(Query.fromSingleMatch(stageBody).dump()); | |||||
}, []); | |||||
} | |||||
it("Returns pipeline stages in correct order for complex case", () => { | |||||
const queries = []; | |||||
let query = new Query(); | |||||
const L1 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
unwind: true, | |||||
}; | |||||
const L1_id = query.lookup(L1); | |||||
const L2 = { | |||||
from: "publisher", | |||||
localField: `${L1_id}.publisher`, | |||||
foreignField: "publisher_id", | |||||
unwind: true, | |||||
}; | |||||
const L2_id = query.lookup(L2); | |||||
const M3_4 = { | |||||
[`${L2_id}.city`]: { $in: ["A", "B"] }, | |||||
$or: [ | |||||
{ [`${L1_id}.first_name`]: "Ann" }, | |||||
{ [`${L2_id}.income`]: { $gt: 1000 } }, | |||||
], | |||||
}; | |||||
query.match(M3_4); | |||||
queries.push(query); | |||||
query = new Query(); | |||||
const M5 = { | |||||
title: { $ne: "The Joy of PHP" }, | |||||
}; | |||||
query.match(M5); | |||||
queries.push(query); | |||||
let subquery1 = new Query(); | |||||
const O6_L1 = { | |||||
from: "libraries", | |||||
localField: "first_library", | |||||
foreignField: "library_id", | |||||
}; | |||||
const O6_L1_id = subquery1.lookup(O6_L1); | |||||
const O6_M1 = { | |||||
[`${O6_L1_id}.street`]: { $in: ["A street", "B street"] }, | |||||
[`${O6_L1_id}.open_at_night`]: { $eq: true }, | |||||
}; | |||||
subquery1.match(O6_M1); | |||||
const O6_M2 = { | |||||
books_count: { $lte: 30 }, | |||||
}; | |||||
let subquery2 = Query.fromSingleMatch(O6_M2); | |||||
const O6 = new Query.Or(subquery1, subquery2); | |||||
queries.push(O6); | |||||
const O7_M1 = { | |||||
title: { | |||||
$in: ["PHP - Python Has Power", "The Good Parts of JS"], | |||||
}, | |||||
}; | |||||
const O7_M2 = O6_M2; | |||||
const O7 = new Query.Or( | |||||
Query.fromSingleMatch(O7_M1), | |||||
Query.fromSingleMatch(O7_M2) | |||||
); | |||||
queries.push(O7); | |||||
query = new Query(); | |||||
const L8 = { | |||||
from: "cover_types", | |||||
localField: "cover", | |||||
foreignField: "cover_type_id", | |||||
unwind: true, | |||||
}; | |||||
const L8_id = query.lookup(L8); | |||||
const M9 = { | |||||
[`${L8_id}.name`]: { $ne: "hard" }, | |||||
}; | |||||
query.match(M9); | |||||
queries.push(query); | |||||
query = new Query(); | |||||
// check if hashing is order insensitive | |||||
const L10 = { | |||||
localField: "cover", | |||||
from: "cover_types", | |||||
foreignField: "cover_type_id", | |||||
unwind: true, | |||||
}; | |||||
const L10_id = query.lookup(L10); | |||||
const M11 = { | |||||
[`${L10_id}.name`]: { $ne: "no_cover" }, | |||||
}; | |||||
query.match(M11); | |||||
queries.push(query); | |||||
const stageBodies = [M5, O7, L8, M9, M11, O6, L1, L2, M3_4]; | |||||
let and = new Query.And(...queries); | |||||
assertStagesAreCorrectlyOrdered(stageBodies, and.toPipeline()); | |||||
assert.deepEqual(makeSteps(stageBodies), and.dump()); | |||||
}); | |||||
it("Returns deny all pipeline when provided Query.DenyAll", () => { | |||||
const queries = []; | |||||
let query = new Query(); | |||||
const L1 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
unwind: true, | |||||
}; | |||||
const L1_id = query.lookup(L1); | |||||
const M2 = { | |||||
[`${L1_id}.last_name`]: { $in: ["Christie", "Rowling"] }, | |||||
}; | |||||
query.match(M2); | |||||
queries.push(query); | |||||
const deny_all_query = new Query.DenyAll(); | |||||
queries.push(deny_all_query); | |||||
const M3 = { | |||||
title: { $ne: "The Joy of PHP" }, | |||||
}; | |||||
queries.push(Query.fromSingleMatch(M3)); | |||||
const and = new Query.And(...queries); | |||||
assert.deepEqual(and.toPipeline(), deny_all_query.toPipeline()); | |||||
assert.deepEqual(and.dump(), deny_all_query.dump()); | |||||
}); | |||||
}); | |||||
}); | |||||
function makeQueryFromStageBodies(stageBodies) { | |||||
const query = new Query(); | |||||
for (let i = 0; i < stageBodies.length; ++i) { | |||||
const stage = stageBodies[i]; | |||||
if (stage instanceof Query) { | |||||
query.steps.push(...stage.dump()); | |||||
} else if (stage.from) { | |||||
query.lookup(stage); | |||||
} else { | |||||
for (let step of Object.keys(stage)) { | |||||
query.match({ [step]: stage[step] }); | |||||
} | |||||
} | |||||
} | |||||
return query; | |||||
} | |||||
function hashLookup({ $lookup }) { | |||||
const { as, ...lookup_without_as } = $lookup; | |||||
return QueryStep.hashBody(lookup_without_as); | |||||
} |
Aby uniknąć powtórzeń, tutaj możemy korzystać z przeniesionej do Query metody statycznej