Angular-RU/angular-ru-sdk

View on GitHub
libs/ngxs/repositories/src/ngxs-data-entity-collections/ngxs-data-entity-collections.repository.ts

Summary

Maintainability
D
2 days
Test Coverage
import { Injectable, isDevMode } from '@angular/core';
import {
    EmptyDictionary,
    EntityCollections,
    EntityComparator,
    EntityDictionary,
    EntityIdType,
    EntitySortBy,
    EntityUpdate,
    KeysDictionary
} from '@angular-ru/cdk/entity';
import { sortByAsc, sortByDesc } from '@angular-ru/cdk/object';
import { PrimaryKey, SortOrderType } from '@angular-ru/cdk/typings';
import { isNil } from '@angular-ru/cdk/utils';
import { Computed, DataAction, Payload } from '@angular-ru/ngxs/decorators';
import { ensureDataStateContext, ensureSnapshot } from '@angular-ru/ngxs/internals';
import { NGXS_DATA_EXCEPTIONS } from '@angular-ru/ngxs/tokens';
import { EntityContext, EntityRepository } from '@angular-ru/ngxs/typings';
import { ActionType, StateContext } from '@ngxs/store';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { AbstractRepository } from '../common/abstract-repository';

@Injectable()
export abstract class AbstractNgxsDataEntityCollectionsRepository<
        V,
        K extends number | string = EntityIdType,
        C = Record<string, any>
    >
    extends AbstractRepository<EntityCollections<V, K, C>>
    implements EntityRepository<V, K, C>
{
    private readonly context!: EntityContext<V, K, C>;
    public primaryKey: string = PrimaryKey.ID;
    public comparator: EntityComparator<V> | null = null;

    @Computed()
    public get snapshot(): EntityCollections<V, K, C> {
        return ensureSnapshot(this.getState());
    }

    @Computed()
    public get ids(): K[] {
        return this.snapshot.ids;
    }

    @Computed()
    public get entities(): EntityDictionary<K, V> {
        return this.snapshot.entities;
    }

    @Computed()
    public get entitiesArray(): V[] {
        const snapshot: EntityCollections<V, K, C> = this.snapshot;

        return snapshot.ids.map((id: K): V => snapshot.entities[id]);
    }

    @Computed()
    public get ids$(): Observable<K[]> {
        return this.state$.pipe(map((value: EntityCollections<V, K, C>): K[] => value.ids));
    }

    @Computed()
    public get entities$(): Observable<EntityDictionary<K, V>> {
        return this.state$.pipe(map((value: EntityCollections<V, K, C>): EntityDictionary<K, V> => value.entities));
    }

    @Computed()
    public get entitiesArray$(): Observable<V[]> {
        return this.state$.pipe(
            map((value: EntityCollections<V, K, C>): V[] => value.ids.map((id: K): V => value.entities[id]))
        );
    }

    protected get ctx(): EntityContext<V, K, C> {
        return ensureDataStateContext<EntityCollections<V, K, C>, StateContext<EntityCollections<V, K, C>>>(
            this.context as StateContext<EntityCollections<V, K, C>>
        );
    }

    @DataAction()
    public reset(): void {
        this.setEntitiesState(this.initialState);
        this.markAsDirtyAfterReset();
    }

    @DataAction()
    public addOne(@Payload('entity') entity: V): void {
        this.addEntityOne(entity);
    }

    @DataAction()
    public addMany(@Payload('entities') entities: V[]): void {
        this.addEntitiesMany(entities);
    }

    @DataAction()
    public setOne(@Payload('entity') entity: V): void {
        this.setEntityOne(entity);
    }

    @DataAction()
    public setMany(@Payload('entities') entities: V[]): void {
        this.setEntitiesMany(entities);
    }

    @DataAction()
    public setAll(@Payload('entities') entities: V[]): void {
        this.setEntitiesAll(entities);
    }

    @DataAction()
    public updateOne(@Payload('update') update: EntityUpdate<V, K>): void {
        this.updateEntitiesMany([update]);
    }

    @DataAction()
    public updateMany(@Payload('updates') updates: EntityUpdate<V, K>[]): void {
        this.updateEntitiesMany(updates);
    }

    @DataAction()
    public upsertOne(@Payload('entity') entity: V): void {
        this.upsertEntitiesMany([entity]);
    }

    @DataAction()
    public upsertMany(@Payload('entities') entities: V[]): void {
        this.upsertEntitiesMany(entities);
    }

    @DataAction()
    public removeOne(@Payload('id') id: K): void {
        this.removeEntitiesMany([id]);
    }

    @DataAction()
    public removeMany(@Payload('ids') ids: K[]): void {
        this.removeEntitiesMany(ids);
    }

    @DataAction()
    public removeByEntity(@Payload('entity') entity: V): void {
        const id: K = this.selectId(entity);

        this.removeEntitiesMany([id]);
    }

    @DataAction()
    public removeByEntities(@Payload('entities') entities: V[]): void {
        const ids: K[] = [];

        for (const entity of entities) {
            const id: K = this.selectId(entity);

            ids.push(id);
        }

        this.removeEntitiesMany(ids);
    }

    @DataAction()
    public removeAll(): void {
        this.setAll([]);
    }

    @DataAction()
    public sort(@Payload('comparator') comparator?: EntityComparator<V>): void {
        this.comparator = comparator ?? this.comparator;

        if (isNil(this.comparator)) {
            console.warn(NGXS_DATA_EXCEPTIONS.NGXS_COMPARE);

            return;
        }

        this.setEntitiesState(this.getState());
    }

    @DataAction()
    public patchState(@Payload('patchValue') value: Partial<EntityCollections<V, K, C>>): void {
        this.ctx.patchState(value);
    }

    public setComparator(comparator: EntityComparator<V> | null): this {
        this.comparator = comparator;

        return this;
    }

    public dispatch(actions: ActionType | ActionType[]): Observable<void> {
        return this.ctx.dispatch(actions);
    }

    public getState(): EntityCollections<V, K, C> {
        return this.ctx.getState();
    }

    public selectId(entity: V): K {
        return (entity as any)?.[this.primaryKey];
    }

    public selectOne(id: K): V | null {
        return this.snapshot.entities[id] ?? null;
    }

    public selectAll(): V[] {
        const state: EntityCollections<V, K, C> = this.getState();

        return state.ids.map((id: K): V => state.entities[id] as V);
    }

    protected addEntityOne(entity: V): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const id: K = this.selectIdValue(entity);

        if (id in state.entities) {
            return;
        }

        this.setEntitiesState({
            ...state,
            ids: [...state.ids, id],
            entities: { ...state.entities, [id]: entity }
        });
    }

    protected addEntitiesMany(entities: V[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const dictionary: EntityDictionary<K, V> | EmptyDictionary<K, V> = {};
        const ids: K[] = [];

        for (const entity of entities) {
            const id: K = this.selectIdValue(entity);

            if (id in state.entities || id in dictionary) {
                continue;
            }

            ids.push(id);
            dictionary[id] = entity;
        }

        if (ids.length > 0) {
            this.setEntitiesState({
                ...state,
                ids: [...state.ids, ...ids],
                entities: { ...state.entities, ...dictionary }
            });
        }
    }

    protected setEntitiesAll(entities: V[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const dictionary: EntityDictionary<K, V> | EmptyDictionary<K, V> = {};
        const ids: K[] = [];

        for (const entity of entities) {
            const id: K = this.selectIdValue(entity);

            if (id in dictionary) {
                continue;
            }

            ids.push(id);
            dictionary[id] = entity;
        }

        this.setEntitiesState({ ...state, ids, entities: dictionary as EntityDictionary<K, V> });
    }

    protected setEntityOne(entity: V): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const id: K = this.selectIdValue(entity);

        if (id in state.entities) {
            this.setEntitiesState({ ...state, entities: { ...state.entities, [id]: entity } });
        } else {
            this.setEntitiesState({ ...state, ids: [...state.ids, id], entities: { ...state.entities, [id]: entity } });
        }
    }

    protected setEntitiesMany(entities: V[]): void {
        for (const entity of entities) {
            this.setEntityOne(entity);
        }
    }

    // eslint-disable-next-line max-lines-per-function
    protected updateEntitiesMany(updates: EntityUpdate<V, K>[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const newUpdates: EntityUpdate<V, K>[] = updates.filter(
            (update: EntityUpdate<V, K>): boolean => update.id in state.entities
        );

        if (newUpdates.length === 0) {
            return;
        }

        const keys: KeysDictionary<K> = this.generateKeyMap(state);
        const entities: EntityDictionary<K, V> = { ...state.entities };

        for (const update of newUpdates) {
            const updated: V = this.updateOrigin(entities, update);
            const newId: K = this.selectIdValue(updated);

            if (newId !== update.id) {
                delete keys[update.id];
                delete entities[update.id];
            }

            keys[update.id] = newId;
            entities[newId] = updated;
        }

        this.setEntitiesState({ ...state, ids: state.ids.map((id: K): K => keys[id] ?? id), entities });
    }

    protected upsertEntitiesMany(entities: V[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const updates: EntityUpdate<V, K>[] = [];
        const added: V[] = [];

        for (const entity of entities) {
            const id: K = this.selectIdValue(entity);

            if (id in state.entities) {
                updates.push({ id, changes: entity });
            } else {
                added.push(entity);
            }
        }

        this.updateMany(updates);
        this.addMany(added);
    }

    protected removeEntitiesMany(ids: K[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const keys: KeysDictionary<K> = this.generateKeyMap(state);
        const entities: EntityDictionary<K, V> = { ...state.entities };

        for (const id of ids) {
            if (id in entities) {
                delete keys[id];
                delete entities[id];
            }
        }

        this.setEntitiesState({ ...state, ids: state.ids.filter((id: K): boolean => id in keys), entities });
    }

    protected setEntitiesState(state: EntityCollections<V, K, C>): void {
        const ids: K[] = this.sortKeysByComparator(state.ids, state.entities);

        this.ctx.patchState({ ...state, ids, entities: state.entities });
    }

    protected sortKeysByComparator(originalIds: K[], entities: EntityDictionary<K, V>): K[] {
        if (isNil(this.comparator)) {
            return originalIds;
        }

        const ids: K[] = originalIds.slice();
        const comparator: EntityComparator<V> = this.comparator;

        if (typeof comparator === 'function') {
            return ids.sort((a: K, b: K): number => comparator(entities[a] as V, entities[b] as V));
        }

        return this.sortByComparatorOptions(ids, comparator, entities);
    }

    private sortByComparatorOptions(ids: K[], comparator: EntitySortBy<V>, entities: EntityDictionary<K, V>): K[] {
        switch (comparator?.sortByOrder) {
            case SortOrderType.ASC:
                return ids.sort((a: K, b: K): number =>
                    sortByAsc(comparator?.sortBy, entities[a] as V, entities[b] as V)
                );

            case SortOrderType.DESC:
                return ids.sort((a: K, b: K): number =>
                    sortByDesc(comparator?.sortBy, entities[a] as V, entities[b] as V)
                );

            default:
                if (isDevMode()) {
                    console.warn(`Invalid --> { sortByOrder: "${comparator?.sortByOrder}" } not supported!`);
                }

                return ids;
        }
    }

    private generateKeyMap(state: EntityCollections<V, K, C>): KeysDictionary<K> {
        return state.ids.reduce((keyDictionary: KeysDictionary<K>, id: K): KeysDictionary<K> => {
            keyDictionary[id] = id;

            return keyDictionary;
        }, {});
    }

    private updateOrigin(entities: EntityDictionary<K, V>, update: EntityUpdate<V, K>): V {
        const original: V | undefined = entities[update.id];

        return { ...original, ...update.changes } as V;
    }

    private selectIdValue(entity: V): K {
        const id: K = this.selectId(entity);
        const invalidId: boolean = isNil(id) && isDevMode();

        if (invalidId) {
            console.warn(
                `The entity passed to the 'selectId' implementation returned ${id}.`,
                `You should probably provide your own 'selectId' implementation.`,
                'The entity that was passed:',
                entity,
                'The current `selectId` implementation: (entity: V): K => entity.id'
            );
        }

        return id;
    }
}