Search code examples
sqlmodel-view-controllerabapopensql

Flatten/Unify Table Rows where criteria matches array with ABAP or SQL


I am trying to achieve flattening a table into an array (or structure). My implementation is in SAP ABAP; however, I believe the question is code agnostic. I am wondering if there is an algorithm I'm not aware of or simple solution for what I'm trying to achieve. I will accept any language or pseudocode as an acceptable answer.

I am selecting from a "User Restrictions" (i.e., lt_restrictions) table stored on a database, e.g.:

enter image description here

This is needed for an SAP BSP MVC web app. When the app loads, I am doing user authorization checks to populate a "User Roles" (i.e., mt_user_roles) local table/array (i.e., attribute on an MVC model class).

The authorization checks involve routine ABAP -- using IF statements to conditionally populate the "User Roles" table, e.g.:

AUTHORITY-CHECK OBJECT 'PROGRAM_MANAGER_AUTH' ID 'xxx' FIELD 'yyy'.
IF ( sy-subrc = 0 ).
  APPEND VALUE #( low = 'program_manager' ) TO me->mt_user_roles.
ENDIF.

If the user has both e.g., program manager AND entry user authorizations, this "User Roles" table/array would look like this:

user roles table

With the two tables above, if the user has program manager and entry user roles, the only restriction is convert; therefore, I need to populate a "Restriction List" (i.e., ms_restrictions) like the following:

enter image description here

Otherwise, if the user has e.g., only the entry user role:

enter image description here ...

...there are 2 restrictions (release AND convert), so I would need to populate a "Restriction List" (i.e., ms_restrictions) like this:

enter image description here

I feel like there is a standard algorithm I could use or perhaps this could be done with SQL in a SELECT statement with my database table WHERE restriction = NULL (e.g., WHERE save = ' ') or using a SELECT ... WHERE x IN clause, perhaps, if the "Restrictions List" was a RANGE table?

I should mention, I already have an undesirable, partial solution which involves looping through the database table and having several IF statements for each restriction condition. "Restriction List" is named ms_restrictions, it is a class attribute ABAP STRUCTURE type (for ABAP newbies, this is basically a 1-D sized array):

LOOP AT lt_restrictions INTO DATA(ls_restriction). 
    LOOP AT mt_user_roles INTO DATA(ls_user_role).
        "Check save restriction 
        IF ( 
            ls_restriction-role = ls_user_role-role AND
            ls_user_role = 'save'
        ).
            ms_restrictions-save = 'x'.
        ENDIF.
       "Check release restriction
       IF ( 
            ls_restriction-role = ls_user_role-role AND
            ls_user_role = 'release'
        ).
            ms_restrictions-release = 'x'.
        ENDIF.
       "Check other restrictions
       "......

     ENDLOOP.
ENDLOOP.
            

The problem with the solution above is I have to write an IF statement for each restriction. More so, it doesn't take into account whether there are conflicts between 2 or more role restrictions, e.g., program manager has only 1 restriction while entry user has 2. My logic above could be extended to take this into account with -- well -- even more IF statements :-( .. I'm wondering if there's a different approach I can take?

Also, I should mention. The resulting "Restrictions List", i.e., mt_restrictions, is needed in my view for disabling HTML buttons, e.g.:

<button value = 'Save'    disabled = '//model/mt_restrictions-save' />
<button value = 'Release' disabled = '//model/mt_restrictions-release' />
<button value = 'Convert' disabled = '//model/mt_restrictions-convert' />
<button ... />

Solution

  • A solution I came up with involves using ABAP <FIELD-SYMBOL>s data-types and an OpenSQL FOR ALL ENTRIES IN SELECT statement addition.

    By using <FIELD-SYMBOL>s, you can avoid any hardcoded attribute and/or structure component names. Now, the restriction rules can grow without having to future modify any code.

    By using FOR ALL ENTRIES IN, you can sync your Roles table with your Restrictions table. Adding this clause allows you to SELECT from your transparent Role/Restrictions database table INTO a table, only including the roles applicable from the roles found in the AUTH-CHECK (because mt_user_roles populates a new row for each true AUTH-CHECK).

    The method is_standard_user( ) is where the AUTH-CHECKs occur. If it equals ABAP_TRUE, then all restrictions are applied -- assuming the ms_restrictions structure components' values all default to ABAP_TRUE.

    Dynamically looping through each structure component of ms_restrictions, you can READ the restrictions table for corresponding value. If value equals SPACE, then this means the current row doesn't have a restriction. So mark the structure component value to SPACE (because it may have been marked ABAP_TRUE in a prior iteration) -- this addresses the "conflicting roles issue".

    "perform authorization checks
    IF ( NOT me->is_standard_user( ) ).
    
      "see if roles found
      IF ( lines( me->mt_user_roles ) > 0 ).
    
        "find pertaining restrictions for roles found in auth checks
        SELECT
          *
          FROM
            zmm_apr_roles
            FOR ALL ENTRIES IN    "select only rows applicable to auth-check
              @me->mt_user_roles
            WHERE
              role = @me->mt_user_roles-role
            INTO TABLE
              @DATA(lt_restrictions).
    
        "dynamically populate restriction list from multi-role restriction table
        DATA: lr_descr_struc TYPE REF TO data.
        DATA: lo_structdescr TYPE REF TO cl_abap_structdescr.
        CREATE DATA lr_descr_struc  LIKE ms_restrictions.
        lo_structdescr ?= cl_abap_structdescr=>describe_by_data_ref( p_data_ref = lr_descr_struc ).
        LOOP AT lo_structdescr->components ASSIGNING FIELD-SYMBOL(<lv_component>).
          ASSIGN COMPONENT <lv_component>-name OF STRUCTURE ms_restrictions TO FIELD-SYMBOL(<lv_field>).
          IF ( <lv_field> IS ASSIGNED ).
            "non-restricted value overwrites restricted value
            READ TABLE lt_restrictions WITH KEY (<lv_component>-name) = space TRANSPORTING NO FIELDS.
            IF ( sy-subrc = 0 ).
              <lv_field> = space.
            ENDIF.
          ENDIF.
        ENDLOOP.
    
      ENDIF.
    
    ENDIF.
    

    To add a new restriction, all that needs to be done is:

    • add new roles to zmm_apr_roles and the corresponding data-dictionary components
    • add new restrictions to corresponding components of ms_restrictions