-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(core): add OneToManyMapStar
- Loading branch information
1 parent
8894b6c
commit cac9518
Showing
2 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
283 changes: 283 additions & 0 deletions
283
packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<unknown, RelationTest>; | ||
|
||
beforeAll(() => { | ||
modelFixture = Symbol(); | ||
relationKeyFixture = RelationKey.foo; | ||
relationValueFixture = 'value-fixture'; | ||
|
||
oneToManyMapStar = new OneToManyMapStar<unknown, RelationTest>({ | ||
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<unknown, RelationTest>; | ||
|
||
beforeAll(() => { | ||
relationKeyFixture = RelationKey.foo; | ||
relationValueFixture = 'value-fixture'; | ||
|
||
oneToManyMapStar = new OneToManyMapStar<unknown, RelationTest>({ | ||
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<RelationTest>; | ||
let oneToManyMapStar: OneToManyMapStar<unknown, RelationTest>; | ||
|
||
beforeAll(() => { | ||
relationFixture = { | ||
bar: 3, | ||
foo: 'foo', | ||
}; | ||
oneToManyMapStar = new OneToManyMapStar<unknown, RelationTest>({ | ||
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<unknown> | 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<unknown> | 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<RelationTest>; | ||
let oneToManyMapStar: OneToManyMapStar<unknown, RelationTest>; | ||
|
||
beforeAll(() => { | ||
modelFixture = Symbol(); | ||
relationFixture = { | ||
bar: 3, | ||
foo: 'foo', | ||
}; | ||
oneToManyMapStar = new OneToManyMapStar<unknown, RelationTest>({ | ||
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<unknown> | 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<unknown> | undefined; | ||
} = { | ||
[RelationKey.bar]: undefined, | ||
[RelationKey.foo]: undefined, | ||
}; | ||
|
||
expect(results).toStrictEqual(expectedResults); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('.set', () => { | ||
let modelFixture: unknown; | ||
let relationFixture: Required<RelationTest>; | ||
let oneToManyMapStar: OneToManyMapStar<unknown, RelationTest>; | ||
|
||
beforeAll(() => { | ||
modelFixture = Symbol(); | ||
relationFixture = { | ||
bar: 3, | ||
foo: 'foo', | ||
}; | ||
oneToManyMapStar = new OneToManyMapStar<unknown, RelationTest>({ | ||
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); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
114 changes: 114 additions & 0 deletions
114
packages/container/libraries/core/src/common/models/OneToManyMapStar.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
export type OneToManyMapStartSpec<TRelation extends object> = { | ||
[TKey in keyof TRelation]: { | ||
isOptional: undefined extends TRelation[TKey] ? true : false; | ||
}; | ||
}; | ||
|
||
type RelationToModelMap<TModel, TRelation extends object> = { | ||
[TKey in keyof TRelation]-?: Map<TRelation[TKey], Set<TModel>>; | ||
}; | ||
|
||
/** | ||
* 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<TModel, TRelation extends object> { | ||
readonly #modelToRelationMap: Map<TModel, TRelation>; | ||
readonly #relationToModelsMaps: RelationToModelMap<TModel, TRelation>; | ||
|
||
constructor(spec: OneToManyMapStartSpec<TRelation>) { | ||
this.#modelToRelationMap = new Map(); | ||
|
||
this.#relationToModelsMaps = {} as RelationToModelMap<TModel, TRelation>; | ||
|
||
for (const specProperty of Reflect.ownKeys(spec) as (keyof TRelation)[]) { | ||
this.#relationToModelsMaps[specProperty] = new Map(); | ||
} | ||
} | ||
|
||
public get<TKey extends keyof TRelation>( | ||
key: TKey, | ||
value: Required<TRelation>[TKey], | ||
): Iterable<TModel> | undefined { | ||
return this.#relationToModelsMaps[key].get(value)?.values(); | ||
} | ||
|
||
public removeByRelation<TKey extends keyof TRelation>( | ||
key: TKey, | ||
value: Required<TRelation>[TKey], | ||
): void { | ||
const models: Iterable<TModel> | 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<TKey extends keyof TRelation>( | ||
relationKey: TKey, | ||
relationValue: TRelation[TKey], | ||
): Set<TModel> { | ||
let modelSet: Set<TModel> | 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<TKey extends keyof TRelation>( | ||
model: TModel, | ||
relationKey: TKey, | ||
relationValue: TRelation[TKey], | ||
): void { | ||
const modelSet: Set<TModel> | undefined = | ||
this.#relationToModelsMaps[relationKey].get(relationValue); | ||
|
||
if (modelSet !== undefined) { | ||
modelSet.delete(model); | ||
|
||
if (modelSet.size === 0) { | ||
this.#relationToModelsMaps[relationKey].delete(relationValue); | ||
} | ||
} | ||
} | ||
} |