AlexAegis/loreplotter

View on GitHub
src/app/service/database.service.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { Tree } from '@alexaegis/avl';
import { Injectable } from '@angular/core';

import { Actor, ActorDelta, exampleActors, exampleLore, Lore, serializeActor, UnixWrapper } from '@app/model/data';
import { actorSchema, loreSchema } from '@app/model/schema';
import { StoreFacade } from '@lore/store/store-facade.service';

import * as idb from 'pouchdb-adapter-idb';
import RxDB, { RxCollection, RxDatabase, RxDocument } from 'rxdb';
import { combineLatest, from, Observable, zip } from 'rxjs';
import { delayWhen, filter, map, mergeMap, shareReplay, switchMap, tap } from 'rxjs/operators';
import { LoreCollectionMethods, LoreDocumentMethods, RxCollections } from './database';
import { environment } from '@env/environment';

@Injectable()
export class DatabaseService {
    public database$ = from(
        RxDB.create<RxCollections>({
            name: 'lore',
            adapter: 'idb'
        })
    ).pipe(
        delayWhen(db =>
            zip(
                db.collection<RxCollection<Lore, LoreDocumentMethods, LoreCollectionMethods>>({
                    name: 'lore',
                    schema: loreSchema,
                    statics: this.loreCollectionMethods,
                    methods: this.loreDocumentMethods as any
                }),
                db.collection<RxCollection<Actor, LoreDocumentMethods, LoreCollectionMethods>>({
                    name: 'actor',
                    schema: actorSchema
                })
            )
        ),
        tap(db => {
            db.actor.preSave(async function preSaveHook(this: RxCollection<Actor>, actor) {
                serializeActor(actor);
            }, true);
            db.actor.preInsert(async function preInsertHook(this: RxCollection<Actor>, actor) {
                serializeActor(actor);
            }, true);
            db.actor.preCreate(async function preCreateHook(this: RxCollection<Actor>, actor) {
                serializeActor(actor);
            }, true);
        }),
        delayWhen(db => this.initData(db)),
        shareReplay(1)
    );

    public currentLore$ = combineLatest([
        this.storeFacade.selectedLore$.pipe(filter(id => id !== undefined)),
        this.database$
    ]).pipe(
        switchMap(([selected, conn]) => conn.lore.findOne({ id: selected.id }).$),
        filter(lore => !!lore),
        shareReplay(1)
    );

    public lores$ = this.database$.pipe(
        switchMap(conn => conn.lore.find().$),
        shareReplay(1)
    );

    public allActors$ = this.database$.pipe(
        switchMap(conn => conn.actor.find().$),
        shareReplay(1)
    );

    public nextActorId$ = this.allActors$.pipe(
        map(
            actors => `${actors.map(actor => Number(actor.id)).reduce((acc, next) => (acc < next ? next : acc), 0) + 1}`
        )
    );

    public nextLoreId$ = this.lores$.pipe(
        map(lores => `${lores.map(lore => Number(lore.id)).reduce((acc, next) => (acc < next ? next : acc), 0) + 1}`),

        shareReplay(1)
    );

    public currentLoreActors$ = combineLatest([this.currentLore$, this.allActors$]).pipe(
        map(([lore, actors]) => actors.filter(actor => actor.loreId === lore.id)),
        map(actors => actors.map(DatabaseService.actorStateMapper) as Array<RxDocument<Actor>>),
        // withTeardown(),
        shareReplay(1)
    );

    public actorCount$ = this.currentLoreActors$.pipe(
        map(actors => actors.length),
        shareReplay(1)
    );

    public constructor(private storeFacade: StoreFacade) {}

    private loreDocumentMethods: LoreDocumentMethods = {
        collectActors: function(
            stateMapper: (actor: RxDocument<Actor, {}>) => RxDocument<Actor, {}>
        ): Observable<RxDocument<Actor>[]> {
            return this.collection.database.actor
                .find({ lore: this.name })
                .$.pipe(map(actors => actors.map(stateMapper)));
        }
    };

    private loreCollectionMethods: LoreCollectionMethods = {
        countAllDocuments: async function() {
            return (await this.find().exec()).length;
        }
    };

    public static actorStateMapper(actor: RxDocument<Actor> | Actor): RxDocument<Actor> | Actor {
        if (actor.states) {
            let parsedStates = actor.states;
            if (environment.production) {
                parsedStates = parsedStates.replace(
                    new RegExp(`"__type":"ActorDelta"`, 'g'),
                    `"__type":"${ActorDelta.name}"`
                );
                parsedStates = parsedStates.replace(
                    new RegExp(`"__type":"UnixWrapper"`, 'g'),
                    `"__type":"${UnixWrapper.name}"`
                );
            } else {
                parsedStates = parsedStates.replace(
                    new RegExp(`"__type":"${ActorDelta.name}"`, 'g'),
                    '"__type":"ActorDelta"'
                );
                parsedStates = parsedStates.replace(
                    new RegExp(`"__type":"${UnixWrapper.name}"`, 'g'),
                    '"__type":"UnixWrapper"'
                );
            }
            try {
                actor._states = Tree.parse<UnixWrapper, ActorDelta>(parsedStates, UnixWrapper, ActorDelta);
            } catch (e) {
                console.log(e);
            }

            if (actor._states) {
                for (const node of actor._states.nodes()) {
                    node.key.unix = Math.floor(node.key.unix);
                    if (typeof node.value.properties === 'string') {
                        node.value.properties = JSON.parse(node.value.properties); // Manual deserialization
                    }
                }
            } else {
                actor._states = new Tree<UnixWrapper, ActorDelta>();
            }

            delete actor.states; // Making it undefined triggers an RxError that the set of a field can't be setted
        } else if (!actor._states) {
            actor._states = new Tree<UnixWrapper, ActorDelta>();
        }
        return actor;
    }

    public initData(conn: RxDatabase<RxCollections>): Observable<RxDocument<Lore>> {
        return zip(
            conn.lore.upsert(exampleLore),
            from(exampleActors).pipe(mergeMap(actor => conn.actor.upsert(actor))),
            from(fetch(`assets/elev_bump_8k.jpg`)).pipe(switchMap(p => p.blob()))
        ).pipe(
            mergeMap(([lore, actors, image]) =>
                from(
                    lore.putAttachment({
                        id: 'texture', // string, name of the attachment like 'cat.jpg'
                        data: image, // (string|Blob|Buffer) data of the attachment
                        type: 'image/jpeg' // (string) type of the attachment-data like 'image/jpeg'
                    })
                ).pipe(map(() => lore))
            )
        );
    }
}