fix(mongodb): preserve long ids in filters and updates
This commit is contained in:
9 files changed
+325
-31
No files matched your search
@@ -3,6 +3,10 @@ export type MongoInputValue = string | number | boolean | null;
|
||||
const MONGO_SHELL_DATE_PATTERN = /^(?:ISODate|new Date)\(\s*(["'])(.+)\1\s*\)$/;
|
||||
const LEGACY_MONGO_DATE_DISPLAY_PATTERN = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?$/;
|
||||
const MONGO_OBJECT_ID_PATTERN = /^[a-fA-F0-9]{24}$/;
|
||||
const MONGO_INTEGER_PATTERN = /^-?\d+$/;
|
||||
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
const MIN_BSON_INT64 = -9223372036854775808n;
|
||||
const MAX_BSON_INT64 = 9223372036854775807n;
|
||||
|
||||
export function mongoShellDateToExtendedJson(value: unknown): unknown {
|
||||
if (typeof value !== "string") return value;
|
||||
@@ -24,7 +28,14 @@ export function parseMongoDocumentInputValue(raw: MongoInputValue): unknown {
|
||||
const legacyDate = legacyMongoDateDisplayToExtendedJson(trimmed);
|
||||
if (legacyDate) return legacyDate;
|
||||
|
||||
if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
||||
if (MONGO_INTEGER_PATTERN.test(trimmed)) {
|
||||
const integer = BigInt(trimmed);
|
||||
if (integer > MAX_SAFE_BIGINT || integer < -MAX_SAFE_BIGINT) {
|
||||
return integer >= MIN_BSON_INT64 && integer <= MAX_BSON_INT64 ? { $numberLong: trimmed } : trimmed;
|
||||
}
|
||||
return Number(trimmed);
|
||||
}
|
||||
if (/^-?\d+\.\d+$/.test(trimmed)) return Number(trimmed);
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith('"')) {
|
||||
return mongoShellDateToExtendedJson(JSON.parse(trimmed));
|
||||
}
|
||||
@@ -155,3 +166,26 @@ export function formatMongoShellLiteral(value: unknown): string {
|
||||
}
|
||||
return JSON.stringify(String(value));
|
||||
}
|
||||
|
||||
export function serializeMongoDocumentId(value: unknown): string {
|
||||
if (typeof value === "string") return `__dbx_mongo_string_id__${JSON.stringify(value)}`;
|
||||
if (isMongoExtendedJsonId(value)) return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function mongoDocumentIdForGrid(value: unknown): MongoInputValue {
|
||||
if (isMongoNumberLong(value)) return value.$numberLong;
|
||||
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function isMongoExtendedJsonId(value: unknown): value is Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const object = value as Record<string, unknown>;
|
||||
const keys = Object.keys(object);
|
||||
return keys.length === 1 && (typeof object.$numberLong === "string" || typeof object.$oid === "string");
|
||||
}
|
||||
|
||||
function isMongoNumberLong(value: unknown): value is { $numberLong: string } {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 1 && typeof (value as Record<string, unknown>).$numberLong === "string";
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import { mongoDocumentIdForGrid } from "@/lib/mongo/mongoDocumentValues";
|
||||
|
||||
export interface MongoFindCommand {
|
||||
collection: string;
|
||||
@@ -638,11 +639,12 @@ function normalizeJsonArgument(value: string): string | null {
|
||||
if (!trimmed) return "{}";
|
||||
// Rewrite mongo shell constructors that are not valid JSON into the extended
|
||||
// JSON the backend understands (mongo_driver::json_value_to_bson): ObjectId(x)
|
||||
// -> {"$oid":x} and ISODate(x)/new Date(x) -> {"$date":x}. Without this a
|
||||
// -> {"$oid":x}, NumberLong(x) -> {"$numberLong":x}, and
|
||||
// ISODate(x)/new Date(x) -> {"$date":x}. Without this a
|
||||
// filter such as { createdAt: { $gte: ISODate("...") } } fails JSON.parse,
|
||||
// the command is left unrecognized and falls through to the SQL executor,
|
||||
// which rejects it with "Use MongoDB-specific commands".
|
||||
const withExtendedJson = trimmed.replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}').replace(/(?:ISODate|new\s+Date)\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$date":"$1"}');
|
||||
const withExtendedJson = replaceMongoShellConstructors(trimmed);
|
||||
const preprocessed = quoteUnquotedObjectKeys(convertSingleQuotedStrings(withExtendedJson));
|
||||
try {
|
||||
JSON.parse(preprocessed);
|
||||
@@ -652,6 +654,41 @@ function normalizeJsonArgument(value: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function replaceMongoShellConstructors(source: string): string {
|
||||
const constructor = /^(ObjectId|NumberLong|ISODate)\s*\(\s*["']([^"']+)["']\s*\)|^(ObjectId|NumberLong)\s*\(\s*(-?\d+)\s*\)|^(?:new\s+Date)\s*\(\s*["']([^"']+)["']\s*\)/;
|
||||
let result = "";
|
||||
let index = 0;
|
||||
while (index < source.length) {
|
||||
const quote = source[index];
|
||||
if (quote === '"' || quote === "'") {
|
||||
const start = index++;
|
||||
while (index < source.length) {
|
||||
if (source[index] === "\\") index += 2;
|
||||
else if (source[index] === quote) {
|
||||
index++;
|
||||
break;
|
||||
} else index++;
|
||||
}
|
||||
result += source.slice(start, index);
|
||||
continue;
|
||||
}
|
||||
const match = source.slice(index).match(constructor);
|
||||
if (!match) {
|
||||
result += source[index++];
|
||||
continue;
|
||||
}
|
||||
if (match[1]) {
|
||||
result += match[1] === "ObjectId" ? `{"$oid":"${match[2]}"}` : match[1] === "NumberLong" ? `{"$numberLong":"${match[2]}"}` : `{"$date":"${match[2]}"}`;
|
||||
} else if (match[3]) {
|
||||
result += match[3] === "NumberLong" ? `{"$numberLong":"${match[4]}"}` : `{"$oid":"${match[4]}"}`;
|
||||
} else {
|
||||
result += `{"$date":"${match[5]}"}`;
|
||||
}
|
||||
index += match[0].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseMethodArgs(source: string, methodCallIndex: number): string[] | null {
|
||||
const openIndex = source.indexOf("(", methodCallIndex);
|
||||
const closeIndex = findMatchingParen(source, openIndex);
|
||||
@@ -1171,5 +1208,5 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
function toCellValue(value: unknown): string | number | boolean | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
||||
return JSON.stringify(value);
|
||||
return mongoDocumentIdForGrid(value);
|
||||
}
|
||||
Reference in new issue
Block a user