@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
326 lines (323 loc) • 14.3 kB
JavaScript
import { dataset } from '../rdfjs.mjs';
import { internal_isDatasetCore } from '../rdfjs.internal.mjs';
import { isLocalNode, internal_isValidUrl, asNamedNode, isNamedNode, isEqual, getLocalNode, resolveLocalIri } from '../datatypes.mjs';
import { hasResourceInfo, SolidClientError } from '../interfaces.mjs';
import { getTermAll } from './get.mjs';
import { getSourceUrl } from '../resource/resource.mjs';
import { internal_cloneResource } from '../resource/resource.internal.mjs';
import { internal_withChangeLog, internal_toNode, internal_getReadableValue } from './thing.internal.mjs';
/**
* Copyright 2020 Inrupt Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* Extract Quads with a given Subject from a [[SolidDataset]] into a [[Thing]].
*
* @param solidDataset The [[SolidDataset]] to extract the [[Thing]] from.
* @param thingUrl The URL of the desired [[Thing]].
* @param options Not yet implemented.
*/
function getThing(solidDataset, thingUrl, options = {}) {
if (!isLocalNode(thingUrl) && !internal_isValidUrl(thingUrl)) {
throw new ValidThingUrlExpectedError(thingUrl);
}
const subject = isLocalNode(thingUrl) ? thingUrl : asNamedNode(thingUrl);
const scope = options.scope
? asNamedNode(options.scope)
: null;
const thingDataset = solidDataset.match(subject, null, null, scope);
if (thingDataset.size === 0) {
return null;
}
if (isLocalNode(subject)) {
const thing = Object.assign(thingDataset, {
internal_localSubject: subject,
});
return thing;
}
else {
const thing = Object.assign(thingDataset, {
internal_url: subject.value,
});
return thing;
}
}
/**
* Get all [[Thing]]s about which a [[SolidDataset]] contains Quads.
*
* @param solidDataset The [[SolidDataset]] to extract the [[Thing]]s from.
* @param options Not yet implemented.
*/
function getThingAll(solidDataset, options = {}) {
const subjectNodes = new Array();
for (const quad of solidDataset) {
// Because NamedNode objects with the same IRI are actually different
// object instances, we have to manually check whether `subjectNodes` does
// not yet include `quadSubject` before adding it.
const quadSubject = quad.subject;
if (isNamedNode(quadSubject) &&
!subjectNodes.some((subjectNode) => isEqual(subjectNode, quadSubject))) {
subjectNodes.push(quadSubject);
}
if (isLocalNode(quadSubject) &&
!subjectNodes.some((subjectNode) => isEqual(subjectNode, quadSubject))) {
subjectNodes.push(quadSubject);
}
}
const things = subjectNodes.map((subjectNode) => getThing(solidDataset, subjectNode, options)
// We can make the type assertion here because `getThing` only returns `null` if no data with
// the given subject node can be found, and in this case the subject node was extracted from
// existing data (i.e. that can be found by definition):
);
return things;
}
/**
* Insert a [[Thing]] into a [[SolidDataset]], replacing previous instances of that Thing.
*
* @param solidDataset The SolidDataset to insert a Thing into.
* @param thing The Thing to insert into the given SolidDataset.
* @returns A new SolidDataset equal to the given SolidDataset, but with the given Thing.
*/
function setThing(solidDataset, thing) {
const newDataset = removeThing(solidDataset, thing);
newDataset.internal_changeLog = {
additions: [...newDataset.internal_changeLog.additions],
deletions: [...newDataset.internal_changeLog.deletions],
};
for (const quad of thing) {
newDataset.add(quad);
const alreadyDeletedQuad = newDataset.internal_changeLog.deletions.find((deletedQuad) => equalsExcludingBlankNodes(quad, deletedQuad));
if (typeof alreadyDeletedQuad !== "undefined") {
newDataset.internal_changeLog.deletions = newDataset.internal_changeLog.deletions.filter((deletion) => deletion !== alreadyDeletedQuad);
}
else {
newDataset.internal_changeLog.additions.push(quad);
}
}
return newDataset;
}
/**
* Compare two Quads but, if both Quads have objects that are Blank Nodes and are otherwise equal, treat them as equal.
*
* The reason we do this is because you cannot write Blank Nodes as Quad
* Subjects using solid-client, so they wouldn't be used in an Object position
* either. Thus, if a SolidDataset has a ChangeLog in which a given Quad with a
* Blank node as object is listed as deleted, and then an otherwise equivalent
* Quad but with a different instance of a Blank Node is added, we can assume
* that they are the same, and that rather than adding the new Quad, we can just
* prevent the old Quad from being removed.
* This occurs in situations in which, for example, you extract a Thing from a
* SolidDataset, change that Thing, then re-fetch that same SolidDataset (to
* make sure you are working with up-to-date data) and add the Thing to _that_.
* When the server returns the data in a serialisation that does not assign a
* consistent value to Blank Nodes (e.g. Turtle), our client-side parser will
* have to instantiate unique instances on every parse. Therefore, the Blank
* Nodes in the refetched SolidDataset will now be different instances from the
* ones in the original SolidDataset, even though they're equivalent.
*/
function equalsExcludingBlankNodes(a, b) {
// Potential future improvement: compare the actual values of the nodes.
// For example, currently a decimal serialised as "1.0" is considered different from a decimal
// serialised as "1.00".
return (a.subject.equals(b.subject) &&
b.predicate.equals(b.predicate) &&
(a.object.equals(b.object) ||
(a.object.termType === "BlankNode" && b.object.termType === "BlankNode")));
}
/**
* Remove a Thing from a SolidDataset.
*
* @param solidDataset The SolidDataset to remove a Thing from.
* @param thing The Thing to remove from `solidDataset`.
* @returns A new [[SolidDataset]] equal to the input SolidDataset, excluding the given Thing.
*/
function removeThing(solidDataset, thing) {
const newSolidDataset = internal_withChangeLog(internal_cloneResource(solidDataset));
newSolidDataset.internal_changeLog = {
additions: [...newSolidDataset.internal_changeLog.additions],
deletions: [...newSolidDataset.internal_changeLog.deletions],
};
const resourceIri = hasResourceInfo(newSolidDataset)
? getSourceUrl(newSolidDataset)
: undefined;
const thingSubject = internal_toNode(thing);
const existingQuads = Array.from(newSolidDataset);
existingQuads.forEach((quad) => {
if (!isNamedNode(quad.subject) && !isLocalNode(quad.subject)) {
// This data is unexpected, and hence unlikely to be added by us. Thus, leave it intact:
return;
}
if (isEqual(thingSubject, quad.subject, { resourceIri: resourceIri })) {
newSolidDataset.delete(quad);
if (newSolidDataset.internal_changeLog.additions.includes(quad)) {
newSolidDataset.internal_changeLog.additions = newSolidDataset.internal_changeLog.additions.filter((addition) => addition !== quad);
}
else {
newSolidDataset.internal_changeLog.deletions.push(quad);
}
}
});
return newSolidDataset;
}
function createThing(options = {}) {
var _a;
if (typeof options.url !== "undefined") {
const url = options.url;
if (!internal_isValidUrl(url)) {
throw new ValidThingUrlExpectedError(url);
}
const thing = Object.assign(dataset(), {
internal_url: url,
});
return thing;
}
const name = (_a = options.name) !== null && _a !== void 0 ? _a : generateName();
const localSubject = getLocalNode(name);
const thing = Object.assign(dataset(), {
internal_localSubject: localSubject,
});
return thing;
}
/**
* @param input An value that might be a [[Thing]].
* @returns Whether `input` is a Thing.
* @since 0.2.0
*/
function isThing(input) {
return (internal_isDatasetCore(input) &&
(isThingLocal(input) ||
typeof input.internal_url === "string"));
}
function asUrl(thing, baseUrl) {
if (isThingLocal(thing)) {
if (typeof baseUrl === "undefined") {
throw new Error("The URL of a Thing that has not been persisted cannot be determined without a base URL.");
}
return resolveLocalIri(thing.internal_localSubject.internal_name, baseUrl);
}
return thing.internal_url;
}
/** @hidden Alias of [[asUrl]] for those who prefer IRI terminology. */
const asIri = asUrl;
/**
* Gets a human-readable representation of the given Thing to aid debugging.
*
* Note that changes to the exact format of the return value are not considered a breaking change;
* it is intended to aid in debugging, not as a serialisation method that can be reliably parsed.
*
* @param thing The Thing to get a human-readable representation of.
* @since 0.3.0
*/
function thingAsMarkdown(thing) {
let thingAsMarkdown = "";
if (isThingLocal(thing)) {
thingAsMarkdown += `## Thing (no URL yet — identifier: \`#${thing.internal_localSubject.internal_name}\`)\n`;
}
else {
thingAsMarkdown += `## Thing: ${thing.internal_url}\n`;
}
const quads = Array.from(thing);
if (quads.length === 0) {
thingAsMarkdown += "\n<empty>\n";
}
else {
const predicates = new Set(quads.map((quad) => quad.predicate.value));
for (const predicate of predicates) {
thingAsMarkdown += `\nProperty: ${predicate}\n`;
const values = getTermAll(thing, predicate);
values.forEach((value) => {
thingAsMarkdown += `- ${internal_getReadableValue(value)}\n`;
});
}
}
return thingAsMarkdown;
}
/**
* @param thing The [[Thing]] of which a URL might or might not be known.
* @return Whether `thing` has no known URL yet.
*/
function isThingLocal(thing) {
var _a;
return (typeof ((_a = thing.internal_localSubject) === null || _a === void 0 ? void 0 : _a.internal_name) ===
"string" && typeof thing.internal_url === "undefined");
}
/**
* This error is thrown when a function expected to receive a [[Thing]] but received something else.
* @since 1.2.0
*/
class ThingExpectedError extends SolidClientError {
constructor(receivedValue) {
const message = `Expected a Thing, but received: [${receivedValue}].`;
super(message);
this.receivedValue = receivedValue;
}
}
/**
* This error is thrown when a function expected to receive a valid URL to identify a property but received something else.
*/
class ValidPropertyUrlExpectedError extends SolidClientError {
constructor(receivedValue) {
const value = isNamedNode(receivedValue)
? receivedValue.value
: receivedValue;
const message = `Expected a valid URL to identify a property, but received: [${value}].`;
super(message);
this.receivedProperty = value;
}
}
/**
* This error is thrown when a function expected to receive a valid URL value but received something else.
*/
class ValidValueUrlExpectedError extends SolidClientError {
constructor(receivedValue) {
const value = isNamedNode(receivedValue)
? receivedValue.value
: receivedValue;
const message = `Expected a valid URL value, but received: [${value}].`;
super(message);
this.receivedValue = value;
}
}
/**
* This error is thrown when a function expected to receive a valid URL to identify a [[Thing]] but received something else.
*/
class ValidThingUrlExpectedError extends SolidClientError {
constructor(receivedValue) {
const value = isNamedNode(receivedValue)
? receivedValue.value
: receivedValue;
const message = `Expected a valid URL to identify a Thing, but received: [${value}].`;
super(message);
this.receivedValue = value;
}
}
/**
* Generate a string that can be used as the unique identifier for a Thing
*
* This function works by starting with a date string (so that Things 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 generateName = () => {
return (Date.now().toString() + Math.random().toString().substring("0.".length));
};
export { ThingExpectedError, ValidPropertyUrlExpectedError, ValidThingUrlExpectedError, ValidValueUrlExpectedError, asIri, asUrl, createThing, getThing, getThingAll, isThing, isThingLocal, removeThing, setThing, thingAsMarkdown };