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,
|
context: profile.context,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flatten: true,
|
shouldFlatten: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import {
|
|||||||
TQueryOptions,
|
TQueryOptions,
|
||||||
} from '@/utils/types/filter.types';
|
} from '@/utils/types/filter.types';
|
||||||
|
|
||||||
|
import { flatten } from '../helpers/flatten';
|
||||||
import { PageQueryDto, QuerySortDto } from '../pagination/pagination-query.dto';
|
import { PageQueryDto, QuerySortDto } from '../pagination/pagination-query.dto';
|
||||||
import { DtoAction, DtoConfig, DtoInfer } from '../types/dto.types';
|
import { DtoAction, DtoConfig, DtoInfer } from '../types/dto.types';
|
||||||
|
|
||||||
@ -99,24 +100,6 @@ export abstract class BaseRepository<
|
|||||||
this.registerLifeCycleHooks();
|
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 {
|
canPopulate(populate: string[]): boolean {
|
||||||
return populate.some((p) => this.populate.includes(p as P));
|
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>>,
|
dto: UpdateQuery<DtoInfer<DtoAction.Update, Dto, D>>,
|
||||||
options?: TQueryOptions<D>,
|
options?: TQueryOptions<D>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const { flatten, ...rest } = {
|
const { shouldFlatten, ...rest } = {
|
||||||
new: true,
|
new: true,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
@ -527,7 +510,7 @@ export abstract class BaseRepository<
|
|||||||
...(typeof criteria === 'string' ? { _id: criteria } : criteria),
|
...(typeof criteria === 'string' ? { _id: criteria } : criteria),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: flatten ? this.flatten(dto) : dto,
|
$set: shouldFlatten ? flatten(dto) : dto,
|
||||||
},
|
},
|
||||||
rest,
|
rest,
|
||||||
);
|
);
|
||||||
@ -567,7 +550,7 @@ export abstract class BaseRepository<
|
|||||||
options?: TFlattenOption,
|
options?: TFlattenOption,
|
||||||
): Promise<UpdateWriteOpResult> {
|
): Promise<UpdateWriteOpResult> {
|
||||||
return await this.model.updateMany<T>(filter, {
|
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 THydratedDocument<T> = TOmitId<HydratedDocument<T>>;
|
||||||
|
|
||||||
export type TFlattenOption = { flatten?: boolean };
|
export type TFlattenOption = { shouldFlatten?: boolean };
|
||||||
|
|
||||||
export type TQueryOptions<D> = (QueryOptions<D> & TFlattenOption) | null;
|
export type TQueryOptions<D> = (QueryOptions<D> & TFlattenOption) | null;
|
||||||
|
Loading…
Reference in New Issue
Block a user