fix: add unit tests for the flatten utility function

This commit is contained in:
yassinedorbozgithub 2025-05-06 11:28:10 +01:00
parent 35a435090c
commit 79b674f2f3
5 changed files with 133 additions and 23 deletions

View File

@ -181,7 +181,7 @@ export class ConversationService extends BaseService<
context: profile.context,
},
{
flatten: true,
shouldFlatten: true,
},
);

View File

@ -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,
});
}

View 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({});
});
});

View 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;
};

View File

@ -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;