3

I have permission records that are tied to each account in my application. Each account can have one or multiple permission records based on account type. Here is example:

<cfquery name="qryUserPerm" datasource="#Application.dsn#">
    SELECT AccessType, AccessLevel, State, City, Building
    FROM Permissions
    WHERE AccountID = <cfqueryparam cfsqltype="cf_sql_integer" value="#trim(session.AccountID)#">
</cfquery>

Query above will produce data like this for one of the accounts:

RecID   AccountID   AccessType  AccessLevel State     City    Building
70      285A637D82B9    F            B        NY    New York    8010
71      285A637D82B9    F            B        NY    New York    5412
73      285A637D82B9    F            B        NY    New York    6103
74      285A637D82B9    F            B        NY    New York    3106

As you can see above this account have 4 records assigned to them. Access Type can be Full F or View Only V. Access Level can be State 'S', City 'C' or Building 'B'. User can have only one access level assigned to them at the time, so for example there is no situation where user can have assigned City and State level. My question is what would be the best way to organize the data from the query for specific access level? In this case I have to merge 4 records in list or array. State level only can have one permission record assigned, City and Building can have multiple records. Here is example of what I have:

<cfset local.permissionType = "">
<cfset local.permissionLevel = "">
<cfset local.permissionList = "">

<cfloop query="qryUserPerm">
    <cfif qryUserPerm.AccessLevel EQ "S">
         <cfset local.permissionType = qryUserPerm.AccessType>
         <cfset local.permissionLevel = qryUserPerm.AccessLevel>
         <cfset local.permissionList = listAppend(permissionList, "", ",")>
    <cfelseif qryUserPerm.AccessLevel EQ "C">
         <cfset local.permissionType = qryUserPerm.AccessType>
         <cfset local.permissionLevel = qryUserPerm.AccessLevel>
         <cfset local.permissionList = listAppend(permissionList, qryUserPerm.City, ",")>
    <cfelseif qryUserPerm.AccessLevel EQ "B">
         <cfset local.permissionType = qryUserPerm.AccessType>
         <cfset local.permissionLevel = qryUserPerm.AccessLevel>
         <cfset local.permissionList = listAppend(permissionList, qryUserPerm.Building, ",")>
    <cfelse>
         <cfset local.permissionType = "">
         <cfset local.permissionLevel = "">
         <cfset local.permissionList = listAppend(permissionList, "", ",")>
    </cfif>
</cfloop>

It seems redundant to keep permissionType and permissionLevel inside of the loop but I do not know better way currently to avoid that. Also this makes process very dificult in case when I have to compare permission list. I would have to run this same process and build the list in order to compare that with Session.premissionList in case where currently logged user change their permissions. Is there any way to merge these records with SQL? Or this approach is better option?

espresso_coffee
  • 5,980
  • 11
  • 83
  • 193
  • How are you using the values in the application? Re: "in case where currently logged user change their permissions" Why would you need to compare, instead of just wiping out the old permissions and storing the new ones? – SOS Jul 25 '18 at 23:06
  • @Ageax that is an option as well. I was thinking there might be some scenario where I have to compare the lists before I run the update. – espresso_coffee Jul 25 '18 at 23:08
  • Comparisons could probably be done in SQL, but delete/replace all is more typical. If you are only returning city and building values, why not do that in SQL? (For that matter, you could build the list in SQL, though just as easy to use CF). – SOS Jul 26 '18 at 00:59
  • @Ageax Any example on how to achieve that in SQL? – espresso_coffee Jul 26 '18 at 01:02
  • What version of SQL Server where you using? – Shawn Jul 26 '18 at 14:11
  • @Shawn SQL Server 2008. – espresso_coffee Jul 26 '18 at 14:20
  • Do you have access to the SQL where you can create a View? That will make it easier, but it should be able to be done in a `cfquery`. – Shawn Jul 26 '18 at 17:56
  • 1
    @espresso_coffee - I noticed your other thread includes "State" when AccessLevel = "S", this one thread doesn't. So I added "state" to my previous answer below. – SOS Jul 26 '18 at 17:57

3 Answers3

3

This can be done in SQL itself, which may be more performant than manipulating the data in code.

One issue with the data is that the State, City and Building columns need to be unpivoted to then be turned into a comma-delimited list.

Since you are using SQL 2008, you have access to the functionality you need.

The query is: http://sqlfiddle.com/#!18/0f4f7/1

; WITH cte AS (
  SELECT
      AccountID, AccessType, AccessLevel
      , CASE AccessLevel
          WHEN 'S' THEN State
          WHEN 'C' THEN City
          WHEN 'B' THEN Building
        END AS Permissions
  FROM Permissions
  WHERE AccountID = 
    <cfqueryparam cfsqltype="cf_sql_integer" value="#session.AccountID#"> 
    /* Dynamic variable here */
)
SELECT DISTINCT AccountID, AccessType, AccessLevel
  , CASE 
      WHEN AccessLevel = 'S' THEN Permissions 
      ELSE LEFT(ca.pl, COALESCE(LEN(ca.pl),0)-1)
    END AS PermissionList
FROM cte
CROSS APPLY (
  SELECT p.Permissions + ', '
  FROM cte p
  WHERE p.AccountID = cte.AccountID
    AND p.AccessType = cte.AccessType
    AND p.AccessLevel = cte.AccessLevel
  FOR XML PATH('')
) ca (pl)     ;

I start with a CTE to build out the "unpivoted" list of Permissions based on the AccessLevel. If this can be put in a SQL View, you can just leave out the WHERE statement here and just call it when you call the View. A View would be my preference, if you can get it into your database.

After I have the CTE, I just select the base columns (AccountID, AccessType and AccessLevel, and then I CROSS APPLY a comma-delimited list of the Permissions. I use FOR XML PATH('') to build that comma-delimited list.

If this is able to be converted to a View, it would be a simple

<cfquery name="qryUserPerm" datasource="#Application.dsn#">
    SELECT AccessType, AccessLevel, PermissionList
    FROM myPermissionsView
    WHERE AccountID = <cfqueryparam cfsqltype="cf_sql_integer" value="#trim(session.AccountID)#">
</cfquery>

If not, you'll have to try running the above full query inside the cfquery tag.

This should give you back a dataset like:

| AccessType | AccessLevel |         PermissionList |
|------------|-------------|------------------------|
|          F |           B | 8010, 5412, 6103, 3106 |

You only have one result to work with and won't have to loop.

======================================================================

If you want to go the in-code route, I'd still recommend trying to use cfscript to build out the structs. But, if you can have more than one AccessLevel, your results may not be what you think they should be. You'll have to doublecheck your data.

  local.permissionType = q2.AccessType ;
  local.permissionLevel = q2.AccessLevel ;

  switch( q2.AccessLevel ) {
    case "S" :  local.permissionList = q2.State ;
      break ;
    case "C" :  local.permissionList = ListRemoveDuplicates(ValueList(q2.City)) ;
      break ;
    case "B" :  local.permissionList = ListRemoveDuplicates(ValueList(q2.Building)) ;
      break ;
  }

https://trycf.com/gist/e811ec86f0d5a52fd9ce703f897cb5aa/acf2016?theme=monokai

Shawn
  • 4,758
  • 1
  • 20
  • 29
  • Thanks for getting back and providing example. So ideal solution would be to create `View` that would look like first query in your answer where you organize Permissions with `case` statement? Then call the `view` in final query? Let me know if this is correct or I misunderstood your answer. – espresso_coffee Jul 26 '18 at 18:16
  • 1
    A View would be my suggestion. But I prefer to do data manipulation as close to the source as possible; which usually means SQL. The more I've worked with SQL, the more I've realized that I was beating up on both my database and my network when I was doing stuff in code. Granted, like all other IT things, that depends. If you're working with thousands of users a minute, then it may be better to spread the data manipulation load across app servers rather than database. You'll have to load test. – Shawn Jul 26 '18 at 18:21
  • The CTE in the View would not contain `WHERE AccountID = `. That would go in the `cfquery`. You're filtering a larger dataset there, but the View will be cachable on the SQL Server. Even with a ton of rows, it should still be pretty efficient. – Shawn Jul 26 '18 at 18:24
  • I'm not sure what your data looks like, but in my SQLFiddle, I added a few different cases to show how it would return different types of data. – Shawn Jul 26 '18 at 18:28
  • I more kind of leaning to solution that @Charles Roberts provided. That way I'm putting more work on ColdFusion instead of my database. Again this is something that I'm not 100% sure about. – espresso_coffee Jul 26 '18 at 18:28
  • 2
    Again, I'm not a huge fan of putting a load on the CF Server that could be more efficiently handled by the database. It took me a while to realize that a db is more than just a place to hold data. – Shawn Jul 26 '18 at 18:30
  • I just know what I could research on my own and what I heard from my senior developers. – espresso_coffee Jul 26 '18 at 18:32
  • 2
    @espresso_coffee - It sounds like the query only ever returns 5-10 rows, so it's probably 6 of one, half dozen of the other. However, I agree with Shawn about putting more work on the db side. Generally you put the load on whatever tool is best equipped for it. For data manipulation, that is the database. It's much better suited to manipulating large data sets and complex filtering. Plus it saves bandwidth by only pulling down what you need. On the other hand, if you need granular string manipulation, CF is better suited for that. Like many things - it all depends on the task. – SOS Jul 26 '18 at 19:56
  • 1
    Shawn - Great minds ;-) Didn't see you'd already posted an example when I previously updated mine. – SOS Jul 26 '18 at 19:57
  • @Ageax Again, get out of my head! It's scary in there! :-) – Shawn Jul 26 '18 at 20:01
  • @espresso_coffee I updated my answer to include the `cfscript` version of manipulation. But if there's a possibility that you may have more than one `AccessLevel`, you could get unintended records in your `PermissionsList`. You'll have to look at your data. – Shawn Jul 26 '18 at 20:04
  • @Shawn User can assign only same access level multiple times if that make sense. So if they Assigned City access level, they can have multiple cities assigned to their permissions but they can't assign State or Building. If they want to change the access level, first they have to delete existing record(s) and then assign new ones. I hope this explains better how my code works. – espresso_coffee Jul 26 '18 at 20:08
  • 1
    When it comes to data, how code works is less important than what the database will allow in a column. If need be, set up composite keys to make sure they can't enter bad data, otherwise it will cause lots of wasted time trying to track down why someone's page suddenly broke. Especially when they _SWEAR_ they didn't make any changes. :-) – Shawn Jul 26 '18 at 20:24
1

I would be tempted to remove the loop. I am thinking that this may make things a little simpler.

<cfset local.permissionType = "">
<cfset local.permissionLevel = "">
<cfset local.permissionList = "">


<cfif qryUserPerm.AccessLevel EQ "S">
     <cfset local.permissionType = qryUserPerm.AccessType>
     <cfset local.permissionLevel = qryUserPerm.AccessLevel>
     <cfset local.permissionList = qryUserPerm.State>
<cfelseif qryUserPerm.AccessLevel EQ "C">
     <cfset local.permissionType = qryUserPerm.AccessType>
     <cfset local.permissionLevel = qryUserPerm.AccessLevel>
     <cfset local.permissionList = ListRemoveDuplicates(ValueList(permissionList,qryUserPerm.City))>
<cfelseif qryUserPerm.AccessLevel EQ "B">
     <cfset local.permissionType = qryUserPerm.AccessType>
     <cfset local.permissionLevel = qryUserPerm.AccessLevel>
     <cfset local.permissionList = ListRemoveDuplicates(ValueList(permissionList,qryUserPerm.Building))>
</cfif>

And, if you want to compare the lists in future for equality, you may want to use:

<cfset local.permissionList = ListSort(local.permissionList,"textnocase","asc")>

UPDATE:

<cfscript>

qryUserPerm = queryExecute("
  SELECT AccessType, AccessLevel, State, City, Building 
  FROM Permissions
  WHERE AccountID = :AccountID 
",
{
  AccountID = {value = Trim(session.AccountID), cfsqltype = "cf_sql_integer"}
},
{
  datasource = Application.dsn 
});

local.permissionType = "";
local.permissionLevel = "";
local.permissionList = "";

if(qryUserPerm.AccessLevel EQ "S"){
   local.permissionType = qryUserPerm.AccessType;
   local.permissionLevel = qryUserPerm.AccessLevel;
   local.permissionList = qryUserPerm.State;
}
else if(qryUserPerm.AccessLevel EQ "C"){
   local.permissionType = qryUserPerm.AccessType;
   local.permissionLevel = qryUserPerm.AccessLevel;
   local.permissionList = ListRemoveDuplicates(ValueList(permissionList,qryUserPerm.City));
}
else if(qryUserPerm.AccessLevel EQ "B"){
   local.permissionType = qryUserPerm.AccessType;
   local.permissionLevel = qryUserPerm.AccessLevel;
   local.permissionList = ListRemoveDuplicates(ValueList(permissionList,qryUserPerm.Building));
}

</cfscript>
Charles Robertson
  • 1,760
  • 16
  • 21
1

You could trim down the code by using CASE to merge everything into a single column, based on the Access Level.

SELECT AccessType
        , AccessLevel
        , CASE AccessLevel
            WHEN 'C' THEN City
            WHEN 'B' THEN Building
            WHEN 'S' THEN State
          END AS AccessValue
 FROM   Permissions
 WHERE  AccountID = <cfqueryparam cfsqltype="cf_sql_integer" value="#session.AccountID#">

Then build your list from that column. No cfif's needed.

 <cfset local.permissionType = qryUserPerm.AccessType>
 <cfset local.permissionLevel = qryUserPerm.AccessLevel>
 <cfset local.permissionList = valueList(qryUserPerm.AccessValue)>

You could also build the CSV list in SQL only, but not sure it's worth it in this scenario, since it's just as easy to build in CF.

SELECT TOP 1 AccessType
        , AccessLevel
        , STUFF(( SELECT ','+ l.AccessValue
                  FROM ( SELECT CASE AccessLevel 
                                  WHEN 'C' THEN City 
                                  WHEN 'B' THEN Building 
                                  WHEN 'S' THEN State 
                                END AS AccessValue
                         FROM   Permissions l
                         WHERE  l.AccountID = p.AccountID 
                     ) l
                  GROUP BY l.AccessValue
                  FOR XML PATH('')
                ),1,1,'') AS PermissionsList
 FROM   Permissions p            
 WHERE  AccountID = <cfqueryparam cfsqltype="cf_sql_integer" value="#session.AccountID#">       

Anyway, using the above the query will return everything you need in a single row: AccessType, AccessLevel and csv list of permissions.

 <cfset local.permissionType = qryUserPerm.AccessType>
 <cfset local.permissionLevel = qryUserPerm.AccessLevel>
 <cfset local.permissionList = qryUserPerm.PermissionsList>
SOS
  • 6,430
  • 2
  • 11
  • 29