Skip to content

Filter state model specification [Draft]

Mike Cherkasov edited this page Sep 5, 2024 · 6 revisions

Filter state model specification [Draft]

Description

This document outlines a generalized model for representing the filtering state of components that offer such functionality.

Acceptance criteria

  • 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).

Existing model(s) and proposed changes

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.

Types

Filter logic

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.


Filter operation

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.


Filter expression

interface IFilteringExpression {
  field: string
  conditionName: string
  searchVal: Serializable
  searchTree?: FilteringExpressionTree
  condition?: IFilteringOperation
  ignoreCase?: boolean
}

FilteringExpressionTree

class FilteringExpressionTree {
  logic: FilteringLogic
+ entity?: string
+ returnFields?: string[]
- filteringOperands: Array<Operand | FilteringExpressionTree>
+ filteringOperands?: Array<Operand | FilteringExpressionTree>
+ operands?: Array<Operand>
+ trees?: Array<FilteringExpressionTree>
}

Examples

Basic serialization

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 ]

Serialization with additional filter tree

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
        }
      ]
    }
  ]
}
Clone this wiki locally