Files
docs.datacontroller.io/docs/developer-guide/filter-pre-edit-hook.md
T
dc cc91f8b1f9 docs(developer-guide): add a Developer Guide section
Two pages, both aimed at SAS developers rather than end users:

- Filter in a Pre-Edit Hook: what editors/getdata returns (sasparams.FILTER_TEXT
  and the query clauses, never FILTER_RK), when PRE_EDIT_HOOK runs relative to
  the filter, and a worked example that widens a date filter to the previous
  quarter - re-reading the table so the rows match, and keeping Row Level
  Security applied to both branches.
- One Table, Two Rule Sets: moved from the datacontroller.io blog, so the
  developer-facing material lives in one place. Images moved to docs/img/.
2026-10-15 16:16:15 +00:00

7.2 KiB

layout, title, description
layout title description
article Updating the Filter in a Pre-Edit Hook Read the filter a user applied, and hand back a modified one, from a Data Controller pre-edit hook - including changing the rows the editor shows.

Updating the Filter in a Pre-Edit Hook

A pre-edit hook can read the filter a user applied and return a modified one. This is useful when the editor should widen a user's selection behind the scenes - for example, when a user filters for the current quarter and the grid should also show the previous one.

The hook runs inside the editors/getdata service, so both the applied filter and the filtered rows are in scope. This page describes what is available at that point and works through an example.

What the editor receives

When a filtered editor loads, editors/getdata returns the filter to the client in two forms:

  • sasparams.FILTER_TEXT - the resolved WHERE clause, which the editor shows above the grid.
  • query - the filter's clauses as structured rows (variable_nm, operator_nm, raw_value), which the editor renders as the filter chips.

The response does not carry the filter key (FILTER_RK). That is an input to getdata, not an output - the editor already holds it in the URL (/editor/<table>/<FILTER_RK>), so there is nothing to return.

When the hook runs

getdata builds the filter, applies it, and sorts the result, all before the pre-edit hook runs. In order:

  1. the WHERE clause is written to a temporary fileref, filtref;
  2. work.OUT is populated with where %inc filtref - the filter is applied here;
  3. the pre-edit hook runs;
  4. FILTER_TEXT is read back out of filtref, and the response is streamed.

So the hook sits between "the rows are filtered" and "the filter is returned". Two consequences follow:

  • It can change what the editor is told the filter was, by rewriting filtref and, if the chips must match, work.query.
  • It cannot change which rows came back that way, because the data step has already run. To return different rows, the hook must re-read the table itself.

Take the value from the data

A hook that wants to extend a date filter needs the date the user is working with. It does not have to parse the filter text for it - the filtered rows are already in work.OUT, so the value can be read from there.

Example: widen a date filter to the previous quarter

The hook below is registered in MPE_TABLES.PRE_EDIT_HOOK for a table with a date column ORDER_DATE. When a user opens the editor with a filter that includes ORDER_DATE, it extends the filter to also cover the previous quarter, re-reads the table so the grid shows both quarters, and returns the widened filter text.

/* PRE_EDIT_HOOK: widen the user's ORDER_DATE filter by one quarter */

%if &filter_rk > 0 %then %do;

  /* the date the user is looking at, taken from the rows already returned */
  %let refdt=;
  proc sql noprint;
    select max(ORDER_DATE) into :refdt from work.out;
  quit;

  %if &refdt ne %then %do;

    /* the previous quarter, relative to that date */
    %let pq_from=%sysfunc(putn(%sysfunc(intnx(quarter,&refdt,-1,b)),date9.));
    %let pq_to  =%sysfunc(putn(%sysfunc(intnx(quarter,&refdt,-1,e)),date9.));

    /* the user's own clauses, as a standalone WHERE */
    %mp_filtergenerate(work.query,outref=userref)

    /* Data Controller's own clause - current-record validity, plus RLS */
    %mpe_filtermaster(EDIT,&orig_libds,
      dclib=&mpelib,
      filter_rk=-1,
      outref=ctlref,
      outds=work.ctlq
    )

    /* read each clause (which may span several lines) into a macro variable */
    %let usertext=;
    data _null_;
      infile userref end=eof;
      input;
      length t $32767;
      retain t;
      t=catx(' ',t,_infile_);
      if eof then call symputx('usertext',t);
    run;
    %let ctltext=;
    data _null_;
      infile ctlref end=eof;
      input;
      length t $32767;
      retain t;
      t=catx(' ',t,_infile_);
      if eof then call symputx('ctltext',t);
    run;

    /* ( user's filter OR previous quarter ) AND ( validity + RLS ) */
    filename newfilt temp;
    data _null_;
      file newfilt lrecl=32767;
      length f $32767;
      f='(( '!!trim(symget('usertext'))!!' ) OR (ORDER_DATE between "&pq_from"d and "&pq_to"d))'
        !!' AND ('!!trim(symget('ctltext'))!!' )';
      put f;
    run;

    /* re-read the table, so the grid shows both quarters */
    data work.out (drop=&var_txfrom &var_txto &var_processed);
      _____DELETE__THIS__RECORD_____="No";
      set &orig_libds;
      where %inc newfilt;
    run;

    /* hand the widened filter back to the editor */
    data _null_;
      file filtref lrecl=32767;
      infile newfilt;
      input;
      put _infile_;
    run;

  %end;
%end;

What the hook does

  • The reference date. work.OUT holds the rows the user's filter returned, so the latest ORDER_DATE on screen is read straight from it. Nothing has to be parsed out of the filter text.
  • The previous quarter. INTNX moves back one quarter from that date and returns its first and last day, formatted as SAS date literals (01APR2026 and 30JUN2026, for example).
  • The user's clause. work.QUERY is the filter's clauses as structured rows, and %mp_filtergenerate turns them back into a WHERE clause. This is the user's filter on its own, with no Row Level Security or validity clause mixed in.
  • Data Controller's clause. Calling %mpe_filtermaster with filter_rk=-1 produces the part of the filter that is not the user's: the current-record validity clause and any Row Level Security, or 1=1 when there is neither.
  • The combined filter. The two clauses are combined as ( user OR previous-quarter ) AND ( validity + RLS ), so the previous quarter is added to the user's selection without widening anything the user is not entitled to see.
  • The rows. work.OUT is rebuilt with the combined filter, mirroring the data step getdata uses itself - including dropping the transaction and processing columns it drops.
  • The returned text. filtref is overwritten with the combined filter, so the clause the editor shows is the one that was actually applied.

Row Level Security

The user's clause and Data Controller's clause have to be kept apart, because Row Level Security lives in the second one. Combining them as ( user OR previous-quarter ) AND ( validity + RLS ) keeps RLS applied to both branches. Wrapping the whole existing filter instead - ( existing OR previous-quarter ) - would let the previous-quarter rows through without their RLS condition, so do not take that shortcut.

The row cap

getdata refuses a result larger than DC_MAXOBS_WEBEDIT (500 by default) with Table is too big. Widening a filter widens the result, so a hook that adds a quarter can push a table over the cap. Check the count before handing the data back, or raise the option for that table in MPE_CONFIG.

Matching the filter chips

The clause above the grid comes from FILTER_TEXT, so overwriting filtref is what updates it. The filter chips are rendered from the query output instead, so if they must reflect the widened filter too, amend work.QUERY in the same hook - append the extra clause as a new subgroup with group_logic='OR' before the response is built.