diff --git a/src/lib/messages/patchop.js b/src/lib/messages/patchop.js index 4bd7302..1506181 100644 --- a/src/lib/messages/patchop.js +++ b/src/lib/messages/patchop.js @@ -349,9 +349,9 @@ export class PatchOp { // Get rid of any empty values from the filter .filter(([, value]) => value !== undefined) // Turn it into an equity filter string - .map(([key, value]) => (`${key} eq ${value}`)).join(" and ")) - .join(" or ") - ) + .map(([key, value]) => (`${key} eq ${typeof value === "string" ? `"${value}"` : value}`)) + // Join all comparisons into one logical expression + .join(" and ")).join(" or ")) // Get any matching values from the filter .match(target[property]) )); diff --git a/src/lib/types/filter.js b/src/lib/types/filter.js index 57ff8f4..0ac2911 100644 --- a/src/lib/types/filter.js +++ b/src/lib/types/filter.js @@ -26,7 +26,7 @@ const comparators = ["eq", "ne", "co", "sw", "ew", "gt", "lt", "ge", "le", "pr", // Regular expressions that represent filter syntax const lexicon = [ // White Space, Number Values - /(\s+)/, /([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/, + /(\s+)/, /([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)(?![\w+-])/, // Boolean Values, Empty Values, String Values /(false|true)+/, /(null)+/, /("(?:[^"]|\\.|\n)*")/, // Logical Groups, Complex Attribute Value Filters @@ -311,8 +311,9 @@ export class Filter extends Array { const negate = (expression[0].toLowerCase() === "not"); let [comparator, expected] = expression.slice(((+negate) - expression.length)); - // Cast true and false strings to boolean values - expected = (expected === "false" ? false : (expected === "true" ? true : expected)); + // For equality tests, cast true and false strings to boolean values, maintaining EntraID support + if (["eq", "ne"].includes(comparator.toLowerCase()) && typeof actual === "boolean" && typeof expected === "string") + expected = (expected.toLowerCase() === "false" ? false : (expected.toLowerCase() === "true" ? true : expected)); switch (comparator.toLowerCase()) { default: @@ -320,11 +321,11 @@ export class Filter extends Array { break; case "eq": - result = (actual === expected); + result = (actual === (expected ?? undefined)); break; case "ne": - result = (actual !== expected); + result = (actual !== (expected ?? undefined)); break; case "co": diff --git a/test/lib/messages/patchop.js b/test/lib/messages/patchop.js index 75b4b2d..41feb5d 100644 --- a/test/lib/messages/patchop.js +++ b/test/lib/messages/patchop.js @@ -19,7 +19,13 @@ const TestSchema = createSchemaClass({ new Attribute("string", "nickName"), new Attribute("string", "password", {direction: "in", returned: false}), new Attribute("complex", "name", {}, [new Attribute("string", "formatted"), new Attribute("string", "honorificPrefix")]), new Attribute("complex", "emails", {multiValued: true}, [new Attribute("string", "value"), new Attribute("string", "type")]), - new Attribute("string", "throws"), new Attribute("dateTime", "date") + new Attribute("string", "throws"), new Attribute("dateTime", "date"), + new Attribute("complex", "members", {multiValued: true, uniqueness: false}, [ + new Attribute("string", "value", {mutable: "immutable"}), + new Attribute("string", "display", {mutable: "immutable"}), + new Attribute("reference", "$ref", {mutable: "immutable", referenceTypes: ["User", "Group"]}), + new Attribute("string", "type", {mutable: "immutable", canonicalValues: ["User", "Group"]}) + ]) ] }); diff --git a/test/lib/messages/patchop.json b/test/lib/messages/patchop.json index 1127d61..c563207 100644 --- a/test/lib/messages/patchop.json +++ b/test/lib/messages/patchop.json @@ -22,55 +22,115 @@ "ops": [ {"op": "add", "path": "name", "value": {"formatted": "Test"}} ] + }, + { + "source": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"value": "123abc"}]}, + "target": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"value": "123abc", "$ref": "User"}]}, + "ops": [ + {"op": "add", "path": "members[value eq 123abc].$ref", "value": "User"} + ] } ], "remove": [ { "source": {"id": "1234", "userName": "asdf", "name": {"honorificPrefix": "Mr"}}, "target": {"id": "1234", "userName": "asdf"}, - "ops": [{"op": "remove", "path": "name"}] + "ops": [ + {"op": "remove", "path": "name"} + ] }, { "source": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "test@example.com"}, {"type": "home", "value": "asdf@dsaf.com"}]}, "target": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "test@example.com"}]}, - "ops": [{"op": "remove", "path": "emails", "value": {"type": "home"}}] + "ops": [ + {"op": "remove", "path": "emails", "value": {"type": "home"}} + ] }, { "source": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "test@example.com"}]}, "target": {"id": "1234", "userName": "asdf"}, - "ops": [{"op": "remove", "path": "emails[type eq \"work\"]"}] + "ops": [ + {"op": "remove", "path": "emails[type eq \"work\"]"} + ] + }, + { + "source": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"$ref": "User", "value": "f648f8d5ea4e4cd38e9c"}]}, + "target": {"id": "1234", "userName": "asdf", "members": [{"$ref": "User", "value": "f648f8d5ea4e4cd38e9c"}]}, + "ops": [ + {"op": "remove", "path": "members", "value": [{"$ref": null, "value": "f648f8d5ea4e4cd38e9c"}]} + ] + }, + { + "source": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"$ref": "User", "value": "f648f8d5ea4e4cd38e9c"}]}, + "target": {"id": "1234", "userName": "asdf", "members": [{"$ref": "User", "value": "f648f8d5ea4e4cd38e9c"}]}, + "ops": [ + {"op": "remove", "path": "members[$ref eq null and value eq \"f648f8d5ea4e4cd38e9c\"]"} + ] + }, + { + "source": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"value": "123abc"}]}, + "target": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}]}, + "ops": [ + {"op": "remove", "path": "members", "value": [{"value": "123abc"}]} + ] + }, + { + "source": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"value": "123abc"}]}, + "target": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}]}, + "ops": [ + {"op": "remove", "path": "members[value eq \"123abc\"]"} + ] + }, + { + "source": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}, {"value": "123abc"}]}, + "target": {"id": "1234", "userName": "asdf", "members": [{"value": "f648f8d5ea4e4cd38e9c"}]}, + "ops": [ + {"op": "Remove", "path": "members[value eq 123abc]"} + ] } ], "replace": [ { "source": {"id": "1234", "userName": "asdf", "name": {"honorificPrefix": "Mr"}}, "target": {"id": "1234", "userName": "ghjk", "name": {"honorificPrefix": "Mr"}}, - "ops": [{"op": "replace", "path": "userName", "value": "ghjk"}] + "ops": [ + {"op": "replace", "path": "userName", "value": "ghjk"} + ] }, { "source": {"id": "1234", "userName": "asdf", "name": {"honorificPrefix": "Mr"}}, "target": {"id": "1234", "userName": "asdf", "name": {"formatted": "Test"}}, - "ops": [{"op": "replace", "path": "name", "value": {"formatted": "Test"}}] + "ops": [ + {"op": "replace", "path": "name", "value": {"formatted": "Test"}} + ] }, { "source": {"id": "1234", "userName": "asdf", "emails": [{"type": "home", "value": "asdf@dsaf.com"}]}, "target": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "test@example.com"}]}, - "ops": [{"op": "replace", "path": "emails", "value": {"type": "work", "value": "test@example.com"}}] + "ops": [ + {"op": "replace", "path": "emails", "value": {"type": "work", "value": "test@example.com"}} + ] }, { "source": {"id": "1234", "userName": "asdf", "emails": [{"type": "home", "value": "asdf@dsaf.com"}]}, "target": {"id": "1234", "userName": "asdf", "emails": [{"type": "home", "value": "asdf@dsaf.com"}, {"type": "work", "value": "test@example.com"}]}, - "ops": [{"op": "replace", "path": "emails[type eq \"work\"]", "value": {"type": "work", "value": "test@example.com"}}] + "ops": [ + {"op": "replace", "path": "emails[type eq \"work\"]", "value": {"type": "work", "value": "test@example.com"}} + ] }, { "source": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "asdf@dsaf.com"}]}, "target": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "test@example.com"}]}, - "ops": [{"op": "replace", "path": "emails[type eq \"work\"]", "value": {"type": "work", "value": "test@example.com"}}] + "ops": [ + {"op": "replace", "path": "emails[type eq \"work\"]", "value": {"type": "work", "value": "test@example.com"}} + ] }, { "source": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "asdf@dsaf.com"}]}, "target": {"id": "1234", "userName": "asdf", "emails": [{"type": "work", "value": "test@example.com"}]}, - "ops": [{"op": "replace", "path": "emails[type eq \"work\"].value", "value": "test@example.com"}] + "ops": [ + {"op": "replace", "path": "emails[type eq \"work\"].value", "value": "test@example.com"} + ] } ] } diff --git a/test/lib/types/filter.json b/test/lib/types/filter.json index 06837b1..09c3353 100644 --- a/test/lib/types/filter.json +++ b/test/lib/types/filter.json @@ -7,12 +7,16 @@ {"source": "displayName co \"Bob\"", "target": [{"displayName": ["co", "Bob"]}]}, {"source": "name.formatted sw \"Bob\"", "target": [{"name": {"formatted": ["sw", "Bob"]}}]}, {"source": "quota gt 1.5", "target": [{"quota": ["gt", 1.5]}]}, + {"source": "quota gt 2e2", "target": [{"quota": ["gt", 2e2]}]}, {"source": "UserType eq null", "target": [{"userType": ["eq", null]}]}, {"source": "$ref eq null", "target": [{"$ref": ["eq", null]}]}, {"source": "valid$Name eq null", "target": [{"valid$Name": ["eq", null]}]}, {"source": "-valid$Name eq -null", "target": [{"-valid$Name": ["eq", "-null"]}]}, {"source": "active eq false", "target": [{"active": ["eq", false]}]}, - {"source": "emails.primary eq true", "target": [{"emails": {"primary": ["eq", true]}}]} + {"source": "emails.primary eq true", "target": [{"emails": {"primary": ["eq", true]}}]}, + {"source": "value eq 123abc", "target": [{"value": ["eq", "123abc"]}]}, + {"source": "value eq 123", "target": [{"value": ["eq", 123]}]}, + {"source": "value eq 123abc5e4", "target": [{"value": ["eq", "123abc5e4"]}]} ], "logical": [ {"source": "not eq pr", "target": [{"eq": ["not", "pr"]}]}, @@ -287,7 +291,12 @@ {"expression": {"userName": ["pr"]}, "expected": [1, 2, 3, 4]}, {"expression": {"exists": ["pr"]}, "expected": [1, 3]}, {"expression": {"userName": ["np"]}, "expected": []}, - {"expression": {"exists": ["np"]}, "expected": [2, 4]} + {"expression": {"exists": ["np"]}, "expected": [2, 4]}, + {"expression": {"exists": ["eq", null]}, "expected": [2, 4]}, + {"expression": {"exists": ["ne", null]}, "expected": [1, 3]}, + {"expression": {"exists": ["eq", true]}, "expected": [1]}, + {"expression": {"exists": ["eq", "True"]}, "expected": [1]}, + {"expression": {"exists": ["eq", "False"]}, "expected": [3]} ], "nesting": [ {"expression": {"name": {"formatted": ["co", "a"]}}, "expected": [1, 2, 4]},