Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Role hierarchy mapper / generator (recursive)

I would like to create an object of arrays converting the single level key - (string) value relation to key - (array) keys collection.

Basically, the code must collect other keys and their values recursively starting from collecting self. At the end the object must be like this;

{
    ROLE_SUPER_ADMIN: [
        'ROLE_SUPER_ADMIN', 
        'ROLE_ADMIN', 
        'ROLE_MODERATOR', 
        'ROLE_AUTHOR'
    ]
}

What i have achieved yet is;

export const roles = {
    ROLE_SUPER_ADMIN: 'ROLE_ADMIN',
    ROLE_ADMIN: 'ROLE_MODERATOR',
    ROLE_MODERATOR: 'ROLE_AUTHOR',
    ROLE_AUTHOR: null,
    ROLE_CLIENT: null
}

export function roleMapper() {
    const roleArray = {}

    const mapper = (key) => {
        roleArray[key] = [key];

        if (!roles[key] || Array.isArray(roles[key])) {
            return;
        } else if (!roles[roles[key]]) {
            roleArray[key].push(roles[key])
        } else {
            if (roleArray.hasOwnProperty(key)) {
                Object.keys(roles).filter(r => r !== key).forEach((role) => {
                    roleArray[key].push(mapper(role))
                })
            }

        }
    }

    Object.keys(roles).forEach((key) => {
        mapper(key)
    });

    console.log(roleArray);
}

I have completely lost solving this. Please help, thanks.

like image 302
Canser Yanbakan Avatar asked Jan 21 '26 06:01

Canser Yanbakan


2 Answers

I would use a function generator for this, taking advantage of the easy recursion approach and taking advantage of Object.entries combined with Array.map.

The below method acquires all the siblings of a defined key from an object, assuming that each key value may be the child of the said key.

As a side note, you could technically do that in many other ways (without relying on function generators), I just think that the generator approach is clever and easier to maintain. Moreover, it allows you to re-use the method later and allows you to eventually iterate the values.

Code explanation is directly in the code below.

const roles = {
    ROLE_SUPER_ADMIN: 'ROLE_ADMIN',
    ROLE_ADMIN: 'ROLE_MODERATOR',
    ROLE_MODERATOR: 'ROLE_AUTHOR',
    ROLE_AUTHOR: null,
    ROLE_CLIENT: null
}

// Acquire all the siblings, where a sibling is a key whose value is the value of another key.
function* getSiblings(v, source) {
  // if the desired key exists in source..
  if (source[v]) {
    // yield the value, which is a role in that case.
    yield source[v];
    // next, yield all the siblings of that value (role).
    yield* [...getSiblings(source[v], source)];
  }
}

// Map all roles by its siblings.
const res = Object.entries(roles).map(([key, role]) => {
  // key is the main role, whereas role is the "child" role.
  // Technically, [key] is not exactly a child role of [key], so we're injecting it manually below to avoid polluting the getSiblings method.
  return {
    [key]: [key, ...getSiblings(key, roles)] // <-- as mentioned above, the array is build by starting from the main role (key) and appending the child roles (siblings). [key] is a shorthand to set the key.
  }
});

console.log(res);
like image 194
briosheje Avatar answered Jan 22 '26 19:01

briosheje


I would separate out the recursive call necessary to fetch the list from the code that builds the output. That allows you to make both of them quite simple:

const listRoles = (rolls, name) => name in roles
  ? [name, ... listRoles (rolls, roles [name] )]
  : []

const roleMapper = (roles) => Object .assign (
  ... Object.keys (roles) .map (name => ({ [name]: listRoles (roles, name) }))
)

const roles = {ROLE_SUPER_ADMIN: 'ROLE_ADMIN', ROLE_ADMIN: 'ROLE_MODERATOR', ROLE_MODERATOR: 'ROLE_AUTHOR', ROLE_AUTHOR: null, ROLE_CLIENT: null}

console .log (
  roleMapper (roles)
)

Here listRoles is the recursive bit, and it simply takes a roles object and a name and returns all the descendant names, so

listRoles(roles, 'ROLE_MODERATOR') //=> ["ROLE_MODERATOR", "ROLE_AUTHOR"]

roleMapper uses that function. It takes the roles object and calls listRoles on each of its keys, combining them into a new object.

Together, these yield the following output:

{
    ROLE_SUPER_ADMIN: ["ROLE_SUPER_ADMIN", "ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"],
    ROLE_ADMIN: ["ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"],
    ROLE_MODERATOR: ["ROLE_MODERATOR", "ROLE_AUTHOR"],
    ROLE_AUTHOR: ["ROLE_AUTHOR"],
    ROLE_CLIENT: ["ROLE_CLIENT"]
}

I see the accepted answer generates a structure more like this:

[
    {ROLE_SUPER_ADMIN: ["ROLE_SUPER_ADMIN", "ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"]},
    {ROLE_ADMIN: ["ROLE_ADMIN", "ROLE_MODERATOR", "ROLE_AUTHOR"]},
    {ROLE_MODERATOR: ["ROLE_MODERATOR", "ROLE_AUTHOR"]},
    {ROLE_AUTHOR: ["ROLE_AUTHOR"]},
    {ROLE_CLIENT: ["ROLE_CLIENT"]}
]

(The difference is that mine was a single object, versus this one which was an array of single-property objects.)

While that feels less logical to me, it would be even easier to write:

const roleMapper = (roles) => Object.keys (roles) .map (n => ({ [n]: listRoles (roles, n) }))
like image 26
Scott Sauyet Avatar answered Jan 22 '26 19:01

Scott Sauyet