如何使用 Ramda 通过 id 查找和注入外来对象?

How to lookup and inject a foreign object by id with Ramda?

我有一些对象集合,它们之间具有基本的一对多关系。我的目标是编写一个函数(或必要时可以组合的函数)来解析/注入外部 ID 字段到外部对象。

例如,我有以下对象:

const store = {
  users: [
    {
      teamId: 'team-1',
      name: 'user 1',
    },
    {
      teamId: 'team-2',
      name: 'user 2',
    },
  ],
  teams: [
    {
      id: 'team-1',
      regionId: 'region-1',
      name: 'Team 1',
    },
    {
      id: 'team-2',
      regionId: 'region-2',
      name: 'Team 2',
    }
  ],
  regions: [
    {
      id: 'region-1',
      name: 'Region 1',
    },
    {
      id: 'region-2',
      name: 'Region 2',
    },
  ],
}

我的目标是将其解决为以下内容:

const users = [
    {
      teamId: 'team-1',
      name: 'user 1',
      team: {
        id: 'team-1',
        regionId: 'region-1',
        region: {
          id: 'region-1',
          name: 'Region 1',
        },
        name: 'Team 1',
      }
    },
    // ...and so on
]

我离解决第一关不远了:

const findObject = (collection, idField = 'id') => id => R.find(R.propEq(idField, id), R.prop(collection, store))
const findTeam = findObject('teams')
const findRegion = findObject('regions')
const inject = field => R.useWith(
  R.merge,
  [R.objOf(field), R.identity]
)
const injectTeam = R.useWith(
  inject('team'),
  [findTeam]
)
const injectRegion = R.useWith(
  inject('region'),
  [findRegion]
)

R.map(injectTeam('team-1'))(store.users)

但这对我来说太过分了,到目前为止我只用 Ramda 做了更简单的事情。 理想的解决方案将允许我以某种方式组合注入器函数,因此解析更深层次将是可选的。

我正在使用 R.converge 提取 users 并创建 teamsregions 的查找,然后通过替换映射 users teamId 与来自查找的团队,并在内部为该地区做同样的事情。

const { pipe, pick, map, indexBy, prop, converge, assoc, identity, flip, evolve } = R

// create a lookup of id -> object from teams and regions
const createLookup = pipe(
  pick(['teams', 'regions']),
  map(indexBy(prop('id')))
)

// add the value from the idPath in the lookup to the resultPath of the current object 
const injectFromLookup = (idKey, lookup, resultKey) => 
  converge(assoc(resultKey), [
    pipe(
      prop(idKey),
      flip(prop)(lookup),
    ),
    identity,
  ])

// extract users, create lookup, and map users to the required form
const inject = converge(
  (users, lookup) => map(
    pipe(
      injectFromLookup('teamId', prop('teams', lookup), 'team'),
      evolve({
        team: injectFromLookup('regionId', prop('regions', lookup), 'region')
      })
    )
  , users),
  [prop('users'), createLookup],
)

const store = {"users":[{"teamId":"team-1","name":"user 1"},{"teamId":"team-2","name":"user 2"}],"teams":[{"id":"team-1","regionId":"region-1","name":"Team 1"},{"id":"team-2","regionId":"region-2","name":"Team 2"}],"regions":[{"id":"region-1","name":"Region 1"},{"id":"region-2","name":"Region 2"}]}

console.log(inject(store))
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

这是您可以使用 folktale/maybe 实现的一种可能方法。我将从高级函数 makeUser 开始,然后从那里开始倒退 -

const makeUser = (user = {}) =>
  injectTeam (user)
    .chain
      ( u =>
          injectRegion (u.team)
            .map
              ( t =>
                  ({ ...u, team: t })
              )
      )
    .getOrElse (user)

每个 injector 接受一个 get 函数,一个 set 函数,以及 store -

const injectTeam =
  injector
    ( o => o.teamId
    , o => team => ({ ...o, team })
    , store.teams
    )

const injectRegion =
  injector
    ( o => o.regionId
    , o => region => ({ ...o, region })
    , store.regions
    )

通用 injector 尝试 find 使用 get 然后 set -

const injector = (get, set, store = []) => (o = {}) =>
  find (get (o), store) .map (set (o))

现在我们实施 find 以便它 returns 一个 可能 -

const { Just, Nothing } =
  require ("folktale/maybe")

const fromNullable = v =>
  v == null
    ? Nothing ()
    : Just (v)

const find = (id = "", vs = []) =>
  fromNullable (vs .find (v => v.id === id))

综上所述,现在我们只需为 store.users -

中的每个项目调用 makeUser
store.users .map (makeUser)

输出

[ { teamId: "team-1"
  , name: "user 1"
  , team: { id: "team-1"
          , regionId: "region-1"
          , name: "Team 1"
          , region: { id: "region-1"
                    , name: "Region 1"
                    }
          }
  }
, { teamId: "team-2"
  , name: "user 2"
  , team: { id: "team-2"
          , regionId: "region-2"
          , name: "Team 2"
          , region: { id: "region-2"
                    , name: "Region 2"
                    }
          }
  }
]

Ramda 是在 ES5 时代写的,需要支持 ES3。自从引入 ES6 以来,Ramda 确实为旧 JS 改进了很多东西,而现在在 vanilla JS 中更容易了。请注意,我是 Ramda 的创始人和忠实拥护者,但是解构、箭头函数、默认参数、模板字符串和许多其他东西使它成为现实,因此跳过 Ramda 通常会更干净。

这是一个普通的 JS 解决方案:

const denormalize = (steps) => (store) => 
  steps.reduce(
    (s, [key, foreignKey, target, targetId, newKey]) => ({
      ...s, 
      [key]: s[key].map(({[foreignKey]: fk, ...rest}) => ({
        ...rest,
        [newKey]: s[target].find(x => x[targetId] == fk)
      }))
    })
    , store
  )

const updateStore = denormalize([
  ['teams', 'regionId', 'regions', 'id', 'region'],
  ['users', 'teamId', 'teams', 'id', 'team'],
])

const store = {users: [{teamId: "team-1", name: "user 1"}, {teamId: "team-2", name: "user 2"}], teams: [{id: "team-1", regionId: "region-1", name: "Team 1"}, {id: "team-2", regionId: "region-2", name: "Team 2"}], regions: [{id: "region-1", name: "Region 1"}, {id: "region-2", name: "Region 2"}]}

console.log(updateStore(store).users)

请注意,这会执行所有非规范化,return创建一个包含所有非规范化数据的对象。我们只是从中提取 users 。显然,我们可以仅向 return 添加一个我们想要的部分的包装器,但看起来这可能仍然有用。 (所以如果你愿意,你可以获得非规范化的 teams 属性。)

这比您的要求更进一步,省略了外键并将它们替换为外部对象。这仅仅是基于对您想要的东西的误解,或者可能认为这就是我想要的(;-))。所以结果看起来像:

[
  {
    name: "user 1",
    team: {
      id: "team-1",
      name: "Team 1",
      region: {
        id: "region-1",
        name: "Region 1"
      }
    }
  }, /*... */
]

如果你想保留那些外键,代码简单一点:

const denormalize = (steps) => (store) => 
  steps.reduce(
    (s, [key, foreignKey, target, targetId, newKey]) => ({
      ...s, 
      [key]: s[key].map(t => ({
        ...t,
        [newKey]: s[target].find(x => x[targetId] == t[foreignKey])
      }))
    })
    , store
  )

steps 参数中所有这些字符串的含义可能不明确。如果是这样,您可以将其替换为:

const config = [
  {key: 'teams', foreignKey: 'regionId', target: 'regions', targetId: 'id', newKey: 'region'},
  {key: 'users', foreignKey: 'teamId', target: 'teams', targetId: 'id', newKey: 'team'},
]

只需将 reduce 的第一行更改为

    (s, {key, foreignKey, target, targetId, newKey}) => ({

(这只是从 [ ... ]{ ... } 的变化。)