mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(mongodb): complete $elemMatch with the operators it accepts
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
ENUM_VALUES,
|
||||
EXTENDED_JSON_VALUES,
|
||||
FIELD_QUERY_OPERATORS,
|
||||
ELEM_MATCH_QUERY_OPERATORS,
|
||||
PIPELINE_STAGES,
|
||||
PUSH_MODIFIERS,
|
||||
BULK_WRITE_OPERATION_FIELDS,
|
||||
@@ -45,6 +46,7 @@ export type MongoCompletionMode =
|
||||
| "field"
|
||||
| "filterField"
|
||||
| "pullCondition"
|
||||
| "elemMatchKey"
|
||||
| "fieldPath"
|
||||
| "fieldRef"
|
||||
| "indexName"
|
||||
@@ -496,6 +498,10 @@ export function buildMongoCompletionItemsFromContext(context: MongoCompletionCon
|
||||
// Fields lead; query operators follow once `$` is typed.
|
||||
items = [...fieldItems(prefix, fields), ...specItems(FIELD_QUERY_OPERATORS, prefix, "query operator", 80)];
|
||||
break;
|
||||
case "elemMatchKey":
|
||||
// Fields lead; field query operators and $and / $or / $nor follow once `$` is typed.
|
||||
items = [...fieldItems(prefix, fields), ...specItems(ELEM_MATCH_QUERY_OPERATORS, prefix, "query operator", 80)];
|
||||
break;
|
||||
case "fieldPath":
|
||||
items = fieldPathItems(prefix, fields);
|
||||
break;
|
||||
@@ -569,7 +575,7 @@ export function buildMongoCompletionItemsFromContext(context: MongoCompletionCon
|
||||
|
||||
/** Modes whose items are built from the target collection's sampled fields. */
|
||||
export function mongoCompletionNeedsFields(mode: MongoCompletionMode): boolean {
|
||||
return mode === "field" || mode === "filterField" || mode === "pullCondition" || mode === "fieldPath" || mode === "fieldRef" || mode === "expression";
|
||||
return mode === "field" || mode === "filterField" || mode === "pullCondition" || mode === "elemMatchKey" || mode === "fieldPath" || mode === "fieldRef" || mode === "expression";
|
||||
}
|
||||
|
||||
/** Modes whose items are built from the database's collection names. */
|
||||
@@ -1049,6 +1055,7 @@ function classifyFilter(scan: MongoCallScan, rootIndex: number): MongoCursorClas
|
||||
if (enumKey) return { mode: "enumValue", enumKey };
|
||||
return { mode: scan.inString ? "none" : "value" };
|
||||
}
|
||||
if (inner.key === "$elemMatch") return { mode: "elemMatchKey" };
|
||||
if (innerDepth(scan, rootIndex) === 0) return { mode: "filterField" };
|
||||
|
||||
// Inside a nested object: whose value is it?
|
||||
@@ -1059,8 +1066,6 @@ function classifyFilter(scan: MongoCallScan, rootIndex: number): MongoCursorClas
|
||||
const parent = scan.stack[scan.stack.length - 2];
|
||||
return { mode: parent?.kind === "array" && VALUE_ARRAY_OPERATORS.has(parent.key ?? "") ? "valueWrapper" : "filterField" };
|
||||
}
|
||||
case "$elemMatch":
|
||||
return { mode: "filterField" };
|
||||
case "$expr":
|
||||
return { mode: "expression" };
|
||||
default:
|
||||
|
||||
@@ -84,6 +84,15 @@ export const TOP_LEVEL_QUERY_OPERATORS: MongoOperatorSpec[] = specs([
|
||||
["$jsonSchema", "Matches documents against a JSON schema", "$jsonSchema: { ${} }"],
|
||||
]);
|
||||
|
||||
/** Logical operators accepted inside `$elemMatch` and at the root of a filter. */
|
||||
export const LOGICAL_QUERY_OPERATORS: MongoOperatorSpec[] = TOP_LEVEL_QUERY_OPERATORS.filter((op) => ["$and", "$or", "$nor"].includes(op.label));
|
||||
|
||||
/**
|
||||
* Operators accepted directly inside the body of `$elemMatch`.
|
||||
* Matches both scalar elements (via field query operators) and document elements (via field names + logical operators).
|
||||
*/
|
||||
export const ELEM_MATCH_QUERY_OPERATORS: MongoOperatorSpec[] = [...FIELD_QUERY_OPERATORS, ...LOGICAL_QUERY_OPERATORS];
|
||||
|
||||
/** Every query operator, for callers that do not care about position. */
|
||||
export const QUERY_OPERATORS: MongoOperatorSpec[] = [...FIELD_QUERY_OPERATORS, ...TOP_LEVEL_QUERY_OPERATORS];
|
||||
|
||||
|
||||
+34
-2
@@ -437,8 +437,8 @@ test("offers whole-filter operators at the top level and field operators under a
|
||||
assert.ok(bare.includes("$or"));
|
||||
});
|
||||
|
||||
test("treats $and / $or sub-filters, $elemMatch bodies and $match as filters", () => {
|
||||
for (const text of ["db.users.find({ $or: [{ $", "db.users.find({ items: { $elemMatch: { $", "db.users.aggregate([{ $match: { $"]) {
|
||||
test("treats $and / $or sub-filters and $match as filters", () => {
|
||||
for (const text of ["db.users.find({ $or: [{ $", "db.users.aggregate([{ $match: { $"]) {
|
||||
const items = labels(text, { fields });
|
||||
assert.ok(items.includes("$or"), text);
|
||||
assert.equal(items.includes("$gte"), false, text);
|
||||
@@ -447,6 +447,34 @@ test("treats $and / $or sub-filters, $elemMatch bodies and $match as filters", (
|
||||
assert.ok(labels("db.users.find({ $or: [{ age: { $", { fields }).includes("$gte"));
|
||||
});
|
||||
|
||||
test("completes $elemMatch body with field names, field query operators and logical operators", () => {
|
||||
const findItems = labels("db.users.find({ tags: { $elemMatch: { $", { fields });
|
||||
for (const op of ["$gte", "$lt", "$in", "$regex", "$exists", "$and", "$or", "$nor"]) {
|
||||
assert.ok(findItems.includes(op), `find $elemMatch must include ${op}`);
|
||||
}
|
||||
for (const op of ["$expr", "$text", "$where", "$jsonSchema"]) {
|
||||
assert.equal(findItems.includes(op), false, `find $elemMatch must exclude ${op}`);
|
||||
}
|
||||
|
||||
const bare = labels("db.users.find({ tags: { $elemMatch: { ", { fields });
|
||||
assert.ok(
|
||||
bare.slice(0, fields.length).every((label) => !label.startsWith("$")),
|
||||
`fields first: ${bare.join(", ")}`,
|
||||
);
|
||||
assert.deepEqual(bare.slice(0, fields.length), ["_id", "createdAt", "name", "profile.email"]);
|
||||
|
||||
assert.ok(labels("db.users.find({ tags: { $elemMatch: { qty: { $", { fields }).includes("$gte"));
|
||||
assert.ok(labels("db.users.find({ tags: { $elemMatch: { $or: [{ ", { fields }).includes("name"));
|
||||
|
||||
const aggItems = labels("db.users.aggregate([{ $match: { tags: { $elemMatch: { $", { fields });
|
||||
for (const op of ["$gte", "$lt", "$in", "$regex", "$exists", "$and", "$or", "$nor"]) {
|
||||
assert.ok(aggItems.includes(op), `aggregate $elemMatch must include ${op}`);
|
||||
}
|
||||
for (const op of ["$expr", "$text", "$where", "$jsonSchema"]) {
|
||||
assert.equal(aggItems.includes(op), false, `aggregate $elemMatch must exclude ${op}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("does not offer filter operators in update or insert documents", () => {
|
||||
assert.equal(labels("db.users.updateOne({}, { $set: { ", { fields }).includes("$or"), false);
|
||||
assert.equal(labels("db.users.insertOne({ $", { fields }).length, 0);
|
||||
@@ -1016,6 +1044,10 @@ test("suggests projection operators in find projections", () => {
|
||||
assert.deepEqual(labels("db.users.find({}, { tags: { $elemMatch: { ", { fields }).slice(0, 3), ["_id", "createdAt", "name"]);
|
||||
assert.ok(labels("db.users.find({}, { tags: { $elemMatch: { score: { ", { fields }).includes("$gt"));
|
||||
assert.ok(labels("db.users.find({}, { tags: { $elemMatch: { score: { $gt: ", { fields }).includes("NumberInt"));
|
||||
const projElemMatch = labels("db.users.find({}, { tags: { $elemMatch: { $", { fields });
|
||||
assert.ok(projElemMatch.includes("$gte"));
|
||||
assert.ok(projElemMatch.includes("$or"));
|
||||
assert.equal(projElemMatch.includes("$expr"), false);
|
||||
});
|
||||
|
||||
test("every suggested projection operator parses", () => {
|
||||
|
||||
Reference in New Issue
Block a user