如何使用 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
并创建 teams
和 regions
的查找,然后通过替换映射 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}) => ({
(这只是从 [ ... ]
到 { ... }
的变化。)
我有一些对象集合,它们之间具有基本的一对多关系。我的目标是编写一个函数(或必要时可以组合的函数)来解析/注入外部 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
并创建 teams
和 regions
的查找,然后通过替换映射 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}) => ({
(这只是从 [ ... ]
到 { ... }
的变化。)