UNPKG

@inrupt/solid-client

Version:
326 lines (323 loc) 14.3 kB
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 };