rescribet/link-lib

View on GitHub
src/RDFStore.ts

Summary

Maintainability
B
4 hrs
Test Coverage
A
97%
import rdfFactory, {
    DataFactory,
    NamedNode,
    QuadPosition,
    Quadruple,
    SomeTerm,
    Term,
} from "@ontologies/core";
import * as ld from "@ontologies/ld";
import * as rdf from "@ontologies/rdf";

import { DataRecord, Id } from "./datastrucures/DataSlice";
import { equals } from "./factoryHelpers";
import ll from "./ontology/ll";
import {
    OptionalNamedNode,
    OptionalNode,
    OptionalTerm,
} from "./rdf";
import { deltaProcessor } from "./store/deltaProcessor";
import { RDFAdapter } from "./store/RDFAdapter";
import { DeltaProcessor, SomeNode, StoreProcessor } from "./types";
import { getPropBestLang, normalizeType, sortByBestLang } from "./utilities";

const EMPTY_ST_ARR: ReadonlyArray<Quadruple> = Object.freeze([]);

export interface RDFStoreOpts {
    data?: Record<Id, DataRecord>;
    deltaProcessorOpts?: { [k: string]: NamedNode[] };
    innerStore?: RDFAdapter;
}

/**
 * Provides a clean consistent interface to stored (RDF) data.
 */
export class RDFStore implements DeltaProcessor {
    public langPrefs: string[] = Array.from(typeof navigator !== "undefined"
        ? (navigator.languages || [navigator.language])
        : ["en"]);
    private changedResources: Set<string> = new Set();

    private deltas: Quadruple[][] = [];
    private deltaProcessor: StoreProcessor;

    private store: RDFAdapter = new RDFAdapter({
        onChange: this.handleChange.bind(this),
    });

    public get rdfFactory(): DataFactory {
        return rdfFactory;
    }
    public set rdfFactory(_: DataFactory) {
        throw new Error("Factory is global (see @ontologies/core)");
    }

    private defaultGraph: NamedNode = this.rdfFactory.defaultGraph();

    constructor({ data, deltaProcessorOpts, innerStore }: RDFStoreOpts = {}) {
        this.processDelta = this.processDelta.bind(this);

        this.store = innerStore || new RDFAdapter({ data, onChange: this.handleChange.bind(this) });

        const defaults =  {
            addGraphIRIS: [ll.add, ld.add],
            purgeGraphIRIS: [ll.purge, ld.purge],
            removeGraphIRIS: [ll.remove, ld.remove],
            replaceGraphIRIS: [
                ll.replace,
                ld.replace,
                rdfFactory.defaultGraph(),
            ],
            sliceGraphIRIS: [ll.slice, ld.slice],
        };

        this.deltaProcessor = deltaProcessor(
            deltaProcessorOpts?.addGraphIRIS || defaults.addGraphIRIS,
            deltaProcessorOpts?.replaceGraphIRIS || defaults.replaceGraphIRIS,
            deltaProcessorOpts?.removeGraphIRIS || defaults.removeGraphIRIS,
            deltaProcessorOpts?.purgeGraphIRIS || defaults.purgeGraphIRIS,
            deltaProcessorOpts?.sliceGraphIRIS || defaults.sliceGraphIRIS,
        )(this.store);
    }

    /** @deprecated */
    public add(subject: SomeNode, predicate: NamedNode, object: SomeTerm): Quadruple {
        return this.store.add(
            subject,
            predicate,
            object,
        );
    }

    /**
     * Add statements to the store.
     * @param data Data to parse and add to the store.
     * @deprecated
     */
    public addQuads(data: Quadruple[]): void {
        if (!Array.isArray(data)) {
            throw new TypeError("An array of quads must be passed to addQuads");
        }

        for (const q of data) {
            this.store.store.addField(
              q[QuadPosition.subject].value,
              q[QuadPosition.predicate].value,
              q[QuadPosition.object],
            );
        }
    }

    /** @deprecated */
    public addQuadruples(data: Quadruple[]): Quadruple[] {
        const statements = new Array(data.length);
        for (let i = 0, len = data.length; i < len; i++) {
            statements[i] = this.store.add(data[i][0], data[i][1], data[i][2], data[i][3]);
        }

        return statements;
    }

    /** @deprecated */
    public primary(term: SomeNode): SomeNode {
        return this.store.primary(term);
    }

    /**
     * Flushes the change buffer to the return value.
     * @return Statements held in memory since the last flush.
     */
    public flush(): Set<string> {
        const deltas = this.deltas;
        this.deltas = [];

        for (const delta of deltas) {
            this.processDelta(delta);
        }

        const changes = this.changedResources;
        this.changedResources = new Set();

        return changes;
    }

    /** @private */
    public getInternalStore(): RDFAdapter {
        return this.store;
    }

    public references(recordId: SomeNode): Id[] {
        return this.store.store.references(recordId.value);
    }

    /** @deprecated */
    public match(
        subj: OptionalNode,
        pred: OptionalNamedNode,
        obj: OptionalTerm,
        justOne: boolean = false,
    ): Quadruple[] {
        return this.store.match(subj, pred, obj, justOne) ?? EMPTY_ST_ARR;
    }

    public processDelta(delta: Quadruple[]): Quadruple[] {
        const [
            addables,
            replacables,
            removables,
        ] = this.deltaProcessor(delta);

        this.removeQuads(removables);

        return this.replaceMatches(replacables).concat(this.addQuadruples(addables));
    }

    public removeResource(subject: SomeNode): void {
        this.touch(subject);
        this.store.deleteRecord(subject);
    }

    /** @deprecated */
    public removeQuads(statements: Quadruple[]): void {
        this.store.removeQuads(statements);
    }

    /** @deprecated */
    public replaceMatches(statements: Quadruple[]): Quadruple[] {
        for (const quad of statements) {
            this.removeQuads(this.match(
                quad[0],
                quad[1],
                null,
            ));
        }

        return this.addQuadruples(statements);
    }

    public getResourcePropertyRaw(subject: SomeNode, property: SomeNode | SomeNode[]): Quadruple[] {
        const properties = normalizeType(property);
        const matched = [];
        for (const prop of properties) {
            const quads = normalizeType(this.store.store.getField(subject.value, prop.value))
              .filter((v) => v !== undefined)
              .map<Quadruple>((v) => [subject, prop as NamedNode, v!, this.defaultGraph]);
            matched.push(...quads);
        }

        if (matched.length === 0) {
            return EMPTY_ST_ARR as Quadruple[];
        }

        return sortByBestLang(matched, this.langPrefs);
    }

    public getResourceProperties<TT extends Term = Term>(subject: SomeNode, property: SomeNode | SomeNode[]): TT[] {
        if (property === rdf.type) {
            const value = this.store.store.getField(subject.value, rdf.type.value);

            if (!value) {
                return EMPTY_ST_ARR as unknown as TT[];
            } else {
                return normalizeType(value) as TT[];
            }
        }

        const properties = normalizeType(property);
        const matched = [];
        for (const prop of properties) {
            const quads = normalizeType(this.store.store.getField(subject.value, prop.value))
              .filter((v) => v !== undefined);
            matched.push(...quads);
        }

        if (matched.length === 0) {
            return EMPTY_ST_ARR as unknown as TT[];
        }

        return matched as TT[];
    }

    public getResourceProperty<T extends Term = Term>(
        subject: SomeNode,
        property: SomeNode | SomeNode[],
    ): T | undefined {

        if (!Array.isArray(property) && equals(property, rdf.type)) {
            return this.store.store.getField(subject.value, rdf.type.value) as T;
        }
        const rawProp = this.getResourcePropertyRaw(subject, property);
        if (rawProp.length === 0) {
            return undefined;
        }

        return getPropBestLang<T>(rawProp, this.langPrefs);
    }

    public queueDelta(delta: Quadruple[]): void {
        this.deltas.push(delta);
    }

    /**
     * Searches the store for all the quads on {iri} (so not all statements relating to {iri}).
     * @param subject The identifier of the resource.
     */
    public quadsFor(subject: SomeNode): Quadruple[] {
        return this.store.quadsForRecord(subject.value);
    }

    public touch(iri: SomeNode): void {
        this.store.store.touch(iri.value);
    }

    public workAvailable(): number {
        return this.deltas.length + this.changedResources.size;
    }

    private handleChange(docId: string): void {
        this.changedResources.add(docId);
    }
}