Skip to content

Commit

Permalink
refactor(core): add OneToManyMapStar
Browse files Browse the repository at this point in the history
  • Loading branch information
notaphplover committed Dec 5, 2024
1 parent 8894b6c commit cac9518
Show file tree
Hide file tree
Showing 2 changed files with 397 additions and 0 deletions.
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);
});
});
});
});
});
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);
}
}
}
}

0 comments on commit cac9518

Please sign in to comment.