contexture-mongo
Version:
Mongo Provider for Contexture
194 lines (193 loc) • 5.9 kB
JavaScript
import F from "futil";
import _ from "lodash/fp.js";
import { ObjectId } from "mongodb";
let projectStageFromLabelFields = (node) => ({
$project: {
count: 1,
...F.arrayToObject(
(fieldName) => `label.${fieldName}`,
_.constant(1)
)(_.flow(_.get("label.fields"), _.castArray)(node))
}
});
let sortAndLimitIfSearching = (shouldSortAndLimit, limit) => shouldSortAndLimit ? [{ $sort: { count: -1 } }, limit !== 0 && { $limit: limit || 10 }] : [];
let sortAndLimitIfNotSearching = (should, limit) => sortAndLimitIfSearching(!should, limit);
let getSearchableKeysList = _.flow(
_.getOr("_nodeFieldStr", "label.fields"),
_.castArray,
_.map((label) => label === "_nodeFieldStr" ? label : `label.${label}`)
);
let getMatchesForMultipleKeywords = (list, filterWords) => ({
$and: _.map(
(option) => ({
$or: _.map(
(key) => ({
[key]: {
$regex: option,
$options: "i"
}
}),
list
)
}),
filterWords
)
});
let setMatchOperators = (node) => {
const list = getSearchableKeysList(node);
const filterWords = _.compact(_.split(/\s+/, node.optionsFilter));
return list.length > 1 ? getMatchesForMultipleKeywords(list, filterWords) : {
$and: _.map(
(option) => ({
[_.first(list)]: {
$options: "i",
$regex: option
}
}),
filterWords
)
};
};
let mapKeywordFilters = (node) => [
// Cast field we're searching on to string to enable regex matches on it.
{ $addFields: { _nodeFieldStr: { $toString: "$_id" } } },
{ $match: setMatchOperators(node) }
];
let lookupLabel = (node) => _.get("label", node) ? [
{
$lookup: {
from: _.get("label.collection", node),
as: "label",
localField: "_id",
foreignField: _.get("label.foreignField", node)
}
},
{
$unwind: {
path: "$label",
preserveNullAndEmptyArrays: true
}
}
] : [];
let facetValueLabel = (node, label) => {
if (!node.label) {
return {};
}
if (!node.label.fields || _.isArray(node.label.fields)) {
return { label };
}
return {
label: _.flow(_.values, _.first)(label)
};
};
let unwindPropOrField = (node) => _.map(
(field) => ({ $unwind: `$${field}` }),
_.castArray(node.unwind || node.field)
);
let runSearch = ({ options, getSchema, getProvider }, node) => (filters, aggs) => getProvider(node).runSearch(
options,
node,
getSchema(node.schema),
filters,
aggs
);
var facet_default = {
hasValue: _.get("values.length"),
filter: (node) => ({
[node.field]: {
[node.mode === "exclude" ? "$nin" : "$in"]: node.isMongoId ? _.map((v) => new ObjectId(v), node.values) : node.values
}
}),
async result(node, search, schema, config = {}) {
let valueIds = _.get("values", node);
let optionsFilterAggs = _.compact([
...lookupLabel(node),
_.get("label.fields", node) && projectStageFromLabelFields(node),
...node.optionsFilter ? mapKeywordFilters(node) : []
]);
let results = await Promise.all([
search(
_.compact([
// Unwind allows supporting array and non array fields - for non arrays, it will treat as an array with 1 value
// https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#non-array-field-path
...unwindPropOrField(node),
{ $group: { _id: `$${node.field}`, count: { $sum: 1 } } },
...sortAndLimitIfNotSearching(node.optionsFilter, node.size),
...optionsFilterAggs,
...sortAndLimitIfSearching(node.optionsFilter, node.size)
])
),
search([
...unwindPropOrField(node),
{ $group: { _id: `$${node.field}` } },
...optionsFilterAggs,
{ $group: { _id: 1, count: { $sum: 1 } } }
])
]).then(([options, cardinality]) => ({
cardinality: _.get("0.count", cardinality),
options: _.map(
({ _id, label, count }) => F.omitNil({
name: _id,
count,
...facetValueLabel(node, label)
}),
options
)
}));
let lostIds = _.difference(
valueIds,
_.map(
({ name }) => F.when(node.isMongoId, _.toString, name),
results.options
)
);
let maybeMapObjectId = F.when(
node.isMongoId,
_.map((x) => new ObjectId(x))
);
if (!_.isEmpty(lostIds)) {
let lostOptions = await search(
_.compact([
...unwindPropOrField(node),
{
$match: { [node.field]: { $in: maybeMapObjectId(lostIds) } }
},
{ $group: { _id: `$${node.field}`, count: { $sum: 1 } } },
...sortAndLimitIfNotSearching(node.optionsFilter, node.size),
...optionsFilterAggs
])
);
let zeroCountIds = _.difference(
//when values are numeric values, stringify missedValues to avoid the bug.
_.map(F.unless(_.isBoolean, _.toString), lostIds),
_.map(({ _id }) => _.toString(_id), lostOptions)
);
let zeroCountOptions = [];
if (!_.isEmpty(zeroCountIds)) {
zeroCountOptions = _.map(
({ _id, label }) => ({ _id, label, count: 0 }),
await runSearch(config, node)(
{
[node.field]: { $in: maybeMapObjectId(zeroCountIds) }
},
_.compact([
{ $group: { _id: `$${node.field}` } },
...lookupLabel(node),
_.get("label.fields", node) && projectStageFromLabelFields(node)
])
)
);
}
let totalMissedOptions = _.map(({ _id, label, count }) => ({
name: _id,
count,
...facetValueLabel(node, label)
}))(_.concat(lostOptions, zeroCountOptions));
results.options = _.concat(totalMissedOptions, results.options);
}
return results;
}
};
export {
facet_default as default
};