diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index efb50301e..9a611ff1a 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { find, objectEntries, objectValues, keyBy } from '../utils/fns'; +import { find, objectEntries, objectValues, keyBy, assignBy } from '../utils/fns'; import { FEATURE_VARIABLE_TYPES } from '../utils/enums'; import configValidator from '../utils/config_validator'; @@ -180,10 +180,10 @@ export const createProjectConfig = function(datafileObj?: JSON, datafileStr: str audience.conditions = JSON.parse(audience.conditions as string); }); - projectConfig.audiencesById = { - ...keyBy(projectConfig.audiences, 'id'), - ...keyBy(projectConfig.typedAudiences, 'id'), - } + + projectConfig.audiencesById = {}; + assignBy(projectConfig.audiences, 'id', projectConfig.audiencesById); + assignBy(projectConfig.typedAudiences, 'id', projectConfig.audiencesById); projectConfig.attributes = projectConfig.attributes || []; projectConfig.attributeKeyMap = {}; @@ -267,11 +267,7 @@ export const createProjectConfig = function(datafileObj?: JSON, datafileStr: str // Creates { : } map inside of the experiment experiment.variationKeyMap = keyBy(experiment.variations, 'key'); - // Creates { : { key: , id: } } mapping for quick lookup - projectConfig.variationIdMap = { - ...projectConfig.variationIdMap, - ...keyBy(experiment.variations, 'id') - }; + assignBy(experiment.variations, 'id', projectConfig.variationIdMap); objectValues(experiment.variationKeyMap || {}).forEach(variation => { if (variation.variables) { @@ -373,10 +369,7 @@ const parseHoldoutsConfig = (projectConfig: ProjectConfig): void => { holdout.variationKeyMap = keyBy(holdout.variations, 'key'); - projectConfig.variationIdMap = { - ...projectConfig.variationIdMap, - ...keyBy(holdout.variations, 'id'), - }; + assignBy(holdout.variations, 'id', projectConfig.variationIdMap); if (holdout.includedFlags.length === 0) { projectConfig.globalHoldouts.push(holdout); diff --git a/lib/utils/fns/index.spec.ts b/lib/utils/fns/index.spec.ts index 3d1cd1502..1eec0d746 100644 --- a/lib/utils/fns/index.spec.ts +++ b/lib/utils/fns/index.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { groupBy, objectEntries, objectValues, find, keyByUtil, sprintf } from '.' +import { groupBy, objectEntries, objectValues, find, sprintf, keyBy, assignBy } from '.' describe('utils', () => { describe('groupBy', () => { @@ -68,7 +68,7 @@ describe('utils', () => { { key: 'baz', firstName: 'james', lastName: 'foxy' }, ] - expect(keyByUtil(input, item => item.key)).toEqual({ + expect(keyBy(input, 'key')).toEqual({ foo: { key: 'foo', firstName: 'jordan', lastName: 'foo' }, bar: { key: 'bar', firstName: 'jordan', lastName: 'bar' }, baz: { key: 'baz', firstName: 'james', lastName: 'foxy' }, @@ -76,6 +76,78 @@ describe('utils', () => { }) }) + describe('assignBy', () => { + it('should assign array elements to an object using the specified key', () => { + const input = [ + { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + { key: 'baz', firstName: 'james', lastName: 'foxy' }, + ] + const base = {} + + assignBy(input, 'key', base) + + expect(base).toEqual({ + foo: { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + bar: { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + baz: { key: 'baz', firstName: 'james', lastName: 'foxy' }, + }) + }) + + it('should append to an existing object', () => { + const input = [ + { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + ] + const base: any = { existing: 'value' } + + assignBy(input, 'key', base) + + expect(base).toEqual({ + existing: 'value', + foo: { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + bar: { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + }) + }) + + it('should handle empty array', () => { + const base: any = { existing: 'value' } + + assignBy([], 'key', base) + + expect(base).toEqual({ existing: 'value' }) + }) + + it('should handle null/undefined array', () => { + const base: any = { existing: 'value' } + + assignBy(null as any, 'key', base) + expect(base).toEqual({ existing: 'value' }) + + assignBy(undefined as any, 'key', base) + expect(base).toEqual({ existing: 'value' }) + }) + + it('should override existing values with the same key', () => { + const input = [ + { key: 'foo', firstName: 'jordan', lastName: 'updated' }, + { key: 'bar', firstName: 'james', lastName: 'new' }, + ] + const base: any = { + foo: { key: 'foo', firstName: 'john', lastName: 'original' }, + existing: 'value' + } + + assignBy(input, 'key', base) + + expect(base).toEqual({ + existing: 'value', + foo: { key: 'foo', firstName: 'jordan', lastName: 'updated' }, + bar: { key: 'bar', firstName: 'james', lastName: 'new' }, + }) + }) + }) + describe('sprintf', () => { it('sprintf(msg)', () => { expect(sprintf('this is my message')).toBe('this is my message') diff --git a/lib/utils/fns/index.ts b/lib/utils/fns/index.ts index 7d9506756..5b07b3aad 100644 --- a/lib/utils/fns/index.ts +++ b/lib/utils/fns/index.ts @@ -25,11 +25,19 @@ export function isSafeInteger(number: unknown): boolean { return typeof number == 'number' && Math.abs(number) <= MAX_SAFE_INTEGER_LIMIT; } -export function keyBy(arr: K[], key: string): { [key: string]: K } { +export function keyBy(arr: K[], key: string): Record { if (!arr) return {}; - return keyByUtil(arr, function(item) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (item as any)[key]; + + const base: Record = {}; + assignBy(arr, key, base); + return base; +} + +export function assignBy(arr: K[], key: string, base: Record): void { + if (!arr) return; + + arr.forEach((e) => { + base[(e as any)[key]] = e; }); } @@ -81,15 +89,6 @@ export function find(arr: K[], cond: (arg: K) => boolean): K | undefined { return found; } -export function keyByUtil(arr: K[], keyByFn: (item: K) => string): { [key: string]: K } { - const map: { [key: string]: K } = {}; - arr.forEach(item => { - const key = keyByFn(item); - map[key] = item; - }); - return map; -} - // TODO[OASIS-6649]: Don't use any type // eslint-disable-next-line @typescript-eslint/no-explicit-any export function sprintf(format: string, ...args: any[]): string { @@ -129,6 +128,5 @@ export default { objectValues, objectEntries, find, - keyByUtil, sprintf, };