mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix: add unit tests for the flatten utility function
This commit is contained in:
parent
35a435090c
commit
79b674f2f3
@ -181,7 +181,7 @@ export class ConversationService extends BaseService<
|
||||
context: profile.context,
|
||||
},
|
||||
{
|
||||
flatten: true,
|
||||
shouldFlatten: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
TQueryOptions,
|
||||
} from '@/utils/types/filter.types';
|
||||
|
||||
import { flatten } from '../helpers/flatten';
|
||||
import { PageQueryDto, QuerySortDto } from '../pagination/pagination-query.dto';
|
||||
import { DtoAction, DtoConfig, DtoInfer } from '../types/dto.types';
|
||||
|
||||
@ -99,24 +100,6 @@ export abstract class BaseRepository<
|
||||
this.registerLifeCycleHooks();
|
||||
}
|
||||
|
||||
private flatten(obj: object, prefix: string = '', result: object = {}) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
this.flatten(value, path, result);
|
||||
} else {
|
||||
result[path] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
canPopulate(populate: string[]): boolean {
|
||||
return populate.some((p) => this.populate.includes(p as P));
|
||||
}
|
||||
@ -518,7 +501,7 @@ export abstract class BaseRepository<
|
||||
dto: UpdateQuery<DtoInfer<DtoAction.Update, Dto, D>>,
|
||||
options?: TQueryOptions<D>,
|
||||
): Promise<T> {
|
||||
const { flatten, ...rest } = {
|
||||
const { shouldFlatten, ...rest } = {
|
||||
new: true,
|
||||
...options,
|
||||
};
|
||||
@ -527,7 +510,7 @@ export abstract class BaseRepository<
|
||||
...(typeof criteria === 'string' ? { _id: criteria } : criteria),
|
||||
},
|
||||
{
|
||||
$set: flatten ? this.flatten(dto) : dto,
|
||||
$set: shouldFlatten ? flatten(dto) : dto,
|
||||
},
|
||||
rest,
|
||||
);
|
||||
@ -567,7 +550,7 @@ export abstract class BaseRepository<
|
||||
options?: TFlattenOption,
|
||||
): Promise<UpdateWriteOpResult> {
|
||||
return await this.model.updateMany<T>(filter, {
|
||||
$set: options?.flatten ? this.flatten(dto) : dto,
|
||||
$set: options?.shouldFlatten ? flatten(dto) : dto,
|
||||
});
|
||||
}
|
||||
|
||||
|
95
api/src/utils/helpers/flatten.spec.ts
Normal file
95
api/src/utils/helpers/flatten.spec.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { flatten } from './flatten';
|
||||
|
||||
describe('flatten', () => {
|
||||
it('should support a nested object with one nested level', () => {
|
||||
const object = {
|
||||
name: 'name',
|
||||
context: { nestedField: 'value' },
|
||||
};
|
||||
const result = flatten(object);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
name: 'name',
|
||||
'context.nestedField': 'value',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support a nested object with multiple nested levels', () => {
|
||||
const object = {
|
||||
name: 'name',
|
||||
context: {
|
||||
nestedField: 'value',
|
||||
country: {
|
||||
isoCode: 'usa',
|
||||
phonePrefix: '+1',
|
||||
countryName: 'United States',
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = flatten(object);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
name: 'name',
|
||||
'context.nestedField': 'value',
|
||||
'context.country.isoCode': 'usa',
|
||||
'context.country.phonePrefix': '+1',
|
||||
'context.country.countryName': 'United States',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support custom prefix', () => {
|
||||
const object = {
|
||||
isoCode: 'tun',
|
||||
phonePrefix: '+216',
|
||||
countryName: 'Tunisia',
|
||||
};
|
||||
const result = flatten(object, 'context.country');
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
'context.country.isoCode': 'tun',
|
||||
'context.country.phonePrefix': '+216',
|
||||
'context.country.countryName': 'Tunisia',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support custom static initial value', () => {
|
||||
const object = {
|
||||
context: {
|
||||
country: { isoCode: 'fra', phonePrefix: '+33', countryName: 'France' },
|
||||
},
|
||||
};
|
||||
const result = flatten(object, undefined, { language: 'fr' });
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
language: 'fr',
|
||||
'context.country.isoCode': 'fra',
|
||||
'context.country.phonePrefix': '+33',
|
||||
'context.country.countryName': 'France',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support an object without nested object', () => {
|
||||
const object = {
|
||||
name: 'name',
|
||||
};
|
||||
const result = flatten(object);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
name: 'name',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support an empty object', () => {
|
||||
const result = flatten({});
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
});
|
||||
});
|
32
api/src/utils/helpers/flatten.ts
Normal file
32
api/src/utils/helpers/flatten.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Flattens a nested object into a single-level object with dot-separated keys.
|
||||
* @param data - The data object to flatten
|
||||
* @param prefix - The optional base key to prefix to the current object's keys
|
||||
* @param result - The optional accumulator for the flattened object data
|
||||
* @returns A new object with flattened keys
|
||||
*/
|
||||
export const flatten = (
|
||||
data: object,
|
||||
prefix: string | undefined = undefined,
|
||||
result: object = {},
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
flatten(value, path, result);
|
||||
} else {
|
||||
result[path] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
@ -143,6 +143,6 @@ export type TFilterQuery<T, S = TReplaceId<T>> = (
|
||||
|
||||
export type THydratedDocument<T> = TOmitId<HydratedDocument<T>>;
|
||||
|
||||
export type TFlattenOption = { flatten?: boolean };
|
||||
export type TFlattenOption = { shouldFlatten?: boolean };
|
||||
|
||||
export type TQueryOptions<D> = (QueryOptions<D> & TFlattenOption) | null;
|
||||
|
Loading…
Reference in New Issue
Block a user