UNPKG

tripledoc

Version:

Library to read, create and update documents on a Solid Pod

416 lines (385 loc) 16 kB
import LinkHeader from 'http-link-header'; import { Quad } from 'rdf-js'; import { Reference } from '.'; import { get } from './pod'; import { TripleSubject, initialiseSubject } from './subject'; import { turtleToTriples } from './turtle'; import { initialiseDataset, Dataset } from './n3dataset'; import { instantiateFullTripleDocument } from './document/stored'; import { instantiateLocalTripleDocument } from './document/local'; import { instantiateLocalTripleDocumentForContainer } from './document/localForContainer'; /** * @ignore This is documented on use. */ export interface NewSubjectOptions { identifier?: string; identifierPrefix?: string; }; /** * Methods that are shared by Documents in every state. * * Note that this does not include the `.save()` method, because that method is implemented * separately for every Document state. * * @ignore For internal use only, to combine with the other Document types. */ export interface BareTripleDocument { /** * Add a Subject — note that it is not written to the Pod until you call [[save]]. * * @param addSubject.options By default, Tripledoc will automatically generate an identifier with * which this Subject can be identified within the Document, and which * is likely to be unique. The `options` parameter has a number of * optional properties. The first, `identifier`, takes a string. If set, * Tripledoc will not automatically generate an identifier. Instead, the * value of this parameter will be used as the Subject's identifier. * The second optional parameter, `identifierPrefix`, is also a string. * If set, it will be prepended before this Subject's identifier, * whether that's autogenerated or not. * @returns A [[TripleSubject]] instance that can be used to define its properties. */ addSubject: (options?: NewSubjectOptions) => TripleSubject; }; /** * An initialised Document that has not been stored in a Pod yet, and has no known location. * * You will obtain a LocalTripleDocumentForContainer when calling [[createDocumentInContainer]]. It * differs from a regular [[TripleDocument]] in that methods like [[TripleDocument.asRef]] are not * available, because the Reference for this Document is not known yet. When you [[save]] this * Document to the Pod, you will get a fully initialised [[TripleDocument]] as a return value. */ export interface LocalTripleDocumentForContainer extends BareTripleDocument { /** * Persist Subjects in this Document to the Pod. * * @param save.subjects Optional array of specific Subjects within this Document that should be * written to the Pod, i.e. excluding Subjects not in this array. * @return The updated Document with persisted Subjects. */ save: (subjects?: TripleSubject[]) => Promise<TripleDocument>; }; /** * An initialised Document that has not been stored to the Pod yet, but whose desired location is already known. * * This will be obtained when you call [[createDocument]]. Compared to a fully initialised * [[TripleDocument]], some methods relating to manipulating existing values on the Pod are not * available yet. They will be available on the [[TripleDocument]] returned when you call [[save]]. */ export interface LocalTripleDocumentWithRef extends LocalTripleDocumentForContainer { /** * @returns The IRI of this Document. */ asRef: () => Reference; /** * @ignore Deprecated. * @deprecated Replaced by [[asRef]]. */ asNodeRef: () => Reference; }; /** * @ignore Not yet a supported API. */ export function hasRef(document: BareTripleDocument): document is LocalTripleDocumentWithRef { return typeof (document as LocalTripleDocumentWithRef).asRef === 'function'; } /** * Local representation of a Document in a Pod. * * A TripleDocument gives you access to the values in the respective Document located on a Pod. They * can be accessed as [[TripleSubject]]s, which will allow you to manipulate their properties using * its `get*`, `add*`, `set*` and `remove*` methods — these changes will be applied to the Pod when * you call [[save]] on this Document. * * Note that these changes can not be _read_ from this TripleDocument; they will be available * on the TripleDocument that is returned when you call [[save]]. */ export interface TripleDocument extends LocalTripleDocumentWithRef { /** * Remove a Subject - note that it is not removed from the Pod until you call [[save]]. * * @param removeSubject.subject The IRI of the Subject to remove. */ removeSubject: (subject: Reference) => void; /** * Find a Subject which has the value of `objectRef` for the Predicate `predicateRef`. * * @param findSubject.predicateRef The Predicate that must match for the desired Subject. * @param findSubject.objectRef The Object that must match for the desired Subject. * @returns `null` if no Subject matching `predicateRef` and `objectRef` is found, * a random one of the matching Subjects otherwise. */ findSubject: (predicateRef: Reference, objectRef: Reference) => TripleSubject | null; /** * Find Subjects which have the value of `objectRef` for the Predicate `predicateRef`. * * @param findSubjects.predicateRef - The Predicate that must match for the desired Subjects. * @param findSubjects.objectRef - The Object that must match for the desired Subjects. * @returns An array with every matching Subject, and an empty array if none match. */ findSubjects: (predicateRef: Reference, objectRef: Reference) => TripleSubject[]; /** * Given the IRI of a Subject, return an instantiated [[TripleSubject]] representing its values. * * @param getSubject.subjectRef IRI of the Subject to inspect. * @returns Instantiation of the Subject at `subjectRef`, ready for inspection. */ getSubject: (subjectRef: Reference) => TripleSubject; /** * Get all Subjects in this Document * * @returns All Subjects in this Document that are of the given type. * @ignore Experimental API. */ experimental_getAllSubjects: () => TripleSubject[]; /** * @ignore Deprecated * @deprecated Replaced by getAllSubjectsOfType */ getSubjectsOfType: (typeRef: Reference) => TripleSubject[]; /** * Get all Subjects in this Document of a given type. * * @param getAllSubjectsOfType.typeRef IRI of the type the desired Subjects should be of. * @returns All Subjects in this Document that are of the given type. */ getAllSubjectsOfType: (typeRef: Reference) => TripleSubject[]; /** * @ignore Experimental API, might change in the future to return an instantiated Document * @deprecated Replaced by [[getAclRef]] */ getAcl: () => Reference | null; /** * @ignore Experimental API, might change in the future to return an instantiated Document */ getAclRef: () => Reference | null; /** * @ignore Experimental API, will probably change as the Solid specification changes to no longer support WebSockets */ getWebSocketRef: () => Reference | null; /** * @deprecated * @ignore This is mostly a convenience function to make it easy to work with n3 and tripledoc * simultaneously. If you rely on this, it's probably best to either file an issue * describing what you want to do that Tripledoc can't do directly, or to just use n3 * directly. * @returns An RDF/JS Dataset containing the Triples pertaining to this Document that are stored * on the user's Pod. Note that this does not contain Triples that have not been saved * yet - those can be retrieved from the respective [[TripleSubject]]s. */ getStore: () => Dataset; /** * @deprecated * @ignore This is mostly a convenience function to make it easy to work with n3 and tripledoc * simultaneously. If you rely on this, it's probably best to either file an issue * describing what you want to do that Tripledoc can't do directly, or to just use n3 * directly. * @returns The Triples pertaining to this Document that are stored on the user's Pod. Note that * this does not return Triples that have not been saved yet - those can be retrieved * from the respective [[TripleSubject]]s. */ getTriples: () => Quad[]; /** * @deprecated Replaced by [[getTriples]] */ getStatements: () => Quad[]; }; /** * @ignore Not yet a supported API. */ export function isSavedToPod(document: BareTripleDocument): document is TripleDocument { return typeof (document as TripleDocument).getTriples === 'function'; } /** * Initialise a new Turtle document * * Note that this Document will not be created on the Pod until you call [[save]] on it. * * @param ref URL where this document should live */ export function createDocument(ref: Reference): LocalTripleDocumentWithRef { return instantiateDocument([], { documentRef: ref, existsOnPod: false }); } /** * Initialise a new Turtle Document in a Container * * Note that this Document will not be created on the Pod until you call [[save]] on it. * * @param containerRef URL of the Container in which this document should live */ export function createDocumentInContainer(containerRef: Reference): LocalTripleDocumentForContainer { return instantiateDocument([], { containerRef: containerRef, existsOnPod: false }); } /** * Retrieve a document containing RDF triples * * @param documentRef Where the document lives. * @returns Representation of triples in the document at `uri`. */ export async function fetchDocument(uri: Reference): Promise<TripleDocument> { // Remove fragment identifiers (e.g. `#me`) from the URI: const docUrl = new URL(uri); const documentRef: Reference = docUrl.origin + docUrl.pathname + docUrl.search; const response = await get(documentRef); if (response.ok === false) { throw new Error(`Fetching the Document failed: ${response.status} ${response.statusText}.`); } const rawDocument = await response.text(); const triples = await turtleToTriples(rawDocument, documentRef); let aclRef: Reference | undefined = extractAclRef(response, documentRef); const webSocketRef: Reference | null = response.headers.get('Updates-Via'); return instantiateDocument( triples, { aclRef: aclRef, documentRef: documentRef, webSocketRef: webSocketRef || undefined, existsOnPod: true, }, ); } /** * @internal */ export function extractAclRef(response: Response, documentRef: Reference) { let aclRef: Reference | undefined; const linkHeader = response.headers.get('Link'); // `LinkHeader` might not be present when using the UMD build in the browser, // in which case we just don't parse the ACL header. It is recommended to use a non-UMD build // that supports code splitting anyway. if (linkHeader && LinkHeader) { const parsedLinks = LinkHeader.parse(linkHeader); const aclLinks = parsedLinks.get('rel', 'acl'); if (aclLinks.length === 1) { aclRef = new URL(aclLinks[0].uri, documentRef).href; } } return aclRef; } type DocOrContainerMetadata = { documentRef: Reference } | { containerRef: Reference }; /** * @ignore For internal use only. */ export type DocumentMetadata = DocOrContainerMetadata & { aclRef?: Reference; webSocketRef?: Reference; existsOnPod?: boolean; }; function hasKnownRef<Metadata extends DocumentMetadata>(metadata: Metadata): metadata is Metadata & { documentRef: Reference } { return typeof (metadata as { documentRef?: Reference }).documentRef === 'string'; } function existsOnPod<Metadata extends DocumentMetadata>(metadata: Metadata): metadata is Metadata & { existsOnPod: true } { return (metadata as { existsOnPod?: boolean }).existsOnPod === true; } /** * @internal */ export function instantiateDocument(triples: Quad[], metadata: DocumentMetadata & {existsOnPod: true, documentRef: Reference}): TripleDocument; export function instantiateDocument(triples: Quad[], metadata: DocumentMetadata & {documentRef: Reference}): LocalTripleDocumentWithRef; export function instantiateDocument(triples: Quad[], metadata: DocumentMetadata): LocalTripleDocumentForContainer; export function instantiateDocument( triples: Quad[], metadata: DocumentMetadata, ): LocalTripleDocumentForContainer | LocalTripleDocumentWithRef | TripleDocument { const dataset = initialiseDataset(); dataset.addAll(triples); const subjectCache = initialiseSubjectCache(); if (!hasKnownRef(metadata)) { return instantiateLocalTripleDocumentForContainer(dataset, subjectCache, metadata); } if (!existsOnPod(metadata)) { return instantiateLocalTripleDocument(dataset, subjectCache, metadata); } return instantiateFullTripleDocument(dataset, subjectCache, metadata); } /** * @internal */ export interface SubjectCache { getSubject: TripleDocument['getSubject']; setDocument: (document: BareTripleDocument) => void; getAccessedSubjects: () => { [iri: string]: TripleSubject }; }; function initialiseSubjectCache(): SubjectCache { let sourceDocument: BareTripleDocument; const accessedSubjects: { [iri: string]: TripleSubject } = {}; const setDocument = (newDocument: BareTripleDocument) => { sourceDocument = newDocument; }; const getSubject = (subjectRef: Reference) => { // Allow relative URLs to access Subjects if we know where the Document is: subjectRef = hasRef(sourceDocument) ? new URL(subjectRef, sourceDocument.asRef()).href : subjectRef; if (!accessedSubjects[subjectRef]) { accessedSubjects[subjectRef] = initialiseSubject(sourceDocument, subjectRef); } return accessedSubjects[subjectRef]; }; const getAccessedSubjects = () => accessedSubjects; return { getSubject, setDocument, getAccessedSubjects, }; } /** * @internal */ export function instantiateBareTripleDocument( subjectCache: SubjectCache, metadata: DocumentMetadata, ): BareTripleDocument { const addSubject = ( { identifier = generateIdentifier(), identifierPrefix = '', }: NewSubjectOptions = {}, ) => { const subjectRef: Reference = (hasKnownRef(metadata) ? metadata.documentRef : '') + '#' + identifierPrefix + identifier; return subjectCache.getSubject(subjectRef); }; const bareTripleDocument: BareTripleDocument = { addSubject: addSubject, }; return bareTripleDocument; } /** * @internal */ export function getPendingChanges( subjects: TripleSubject[], document: BareTripleDocument, dataset: Dataset, ) { const relevantSubjects = subjects.filter((subject) => subject.getDocument() === document); type UpdateTriples = [Quad[], Quad[]]; const [allDeletions, allAdditions] = relevantSubjects.reduce<UpdateTriples>( ([deletionsSoFar, additionsSoFar], subject) => { const [deletions, additions] = subject.getPendingTriples(); return [deletionsSoFar.concat(deletions), additionsSoFar.concat(additions)]; }, [[], []], ); let newTriples: Quad[] = dataset.toArray() .concat(allAdditions) .filter(tripleToDelete => allDeletions.findIndex((triple) => triple.equals(tripleToDelete)) === -1); return { allAdditions, allDeletions, newTriples, }; } /** * Generate a string that can be used as the unique identifier for a Subject * * This function works by starting with a date string (so that Subjects can be * sorted chronologically), followed by a random number generated by taking a * random number between 0 and 1, and cutting off the `0.`. * * @internal * @returns An string that's likely to be unique */ const generateIdentifier = () => { return Date.now().toString() + Math.random().toString().substring('0.'.length); }