0
votes

Filtrer les objets imbriqués de manière récursive

J'ai un objet qui ressemble à ceci:

function getRoutes(routes, restrictions){
   //...
}

const USER_RESTRICTIONS = {
    shouldBeLoggedIn: true,
    permissions: ['EMAIL'],
}

const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)

allowedRoutes === {
  ACCOUNT: {
    TO: '/account',
    RESTRICTIONS: {
      shouldBeLoggedIn: true,
    },
    ROUTES: {
      PROFILE: {
        TO: '/account/profile',
        RESTRICTIONS: {
          shouldBeLoggedIn: true,
        },
        ROUTES: {
          INFORMATION: {
            TO: '/account/profile/information',
            RESTRICTIONS: {
              shouldBeLoggedIn: true,
              permissions: ['EMAIL'],
            },
          },
        },
      },
      LIKES: {
        TO: '/account/likes',
        RESTRICTIONS: {
          shouldBeLoggedIn: true,
        },
      },
    },
  },
} ? 'YAY' : 'NAY'

Je veux créer une fonction ( getRoutes ) qui filtre / réduit cet objet en fonction du RESTRICTIONS transmises, toutes les permissions doivent correspondre.

const ROUTES = {
  ACCOUNT: {
    TO: '/account',
    RESTRICTIONS: {
      shouldBeLoggedIn: true,
    },
    ROUTES: {
      PROFILE: {
        TO: '/account/profile',
        RESTRICTIONS: {
          shouldBeLoggedIn: true,
        },
        ROUTES: {
          INFORMATION: {
            TO: '/account/profile/information',
            RESTRICTIONS: {
              shouldBeLoggedIn: true,
              permissions: ['EMAIL'],
            },
          },
          PASSWORD: {
            TO: '/account/profile/password',
            RESTRICTIONS: {
              shouldBeLoggedIn: true,
              permissions: ['EMAIL', 'ADMIN'],
            },
          },
        },
      },
      COLLECTIONS: {
        TO: '/account/collections',
        RESTRICTIONS: {
          shouldBeLoggedIn: true,
          permissions: ['ADMIN'],
        },
      },
      LIKES: {
        TO: '/account/likes',
        RESTRICTIONS: {
          shouldBeLoggedIn: true,
        },
      },
    },
  },
};


4 commentaires

où est le problème réel?


Rester coincé sur la façon dont je dois créer la fonction getRoutes pour produire la sortie souhaitée.


Est-ce que c'est ? 'YAY': 'NAY' à la toute fin de votre sortie souhaitée censée être là?


@ScottSauyet Oui, «YAY» signifierait que la fonction getRoutes a fait ce que je voulais, c'est-à-dire que allowedRoutes serait égal à cet objet attendu. Je pensais que ce serait plus facile à comprendre si j'écrivais la plupart de la question avec du code, je suppose que j'avais tort: ​​P.


3 Réponses :


2
votes

Premièrement, sans penser aux éléments récursifs, assurez-vous que votre logique de règle est bien définie.

J'ai essayé d'écrire une fonction de validation en utilisant votre API requise, mais je ne pense pas qu'elle soit très lisible. Vous voudrez peut-être le refactoriser plus tard. (Astuce: écrivez des tests unitaires!)

L'exemple ci-dessous prend un objet de configuration de règle et un nœud de votre arborescence. Il renvoie un booléen indiquant si le nœud correspond aux exigences.

const traverse = (obj, pred) => Object
  .fromEntries(
    Object
      .entries(obj)
      .filter(
        ([k, v]) => pred(v) // Get rid of the paths that don't match restrictions
      )
      .map(
        ([k, v]) => [
          k, v.ROUTES
            // If there are child paths, filter those as well (i.e. recurse)
            ? Object.assign({}, v, { ROUTES: traverse(v.ROUTES, pred) })
            : v
          ]
      )
  );


const includedIn = xs => x => xs.includes(x);
const isAllowed = ({ shouldBeLoggedIn = false, permissions = [] }) => 
  ({ RESTRICTIONS }) => (
    (shouldBeLoggedIn ? RESTRICTIONS.shouldBeLoggedIn : true) &&
    (RESTRICTIONS.permissions || []).every(includedIn(permissions))
  );
  
console.log(
  traverse(
    getRoutes(),
    isAllowed({ shouldBeLoggedIn: true, permissions: [ 'EMAIL'] })
  )
)

function getRoutes() { 
  return {
    ACCOUNT: {
      TO: '/account',
      RESTRICTIONS: {
        shouldBeLoggedIn: true,
      },
      ROUTES: {
        PROFILE: {
          TO: '/account/profile',
          RESTRICTIONS: {
            shouldBeLoggedIn: true,
          },
          ROUTES: {
            INFORMATION: {
              TO: '/account/profile/information',
              RESTRICTIONS: {
                shouldBeLoggedIn: true,
                permissions: ['EMAIL'],
              },
            },
            PASSWORD: {
              TO: '/account/profile/password',
              RESTRICTIONS: {
                shouldBeLoggedIn: true,
                permissions: ['EMAIL', 'ADMIN'],
              },
            },
          },
        },
        COLLECTIONS: {
          TO: '/account/collections',
          RESTRICTIONS: {
            shouldBeLoggedIn: true,
            permissions: ['ADMIN'],
          },
        },
        LIKES: {
          TO: '/account/likes',
          RESTRICTIONS: {
            shouldBeLoggedIn: true,
          },
        },
      },
    },
  };
};

Avec ce morceau de code trié, vous pouvez commencer à réfléchir à la façon de parcourir l'arbre. Ce que vous définissez fondamentalement, c'est comment faire une boucle sur chaque chemin et quand revenir.

Si nous voulons juste nous connecter, il s'agit de (1) vérifier ROUTES , et (2) boucle sur les entrées à l'intérieur de l'objet v.ROUTES .

const traverse = obj => {
  Object
    .entries(obj)
    .forEach(
      ([k, v]) => {
        console.log(v.TO);
        if (v.ROUTES) traverse(v.ROUTES)         
      }
    )
};

traverse(getRoutes());

function getRoutes() { 
  return {
    ACCOUNT: {
      TO: '/account',
      RESTRICTIONS: {
        shouldBeLoggedIn: true,
      },
      ROUTES: {
        PROFILE: {
          TO: '/account/profile',
          RESTRICTIONS: {
            shouldBeLoggedIn: true,
          },
          ROUTES: {
            INFORMATION: {
              TO: '/account/profile/information',
              RESTRICTIONS: {
                shouldBeLoggedIn: true,
                permissions: ['EMAIL'],
              },
            },
            PASSWORD: {
              TO: '/account/profile/password',
              RESTRICTIONS: {
                shouldBeLoggedIn: true,
                permissions: ['EMAIL', 'ADMIN'],
              },
            },
          },
        },
        COLLECTIONS: {
          TO: '/account/collections',
          RESTRICTIONS: {
            shouldBeLoggedIn: true,
            permissions: ['ADMIN'],
          },
        },
        LIKES: {
          TO: '/account/likes',
          RESTRICTIONS: {
            shouldBeLoggedIn: true,
          },
        },
      },
    },
  };
};

Puis vient le plus dur: créer une nouvelle arborescence.

J'ai choisi de faire deux étapes:

  • Tout d'abord, nous filtrons les valeurs qui ne réussissent pas la validation,
  • Deuxièmement, nous vérifions si nous devons nous soucier des routes enfants.

S'il y a des routes enfants, nous créons un nouvel objet chemin qui a une valeur ROUTES filtrée.

const includedIn = xs => x => xs.includes(x);

// RuleSet -> Path -> bool
const isAllowed = ({ shouldBeLoggedIn = false, permissions = [] }) => 
  ({ RESTRICTIONS }) => (
    (shouldBeLoggedIn ? RESTRICTIONS.shouldBeLoggedIn : true) &&
    RESTRICTIONS.permissions.every(includedIn(permissions))
  );

console.log(
  [ 
    { RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ ] } },
    { RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ 'EMAIL' ] } },
    { RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ 'EMAIL', 'ADMIN' ] } }
  ].map(
    isAllowed({ shouldBeLoggedIn: true, permissions: [ 'EMAIL'] })
  )
)

J'espère que cet exemple peut vous aider à démarrer et vous permettre d'écrire votre propre version / polie. Faites-moi savoir si j'ai manqué des exigences.


1 commentaires

Merci! Cela fonctionne en effet :). On dirait aussi une bonne solution! Je suis toujours resté coincé dans la partie .map , je basculais entre l'utilisation de .map et .reduce mais je ne l'ai jamais fait fonctionner alors j'ai créé un mauvaise solution en utilisant .forEach pour sortir le chemin de la route à supprimer.



0
votes

Je l'ai "résolu" comme ceci:

const USER_RESTRICTIONS = {
    shouldBeLoggedIn: true,
    permissions: ['EMAIL'],
}

const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)

À utiliser comme:

export const checkLoggedIn = (shouldBeLoggedIn, isAuthenticated) => {
  if (!shouldBeLoggedIn) {
    return true;
  }

  return isAuthenticated;
};

function isRouteAllowed(route, restrictions) {
  const routeShouldBeLoggedIn = route.RESTRICTIONS.shouldBeLoggedIn;

  const passedLoggedInCheck = checkLoggedIn(
    routeShouldBeLoggedIn,
    restrictions.get('shouldBeLoggedIn')
  );

  if (!passedLoggedInCheck) {
    return false;
  } else {
    const routePermissions = route.RESTRICTIONS.permissions;

    if (!routePermissions) {
      return true;
    } else {
      const passedPermissions = routePermissions.every((permission) => {
        const restrictPermissions = restrictions.get('permissions');
        return (
          restrictPermissions &&
          restrictPermissions.find &&
          restrictPermissions.find(
            (userPermission) => userPermission === permission
          )
        );
      });

      return passedLoggedInCheck && passedPermissions;
    }
  }
}

function forEachRoute(
  routes,
  restrictions,
  routesToDelete = [],
  parentPath = []
) {
  const routeSize = Object.keys(routes).length - 1;

  Object.entries(routes).forEach(([key, route], index) => {
    const childRoutes = route.ROUTES;

    if (childRoutes) {
      parentPath.push(key);
      parentPath.push('ROUTES');
      forEachRoute(childRoutes, restrictions, routesToDelete, parentPath);
    } else {
      const allowed = isRouteAllowed(route, restrictions);
      if (!allowed) {
        const toAdd = [...parentPath, key];
        routesToDelete.push(toAdd);
      }
    }

    if (routeSize === index) {
      // new parent
      parentPath.pop();
      parentPath.pop();
    }
  });
}

const deletePropertyByPath = (object, path) => {
  let currentObject = object;
  let parts = path.split('.');
  const last = parts.pop();
  for (const part of parts) {
    currentObject = currentObject[part];
    if (!currentObject) {
      return;
    }
  }
  delete currentObject[last];
};

export function removeRestrictedRoutes(routes, restrictions) {
  let routesToDelete = [];

  forEachRoute(routes, restrictions, routesToDelete);

  let allowedRoutes = routes;

  routesToDelete.forEach((path) => {
    deletePropertyByPath(allowedRoutes, path.join('.'));
  });

  return allowedRoutes;
}

Ce n'est pas la solution la plus performante mais cela a fonctionné . La solution @ user3297291 semble bien meilleure, donc sera refactorisée à cela, il suffit de la rendre un peu plus lisible. Je pensais qu'une solution avec .reduce () aurait été la meilleure, mais peut-être pas possible.


0 commentaires

0
votes

Ma version n'est pas différente sur le plan algorithmique de celle de user3297291. Mais la conception du code est un peu différente.

J'essaie d'être plus générique à la fois dans la traversée des objets et dans les tests de correspondance. J'espère que les deux seraient des fonctions réutilisables. Le parcours prend un prédicat et un nom de propriété sur lesquels les enfants doivent récurer (dans votre cas 'ROUTES' ) et retourne une fonction qui filtre l'objet qui lui est fourni.

Pour le prédicat, je passe le résultat de l'appel de matchesRestrictions avec quelque chose comme votre objet USER_RESTRICTIONS . L'idée est qu'il y aura probablement d'autres restrictions possibles. Je suppose que si la valeur est un booléen, l'objet doit avoir la même valeur booléenne pour cette clé. S'il s'agit d'un tableau, chaque élément qu'il contient doit apparaître dans le tableau à cette clé. Il est assez facile d'ajouter d'autres types. Cela pourrait être trop générique, cependant; Je ne sais vraiment pas ce qui pourrait apparaître dans USER_PERMMISSIONS ou dans une section RESTRICTIONS .

Voici le code que j'ai trouvé: p >

{x: {foo: 1, kids: {a: {foo: 2, kids: {c: {foo: 4, val: 17}, d: {foo: 5, val: 12}}, val: 15}, h: {foo: 9, kids: {}, val: 11}}, val: 20}, z: {foo: 13, kids: {k: {foo: 14, kids: {}, val: 18}}, val: 25}}

Je ne sais pas comment filterObj a fini par être générique. Mais je l'ai testé avec un autre objet et un chemin différent vers les enfants:

const obj = {x: {foo: 1, val: 20, kids: {a: {foo: 2, val: 15, kids: {b: {foo: 3, val: 8}, c: {foo: 4, val: 17}, d: {foo: 5, val: 12}}}, e: {foo: 6, val: 5, kids: {f: {foo: 7, val: 23}, g: {foo: 8, val: 17}}}, h: {foo: 9, val: 11, kids: {i: {foo: 10, val: 3}, j: {foo: 11, val: 7}}}}}, y: {foo: 12, val: 8}, z: {foo: 13, val: 25, kids: {k: {foo: 14, val: 18, kids: {l: {foo: 5, val: 3}, m: {foo: 11, val: 7}}}}}}

const pred = ({val}) => val > 10

filterObj ( pred, 'kids') (obj)

obtenant ce résultat:

const filterObj = (pred, children) => (obj) => 
  Object .fromEntries (
    Object .entries (obj)
      .filter ( ([k, v]) => pred (v))
      .map ( ([k, v]) => [
        k, 
        v [children]
          ? {
              ...v, 
              [children]: filterObj (pred, children) (v [children]) 
            }
          : v
        ]
      )
  )

const matchesRestrictions = (config) => ({RESTRICTIONS = {}}) =>
  Object .entries (RESTRICTIONS) .every (([key, val]) => 
    typeof val == 'boolean'
      ? config [key] === val
    : Array.isArray (val)
      ? val .every (v => (config [key] || []) .includes (v))
    : true // What else do you want to handle?                              
  )


const ROUTES = {ACCOUNT: {TO: "/account", RESTRICTIONS: {shouldBeLoggedIn: true}, ROUTES: {PROFILE: {TO: "/account/profile", RESTRICTIONS: {shouldBeLoggedIn: true}, ROUTES: {INFORMATION: {TO: "/account/profile/information", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["EMAIL"]}}, PASSWORD: {TO: "/account/profile/password", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["EMAIL", "ADMIN"]}}}}, COLLECTIONS: {TO: "/account/collections", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["ADMIN"]}}, LIKES: {TO: "/account/likes", RESTRICTIONS: {shouldBeLoggedIn: true}}}}};
const USER_RESTRICTIONS = {shouldBeLoggedIn: true, permissions: ['EMAIL']}

console .log (
  filterObj (matchesRestrictions (USER_RESTRICTIONS), 'ROUTES') (ROUTES)
)

donc c'est au moins un peu réutilisable.


0 commentaires