diff --git a/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts b/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts new file mode 100644 index 0000000..507b277 --- /dev/null +++ b/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts @@ -0,0 +1,283 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import { OneToManyMapStar } from './OneToManyMapStar'; + +enum RelationKey { + bar = 'bar', + foo = 'foo', +} + +interface RelationTest { + [RelationKey.bar]?: number; + [RelationKey.foo]: string; +} + +describe(OneToManyMapStar.name, () => { + describe('.get', () => { + describe('having a OneToManyMapStartSpec with model', () => { + let modelFixture: unknown; + let relationKeyFixture: RelationKey.foo; + let relationValueFixture: string; + + let oneToManyMapStar: OneToManyMapStar; + + beforeAll(() => { + modelFixture = Symbol(); + relationKeyFixture = RelationKey.foo; + relationValueFixture = 'value-fixture'; + + oneToManyMapStar = new OneToManyMapStar({ + bar: { + isOptional: true, + }, + foo: { + isOptional: false, + }, + }); + + oneToManyMapStar.set(modelFixture, { + [relationKeyFixture]: relationValueFixture, + }); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = [ + ...(oneToManyMapStar.get( + relationKeyFixture, + relationValueFixture, + ) ?? []), + ]; + }); + + it('should return expected result', () => { + expect(result).toStrictEqual([modelFixture]); + }); + }); + }); + + describe('having a OneToManyMapStart with no model', () => { + let relationKeyFixture: RelationKey.foo; + let relationValueFixture: string; + + let oneToManyMapStar: OneToManyMapStar; + + beforeAll(() => { + relationKeyFixture = RelationKey.foo; + relationValueFixture = 'value-fixture'; + + oneToManyMapStar = new OneToManyMapStar({ + bar: { + isOptional: true, + }, + foo: { + isOptional: false, + }, + }); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = oneToManyMapStar.get( + relationKeyFixture, + relationValueFixture, + ); + }); + + it('should return expected result', () => { + expect(result).toBeUndefined(); + }); + }); + }); + }); + + describe('.removeByRelation', () => { + describe('having a OneToManyMapStart with a no models', () => { + let relationFixture: Required; + let oneToManyMapStar: OneToManyMapStar; + + beforeAll(() => { + relationFixture = { + bar: 3, + foo: 'foo', + }; + oneToManyMapStar = new OneToManyMapStar({ + bar: { + isOptional: true, + }, + foo: { + isOptional: false, + }, + }); + }); + + describe('when called', () => { + beforeAll(() => { + oneToManyMapStar.removeByRelation( + RelationKey.bar, + relationFixture[RelationKey.bar], + ); + }); + + describe('when called .get()', () => { + let results: { + [TKey in RelationKey]-?: Iterable | undefined; + }; + + beforeAll(() => { + results = { + [RelationKey.bar]: oneToManyMapStar.get( + RelationKey.bar, + relationFixture[RelationKey.bar], + ), + [RelationKey.foo]: oneToManyMapStar.get( + RelationKey.foo, + relationFixture[RelationKey.foo], + ), + }; + }); + + it('should return expected results', () => { + const expectedResults: { + [TKey in RelationKey]-?: Iterable | undefined; + } = { + [RelationKey.bar]: undefined, + [RelationKey.foo]: undefined, + }; + + expect(results).toStrictEqual(expectedResults); + }); + }); + }); + }); + + describe('having a OneToManyMapStart with a single model with each relation', () => { + let modelFixture: unknown; + let relationFixture: Required; + let oneToManyMapStar: OneToManyMapStar; + + beforeAll(() => { + modelFixture = Symbol(); + relationFixture = { + bar: 3, + foo: 'foo', + }; + oneToManyMapStar = new OneToManyMapStar({ + bar: { + isOptional: true, + }, + foo: { + isOptional: false, + }, + }); + + oneToManyMapStar.set(modelFixture, relationFixture); + }); + + describe('when called', () => { + beforeAll(() => { + oneToManyMapStar.removeByRelation( + RelationKey.bar, + relationFixture[RelationKey.bar], + ); + }); + + describe('when called .get()', () => { + let results: { + [TKey in RelationKey]-?: Iterable | undefined; + }; + + beforeAll(() => { + results = { + [RelationKey.bar]: oneToManyMapStar.get( + RelationKey.bar, + relationFixture[RelationKey.bar], + ), + [RelationKey.foo]: oneToManyMapStar.get( + RelationKey.foo, + relationFixture[RelationKey.foo], + ), + }; + }); + + it('should return expected results', () => { + const expectedResults: { + [TKey in RelationKey]-?: Iterable | undefined; + } = { + [RelationKey.bar]: undefined, + [RelationKey.foo]: undefined, + }; + + expect(results).toStrictEqual(expectedResults); + }); + }); + }); + }); + }); + + describe('.set', () => { + let modelFixture: unknown; + let relationFixture: Required; + let oneToManyMapStar: OneToManyMapStar; + + beforeAll(() => { + modelFixture = Symbol(); + relationFixture = { + bar: 3, + foo: 'foo', + }; + oneToManyMapStar = new OneToManyMapStar({ + bar: { + isOptional: true, + }, + foo: { + isOptional: false, + }, + }); + }); + + describe('when called', () => { + beforeAll(() => { + oneToManyMapStar.set(modelFixture, relationFixture); + }); + + describe('when called .get() with relation values', () => { + let results: { + [TKey in RelationKey]-?: unknown[]; + }; + + beforeAll(() => { + results = { + [RelationKey.bar]: [ + ...(oneToManyMapStar.get( + RelationKey.bar, + relationFixture[RelationKey.bar], + ) ?? []), + ], + [RelationKey.foo]: [ + ...(oneToManyMapStar.get( + RelationKey.foo, + relationFixture[RelationKey.foo], + ) ?? []), + ], + }; + }); + + it('should return expected results', () => { + const expected: { + [TKey in RelationKey]-?: unknown[]; + } = { + [RelationKey.bar]: [modelFixture], + [RelationKey.foo]: [modelFixture], + }; + + expect(results).toStrictEqual(expected); + }); + }); + }); + }); +}); diff --git a/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts b/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts new file mode 100644 index 0000000..b78e39f --- /dev/null +++ b/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts @@ -0,0 +1,114 @@ +export type OneToManyMapStartSpec = { + [TKey in keyof TRelation]: { + isOptional: undefined extends TRelation[TKey] ? true : false; + }; +}; + +type RelationToModelMap = { + [TKey in keyof TRelation]-?: Map>; +}; + +/** + * Data structure able to efficiently manage a set of models related to a set of properties in a one to many relation. + */ +export class OneToManyMapStar { + readonly #modelToRelationMap: Map; + readonly #relationToModelsMaps: RelationToModelMap; + + constructor(spec: OneToManyMapStartSpec) { + this.#modelToRelationMap = new Map(); + + this.#relationToModelsMaps = {} as RelationToModelMap; + + for (const specProperty of Reflect.ownKeys(spec) as (keyof TRelation)[]) { + this.#relationToModelsMaps[specProperty] = new Map(); + } + } + + public get( + key: TKey, + value: Required[TKey], + ): Iterable | undefined { + return this.#relationToModelsMaps[key].get(value)?.values(); + } + + public removeByRelation( + key: TKey, + value: Required[TKey], + ): void { + const models: Iterable | undefined = this.get(key, value); + + if (models === undefined) { + return; + } + + for (const model of models) { + const relation: TRelation | undefined = + this.#modelToRelationMap.get(model); + + if (relation === undefined) { + throw new Error('Expecting model relation, none found'); + } + + this.#removeModelFromRelationMaps(model, relation); + this.#modelToRelationMap.delete(model); + } + } + + public set(model: TModel, relation: TRelation): void { + this.#modelToRelationMap.set(model, relation); + + for (const relationKey of Reflect.ownKeys( + relation, + ) as (keyof TRelation)[]) { + this.#buildOrGetRelationModelSet(relationKey, relation[relationKey]).add( + model, + ); + } + } + + #buildOrGetRelationModelSet( + relationKey: TKey, + relationValue: TRelation[TKey], + ): Set { + let modelSet: Set | undefined = + this.#relationToModelsMaps[relationKey].get(relationValue); + + if (modelSet === undefined) { + modelSet = new Set(); + + this.#relationToModelsMaps[relationKey].set(relationValue, modelSet); + } + + return modelSet; + } + + #removeModelFromRelationMaps(model: TModel, relation: TRelation): void { + for (const relationKey of Reflect.ownKeys( + relation, + ) as (keyof TRelation)[]) { + this.#removeModelFromRelationMap( + model, + relationKey, + relation[relationKey], + ); + } + } + + #removeModelFromRelationMap( + model: TModel, + relationKey: TKey, + relationValue: TRelation[TKey], + ): void { + const modelSet: Set | undefined = + this.#relationToModelsMaps[relationKey].get(relationValue); + + if (modelSet !== undefined) { + modelSet.delete(model); + + if (modelSet.size === 0) { + this.#relationToModelsMaps[relationKey].delete(relationValue); + } + } + } +}