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).
1. One JSON, two runtimes
Section titled “1. One JSON, two runtimes”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" } ]}| Runtime | Who | How |
|---|---|---|
| SQL | User Views | MJUserViewEntityExtended.GenerateWhereClause → WHERE on one entity |
| In-memory | Prices, processes, Open Apps | CompositeFilter.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.
2. Field names: write prefix, read both
Section titled “2. Field names: write prefix, read both”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:
GenerateWhereClauseuses the name part (the view is one table). PrefixedOrganizations.Typeand bareTypeboth become[Type]. - In-memory: prefixed fields read
context[source][name]. Bare fields readcontext[''][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.
3. Builder UX
Section titled “3. Builder UX”<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>sources | Picker | JSON field |
|---|---|---|
| omitted | Flat list (views, as today) | Bare Type |
| one source | Flat list of that source’s fields | Always Key.Type |
| several sources | Two-pane picker (source list | fields). Closed row: source chip + field label | Always 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: [...] }]}4. Summary helper (no Angular)
Section titled “4. Summary helper (no Angular)”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 injectingThe builder accordion calls SummaryHTML. Grids call SummaryText. Do not duplicate the sentence builder in Open Apps.
5. In-memory evaluation
Section titled “5. In-memory evaluation”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, …).
6. Orders (thin wrap)
Section titled “6. Orders (thin wrap)”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.
7. Files
Section titled “7. Files”| Piece | Package |
|---|---|
CompositeFilterDescriptor + CompositeFilter (FromJSON, Evaluate, SummaryText / SummaryHTML) | @memberjunction/core → generic/filters/ |
| UI | @memberjunction/ng-filter-builder |
| View SQL | MJUserViewEntityExtended.GenerateWhereClause (strips prefix) |
| Authored JS expressions | @memberjunction/global SafeExpressionEvaluator — not this JSON |