@rokkit/data 1.0.0-next.106 → 1.0.0-next.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/data",
3
- "version": "1.0.0-next.106",
3
+ "version": "1.0.0-next.108",
4
4
  "description": "Contains generic data manipulation functions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@lukeed/uuid": "^2.0.1",
34
- "@rokkit/core": "1.0.0-next.105",
34
+ "@rokkit/core": "1.0.0-next.106",
35
35
  "d3-array": "^3.2.4",
36
36
  "d3-collection": "^1.0.7",
37
37
  "ramda": "^0.30.1",
package/src/dataset.js CHANGED
@@ -1,54 +1,18 @@
1
1
  import { equals, identity, pick, omit, clone } from 'ramda'
2
2
  import { descending } from 'd3-array'
3
- import { pickAllowedConfig, defaultConfig, includeAll } from './constants'
3
+ import { pickAllowedConfig, defaultConfig } from './constants'
4
4
  import { deriveSortableColumn } from './infer'
5
5
  import { groupDataByKeys, fillAlignedData, getAlignGenerator, aggregateData } from './rollup'
6
-
7
- /**
8
- * Adds a filter to the dataset using the provided condition. This filter is applied
9
- * in subsequent operations like select, delete, and update.
10
- *
11
- * @param {Object} config - The configuration object to be updated.
12
- * @param {Function} condition - The condition function to apply.
13
- * @returns {Object} - The updated configuration
14
- */
15
- function where(config, condition) {
16
- config.filter = condition
17
- return config
18
- }
19
- /**
20
- * Groups the dataset by the specified columns.
21
- * @param {Object} config - The configuration object to be updated.
22
- * @param {Object} actions - The actions object for method chaining.
23
- * @param {...string} fields - The columns to group by.
24
- * @returns {Object} - The updated configurations.
25
- */
26
- function groupBy(config, ...fields) {
27
- config.group_by = fields
28
- return config
29
- }
30
-
31
- /**
32
- * Aligns the columns of the dataset using the provided fields.
33
- *
34
- * @param {Object} config - The configuration object to be updated.
35
- * @param {...string} fields - The fields to align.
36
- * @returns {Object} - The updated configuration.
37
- */
38
- function alignBy(config, ...fields) {
39
- config.align_by = fields
40
- return config
41
- }
42
- /**
43
- * Sets the template for adding empty rows in the dataset.
44
- * @param {Object} config - The configuration object to be updated.
45
- * @param {Object} template - The template to use for adding empty rows.
46
- * @returns {Object} - The updated configuration.
47
- */
48
- function usingTemplate(config, template) {
49
- config.template = template
50
- return config
51
- }
6
+ import {
7
+ leftJoin,
8
+ rightJoin,
9
+ fullJoin,
10
+ innerJoin,
11
+ crossJoin,
12
+ semiJoin,
13
+ antiJoin,
14
+ nestedJoin
15
+ } from './join'
52
16
 
53
17
  /**
54
18
  * Adds a summary field to the dataset.
@@ -71,161 +35,6 @@ function summarize(config, from, using) {
71
35
  return config
72
36
  }
73
37
 
74
- /**
75
- * Joins two datasets together based on a condition. Result includes all rows from the first
76
- * dataset and matching rows from the second dataset. In case of multiple matches, all
77
- * combinations are returned. When combining the rows, the columns from the first dataset take
78
- * precedence.
79
- *
80
- * inner: only the rows that have a match in both datasets.
81
- * outer: all rows from the first dataset and matching rows from the second dataset.
82
- *
83
- * @param {Array} first - The first dataset to join.
84
- * @param {Array} second - The second dataset to join.
85
- * @param {Function} condition - The condition to join the datasets on.
86
- * @returns {Object} - An object with the inner and outer join result.
87
- */
88
- function joinData(first, second, condition) {
89
- let inner = []
90
- const outer = []
91
-
92
- first.forEach((f) => {
93
- const matches = second.filter((s) => condition(f, s)).map((m) => ({ ...m, ...f }))
94
- inner = inner.concat(matches)
95
- if (matches.length === 0) outer.push(f)
96
- })
97
- return { inner, outer }
98
- }
99
-
100
- /**
101
- * Filters the results of the first dataset based on the condition using the second dataset.
102
- *
103
- * @param {Array} first - The first dataset to filter.
104
- * @param {Array} second - The second dataset to filter by.
105
- * @param {Function} condition - The condition to filter the first dataset by.
106
- * @returns {Array} - The filtered dataset.
107
- */
108
- function antiJoin(first, second, condition) {
109
- return first.filter((f) => !second.find((s) => condition(f, s)))
110
- }
111
-
112
- /**
113
- * Joins two datasets together based on a condition. Result includes all rows from the first
114
- * dataset and matching rows from the second dataset. In case of multiple matches, all
115
- * combinations are returned. When combining the rows, the columns from the first dataset take
116
- * precedence.
117
- *
118
- * @param {Array} first - The first dataset to join.
119
- * @param {Array} second - The second dataset to join.
120
- * @param {Function} condition - The condition to join the datasets on.
121
- * @returns {Array} - The joined dataset.
122
- */
123
- function innerJoin(first, second, condition) {
124
- const { inner } = joinData(first, second, condition)
125
- return inner
126
- }
127
-
128
- /**
129
- * Performs a left join on two datasets based on a condition. Result includes all rows from the first
130
- * dataset and matching rows from the second dataset. In case of multiple matches, all combinations
131
- * are returned. When combining the rows, the columns from the first dataset take precedence.
132
- * If there is no match in the second dataset, only the row from the first dataset is returned.
133
- *
134
- * This can be used to perform a right join by swapping the datasets.
135
- *
136
- * @param {Array} first - The first dataset to join.
137
- * @param {Array} second - The second dataset to join.
138
- * @param {Function} condition - The condition to join the datasets on.
139
- * @returns {Array} - The joined dataset.
140
- */
141
- function leftJoin(first, second, condition) {
142
- const { inner, outer } = joinData(first, second, condition)
143
- return inner.concat(outer)
144
- }
145
-
146
- /**
147
- * Performs a full join on two datasets based on a condition. Result includes all rows from both
148
- * datasets. In case of multiple matches, all combinations are returned. When combining the rows,
149
- * the columns from the first dataset take precedence. If there is no match in the second dataset,
150
- * only the row from the first dataset is returned. If there is no match in the first dataset, only
151
- * the row from the second dataset is returned.
152
- *
153
- * @param {Array} first - The first dataset to join.
154
- * @param {Array} second - The second dataset to join.
155
- * @param {Function} condition - The condition to join the datasets on.
156
- * @returns {Array} - The joined dataset.
157
- */
158
- function fullJoin(first, second, condition) {
159
- const { inner, outer } = joinData(first, second, condition)
160
- const rightOuter = antiJoin(second, first, condition)
161
- return inner.concat(outer).concat(rightOuter)
162
- }
163
-
164
- /**
165
- * Performs a cross join on two datasets. Result includes all combinations of rows from both datasets.
166
- *
167
- * @param {Array} first - The first dataset to join.
168
- * @param {Array} second - The second dataset to join.
169
- * @returns {Array} - The joined dataset.
170
- */
171
- function crossJoin(first, second) {
172
- return first.map((f) => second.map((s) => ({ ...f, ...s }))).flat()
173
- }
174
-
175
- /**
176
- * Performs a semi join on two datasets based on a condition. Result includes all rows from the first
177
- * dataset that have a match in the second dataset.
178
- *
179
- * @param {Array} first - The first dataset to join.
180
- * @param {Array} second - The second dataset to join.
181
- * @param {Function} condition - The condition to join the datasets on.
182
- * @returns {Array} - The joined dataset.
183
- */
184
- function semiJoin(first, second, condition) {
185
- return first.filter((f) => second.find((s) => condition(f, s)))
186
- }
187
-
188
- /**
189
- * Performs a nested join on two datasets based on a condition. Result includes all rows from the first
190
- * dataset that have a match in the second dataset. The result is nested with the matching rows from the
191
- * second dataset.
192
- *
193
- * @param {Array} first - The first dataset to join.
194
- * @param {Array} second - The second dataset to join.
195
- * @param {Function} condition - The condition to join the datasets on.
196
- * @param {String} [key='children'] - The key to nest the matching rows under.
197
- * @returns {Array} - The joined dataset.
198
- */
199
- function nestedJoin(first, second, condition, key = 'children') {
200
- const result = first.map((f) => ({
201
- ...f,
202
- [key]: second.filter((s) => condition(f, s))
203
- }))
204
- return result
205
- }
206
-
207
- /**
208
- * Drops the specified keys from the data.
209
- *
210
- * @param {Array<Object>} data - The data to drop keys from.
211
- * @param {Array<String>} keys - The keys to drop.
212
- * @returns {Array<Object>} - The updated data with the specified keys dropped.
213
- */
214
- function dropKeys(data, ...keys) {
215
- return data.map((row) => omit(keys, row))
216
- }
217
-
218
- /**
219
- * Deletes rows from the data based on the condition
220
- *
221
- * @param {Array<Object>} data - The data to delete rows from.
222
- * @param {Function} condition - The condition to delete rows on.
223
- * @returns {Array<Object>} - The updated data with rows deleted based on the condition.
224
- */
225
- function deleteRows(data, condition) {
226
- return data.filter((row) => !condition(row))
227
- }
228
-
229
38
  /**
230
39
  * Updates rows in the data based on the condition
231
40
  *
@@ -315,21 +124,6 @@ function sortDataBy(data, ...fields) {
315
124
  return data
316
125
  }
317
126
 
318
- /**
319
- * Selects the specified columns from the dataset.
320
- *
321
- * @param {Array<Object>} data - The data to select from.
322
- * @param {Object} config - The configuration object.
323
- * @param {Function} config.filter - The filter function to apply to the data.
324
- * @param {Array<String>} cols - The columns to select.
325
- * @returns {Array<Object>} The selected data.
326
- */
327
- function selectKeys(data, config, ...cols) {
328
- const result = config.filter ? data.filter(config.filter) : data
329
- if (cols.length > 0) return result.map(pick(cols))
330
- return result
331
- }
332
-
333
127
  /**
334
128
  * Returns the default aggregator which rolls up the columns other than the group_by columns into a single object.
335
129
  *
@@ -379,51 +173,140 @@ function rollup(data, config) {
379
173
  return aggregatedData
380
174
  }
381
175
 
382
- /**
383
- * Dataset is a collection of data with a set of operations that can be performed on it.
384
- * @param {Array} data - The data to be stored in the dataset.
385
- * @param {Object} options - The configuration options for the dataset.
386
- * @returns {Object} - An object with a set of operations that can be performed on the dataset.
387
- */
388
- // eslint-disable-next-line max-lines-per-function
389
- export function dataset(data, options = {}) {
390
- const config = { ...clone(defaultConfig), ...options }
176
+ export class DataSet {
177
+ #config = clone(defaultConfig)
178
+ #data = []
179
+
180
+ constructor(data) {
181
+ this.#data = data
182
+ }
183
+ get data() {
184
+ return this.#data
185
+ }
186
+
187
+ set data(values) {
188
+ this.#data = values
189
+ }
190
+
191
+ // configuration
192
+ set config(props = {}) {
193
+ this.#config = { ...this.#config, ...pickAllowedConfig(props) }
194
+ }
195
+
196
+ override(props) {
197
+ this.#config = { ...this.#config, ...pickAllowedConfig(props) }
198
+ return this
199
+ }
391
200
 
392
- const actions = {
393
- // configuration
394
- override: (props) => dataset(data, { ...config, ...pickAllowedConfig(props) }),
395
- where: (condition) => dataset(data, where(config, condition)),
396
- groupBy: (...fields) => dataset(data, groupBy(config, ...fields)),
397
- alignBy: (...fields) => dataset(data, alignBy(config, ...fields)),
398
- using: (template) => dataset(data, usingTemplate(config, template)),
399
- summarize: (from, fields) => dataset(data, summarize(config, from, fields)),
400
- // alter keys
401
- rename: (how) => dataset(renameKeys(data, how)),
402
- drop: (...fields) => dataset(dropKeys(data, ...fields)),
403
- // alter rows
404
- sortBy: (...fields) => dataset(sortDataBy(data, ...fields)),
405
- delete: () => dataset(deleteRows(data, config.filter || includeAll)),
406
- update: (value) => dataset(updateRows(data, config.filter || identity, value)),
407
- fillNA: (value) => dataset(fillNA(data, value)),
408
- // transform data
409
- apply: (callback) => dataset(data.map(callback)),
410
- rollup: () => dataset(rollup(data, config)),
411
- select: (...cols) => selectKeys(data, config, ...cols),
412
- // set operations
413
- union: (other) => dataset(data.concat(other)),
414
- minus: (other) => dataset(data.filter((d) => !other.find((x) => equals(x, d)))),
415
- intersect: (other) => dataset(data.filter((d) => other.find((x) => equals(x, d)))),
416
- // joins
417
- innerJoin: (other, condition) => dataset(innerJoin(data, other.select(), condition)),
418
- leftJoin: (other, condition) => dataset(leftJoin(data, other.select(), condition)),
419
- rightJoin: (other, condition) => dataset(leftJoin(other.select(), data, condition)),
420
- fullJoin: (other, condition) => dataset(fullJoin(data, other.select(), condition)),
421
- crossJoin: (other) => dataset(crossJoin(data, other.select())),
422
- semiJoin: (other, condition) => dataset(semiJoin(data, other.select(), condition)),
423
- antiJoin: (other, condition) => dataset(antiJoin(data, other.select(), condition)),
424
- nestedJoin: (other, condition) =>
425
- dataset(nestedJoin(data, other.select(), condition, config.children))
201
+ where(condition) {
202
+ this.#config.filter = condition
203
+ return this
426
204
  }
205
+ groupBy(...fields) {
206
+ this.#config.group_by = fields
207
+ return this
208
+ }
209
+ alignBy(...fields) {
210
+ this.#config.align_by = fields
211
+ return this
212
+ }
213
+ usingTemplate(template) {
214
+ this.#config.template = template
215
+ return this
216
+ }
217
+ summarize(from, fields) {
218
+ this.config = summarize(this.#config, from, fields)
219
+ return this
220
+ }
221
+
222
+ rename(how) {
223
+ this.data = renameKeys(this.data, how)
224
+ return this
225
+ }
226
+ drop(...fields) {
227
+ const newDataSet = new DataSet(this.data.map((row) => omit(fields, row)))
228
+ newDataSet.config = this.config
229
+ return newDataSet
230
+ }
231
+ // alter rows
232
+ sortBy(...fields) {
233
+ this.data = sortDataBy(this.data, ...fields)
234
+ return this
235
+ }
236
+ remove() {
237
+ if (this.#config.filter) {
238
+ this.#data = this.data.filter((row) => !this.#config.filter(row))
239
+ } else {
240
+ this.#data = []
241
+ }
242
+ this.#config.filter = undefined
243
+ // this.data = deleteRows(this.data, this.#config.filter || includeAll)
244
+ return this
245
+ }
246
+ update(value) {
247
+ this.data = updateRows(this.data, this.#config.filter || identity, value)
248
+ this.#config.filter = undefined
249
+ return this
250
+ }
251
+ fillNA(value) {
252
+ this.data = fillNA(this.#data, value)
253
+ return this
254
+ }
255
+ // transform data
256
+ apply(callback) {
257
+ this.data = this.data.map(callback)
258
+ return this
259
+ }
260
+ rollup() {
261
+ return new DataSet(rollup(this.data, this.#config))
262
+ }
263
+ select(...cols) {
264
+ const result = this.#config.filter ? this.data.filter(this.#config.filter) : this.data
265
+ if (cols.length > 0) return result.map(pick(cols))
266
+ return result
267
+ }
268
+ // set operations
269
+ union(other) {
270
+ if (Array.isArray(other)) {
271
+ return new DataSet(this.data.concat(other))
272
+ }
273
+ return new DataSet(this.data.concat(other.data))
274
+ }
275
+ minus(other) {
276
+ return new DataSet(this.data.filter((d) => !other.find((x) => equals(x, d))))
277
+ }
278
+ intersect(other) {
279
+ return new DataSet(this.data.filter((d) => other.find((x) => equals(x, d))))
280
+ }
281
+ // joins
282
+ innerJoin(other, condition) {
283
+ return new DataSet(innerJoin(this.data, other.data, condition))
284
+ }
285
+ leftJoin(other, condition) {
286
+ return new DataSet(leftJoin(this.data, other.data, condition))
287
+ }
288
+ rightJoin(other, condition) {
289
+ return new DataSet(rightJoin(this.data, other.data, condition))
290
+ }
291
+ fullJoin(other, condition) {
292
+ return new DataSet(fullJoin(this.data, other.data, condition))
293
+ }
294
+ crossJoin(other) {
295
+ return new DataSet(crossJoin(this.data, other.data))
296
+ }
297
+ semiJoin(other, condition) {
298
+ return new DataSet(semiJoin(this.data, other.data, condition))
299
+ }
300
+ antiJoin(other, condition) {
301
+ return new DataSet(antiJoin(this.data, other.data, condition))
302
+ }
303
+ nestedJoin(other, condition) {
304
+ return new DataSet(nestedJoin(this.data, other.data, condition, this.#config.children))
305
+ }
306
+ }
427
307
 
428
- return actions
308
+ export function dataset(data, options = {}) {
309
+ const ds = new DataSet(data)
310
+ ds.config = options
311
+ return ds
429
312
  }
package/src/index.js CHANGED
@@ -3,5 +3,6 @@ export * from './types'
3
3
  export { typeOf } from './utils'
4
4
  export { renamer } from './renamer'
5
5
  export { model } from './model'
6
+ export { innerJoin, leftJoin, rightJoin, fullJoin, crossJoin, antiJoin, semiJoin } from './join'
6
7
  export { dataset } from './dataset'
7
8
  export { dataview } from './view'
package/src/join.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Joins two datasets together based on a condition. Result includes all rows from the first
3
+ * dataset and matching rows from the second dataset. In case of multiple matches, all
4
+ * combinations are returned. When combining the rows, the columns from the first dataset take
5
+ * precedence.
6
+ *
7
+ * inner: only the rows that have a match in both datasets.
8
+ * outer: all rows from the first dataset and matching rows from the second dataset.
9
+ *
10
+ * @param {Array<Object>} first - The first dataset to join.
11
+ * @param {Array<Object>} second - The second dataset to join.
12
+ * @param {Function} condition - The condition to join the datasets on.
13
+ * @returns {Array<Object>} - An object with the inner and outer join result.
14
+ */
15
+ function joinData(first, second, condition) {
16
+ let inner = []
17
+ const outer = []
18
+
19
+ first.forEach((f) => {
20
+ const matches = second.filter((s) => condition(f, s)).map((m) => ({ ...m, ...f }))
21
+ inner = inner.concat(matches)
22
+ if (matches.length === 0) outer.push(f)
23
+ })
24
+ return { inner, outer }
25
+ }
26
+
27
+ /**
28
+ * Filters the results of the first dataset based on the condition using the second dataset.
29
+ *
30
+ * @param {Array<Object>} first - The first dataset to filter.
31
+ * @param {Array<Object>} second - The second dataset to filter by.
32
+ * @param {Function} condition - The condition to filter the first dataset by.
33
+ * @returns {Array<Object>} - The filtered dataset.
34
+ */
35
+ export function antiJoin(first, second, condition) {
36
+ return first.filter((f) => !second.find((s) => condition(f, s)))
37
+ }
38
+
39
+ /**
40
+ * Joins two datasets together based on a condition. Result includes all rows from the first
41
+ * dataset and matching rows from the second dataset. In case of multiple matches, all
42
+ * combinations are returned. When combining the rows, the columns from the first dataset take
43
+ * precedence.
44
+ *
45
+ * @param {Array<Object>} first - The first dataset to join.
46
+ * @param {Array<Object>} second - The second dataset to join.
47
+ * @param {Function} condition - The condition to join the datasets on.
48
+ * @returns {Array<Object>} - The joined dataset.
49
+ */
50
+ export function innerJoin(first, second, condition) {
51
+ const { inner } = joinData(first, second, condition)
52
+ return inner
53
+ }
54
+
55
+ /**
56
+ * Performs a left join on two datasets based on a condition.
57
+ *
58
+ * - Result includes all rows from the first dataset and matching rows from the second dataset.
59
+ * - In case of multiple matches, all combinations are returned.
60
+ * - When combining the rows, the columns from the first dataset take precedence.
61
+ * - If there is no match in the second dataset, only the row from the first dataset is returned.
62
+ *
63
+ * This can be used to perform a right join by swapping the datasets.
64
+ *
65
+ * @param {Array<Object>} first - The first dataset to join.
66
+ * @param {Array<Object>} second - The second dataset to join.
67
+ * @param {Function} condition - The condition to join the datasets on.
68
+ * @returns {Array<Object>} - The joined dataset.
69
+ */
70
+ export function leftJoin(first, second, condition) {
71
+ const { inner, outer } = joinData(first, second, condition)
72
+ return inner.concat(outer)
73
+ }
74
+
75
+ /**
76
+ * Performs a right join on two datasets based on a condition.
77
+ *
78
+ * - Result includes all rows from both datasets.
79
+ * - In case of multiple matches, all combinations are returned.
80
+ * - When combining the rows, the columns from the second dataset take precedence.
81
+ * - If there is no match in the first dataset, only the row from the second dataset is returned.
82
+ *
83
+ * @param {Array<Object>} first - The first dataset to join.
84
+ * @param {Array<Object>} second - The second dataset to join.
85
+ * @param {Function} condition - The condition to join the datasets on.
86
+ * @returns {Array<Object>} - The joined dataset.
87
+ */
88
+ export function rightJoin(first, second, condition) {
89
+ return leftJoin(second, first, condition)
90
+ }
91
+
92
+ /**
93
+ * Performs a full join on two datasets based on a condition.
94
+ *
95
+ * - Result includes all rows from both datasets.
96
+ * - In case of multiple matches, all combinations are returned.
97
+ * - When combining the rows, the columns from the second dataset take precedence.
98
+ * - If there is no match in the first dataset, only the row from the second dataset is returned.
99
+ *
100
+ * @param {Array<Object>} first - The first dataset to join.
101
+ * @param {Array<Object>} second - The second dataset to join.
102
+ * @param {Function} condition - The condition to join the datasets on.
103
+ * @returns {Array<Object>} - The joined dataset.
104
+ */
105
+ export function fullJoin(first, second, condition) {
106
+ const { inner, outer } = joinData(first, second, condition)
107
+ const rightOuter = antiJoin(second, first, condition)
108
+ return inner.concat(outer).concat(rightOuter)
109
+ }
110
+
111
+ /**
112
+ * Performs a cross join on two datasets. Result includes all combinations of rows from both datasets.
113
+ *
114
+ * @param {Array<Object>} first - The first dataset to join.
115
+ * @param {Array<Object>} second - The second dataset to join.
116
+ * @returns {Array<Object>} - The joined dataset.
117
+ */
118
+ export function crossJoin(first, second) {
119
+ return first.map((f) => second.map((s) => ({ ...f, ...s }))).flat()
120
+ }
121
+
122
+ /**
123
+ * Performs a semi join on two datasets based on a condition. Result includes all rows from the first
124
+ * dataset that have a match in the second dataset.
125
+ *
126
+ * @param {Array<Object>} first - The first dataset to join.
127
+ * @param {Array<Object>} second - The second dataset to join.
128
+ * @param {Function} condition - The condition to join the datasets on.
129
+ * @returns {Array<Object>} - The joined dataset.
130
+ */
131
+ export function semiJoin(first, second, condition) {
132
+ return first.filter((f) => second.find((s) => condition(f, s)))
133
+ }
134
+
135
+ /**
136
+ * Performs a nested join on two datasets based on a condition. Result includes all rows from the first
137
+ * dataset that have a match in the second dataset. The result is nested with the matching rows from the
138
+ * second dataset.
139
+ *
140
+ * @param {Array<Object>} first - The first dataset to join.
141
+ * @param {Array<Object>} second - The second dataset to join.
142
+ * @param {Function} condition - The condition to join the datasets on.
143
+ * @param {String} [key='children'] - The key to nest the matching rows under.
144
+ * @returns {Array<Object>} - The joined dataset.
145
+ */
146
+ export function nestedJoin(first, second, condition, key = 'children') {
147
+ const result = first.map((f) => ({
148
+ ...f,
149
+ [key]: second.filter((s) => condition(f, s))
150
+ }))
151
+ return result
152
+ }