@drip_sync/drip
Version:
Scalable incremental sync for MongoDB aggregation pipelines
110 lines (109 loc) • 4.25 kB
JavaScript
;
// The "query" here refers to the MQL.
// See https://www.mongodb.com/docs/manual/reference/operator/query/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scopeQueryClause = scopeQueryClause;
exports.scopeQueryPredicate = scopeQueryPredicate;
const zod_1 = __importDefault(require("zod"));
const atoms_1 = require("./atoms");
const invalid_expression_1 = require("./invalid_expression");
const scope_expression_1 = require("./scope_expression");
function scopeQueryClause(c, root) {
const res = {};
for (const [k, v] of Object.entries(c)) {
if (!k.startsWith("$")) {
// field name - scope
res[`${root}.${k}`] = scopeQueryPredicate(v, root);
}
else {
// a query selector
switch (k) {
case "$and":
case "$nor":
case "$or": {
if (!Array.isArray(v)) {
throw new invalid_expression_1.InvalidExpression(`${k} argument must be an array`);
}
const vParsed = zod_1.default
.array(zod_1.default.record(zod_1.default.string(), zod_1.default.unknown()))
.safeParse(v);
if (!vParsed.success) {
throw new invalid_expression_1.InvalidExpression(`${k} argument's entries must be objects`);
}
res[k] = vParsed.data.map((c) => scopeQueryClause(c, root));
break;
}
case "$expr":
res[k] = (0, scope_expression_1.scopeExpression)(v, root, {});
break;
default:
throw new invalid_expression_1.InvalidExpression(`unknown top level operator: ${k}. If you have a field name that starts with a '$' symbol, consider using $getField or $setField.`);
}
}
}
return res;
}
// TODO: how about mongo-recognized classes like
// regex, uuid, timestamp, ...
// they'll appear as objects, but must not be recursed into
function scopeQueryPredicate(p, root) {
if (!(0, atoms_1.isComposite)(p) || Array.isArray(p)) {
// shorthand notation for equality/regex match
// can pass as-is
return p;
}
// a query selector/object equality
// We don't really need to change them, but
// we do need to validate them.
const res = {};
for (const [k, v] of Object.entries(p)) {
if (!k.startsWith("$")) {
// No need to scope the key - scopeQueryClause already did it
res[k] = scopeQueryPredicate(v, root);
}
else {
// an operator - validate
switch (k) {
case "$eq":
case "$gt":
case "$gte":
case "$in":
case "$lt":
case "$lte":
case "$ne":
case "$nin":
case "$not":
case "$exists":
case "$type":
case "$mod":
case "$regex":
case "$geoIntersects":
case "$geoWithin":
case "$near":
case "$nearSphere":
case "$all":
case "$size":
case "$bitsAllClear":
case "$bitsAllSet":
case "$bitsAnyClear":
case "$bitsAnySet":
// can pass as-is
res[k] = v;
break;
case "$elemMatch":
if (!(0, scope_expression_1.isObjectExpression)(v)) {
throw new invalid_expression_1.InvalidExpression("$elemMatch needs an Object");
}
// must recurse
res[k] = scopeQueryPredicate(v, root);
break;
default:
throw new invalid_expression_1.InvalidExpression(`unknown operator: ${k}`);
}
}
}
return res;
}