From 79b674f2f3fe263774d56738ccf824ec1f562e08 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 6 May 2025 11:28:10 +0100 Subject: [PATCH] fix: add unit tests for the flatten utility function --- api/src/chat/services/conversation.service.ts | 2 +- api/src/utils/generics/base-repository.ts | 25 +---- api/src/utils/helpers/flatten.spec.ts | 95 +++++++++++++++++++ api/src/utils/helpers/flatten.ts | 32 +++++++ api/src/utils/types/filter.types.ts | 2 +- 5 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 api/src/utils/helpers/flatten.spec.ts create mode 100644 api/src/utils/helpers/flatten.ts diff --git a/api/src/chat/services/conversation.service.ts b/api/src/chat/services/conversation.service.ts index 0da1f443..332a64d2 100644 --- a/api/src/chat/services/conversation.service.ts +++ b/api/src/chat/services/conversation.service.ts @@ -181,7 +181,7 @@ export class ConversationService extends BaseService< context: profile.context, }, { - flatten: true, + shouldFlatten: true, }, ); diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index db6feea1..16672835 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -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>, options?: TQueryOptions, ): Promise { - 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 { return await this.model.updateMany(filter, { - $set: options?.flatten ? this.flatten(dto) : dto, + $set: options?.shouldFlatten ? flatten(dto) : dto, }); } diff --git a/api/src/utils/helpers/flatten.spec.ts b/api/src/utils/helpers/flatten.spec.ts new file mode 100644 index 00000000..ec074833 --- /dev/null +++ b/api/src/utils/helpers/flatten.spec.ts @@ -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({}); + }); +}); diff --git a/api/src/utils/helpers/flatten.ts b/api/src/utils/helpers/flatten.ts new file mode 100644 index 00000000..f05dbe19 --- /dev/null +++ b/api/src/utils/helpers/flatten.ts @@ -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; +}; diff --git a/api/src/utils/types/filter.types.ts b/api/src/utils/types/filter.types.ts index 9ec03c23..2c5f2d47 100644 --- a/api/src/utils/types/filter.types.ts +++ b/api/src/utils/types/filter.types.ts @@ -143,6 +143,6 @@ export type TFilterQuery> = ( export type THydratedDocument = TOmitId>; -export type TFlattenOption = { flatten?: boolean }; +export type TFlattenOption = { shouldFlatten?: boolean }; export type TQueryOptions = (QueryOptions & TFlattenOption) | null;