Фильтровать вложенный объект рекурсивно - PullRequest
0 голосов
/ 06 октября 2019

У меня есть объект, который выглядит следующим образом:

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

Я хочу создать функцию (getRoutes), которая фильтрует / уменьшает этот объект в зависимости от переданного RESTRICTIONS, все 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'

Ответы [ 3 ]

2 голосов
/ 07 октября 2019

Во-первых, не думая о рекурсивных вещах, убедитесь, что у вас правильно определена логика правил.

Я попытался написать функцию проверки с использованием требуемого API, но не думаю, что она очень удобочитаема. Возможно, вы захотите изменить его позже. (Совет: напишите несколько модульных тестов!)

Пример ниже берет объект конфигурации правила и узел из вашего дерева. Он возвращает логическое значение, указывающее, соответствует ли узел требованиям.

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.

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 выводим значения, которые не проходят проверку,
  • Во-вторых, мы проверяем, нужно ли нам беспокоиться о каких-либо дочерних маршрутах.

Если есть дочерние маршруты,мы создаем новый объект пути с отфильтрованным значением 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 (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,
          },
        },
      },
    },
  };
};

Надеюсь, этот пример поможет вам начать работу и позволит вам написать собственную / отшлифованную версию. Дайте мне знать, если я пропустил какие-либо требования.

0 голосов
/ 07 октября 2019

Моя версия алгоритмически не отличается от той, что была у пользователя 3297291. Но дизайн кода немного отличается.

Я стараюсь быть более общим как в обходе объекта, так и в тестировании на совпадения. Я надеюсь, что обе функции будут многоразовыми. Обратный путь принимает предикат и имя свойства для дочерних элементов (в вашем случае 'ROUTES') и возвращает функцию, которая фильтрует предоставленный ему объект.

Для предиката я передаю результатвызов matchesRestrictions с чем-то вроде вашего USER_RESTRICTIONS объекта. Мысль заключается в том, что, вероятно, будут и другие возможные ограничения. Я предполагаю, что если значение является логическим, то объект должен иметь то же логическое значение для этого ключа. Если это массив, то каждый элемент в нем должен появляться в массиве с этим ключом. Достаточно просто добавить другие типы. Это может быть слишком общим, хотя;Я действительно не знаю, что еще может появиться в USER_PERMMISSIONS или RESTRICTIONS разделе.

Это код, который я придумал:

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. Но я проверил его с другим объектом и другим путем к детям:

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

, так что он, по крайней мере, несколько раз можно использовать.

0 голосов
/ 07 октября 2019

Я «решил» это так:

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() было бы лучшим, но, возможно, не возможным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...