diaspora-orm/diaspora

View on GitHub
src/adapters/webStorage/adapter.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { defaults, Dictionary, cloneDeep, isNil, isEqual, keys, each, pull, pullAll, map, forEach } from 'lodash';

import { Adapter as _AAdapter } from '../base';
import AAdapter = _AAdapter.Base.AAdapter;
import EAdapterState = _AAdapter.EAdapterState;
import { Adapter as _WebStorageEntity } from './entity';
import WebStorageEntity = _WebStorageEntity.WebStorage.WebStorageEntity;
import * as Utils from '../../utils';
import { _QueryLanguage } from '../../types/queryLanguage';
import { EntityUid, IEntityAttributes, IEntityProperties } from '../../types/entity';

export namespace Adapter.WebStorage {
    /**
     * This class is used to use local storage or session storage as a data store. This adapter should be used only by the browser.
     */
    export class WebStorageAdapter extends AAdapter<WebStorageEntity> {
        /**
         * {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage Storage api} where to store data.
         *
         * @author Gerkin
         * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage localStorage} and {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage sessionStorage} on MDN web docs.
         * @see {@link Adapters.WebStorageDiasporaAdapter}:config.session parameter.
         */
        private readonly source: Storage;
        
        private readonly config: WebStorageAdapter.IOptions;
        
        /**
         * Create a new instance of local storage adapter.
         *
         * @author gerkin
         * @param config - Configuration object.
         */
        public constructor(
            dataSourceName: string,
            config?: WebStorageAdapter.IOptionsRaw
        ) {
            super( WebStorageEntity, dataSourceName );
            this.config = defaults( config, {
                session: false,
            } );
            this.state = EAdapterState.READY;
            this.source = this.config.session
            ? window.sessionStorage
            : window.localStorage;
        }
        
        /**
         * Deduce the item name from table name and item ID.
         *
         * @author gerkin
         * @param   table - Name of the table to construct name for.
         * @param   id    - Id of the item to find.
         * @returns Name of the item.
         */
        private static getItemName( table: string, id: EntityUid ): string {
            return `${table}.id=${id}`;
        }
        
        /**
         * Create the collection index and call {@link Adapters.DiasporaAdapter#configureCollection}.
         *
         * @author gerkin
         * @param tableName - Name of the table (usually, model name).
         * @param remaps    - Associative hash that links entity field names with data source field names.
         * @returns This function does not return anything.
         */
        public configureCollection(
            tableName: string,
            remaps: Dictionary<string>,
            filters: Dictionary<any>
        ) {
            super.configureCollection( tableName, remaps );
            this.ensureCollectionExists( tableName );
            return this;
        }
        
        // -----
        // ### Insert
        
        /**
         * Insert a single entity in the local storage.
         *
         * @summary This reimplements {@link Adapters.DiasporaAdapter#insertOne}, modified for local storage or session storage interactions.
         * @author gerkin
         * @param   table  - Name of the table to insert data in.
         * @param   entity - Hash representing the entity to insert.
         * @returns Promise resolved once insertion is done. Called with (*{@link DataStoreEntities.WebStorageEntity}* `entity`).
         */
        public async insertOne(
            table: string,
            entity: IEntityAttributes
        ): Promise<IEntityProperties | undefined> {
            const rawAdapterAttributes = WebStorageEntity.setId(
                entity,
                this,
                Utils.generateUUID()
            );
            const tableIndex = this.ensureCollectionExists( table );
            tableIndex.push( rawAdapterAttributes.id as string );
            this.source.setItem( table, JSON.stringify( tableIndex ) );
            this.source.setItem(
                WebStorageAdapter.getItemName( table, entity.id ),
                JSON.stringify( rawAdapterAttributes )
            );
            return rawAdapterAttributes;
        }
        
        /**
         * Insert several entities in the local storage.
         *
         * @summary This reimplements {@link Adapters.DiasporaAdapter#insertMany}, modified for local storage or session storage interactions.
         * @author gerkin
         * @param   table    - Name of the table to insert data in.
         * @param   entities - Array of hashes representing entities to insert.
         * @returns Promise resolved once insertion is done. Called with (*{@link DataStoreEntities.WebStorageEntity}[]* `entities`).
         */
        public async insertMany(
            table: string,
            entities: IEntityAttributes[]
        ): Promise<IEntityProperties[]> {
            entities = cloneDeep( entities );
            const tableIndex = this.ensureCollectionExists( table );
            const rawAdapterAttributesArr = entities.map( entity => {
                const rawAdapterAttributes = WebStorageEntity.setId(
                    entity,
                    this,
                    Utils.generateUUID()
                );
                tableIndex.push( rawAdapterAttributes.id as string );
                this.source.setItem(
                    WebStorageAdapter.getItemName( table, rawAdapterAttributes.id ),
                    JSON.stringify( rawAdapterAttributes )
                );
                return rawAdapterAttributes;
            } );
            this.source.setItem( table, JSON.stringify( tableIndex ) );
            return rawAdapterAttributesArr;
        }
        
        // -----
        // ### Find
        
        /**
         * Find a single local storage entity using its id.
         *
         * @author gerkin
         * @param   table - Name of the collection to search entity in.
         * @param   id    - Id of the entity to search.
         * @returns Found entity, or undefined if not found.
         */
        public findOneById( table: string, id: string ): IEntityProperties | undefined {
            const item = this.source.getItem( WebStorageAdapter.getItemName( table, id ) );
            if ( !isNil( item ) ) {
                return JSON.parse( item );
            }
            return undefined;
        }
        
        /**
         * Retrieve a single entity from the local storage.
         *
         * @summary This reimplements {@link Adapters.DiasporaAdapter#findOne}, modified for local storage or session storage interactions.
         * @author gerkin
         * @param   table     - Name of the model to retrieve data from.
         * @param   queryFind - Hash representing the entity to find.
         * @param   options   - Hash of options.
         * @returns Promise resolved once item is found. Called with (*{@link DataStoreEntities.WebStorageEntity}* `entity`).
         */
        public async findOne(
            table: string,
            queryFind: _QueryLanguage.ISelectQuery,
            options: _QueryLanguage.IQueryOptions
        ): Promise<IEntityProperties | undefined> {
            options = defaults( options, { skip: 0 } );
            if ( isEqual( keys( queryFind ), ['id'] ) && isEqual( keys( queryFind.id ), ['$equal'] ) ) {
                return this.findOneById( table, queryFind.id.$equal );
            }
            const itemIds = this.ensureCollectionExists( table );
            let returnedItem;
            let matched = 0;
            // Iterate on each item ID, to test each one.
            each( itemIds, itemId => {
                // Retrieve the item by its complete name
                const itemInWebStorage = this.source.getItem( WebStorageAdapter.getItemName( table, itemId ) );
                // If the item simply does not exist, just ignore and skip to the next
                // TODO: Repair the table?
                if ( !itemInWebStorage ) {
                    return true;
                }
                // Parse the item to match against its content
                const item = JSON.parse( itemInWebStorage );
                // Following tests are to match the entity and skip the right number of items
                if ( WebStorageEntity.matches( item, queryFind ) ) {
                    matched++;
                    // If we matched enough items
                    if ( matched > options.skip ) {
                        returnedItem = item;
                        // Kill the iteration
                        return false;
                    }
                }
                return true;
            } );
            return returnedItem;
        }
        
        // -----
        // ### Update
        
        /**
         * Update a single entity in the memory.
         *
         * @summary This reimplements {@link Adapters.DiasporaAdapter#updateOne}, modified for local storage or session storage interactions.
         * @author gerkin
         * @param   table     - Name of the table to update data in.
         * @param   queryFind - Hash representing the entity to find.
         * @param   update    - Object properties to set.
         * @param   options   - Hash of options.
         * @returns Promise resolved once update is done. Called with (*{@link DataStoreEntities.WebStorageEntity}* `entity`).
         */
        public async updateOne(
            table: string,
            queryFind: _QueryLanguage.ISelectQuery,
            update: IEntityAttributes,
            options: _QueryLanguage.IQueryOptions
        ): Promise<IEntityProperties | undefined> {
            defaults( options, {
                skip: 0,
            } );
            const entity = await this.findOne( table, queryFind, options );
            
            if ( isNil( entity ) ) {
                return undefined;
            }
            Utils.applyUpdateEntity( update, entity );
            this.source.setItem(
                WebStorageAdapter.getItemName( table, entity.id ),
                JSON.stringify( entity )
            );
            return entity;
        }
        
        // -----
        // ### Delete
        
        /**
         * Delete a single entity from the local storage.
         *
         * @summary This reimplements {@link Adapters.DiasporaAdapter#deleteOne}, modified for local storage or session storage interactions.
         * @author gerkin
         * @param   table     - Name of the table to delete data from.
         * @param   queryFind - Hash representing the entity to find.
         * @param   options   - Hash of options.
         * @returns Promise resolved once item is deleted. Called with (*undefined*).
         */
        public async deleteOne(
            table: string,
            queryFind: _QueryLanguage.ISelectQuery,
            options: _QueryLanguage.IQueryOptions
        ): Promise<void> {
            const entityToDelete = await this.findOne( table, queryFind, options );
            
            if ( !entityToDelete ) {
                return;
            }
            const tableIndex = this.ensureCollectionExists( table );
            pull( tableIndex, entityToDelete.id );
            this.source.setItem( table, JSON.stringify( tableIndex ) );
            this.source.removeItem(
                WebStorageAdapter.getItemName( table, entityToDelete.id )
            );
        }
        
        /**
         * Delete several entities from the local storage.
         *
         * @summary This reimplements {@link Adapters.DiasporaAdapter#deleteMany}, modified for local storage or session storage interactions.
         * @author gerkin
         * @param   table     - Name of the table to delete data from.
         * @param   queryFind - Hash representing entities to find.
         * @param   options   - Hash of options.
         * @returns Promise resolved once items are deleted. Called with (*undefined*).
         */
        public async deleteMany(
            table: string,
            queryFind: _QueryLanguage.ISelectQuery,
            options: _QueryLanguage.IQueryOptions
        ): Promise<void> {
            const entitiesToDelete = await this.findMany( table, queryFind, options );
            
            const tableIndex = this.ensureCollectionExists( table );
            pullAll( tableIndex, map( entitiesToDelete, 'id' ) );
            this.source.setItem( table, JSON.stringify( tableIndex ) );
            forEach( entitiesToDelete, entityToDelete => {
                this.source.removeItem(
                    WebStorageAdapter.getItemName( table, entityToDelete.id )
                );
            } );
        }
        
        /**
         * Create the table key if it does not exist.
         *
         * @author gerkin
         * @param   table - Name of the table.
         * @returns Index of the collection.
         */
        private ensureCollectionExists( table: string ) {
            const index = this.source.getItem( table );
            if ( isNil( index ) ) {
                const newIndex: string[] = [];
                this.source.setItem( table, JSON.stringify( newIndex ) );
                return newIndex;
            } else {
                return JSON.parse( index ) as string[];
            }
        }
    }

    export namespace WebStorageAdapter {
        export interface IOptions {
            session: boolean;
        }
        export interface IOptionsRaw {
            session?: boolean;
        }
    }
}