Skip to content

Filter builder, FilterState, and in-memory evaluation

Read this before adding a new “when does this apply?” UI, compiling filters to SQL, or evaluating a saved filter against a bag of records (prices, record processes, anything that is not a User View).

Related: Remote Operations (Authorization.Check), Record Set Processing (filters as a source, not this JSON).


Staff build filters in mj-filter-builder (@memberjunction/ng-filter-builder). The portable payload is a Kendo CompositeFilterDescriptor:

{
"logic": "and",
"filters": [
{ "field": "BillToOrganization.Type", "operator": "eq", "value": "Member" }
]
}
RuntimeWhoHow
SQLUser ViewsMJUserViewEntityExtended.GenerateWhereClauseWHERE on one entity
In-memoryPrices, processes, Open AppsCompositeFilter.FromJSON(json).Evaluate(context) in @memberjunction/core

Do not invent a second tree (custom scope / groups / rules). Do not ask staff to type SafeExpression strings. SafeExpressionEvaluator (@memberjunction/global) stays the sandbox for authored JS-like expressions (Flow agents). Filter JSON is data; CompositeFilter walks it.


Write (when sources is passed to the builder): always SourceKey.FieldName. Even one source. The JSON is self-describing as multi-entity.

Write (legacy): if the caller still passes only fields (User Views today), keep bare names (Type). Existing FilterState rows stay valid.

Read:

CompositeFilter.ParseFilterField('Type') // { Source: null, Name: 'Type' }
CompositeFilter.ParseFilterField('BillToOrganization.Type') // { Source: 'BillToOrganization', Name: 'Type' }
  • SQL views: GenerateWhereClause uses the name part (the view is one table). Prefixed Organizations.Type and bare Type both become [Type].
  • In-memory: prefixed fields read context[source][name]. Bare fields read context[''][name] so a single-record caller passes { '': row }.

Source keys have no dots (BillToOrganization, Order, Product). MJ entity names with colons (MJ_BizApps_Common: Organizations) are FilterSource.entityName, not the JSON prefix.


<mj-filter-builder
[fields]="orgFields"
[filter]="filter"
(filterChange)="onChange($event)">
</mj-filter-builder>
<mj-filter-builder
[sources]="priceSources"
[filter]="filter"
[showSummary]="true"
(filterChange)="onChange($event)">
</mj-filter-builder>
sourcesPickerJSON field
omittedFlat list (views, as today)Bare Type
one sourceFlat list of that source’s fieldsAlways Key.Type
several sourcesTwo-pane picker (source list | fields). Closed row: source chip + field labelAlways Key.Type

AND/OR, Add Condition, Add Group, typed operators, lookup/value-list editors — unchanged.

FilterSource:

{
key: 'BillToOrganization', // JSON prefix
label: 'Bill-to organization', // chip / pane
entityName: 'MJ_BizApps_Common: Organizations',
fields: [{ name: 'Type', displayName: 'Type', type: 'string', valueList: [...] }]
}

The accordion “View Filter Expression” is not shown by default (showSummary). The same wording belongs on a pricing grid cell.

import { CompositeFilter } from '@memberjunction/core';
const filter = CompositeFilter.FromDescriptor(descriptor);
filter.SummaryText({
Fields: [{ Name: 'BillToOrganization.Type', DisplayName: 'Type' }],
SourceLabels: { BillToOrganization: 'Bill-to organization' },
});
// "Bill-to organization Type equals Member"
filter.SummaryHTML(options); // highlighted HTML; host sanitizes if injecting

The builder accordion calls SummaryHTML. Grids call SummaryText. Do not duplicate the sentence builder in Open Apps.


import { CompositeFilter } from '@memberjunction/core';
const ok = CompositeFilter.FromJSON(json).Evaluate({
Order: { CompanyID: '', Status: 'Open' },
Product: { SKU: 'CONF-2027' },
BillToOrganization: { Type: 'Member' },
BillToPerson: null,
ShipToOrganization: null,
ShipToPerson: null,
});
// Or build by hand:
const built = new CompositeFilter();
built.Add({ field: 'BillToOrganization.Type', operator: 'eq', value: 'Member' });
built.Evaluate(context);

A missing source record makes eq false and isnull / isempty true. Empty filters is true (no restriction). Operators match the view SQL set (eq, contains, isnull, …).


Product prices store this JSON in ProductPrice.Applicability. The resolver builds the context bag from the order (header, product, bill-to/ship-to person/org/address) and calls CompositeFilter.FromJSON(row.Applicability).Evaluate(context). No second builder. No formula language.


PiecePackage
CompositeFilterDescriptor + CompositeFilter (FromJSON, Evaluate, SummaryText / SummaryHTML)@memberjunction/coregeneric/filters/
UI@memberjunction/ng-filter-builder
View SQLMJUserViewEntityExtended.GenerateWhereClause (strips prefix)
Authored JS expressions@memberjunction/global SafeExpressionEvaluatornot this JSON