Class: SCIMMY.Types.Filter

SCIM Filter Type

Summary:

This class provides a lexer implementation to tokenise and parse SCIM filter expression strings into meaningful object representations.
It is used to automatically parse attributes, excludedAttributes, and filter expressions in the SCIMMY.Types.Resource class, and by extension, each Resource implementation. The SchemaDefinition #coerce() method uses instances of this class, typically sourced from a Resource instance's attributes property, to determine which attributes to include or exclude on coerced resources. It is also used for resolving complex multi-valued attribute operations in SCIMMY's PatchOp implementation.

Object Representation

When instantiated with a valid filter expression string, the expression is parsed into an array of objects representing the given expression.

Note:
It is also possible to substitute the expression string with an existing or well-formed expression object or set of objects. As such, valid filters can be instantiated using any of the object representations below. When instantiated this way, the expression property is dynamically generated from the supplied expression objects.

The properties of each object are directly sourced from attribute names parsed in the expression. As the class intentionally has no knowledge of the underlying attribute names associated with a schema, the properties of the object are case-sensitive, and will match the case of the attribute name provided in the filter.

// For the filter expressions...
'userName eq "Test"', and 'uSerName eq "Test"'
// ...the object representations are
[ {userName: ["eq", "Test"]} ], and [ {uSerName: ["eq", "Test"]} ]

As SCIM attribute names MUST begin with a lower-case letter, they are the exception to this rule, and will automatically be cast to lower-case.

// For the filter expressions...
'UserName eq "Test"', and 'Name.FamilyName eq "Test"'
// ...the object representations are
[ {userName: ["eq", "Test"]} ], and [ {name: {familyName: ["eq", "Test"]}} ]

Logical Operations

and

For each logical and operation in the expression, a new property is added to the object.

// For the filter expression...
'userName co "a" and name.formatted sw "Bob" and name.honoraryPrefix eq "Mr"'
// ...the object representation is
[ {userName: ["co", "a"], name: {formatted: ["sw", "Bob"], honoraryPrefix: ["eq", "Mr"]}} ]

When an attribute name is specified multiple times in a logical and operation, the expressions are combined into a new array containing each individual expression.

// For the filter expression...
'userName sw "A" and userName ew "z"'
// ...the object representation is
[ {userName: [["sw", "A"], ["ew", "Z"]]} ]
or

For each logical or operation in the expression, a new object is added to the filter array.

// For the filter expression...
'userName eq "Test" or displayName co "Bob"'
// ...the object representation is
[
    {userName: ["eq", "Test"]},
    {displayName: ["co", "Bob"]}
]

When the logical or operation is combined with the logical and operation, the and operation takes precedence.

// For the filter expression...
'userName eq "Test" or displayName co "Bob" and quota gt 5'
// ...the object representation is
[
    {userName: ["eq", "Test"]},
    {displayName: ["co", "Bob"], quota: ["gt", 5]}
]
not

Logical not operations in an expression are added to an object property's array of conditions.

// For the filter expression...
'not userName eq "Test"'
// ...the object representation is
[ {userName: ["not", "eq", "Test"]} ]

For simplicity, the logical not operation is assumed to only apply to the directly following comparison statement in an expression.

// For the filter expression...
'userName sw "A" and not userName ew "Z" or displayName co "Bob"'
// ...the object representation is
[
    {userName: [["sw", "A"], ["not", "ew", "Z"]]},
    {displayName: ["co", "Bob"]}
]

If needed, logical not operations can be applied to multiple comparison statements using grouping operations.

// For the filter expression...
'userName sw "A" and not (userName ew "Z" or displayName co "Bob")'
// ...the object representation is
[
    {userName: [["sw", "A"], ["not", "ew", "Z"]]},
    {userName: ["sw", "A"], displayName: ["not", "co", "Bob"]}
]

Grouping Operations

As per the order of operations in the SCIM protocol specification, grouping operations are evaluated ahead of any simpler expressions.

In more complex scenarios, expressions can be grouped using ( and ) parentheses to change the standard order of operations.
This is referred to as precedence grouping.

// For the filter expression...
'userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")'
// ...the object representation is
[
    {userType: ["eq", "Employee"], emails: ["co", "example.com"]},
    {userType: ["eq", "Employee"], emails: {value: ["co", "example.org"]}}
]

Grouping operations can also be applied to complex attributes using the [ and ] brackets to create filters that target sub-attributes.
This is referred to as complex attribute filter grouping.

// For the filter expression...
'emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]'
// ...the object representation is
[
    {emails: {type: ["eq", "work"], value: ["co", "@example.com"]}},
    {ims: {type: ["eq", "xmpp"], value: ["co", "@foo.com"]}}
]

Complex attribute filter grouping can also be used to target sub-attribute values of multi-valued attributes with specific values.

// For the filter expression...
'emails[type eq "work" or type eq "home"].values[domain ew "@example.org" or domain ew "@example.com"]'
// ...the object representation is
[
    {emails: {type: ["eq", "work"], values: {domain: ["ew", "@example.org"]}}},
    {emails: {type: ["eq", "work"], values: {domain: ["ew", "@example.com"]}}},
    {emails: {type: ["eq", "home"], values: {domain: ["ew", "@example.org"]}}},
    {emails: {type: ["eq", "home"], values: {domain: ["ew", "@example.com"]}}}
]

Precedence and complex attribute filter grouping can also be combined.

// For the filter expression...
'(userType eq "Employee" or userType eq "Manager") and emails[type eq "work" or (primary eq true and value co "@example.com")].display co "Work"'
// ...the object representation is
[
    {userType: ["eq", "Employee"], emails: {type: ["eq", "work"], display: ["co", "Work"]}},
    {userType: ["eq", "Employee"], emails: {primary: ["eq", true], value: ["co", "@example.com"], display: ["co", "Work"]}},
    {userType: ["eq", "Manager"], emails: {type: ["eq", "work"], display: ["co", "Work"]}},
    {userType: ["eq", "Manager"], emails: {primary: ["eq", true], value: ["co", "@example.com"], display: ["co", "Work"]}}
]

Other Implementations

It is not possible to replace internal use of the Filter class inside SCIMMY's PatchOp and SchemaDefinition implementations. Replacing use in the attributes property of an instance of SCIMMY.Types.Resource, while technically possible, is not recommended, as it may break attribute filtering in the #coerce() method of SchemaDefinition instances.

If SCIMMY's filter expression resource matching does not meet your needs, it can be substituted for another implementation (e.g. scim2-parse-filter) when filtering results within your implementation of each resource type's ingress/egress/degress handler methods.

Note:
For more information on implementing handler methods, see the IngressHandler/EgressHandler/DegressHandler type definitions of the SCIMMY.Types.Resource class.

// Import the necessary methods from the other implementation, and for accessing your data source
import {parse, filter} from "scim2-parse-filter";
import {users} from "some-database-client";

// Register your ingress/egress/degress handler method
SCIMMY.Resources.User.egress(async (resource) => {
    // Get the original expression string from the resource's filter property...
    const {expression} = resource.filter;
    // ...and parse/handle it with the other implementation
    const f = filter(parse(expression));
    
    // Retrieve the data from your data source, and filter it as necessary
    return await users.find(/some query returning array/).filter(f);
});

Usage

Instantiate and parse a new SCIM filter string or expression

                    
                        new SCIMMY.Types.Filter(expression)
                    
                
Parameters:
Name Type Description
expression String|Object|Object[]

the query string to parse, or an existing filter expression object or set of objects

Members

expression: String

The original string that was parsed by the filter, or the stringified representation of filter expression objects

Type:
{String}

Methods

match(values) → {Object[]}

Compare and filter a given set of values against this filter instance

Parameters:
Name Type Description
values Object[]

values to evaluate filters against

Returns:

subset of values that match any expressions of this filter instance

Type:
{Object[]}

Constants

(inner) ValidLogicStrings: String[]

Collection of valid logical operator strings in a filter expression

Values:
["and","or","not"]
Type:
{String[]}

(inner) ValidComparisonStrings: String[]

Collection of valid comparison operator strings in a filter expression

Values:
["eq","ne","co","sw","ew","gt","lt","ge","le","pr","np"]
Type:
{String[]}