fix(mongodb): support numeric contains filters
This commit is contained in:
2 files changed
+59
No files matched your search
@@ -33,6 +33,46 @@ describe("document store structured filters", () => {
|
|||||||
expect(() => buildDocumentFilterCondition({ ...baseRule, rawValue: "not-a-number", valueType: "number" }, { kind: "mongodb" })).toThrow("Invalid MongoDB number filter value");
|
expect(() => buildDocumentFilterCondition({ ...baseRule, rawValue: "not-a-number", valueType: "number" }, { kind: "mongodb" })).toThrow("Invalid MongoDB number filter value");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches numeric MongoDB fields by their string representation", () => {
|
||||||
|
expect(buildDocumentFilterCondition({ id: "contains", fieldName: "age", mode: "like", rawValue: "2", conjunction: "AND" }, { kind: "mongodb", sampleValue: 28 })).toEqual({
|
||||||
|
$expr: {
|
||||||
|
$regexMatch: {
|
||||||
|
input: { $convert: { input: "$age", to: "string", onError: "", onNull: "" } },
|
||||||
|
regex: "2",
|
||||||
|
options: "i",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(buildDocumentFilterCondition({ id: "contains", fieldName: "age", mode: "like", rawValue: "2", valueType: "number", conjunction: "AND" }, { kind: "mongodb" })).toEqual({
|
||||||
|
$expr: {
|
||||||
|
$regexMatch: {
|
||||||
|
input: { $convert: { input: "$age", to: "string", onError: "", onNull: "" } },
|
||||||
|
regex: "2",
|
||||||
|
options: "i",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(buildDocumentFilterCondition({ id: "not-contains", fieldName: "age", mode: "not-like", rawValue: "2", valueType: "number", conjunction: "AND" }, { kind: "mongodb" })).toEqual({
|
||||||
|
$expr: {
|
||||||
|
$not: [
|
||||||
|
{
|
||||||
|
$regexMatch: {
|
||||||
|
input: { $convert: { input: "$age", to: "string", onError: "", onNull: "" } },
|
||||||
|
regex: "2",
|
||||||
|
options: "i",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps MongoDB string contains filters on the field regex path", () => {
|
||||||
|
expect(buildDocumentFilterCondition({ id: "contains", fieldName: "name", mode: "like", rawValue: "张", conjunction: "AND" }, { kind: "mongodb", sampleValue: "张三" })).toEqual({
|
||||||
|
name: { $regex: "张", $options: "i" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the MongoDB _id sample for automatic type inference", () => {
|
it("keeps the MongoDB _id sample for automatic type inference", () => {
|
||||||
const tree = documentFieldPathTreeFromDocuments([{ _id: "001", name: "Alice" }]);
|
const tree = documentFieldPathTreeFromDocuments([{ _id: "001", name: "Alice" }]);
|
||||||
|
|
||||||
|
|||||||
@@ -501,9 +501,15 @@ export function buildDocumentFilterCondition(rule: DocumentFilterRule, options:
|
|||||||
return { [rule.fieldName]: { $ne: value } };
|
return { [rule.fieldName]: { $ne: value } };
|
||||||
case "like":
|
case "like":
|
||||||
if (options.kind === "dynamodb") return { [rule.fieldName]: { $contains: value } };
|
if (options.kind === "dynamodb") return { [rule.fieldName]: { $contains: value } };
|
||||||
|
if (options.kind === "mongodb" && mongoFilterValueIsNumeric(rule.valueType, options.sampleValue)) {
|
||||||
|
return mongoNumericContainsCondition(rule.fieldName, textValue);
|
||||||
|
}
|
||||||
return { [rule.fieldName]: { $regex: escapeRegexLiteral(textValue), $options: "i" } };
|
return { [rule.fieldName]: { $regex: escapeRegexLiteral(textValue), $options: "i" } };
|
||||||
case "not-like":
|
case "not-like":
|
||||||
if (options.kind === "dynamodb") return { [rule.fieldName]: { $notContains: value } };
|
if (options.kind === "dynamodb") return { [rule.fieldName]: { $notContains: value } };
|
||||||
|
if (options.kind === "mongodb" && mongoFilterValueIsNumeric(rule.valueType, options.sampleValue)) {
|
||||||
|
return mongoNumericContainsCondition(rule.fieldName, textValue, true);
|
||||||
|
}
|
||||||
return { [rule.fieldName]: { $not: { $regex: escapeRegexLiteral(textValue), $options: "i" } } };
|
return { [rule.fieldName]: { $not: { $regex: escapeRegexLiteral(textValue), $options: "i" } } };
|
||||||
case "greater-than":
|
case "greater-than":
|
||||||
return { [rule.fieldName]: { $gt: value } };
|
return { [rule.fieldName]: { $gt: value } };
|
||||||
@@ -524,6 +530,19 @@ function escapeRegexLiteral(value: string): string {
|
|||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mongoFilterValueIsNumeric(valueType?: DocumentFilterValueType, sampleValue?: unknown): boolean {
|
||||||
|
if (valueType === "number" || valueType === "int32" || valueType === "int64" || valueType === "decimal128") return true;
|
||||||
|
if (typeof sampleValue === "number") return true;
|
||||||
|
if (!isPlainRecord(sampleValue)) return false;
|
||||||
|
const keys = Object.keys(sampleValue);
|
||||||
|
return keys.length === 1 && ["$numberInt", "$numberLong", "$numberDouble", "$numberDecimal"].includes(keys[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mongoNumericContainsCondition(fieldName: string, textValue: string, negate = false): Record<string, unknown> {
|
||||||
|
const regexMatch = { $regexMatch: { input: { $convert: { input: `$${fieldName}`, to: "string", onError: "", onNull: "" } }, regex: escapeRegexLiteral(textValue), options: "i" } };
|
||||||
|
return negate ? { $expr: { $not: [regexMatch] } } : { $expr: regexMatch };
|
||||||
|
}
|
||||||
|
|
||||||
export function combineDocumentFilterConditions(conditions: Record<string, unknown>[], rules: Pick<DocumentFilterRule, "conjunction">[], arrayObjectParents: Array<string | null> = []): Record<string, unknown> | null {
|
export function combineDocumentFilterConditions(conditions: Record<string, unknown>[], rules: Pick<DocumentFilterRule, "conjunction">[], arrayObjectParents: Array<string | null> = []): Record<string, unknown> | null {
|
||||||
if (conditions.length === 0) return null;
|
if (conditions.length === 0) return null;
|
||||||
const grouped = groupLeadingArrayObjectConditions(conditions, rules, arrayObjectParents);
|
const grouped = groupLeadingArrayObjectConditions(conditions, rules, arrayObjectParents);
|
||||||
|
|||||||
Reference in new issue
Block a user