fix(mongodb): preserve int64 document filters

This commit is contained in:
t8y2 committed 2026-07-03 17:39:48 +08:00
1 parent 5c79c26954
commit 55aa0f8624
6 files changed
+182 -42

No files matched your search

+118 -25
View File
@@ -1,6 +1,7 @@
import type { ComposerTranslation } from "vue-i18n";
import type { DatabaseType } from "@/types/database";
import { quoteUnquotedObjectKeys } from "@/lib/mongoShellCommand";
import { formatMongoShellLiteral } from "@/lib/mongoDocumentValues";
export type DocumentStoreKind = "mongodb" | "elasticsearch";
export type DocumentFilterMode = "equals" | "not-equals" | "like" | "not-like" | "greater-than" | "less-than" | "is-null" | "is-not-null";
@@ -48,14 +49,24 @@ const mongoDocumentProvider: DocumentStoreProvider = {
documentsLabel: ({ total, t }) => t("mongo.documents", { count: total }),
queryPreview: ({ collection, filterJson, sortJson, skip, limit }) => {
const collectionRef = `db.getCollection(${JSON.stringify(collection)})`;
const parts = [`${collectionRef}.find(${filterJson || "{}"})`];
if (sortJson?.trim()) parts.push(`.sort(${sortJson.trim()})`);
const parts = [`${collectionRef}.find(${mongoShellPreviewLiteral(filterJson || "{}")})`];
if (sortJson?.trim()) parts.push(`.sort(${mongoShellPreviewLiteral(sortJson)})`);
parts.push(`.skip(${skip}).limit(${limit})`);
return parts.join("");
},
sortInputForColumn: (column, direction) => (direction ? JSON.stringify({ [column]: direction === "asc" ? 1 : -1 }) : ""),
};
function mongoShellPreviewLiteral(json: string): string {
const trimmed = json.trim();
if (!trimmed) return "{}";
try {
return formatMongoShellLiteral(JSON.parse(trimmed));
} catch {
return trimmed;
}
}
const elasticsearchDocumentProvider: DocumentStoreProvider = {
kind: "elasticsearch",
filterInputLabel: "filter",
@@ -86,19 +97,24 @@ export function documentFilterModeNeedsValue(mode: DocumentFilterMode): boolean
return mode !== "is-null" && mode !== "is-not-null";
}
export function buildDocumentFilterCondition(rule: DocumentFilterRule): Record<string, unknown> | null {
type DocumentFilterParseOptions = {
kind?: DocumentStoreKind;
};
export function buildDocumentFilterCondition(rule: DocumentFilterRule, options: DocumentFilterParseOptions = {}): Record<string, unknown> | null {
if (!rule.fieldName) return null;
if (documentFilterModeNeedsValue(rule.mode) && !rule.rawValue.trim()) return null;
const value = documentFilterModeNeedsValue(rule.mode) ? parseDocumentFilterValue(rule.rawValue) : null;
const value = documentFilterModeNeedsValue(rule.mode) ? parseDocumentFilterValue(rule.rawValue, options) : null;
const textValue = documentFilterModeNeedsValue(rule.mode) ? String(parseDocumentFilterValue(rule.rawValue)) : "";
switch (rule.mode) {
case "equals":
return { [rule.fieldName]: value };
case "not-equals":
return { [rule.fieldName]: { $ne: value } };
case "like":
return { [rule.fieldName]: { $regex: String(value), $options: "i" } };
return { [rule.fieldName]: { $regex: textValue, $options: "i" } };
case "not-like":
return { [rule.fieldName]: { $not: { $regex: String(value), $options: "i" } } };
return { [rule.fieldName]: { $not: { $regex: textValue, $options: "i" } } };
case "greater-than":
return { [rule.fieldName]: { $gt: value } };
case "less-than":
@@ -121,32 +137,109 @@ export function combineDocumentFilterConditions(conditions: Record<string, unkno
}
const MAX_SAFE_BIGINT = 9007199254740991n;
const MIN_BSON_INT64 = -9223372036854775808n;
const MAX_BSON_INT64 = 9223372036854775807n;
function parseJsonPreservingLargeIntegers(json: string): unknown {
const safe = json.replace(/([\[:,\s]\s*?)(-?\d+)(\s*[,\]\}])/g, (_match, before, num, after) => {
try {
const n = BigInt(num);
if (n > MAX_SAFE_BIGINT || n < -MAX_SAFE_BIGINT) {
return `${before}"${num}"${after}`;
}
} catch {
/* not a valid integer */
}
return _match;
});
return JSON.parse(safe);
function parseJsonPreservingLargeIntegers(json: string, options: DocumentFilterParseOptions = {}): unknown {
return JSON.parse(rewriteUnsafeIntegerTokens(json, options));
}
export function parseDocumentFilterInput(input: string): Record<string, unknown> {
function rewriteUnsafeIntegerTokens(json: string, options: DocumentFilterParseOptions): string {
let output = "";
let i = 0;
while (i < json.length) {
const ch = json[i];
if (ch === '"') {
const start = i;
i++;
while (i < json.length) {
const current = json[i++];
if (current === "\\") {
i++;
} else if (current === '"') {
break;
}
}
output += json.slice(start, i);
continue;
}
if (ch === "-" || isDigit(ch)) {
const start = i;
let end = i;
if (json[end] === "-") end++;
if (!isDigit(json[end])) {
output += ch;
i++;
continue;
}
if (json[end] === "0") {
end++;
} else {
while (isDigit(json[end])) end++;
}
let decimalOrExponent = false;
if (json[end] === ".") {
decimalOrExponent = true;
end++;
while (isDigit(json[end])) end++;
}
if (json[end] === "e" || json[end] === "E") {
decimalOrExponent = true;
end++;
if (json[end] === "+" || json[end] === "-") end++;
while (isDigit(json[end])) end++;
}
const token = json.slice(start, end);
if (!decimalOrExponent) {
try {
const n = BigInt(token);
if (n > MAX_SAFE_BIGINT || n < -MAX_SAFE_BIGINT) {
output += unsafeIntegerReplacement(token, n, options);
i = end;
continue;
}
} catch {
/* not a valid integer */
}
}
output += token;
i = end;
continue;
}
output += ch;
i++;
}
return output;
}
function unsafeIntegerReplacement(token: string, value: bigint, options: DocumentFilterParseOptions): string {
if (options.kind === "mongodb" && value >= MIN_BSON_INT64 && value <= MAX_BSON_INT64) {
// MongoDB int64 filters must use Extended JSON so JS Number never rounds snowflake-style IDs.
return `{"$numberLong":${JSON.stringify(token)}}`;
}
return JSON.stringify(token);
}
function isDigit(value: string | undefined): boolean {
return value !== undefined && value >= "0" && value <= "9";
}
export function parseDocumentFilterInput(input: string, options: DocumentFilterParseOptions = {}): Record<string, unknown> {
const trimmed = input.trim();
if (!trimmed) return {};
const safe = quoteUnquotedObjectKeys(trimmed);
const parsed = parseJsonPreservingLargeIntegers(safe);
const parsed = parseJsonPreservingLargeIntegers(safe, options);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
}
export function currentDocumentFilterJson(input: string, structured: Record<string, unknown> | null): string | undefined {
const manual = parseDocumentFilterInput(input);
export function currentDocumentFilterJson(input: string, structured: Record<string, unknown> | null, kind?: DocumentStoreKind): string | undefined {
const manual = parseDocumentFilterInput(input, { kind });
const filter = structured ? (Object.keys(manual).length ? { $and: [manual, structured] } : structured) : manual;
return Object.keys(filter).length ? JSON.stringify(filter) : undefined;
}
@@ -156,11 +249,11 @@ export function currentDocumentSortJson(input: string): string | undefined {
return Object.keys(sort).length ? JSON.stringify(sort) : undefined;
}
function parseDocumentFilterValue(raw: string): unknown {
function parseDocumentFilterValue(raw: string, options: DocumentFilterParseOptions = {}): unknown {
const trimmed = raw.trim();
if (!trimmed) return "";
try {
return parseJsonPreservingLargeIntegers(trimmed);
return parseJsonPreservingLargeIntegers(trimmed, options);
} catch {
return trimmed;
}
@@ -98,6 +98,9 @@ export function formatMongoShellLiteral(value: unknown): string {
if (keys.length === 1 && typeof object.$oid === "string" && MONGO_OBJECT_ID_PATTERN.test(object.$oid)) {
return `ObjectId(${JSON.stringify(object.$oid)})`;
}
if (keys.length === 1 && typeof object.$numberLong === "string") {
return `NumberLong(${JSON.stringify(object.$numberLong)})`;
}
return `{${keys.map((key) => `${JSON.stringify(key)}:${formatMongoShellLiteral(object[key])}`).join(",")}}`;
}
return JSON.stringify(String(value));