-
Notifications
You must be signed in to change notification settings - Fork 161
Filter state model specification [Draft]
This document outlines a generalized model for representing the filtering state of components that offer such functionality.
- The model can be serialized/deserialized in JSON format, making it easily transferable between client and server.
- The serialized model contains sufficient information about its state to construct data queries based on that state.
- Consumers (components/services) can restore the exact state of the model from its serialized representation.
- Additional APIs are documented and exposed, allowing backend interpretation as state that should be transformed into additional data sub-queries or relational joins, depending on the type of data persistence service available.
Note
The following points apply only to data grids
- Data grids can "hydrate" any filtering logic callbacks based on the serialized state (declarative filtering).
- Implement the above points with minimal breaking changes to the existing API.
Warning
This document does not cover additional functionality such as builders/transformers that can construct data queries based on the model state (serialized or not).
The model should reuse as much of the existing API and types available in the package, and used by the data grids and the query builder, as possible. While additional changes will probably be required, they should be scoped to be non-breaking if possible. It is better to wrap and expose new APIs and then deprecate the obsolete ones, rather than break them outright.
Note
What follows is an "annotated" overview of the types and the proposed changes to the API.
enum FilteringLogic {
And,
Or,
}
Represents how a data record should resolve against the tree operands
- AND - the data record must fulfil all conditions
- OR - the data record must fulfil at least one condition
No changes from the current implementation.
type FilterOperation = (value: any, searchVal?: any, ignoreCase?: boolean) => boolean
interface IFilteringOperation {
name: string
isUnary: boolean
iconName: string
hidden?: boolean
logic: FilterOperation
}
The main use of this mishmash of an interface is client-side filtering. The main consumer is of course the data grid, being evident in the additional intrinsic UI state embedded in the model.
This is a proposal for a more generic object that can represent state for both client/server operations and without major breaking changes:
/**
* The following type is the actual serialization state that would be transferred
*/
interface BaseFilterOperation {
/**
* The serializable name of the operation which should
* take place either on the front-end/back-end.
*/
name: string
/**
* Whether the operation is takes only one operand
*/
isUnary?: boolean
}
Then for existing data grids and/or components that exhibit client-side filtering behavior the IFilteringOperation will be something along the lines of:
/**
* The following type is client-side only and is created
* based on a serialized BaseFilterOperation with additional handling by data grid(s)/query builder components.
*/
interface IFilteringOperation extends BaseFilterOperation {
/**
* Associate an icon name for the given operation for use in the UI of a given component.
* Currently used by the data grid and the query builder components.
*/
iconName?: string
/**
* The client-side function which should do the actual filtering.
*
* Used by the data grid(s). Since functions in JavaScript are not serializable,
* the grid should implement additional logic to "hydrate" the functions
* when receiving a serialized filter state.
*/
logic?: FilterOperation
/**
* Whether the operation should be hidden in the component UI.
* Used by the data grid(s)
*/
hidden?: boolean
}
Data grids can still keep the old interface which will be an extension over the proposed one, potentially minimizing the amount of API breaking changes.
interface IFilteringExpression {
field: string
conditionName: string
searchVal: Serializable
searchTree?: FilteringExpressionTree
condition?: IFilteringOperation
ignoreCase?: boolean
}
class FilteringExpressionTree {
logic: FilteringLogic
+ entity?: string
+ returnFields?: string[]
- filteringOperands: Array<Operand | FilteringExpressionTree>
+ filteringOperands?: Array<Operand | FilteringExpressionTree>
+ operands?: Array<Operand>
+ trees?: Array<FilteringExpressionTree>
}
JSON.stringify(
createFilteringTree(FilteringLogic.Or, 'records')
.add({ field: 'id', conditionName: '>', searchVal: 1 })
.add({ field: 'id', conditionName: '<', searchVal: 5 })
)
would be transferred as:
{
"logic": 1,
"entity": "records",
"operands": [
{
"field": "id",
"conditionName": ">",
"searchVal": 1
},
{
"field": "id",
"conditionName": "<",
"searchVal": 5
}
]
}
which would be used to build the following SQL query on a server for example:
SELECT * FROM `records` WHERE `id` > ? OR `id` < ? [ 1, 5 ]
const state = createFilteringTree(FilteringLogic.Or)
.add({ field: 'id', condition: 'equals', searchVal: 1 })
.add({ field: 'id', condition: 'equals', searchVal: 3 })
.addTree(
createFilteringTree()
.add({ field: 'foo', condition: 'equals', searchVal: 1 })
.add({ field: 'bar', condition: 'equals', searchVal: 1 })
)
JSON.stringify(state)
would amount to:
{
"logic": 1,
"entity": "",
"operands": [
{
"field": "id",
"condition": "equals",
"searchVal": 1
},
{
"field": "id",
"condition": "equals",
"searchVal": 3
},
{
"logic": 0,
"entity": "",
"operands": [
{
"field": "foo",
"condition": "equals",
"searchVal": 1
},
{
"field": "bar",
"condition": "equals",
"searchVal": 1
}
]
}
]
}