tripledoc
Version:
Library to read, create and update documents on a Solid Pod
416 lines (385 loc) • 16 kB
text/typescript
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);
}