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 object_hash = require("object-hash"); | |||||
const hash_item = value => | |||||
object_hash(value, { | |||||
algorithm: "md5", | |||||
excludeKeys: key => key === "as", | |||||
}); | |||||
kuba-orlik: Aby uniknąć powtórzeń, tutaj możemy korzystać z przeniesionej do `Query` metody statycznej | |||||
describe("Query", () => { | |||||
describe("Query general", () => { | |||||
it("Creates correct query from custom pipeline", () => { | |||||
const pipeline = [ | |||||
{ $match: { title: { $ne: "The Joy of PHP" }, edition: 1 } }, | |||||
kuba-orlikUnsubmitted Done Inline Actionsbeka :D kuba-orlik: beka :D | |||||
{ | |||||
$lookup: { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
as: "author_item", | |||||
}, | |||||
}, | |||||
{ | |||||
$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); | |||||
// the following is only to break compound matches and hash "as" fields | |||||
// in expected pipeline | |||||
pipeline.unshift({ $match: { title: pipeline[0].$match.title } }); | |||||
delete pipeline[1].$match.title; | |||||
const last_item = pipeline[pipeline.length - 1]; | |||||
pipeline.push({ | |||||
$match: { | |||||
"state_item.abbrevation": | |||||
last_item.$match["state_item.abbrevation"], | |||||
}, | |||||
}); | |||||
delete last_item.$match["state_item.abbrevation"]; | |||||
const expected_json = JSON.stringify(pipeline) | |||||
kuba-orlikUnsubmitted 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? | |||||
piotr-ptaszynskiAuthorUnsubmitted 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… | |||||
kuba-orlikUnsubmitted 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… | |||||
.replace(/author_item/g, hashLookup(pipeline[2])) | |||||
.replace(/state_item/g, hashLookup(pipeline[5])); | |||||
assert.equal(JSON.stringify(query.toPipeline()), expected_json); | |||||
}); | |||||
}); | |||||
describe("Query.Or", () => { | |||||
it("Returns correct pipeline stages", () => { | |||||
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", | |||||
}; | |||||
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: Object.assign(L2, { as: L2_id }) }, | |||||
{ $unwind: `$${L2_id}` }, | |||||
{ $match: { $or: [M1, M3] } }, | |||||
]; | |||||
assert.deepEqual(or.toPipeline(), expected_pipeline); | |||||
}); | |||||
}); | |||||
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", | |||||
}; | |||||
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); | |||||
assertStagesAreCorrectlyOrdered([M3, L1, M2], and.toPipeline()); | |||||
}); | |||||
function assertStagesAreCorrectlyOrdered( | |||||
expectedRawPipeline, | |||||
actualPipeline | |||||
) { | |||||
const query = new Query(); | |||||
for (let i = 0; i < expectedRawPipeline.length; ++i) { | |||||
const stage = expectedRawPipeline[i]; | |||||
if (stage instanceof Query) { | |||||
query.steps = query.steps.concat(stage.dump()); | |||||
} else if (stage.from) { | |||||
query.lookup(stage); | |||||
} else { | |||||
for (let step of Object.keys(stage)) { | |||||
query.match({ [step]: stage[step] }); | |||||
} | |||||
} | |||||
} | |||||
assert.deepEqual(actualPipeline, query.toPipeline()); | |||||
} | |||||
it("Returns pipeline stages in correct order for complex case", () => { | |||||
const queries = []; | |||||
let query = new Query(); | |||||
const L1 = { | |||||
from: "authors", | |||||
localField: "author", | |||||
foreignField: "_id", | |||||
}; | |||||
const L1_id = query.lookup(L1); | |||||
const L2 = { | |||||
from: "publisher", | |||||
localField: `${L1_id}.publisher`, | |||||
foreignField: "publisher_id", | |||||
}; | |||||
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", | |||||
}; | |||||
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", | |||||
}; | |||||
const L10_id = query.lookup(L10); | |||||
const M11 = { | |||||
[`${L10_id}.name`]: { $ne: "no_cover" }, | |||||
}; | |||||
query.match(M11); | |||||
queries.push(query); | |||||
let and = new Query.And(...queries); | |||||
assertStagesAreCorrectlyOrdered( | |||||
[M5, O7, L8, M9, M11, O6, L1, L2, M3_4], | |||||
and.toPipeline() | |||||
); | |||||
}); | |||||
}); | |||||
}); | |||||
function hashLookup({ $lookup }) { | |||||
const { as, ...lookup_without_as } = $lookup; | |||||
return hash_item(lookup_without_as); | |||||
} |
Aby uniknąć powtórzeń, tutaj możemy korzystać z przeniesionej do Query metody statycznej