fix(mongo): stop suggesting extended json keys as fields

This commit is contained in:
Loc Nguyen
2026-09-11 13:25:54 +08:00
committed by GitHub
parent 5ba5813247
commit 7c2db2d142
3 changed files with 66 additions and 2 deletions
@@ -1,3 +1,4 @@
import { mongoExtendedJsonValueType } from "@/lib/mongo/mongoDocumentValues";
import { ACCUMULATORS, COMMON_OPERATORS, EXPRESSION_OPERATORS, PIPELINE_STAGES, PUSH_MODIFIERS, QUERY_OPERATORS, STAGE_OPTION_KEYS, UPDATE_OPERATORS, UPDATE_OPERATOR_LABELS, VALUE_SNIPPETS, mongoOperatorItemType, type MongoOperatorSpec } from "@/lib/mongo/mongoCompletionTables";
/**
@@ -1015,6 +1016,9 @@ function extractActiveCollection(text: string, cursor: number): string | undefin
function collectFieldTypes(value: unknown, prefix: string, out: Map<string, Set<string>>, depth: number) {
if (depth > 4 || value == null || typeof value !== "object") return;
// A wrapper such as {$oid: "..."} is one BSON value. Walking into it would offer
// `_id.$oid`, a path that exists only in transport and matches nothing on the server.
if (mongoExtendedJsonValueType(value)) return;
if (Array.isArray(value)) {
for (const item of value.slice(0, 3)) collectFieldTypes(item, prefix, out, depth + 1);
return;
@@ -1031,7 +1035,7 @@ function describeMongoValueType(value: unknown): string {
if (value == null) return "null";
if (Array.isArray(value)) return "array";
if (value instanceof Date) return "date";
return typeof value === "object" ? "object" : typeof value;
return mongoExtendedJsonValueType(value) ?? (typeof value === "object" ? "object" : typeof value);
}
function quoteMongoFieldName(field: string, prefix: string): string {
@@ -15,7 +15,38 @@ export const MONGO_DOCUMENT_GRID_NULL = `${MONGO_DOCUMENT_GRID_PREFIX}null`;
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
const MIN_BSON_INT64 = -9223372036854775808n;
const MAX_BSON_INT64 = 9223372036854775807n;
const MONGO_EXTENDED_JSON_VALUE_KEYS = new Set(["$binary", "$code", "$date", "$dbPointer", "$maxKey", "$minKey", "$numberDecimal", "$numberDouble", "$numberInt", "$numberLong", "$oid", "$regularExpression", "$symbol", "$timestamp", "$undefined", "$uuid"]);
/** Extended JSON wrapper key -> the BSON scalar it stands for. */
const MONGO_EXTENDED_JSON_VALUE_TYPES = new Map([
["$binary", "binary"],
["$code", "javascript"],
["$date", "date"],
["$dbPointer", "dbPointer"],
["$maxKey", "maxKey"],
["$minKey", "minKey"],
["$numberDecimal", "decimal128"],
["$numberDouble", "double"],
["$numberInt", "int32"],
["$numberLong", "int64"],
["$oid", "objectId"],
["$regularExpression", "regex"],
["$symbol", "symbol"],
["$timestamp", "timestamp"],
["$undefined", "undefined"],
["$uuid", "uuid"],
]);
const MONGO_EXTENDED_JSON_VALUE_KEYS = new Set(MONGO_EXTENDED_JSON_VALUE_TYPES.keys());
/**
* The BSON scalar an extended JSON wrapper stands for, or undefined when the value is
* a plain object. `{$oid: "..."}` is how the driver ships an ObjectId over JSON; it is
* one value, not a subdocument with a `$oid` field.
*/
export function mongoExtendedJsonValueType(value: unknown): string | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const keys = Object.keys(value as Record<string, unknown>);
if (keys.length === 2 && keys.includes("$code") && keys.includes("$scope")) return "javascript";
return keys.length === 1 ? MONGO_EXTENDED_JSON_VALUE_TYPES.get(keys[0] ?? "") : undefined;
}
const MONGO_EXTENDED_JSON_NUMERIC_TYPES = new Map([
["$numberInt", "int32"],
["$numberLong", "int64"],
+29
View File
@@ -466,6 +466,35 @@ test("snippet templates use placeholder syntax CodeMirror actually honours", ()
}
});
test("treats extended JSON wrappers as scalars, not subdocuments", () => {
// The driver ships BSON scalars as extended JSON. Walking into them would offer
// `_id.$oid`, which is valid syntax that matches nothing on the server.
const inferred = inferMongoCompletionFields([
{
_id: { $oid: "6743e4bfa3f6f84bc3fff6c8" },
created_at: { $date: "2025-01-01T00:00:00Z" },
count: { $numberLong: "42" },
raw: { $binary: { base64: "AQID", subType: "00" } },
profile: { email: "a@example.com" },
},
]);
for (const phantom of ["_id.$oid", "created_at.$date", "count.$numberLong", "raw.$binary"]) {
assert.equal(
inferred.some((field) => field.name === phantom),
false,
phantom,
);
}
assert.ok(inferred.find((field) => field.name === "_id" && field.type === "objectId"));
assert.ok(inferred.find((field) => field.name === "created_at" && field.type === "date"));
assert.ok(inferred.find((field) => field.name === "count" && field.type === "int64"));
assert.ok(inferred.find((field) => field.name === "raw" && field.type === "binary"));
// Genuine subdocuments are still walked.
assert.ok(inferred.find((field) => field.name === "profile.email" && field.type === "string"));
});
test("infers dotted MongoDB fields from sampled documents", () => {
const inferred = inferMongoCompletionFields([
{ _id: "1", profile: { email: "a@example.com" }, tags: ["a"] },