Skip to content

Commit ad65e00

Browse files
create aggregateObjects and flattenObject helpers
1 parent db7e059 commit ad65e00

File tree

6 files changed

+379
-1
lines changed

6 files changed

+379
-1
lines changed

src/aggregateObjects.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// eslint-disable-next-line eslint-comments/disable-enable-pair
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
4+
/**
5+
*
6+
* Aggregates multiple objects into a single object by combining values of matching keys.
7+
* For each key present in any of the input objects, creates a comma-separated string
8+
* of values from all objects.
9+
* @param param - the objects to aggregate and the aggregation method
10+
* @returns a single object containing all unique keys with aggregated values
11+
* @example
12+
* const obj1 = { name: 'John', age: 30 };
13+
* const obj2 = { name: 'Jane', city: 'NY' };
14+
* const obj3 = { name: 'Bob', age: 25 };
15+
*
16+
* // Without wrap
17+
* aggregateObjects({ objs: [obj1, obj2, obj3] })
18+
* // Returns: { name: 'John,Jane,Bob', age: '30,,25', city: ',NY,' }
19+
*
20+
* // With wrap
21+
* aggregateObjects({ objs: [obj1, obj2, obj3], wrap: true })
22+
* // Returns: { name: '[John],[Jane],[Bob]', age: '[30],[],[25]', city: '[],[NY],[]' }
23+
*/
24+
export const aggregateObjects = ({
25+
objs,
26+
wrap = false,
27+
}: {
28+
/** the objects to aggregate in a single one */
29+
objs: any[];
30+
/** whether to wrap the concatenated values in a [] */
31+
wrap?: boolean;
32+
}): any => {
33+
const allKeys = Array.from(
34+
new Set(
35+
objs.flatMap((a) => (a && typeof a === 'object' ? Object.keys(a) : []))
36+
)
37+
);
38+
39+
// Reduce into a single object, where each key contains concatenated values from all input objects
40+
return allKeys.reduce((acc, key) => {
41+
const values = objs
42+
.map((o) => (wrap ? `[${o?.[key] ?? ''}]` : o?.[key] ?? ''))
43+
.join(',');
44+
acc[key] = values;
45+
return acc;
46+
}, {} as Record<string, any>);
47+
};

src/flattenObject.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { aggregateObjects } from './aggregateObjects';
2+
3+
/**
4+
*
5+
*Flattens a nested object into a single-level object with concatenated key names.
6+
* @param param - The information about the object to flatten
7+
* @returns A flattened object where nested keys are joined with underscores
8+
* @example
9+
* const nested = {
10+
* user: {
11+
* name: 'John',
12+
* address: {
13+
* city: 'NY',
14+
* zip: 10001
15+
* },
16+
* hobbies: ['reading', 'gaming']
17+
* }
18+
* };
19+
*
20+
* flattenObject(nested)
21+
* // Returns: {
22+
* // user_name: 'John',
23+
* // user_address_city: 'NY',
24+
* // user_address_zip: 10001,
25+
* // user_hobbies: 'reading,gaming'
26+
* // }
27+
*/
28+
export const flattenObject = ({
29+
obj,
30+
prefix = '',
31+
remove = ',',
32+
}: {
33+
/** */
34+
obj: any;
35+
/** The prefix to prepend to keys (used in recursion) */
36+
prefix?: string;
37+
/** */
38+
remove?: string;
39+
}): any =>
40+
!obj
41+
? {}
42+
: Object.keys(obj ?? []).reduce((acc, key) => {
43+
const newKey = prefix ? `${prefix}_${key}` : key;
44+
const entry = obj[key];
45+
46+
// Handle arrays of objects
47+
if (Array.isArray(entry) &&
48+
entry.length > 0 &&
49+
entry.some((item) => typeof item === 'object' && item !== null)) {
50+
// Flatten each object in the array
51+
const objEntries = entry.filter((item) => typeof item === 'object' && item !== null);
52+
const flattenedObjects = objEntries.map((item) =>
53+
flattenObject({ obj: item, remove })
54+
);
55+
// Aggregate the flattened objects
56+
const aggregated = aggregateObjects({ objs: flattenedObjects });
57+
// Add prefix to all keys
58+
Object.entries(aggregated).forEach(([k, v]) => {
59+
acc[`${newKey}_${k}`] = v;
60+
});
61+
}
62+
// Handle regular objects
63+
else if (
64+
typeof entry === 'object' &&
65+
entry !== null &&
66+
!Array.isArray(entry)
67+
) {
68+
Object.assign(
69+
acc,
70+
flattenObject({ obj: entry, prefix: newKey, remove }),
71+
);
72+
}
73+
// Handle primitive arrays and other values
74+
else {
75+
acc[newKey] = Array.isArray(entry)
76+
? entry
77+
.map((e) => {
78+
if (typeof e === 'string') {
79+
return e.replaceAll(remove, '');
80+
}
81+
return e ?? '';
82+
})
83+
.join(',')
84+
: typeof entry === 'string'
85+
? entry.replaceAll(remove, '')
86+
: entry ?? '';
87+
}
88+
return acc;
89+
}, {} as Record<string, any>);

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './invert';
1111
export * from './types';
1212
export * from './valuesOf';
1313
export * from './findAllWithRegex';
14+
export * from './flattenObject';

src/tests/aggregateObjects.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { expect } from 'chai';
2+
import { aggregateObjects } from '../aggregateObjects';
3+
4+
describe.only('aggregateObjects', () => {
5+
it('should return empty object for empty input array', () => {
6+
const result = aggregateObjects({ objs: [] });
7+
expect(result).to.deep.equal({});
8+
});
9+
10+
it('should aggregate objects with same keys', () => {
11+
const objs = [
12+
{ name: 'John', age: 30 },
13+
{ name: 'Jane', age: 25 },
14+
{ name: 'Bob', age: 35 }
15+
];
16+
const result = aggregateObjects({ objs });
17+
expect(result).to.deep.equal({
18+
name: 'John,Jane,Bob',
19+
age: '30,25,35'
20+
});
21+
});
22+
23+
it('should handle missing properties with missing keys', () => {
24+
const objs = [
25+
{ name: 'John', age: 30 },
26+
{ name: 'Jane' },
27+
{ name: 'Bob', age: 35 }
28+
];
29+
const result = aggregateObjects({ objs });
30+
expect(result).to.deep.equal({
31+
name: 'John,Jane,Bob',
32+
age: '30,,35'
33+
});
34+
});
35+
36+
it('should handle null and undefined values', () => {
37+
const objs = [
38+
{ name: 'John', age: null },
39+
{ name: undefined, hobby: 'reading' },
40+
{ name: 'Bob', hobby: null }
41+
];
42+
const result = aggregateObjects({ objs });
43+
expect(result).to.deep.equal({
44+
name: 'John,,Bob',
45+
age: ',,',
46+
hobby: ',reading,'
47+
});
48+
});
49+
50+
it('should wrap values in brackets when wrap option is true', () => {
51+
const objs = [
52+
{ name: 'John', age: 30 },
53+
{ name: 'Jane' },
54+
{ name: 'Bob', age: 35 }
55+
];
56+
const result = aggregateObjects({ objs, wrap: true });
57+
expect(result).to.deep.equal({
58+
name: '[John],[Jane],[Bob]',
59+
age: '[30],[],[35]'
60+
});
61+
});
62+
63+
it('should handle objects with different keys', () => {
64+
const objs = [
65+
{ name: 'John', age: 30 },
66+
{ city: 'NY', country: 'USA' },
67+
{ name: 'Bob', country: 'UK' }
68+
];
69+
const result = aggregateObjects({ objs });
70+
expect(result).to.deep.equal({
71+
name: 'John,,Bob',
72+
age: '30,,',
73+
city: ',NY,',
74+
country: ',USA,UK'
75+
});
76+
});
77+
78+
it('should handle empty objects', () => {
79+
const objs = [
80+
{ name: 'John' },
81+
{},
82+
{ name: 'Bob' }
83+
];
84+
const result = aggregateObjects({ objs });
85+
expect(result).to.deep.equal({
86+
name: 'John,,Bob'
87+
});
88+
});
89+
90+
it('should handle array with null or undefined objects', () => {
91+
const objs = [
92+
{ name: 'John' },
93+
null,
94+
undefined,
95+
{ name: 'Bob' }
96+
];
97+
const result = aggregateObjects({ objs });
98+
expect(result).to.deep.equal({
99+
name: 'John,,,Bob'
100+
});
101+
});
102+
103+
it('should handle numeric and boolean values', () => {
104+
const objs = [
105+
{ count: 1, active: true },
106+
{ count: 2, active: false },
107+
{ count: 3, active: true }
108+
];
109+
const result = aggregateObjects({ objs });
110+
expect(result).to.deep.equal({
111+
count: '1,2,3',
112+
active: 'true,false,true'
113+
});
114+
});
115+
});

src/tests/createDefaultCodec.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createDefaultCodec } from '../codecTools';
66

77
chai.use(deepEqualInAnyOrder);
88

9-
describe.only('buildDefaultCodec', () => {
9+
describe('buildDefaultCodec', () => {
1010
it('should correctly build a default codec for null', () => {
1111
const result = createDefaultCodec(t.null);
1212
expect(result).to.equal(null);

src/tests/flattenObject.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { expect } from 'chai';
2+
import { flattenObject } from '../flattenObject';
3+
4+
describe('flattenObject', () => {
5+
it('should return empty object for null input', () => {
6+
const result = flattenObject({ obj: null });
7+
expect(result).to.deep.equal({});
8+
});
9+
10+
it('should return empty object for undefined input', () => {
11+
let obj;
12+
const result = flattenObject({ obj });
13+
expect(result).to.deep.equal({});
14+
});
15+
16+
it('should flatten list of objects with some entries missing properties', () => {
17+
// create a list of users with one of them missiging the age property
18+
const obj = {
19+
users: [
20+
{
21+
name: 'Bob',
22+
},
23+
{
24+
name: 'Alice',
25+
age: 18
26+
}
27+
]
28+
};
29+
const result = flattenObject({ obj });
30+
console.log({ result: JSON.stringify(result, null, 2) });
31+
32+
// the flattened object should include the missing age property
33+
expect(result).to.deep.equal({
34+
users_name: 'Bob,Alice',
35+
users_age: ',18',
36+
});
37+
});
38+
39+
it('should ignore primitive and null types within list containing objects', () => {
40+
// create a list of a mix of objects, strings, and null
41+
const obj = {
42+
users: [
43+
'example@domain.com',
44+
null,
45+
{
46+
name: 'Bob',
47+
},
48+
{
49+
name: 'Alice',
50+
age: 18
51+
}
52+
]
53+
};
54+
const result = flattenObject({ obj });
55+
console.log({ result: JSON.stringify(result, null, 2) });
56+
57+
// the flattened object should not include the strings
58+
expect(result).to.deep.equal({
59+
users_name: 'Bob,Alice',
60+
users_age: ',18',
61+
});
62+
});
63+
64+
it('should flatten an object with null entries', () => {
65+
const obj = {
66+
user: {
67+
siblings: null
68+
}
69+
};
70+
const result = flattenObject({obj});
71+
expect(result).to.deep.equal({
72+
user_siblings: ""
73+
});
74+
});
75+
76+
it('should flatten an object with empty entries', () => {
77+
const obj = {
78+
user: {
79+
siblings: []
80+
}
81+
};
82+
const result = flattenObject({obj});
83+
expect(result).to.deep.equal({
84+
user_siblings: ""
85+
});
86+
});
87+
88+
it('should flatten a deep nested object', () => {
89+
const obj = {
90+
user: {
91+
name: 'John',
92+
address: {
93+
city: 'NY',
94+
zip: 10001
95+
},
96+
hobbies: ['reading', 'gaming'],
97+
parents: [
98+
{
99+
name: 'Alice',
100+
biological: true,
101+
age: 52,
102+
},
103+
{
104+
name: 'Bob',
105+
biological: false,
106+
}
107+
],
108+
siblings: [],
109+
grandParents: null,
110+
}
111+
};
112+
const result = flattenObject({ obj });
113+
console.log({ result: JSON.stringify(result, null, 2) });
114+
expect(result).to.deep.equal({
115+
user_name: 'John',
116+
user_address_city: 'NY',
117+
user_address_zip: 10001,
118+
user_hobbies: 'reading,gaming',
119+
user_parents_name: 'Alice,Bob',
120+
user_parents_biological: 'true,false',
121+
user_parents_age: '52,',
122+
user_siblings: "",
123+
user_grandParents: ""
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)