繁体   English   中英

递归过滤嵌套的 object

[英]Filter nested object recursively

我有一个看起来像这样的 object:

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,
        },
      },
    },
  },
};

我想创建一个 function ( getRoutes ),它根据传入的RESTRICTIONS过滤/减少 object,所有permissions必须匹配。

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'

首先,不用考虑递归的东西,确保你的规则逻辑定义得很好。

我尝试使用您所需的 API 编写验证 function,但不认为它非常易读。 您可能希望稍后对其进行重构。 (提示:编写一些单元测试!)

下面的示例采用规则配置 object 和树中的节点。 它返回一个 boolean 指示节点是否符合要求。

 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'] }) ) )

将这段代码排序后,就可以开始思考如何遍历树了。 您基本上定义的是如何遍历每条路径以及何时返回。

如果我们只是想记录,只需 (1) 检查ROUTES和 (2) 遍历v.ROUTES object 中的条目

 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, }, }, }, }, }; };

然后是最难的部分:创建一个新的树结构。

我选择采取两个步骤:

  • 首先,我们filter掉不通过验证的值,
  • 其次,我们检查是否需要担心任何子路由。

如果有子路由,我们创建一个新的路径 object,它具有过滤的 ROUTES 值。

 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 (ie 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, }, }, }, }, }; };

我希望这个示例可以帮助您入门并让您编写自己的/完善的版本。 如果我错过了任何要求,请告诉我。

我这样“解决”了它:

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;
}

像这样使用:

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

const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)

不是最高效的解决方案,但它有效。 @user3297291 解决方案似乎要好得多,因此将对其进行重构,只需使其更具可读性即可。 我认为.reduce()的解决方案是最好的解决方案,但也许不可能。

我的版本在算法上与 user3297291 的版本没有什么不同。 但是代码设计有点不同。

我尝试在 object 遍历和匹配测试中都更加通用。 我希望两者都是可重用的功能。 遍历采用谓词和属性名称供子级递归(在您的情况下为'ROUTES' )并返回 function 过滤提供给它的 object 。

对于谓词,我将调用matchesRestrictions的结果传递给您的USER_RESTRICTIONS object。 人们的想法是可能会有其他限制。 我假设如果该值为 boolean,那么 object 必须具有与该键相同的 boolean 值。 如果它是一个数组,那么其中的每个项目都必须出现在数组中的那个键处。 添加其他类型很容易。 不过,这可能太笼统了; 我真的不知道USER_PERMMISSIONSRESTRICTIONS部分中还会出现什么。

这是我想出的代码:

 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) )

我不知道通用filterObj最终是怎样的。 但我确实用另一个 object 和不同的路径对它进行了测试:

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)

得到这个结果:

{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}}

所以它至少有点可重用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM