UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

779 lines (776 loc) 40 kB
import { Store, DataFactory } from 'n3'; import { toRdfJsQuads } from '../rdfjs.internal.mjs'; import { ldp } from '../constants.mjs'; import { getTurtleParser, triplesToTurtle } from '../formats/turtle.mjs'; import { isLocalNode, resolveIriForLocalNode, isNamedNode } from '../datatypes.mjs'; import { hasResourceInfo, hasChangelog } from '../interfaces.mjs'; import { normalizeUrl, internal_toIriString } from '../interfaces.internal.mjs'; import { FetchError, responseToResourceInfo, getContentType, getSourceUrl, isContainer } from './resource.mjs'; import { internal_isUnsuccessfulResponse, internal_parseResourceInfo } from './resource.internal.mjs'; import { getThing, getThingAll, thingAsMarkdown } from '../thing/thing.mjs'; import { internal_withChangeLog, internal_getReadableValue } from '../thing/thing.internal.mjs'; import { getIriAll } from '../thing/get.mjs'; import { normalizeServerSideIri } from './iri.internal.mjs'; import { freeze, isLocalNodeIri, getLocalNodeName } from '../rdf.internal.mjs'; import { fromRdfJsDataset } from '../rdfjs.mjs'; // Copyright 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. // /** * Initialise a new [[SolidDataset]] in memory. * * @returns An empty [[SolidDataset]]. */ function createSolidDataset() { return freeze({ type: "Dataset", graphs: { default: {}, }, }); } /** * @hidden This interface is not exposed yet until we've tried it out in practice. */ async function responseToSolidDataset(response, parseOptions = {}) { if (internal_isUnsuccessfulResponse(response)) { const errorBody = await response.clone().text(); throw new FetchError(`Fetching the SolidDataset at [${response.url}] failed: [${response.status}] [${response.statusText}] ${errorBody}.`, response, errorBody); } const resourceInfo = responseToResourceInfo(response); const parsers = { "text/turtle": getTurtleParser(), ...parseOptions.parsers, }; const contentType = getContentType(resourceInfo); if (contentType === null) { throw new Error(`Could not determine the content type of the Resource at [${getSourceUrl(resourceInfo)}].`); } const mimeType = contentType.split(";")[0]; const parser = parsers[mimeType]; if (typeof parser === "undefined") { throw new Error(`The Resource at [${getSourceUrl(resourceInfo)}] has a MIME type of [${mimeType}], but the only parsers available are for the following MIME types: [${Object.keys(parsers).join(", ")}].`); } const data = await response.text(); const rdfjsDataset = await new Promise((resolve, reject) => { const store = new Store(); parser.onError((error) => { reject(new Error(`Encountered an error parsing the Resource at [${getSourceUrl(resourceInfo)}] with content type [${contentType}]: ${error}`)); }); parser.onQuad((quad) => { store.add(quad); }); parser.onComplete(() => { resolve(store); }); parser.parse(data, resourceInfo); }); const solidDataset = freeze(fromRdfJsDataset(rdfjsDataset)); return freeze({ ...solidDataset, ...resourceInfo, }); } /** * Fetch a SolidDataset from the given URL. Currently requires the SolidDataset to be available as [Turtle](https://www.w3.org/TR/turtle/). * * Note that the URL of a container ends with a [trailing slash "/"](https://solidproject.org/TR/protocol#uri). * If it is missing, some libraries will add it automatically, which may result in additional round-trips, possibly including * authentication errors ([more information](https://github.com/inrupt/solid-client-js/issues/1216#issuecomment-904703695)). * * @param url URL to fetch a [[SolidDataset]] from. * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * @returns Promise resolving to a [[SolidDataset]] containing the data at the given Resource, or rejecting if fetching it failed. */ async function getSolidDataset(url, options) { var _a, _b; const normalizedUrl = normalizeUrl(internal_toIriString(url)); const parserContentTypes = Object.keys((_a = options === null || options === void 0 ? void 0 : options.parsers) !== null && _a !== void 0 ? _a : {}); const acceptedContentTypes = parserContentTypes.length > 0 ? parserContentTypes.join(", ") : "text/turtle"; const response = await ((_b = options === null || options === void 0 ? void 0 : options.fetch) !== null && _b !== void 0 ? _b : fetch)(normalizedUrl, { headers: { Accept: acceptedContentTypes, }, }); if (internal_isUnsuccessfulResponse(response)) { const errorBody = await response.clone().text(); throw new FetchError(`Fetching the Resource at [${normalizedUrl}] failed: [${response.status}] [${response.statusText}] ${errorBody}.`, response, errorBody); } const solidDataset = await responseToSolidDataset(response, options); return solidDataset; } /** * Create a SPARQL UPDATE Patch request from a [[SolidDataset]] with a changelog. * @param solidDataset the [[SolidDataset]] that has been locally updated, and that should be persisted. * @returns an HTTP PATCH request configuration object, aligned with the [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters), containing a SPARQL UPDATE. * @hidden */ async function prepareSolidDatasetUpdate(solidDataset) { const deleteStatement = solidDataset.internal_changeLog.deletions.length > 0 ? `DELETE DATA {${(await triplesToTurtle(solidDataset.internal_changeLog.deletions.map(getNamedNodesForLocalNodes))).trim()}};` : ""; const insertStatement = solidDataset.internal_changeLog.additions.length > 0 ? `INSERT DATA {${(await triplesToTurtle(solidDataset.internal_changeLog.additions.map(getNamedNodesForLocalNodes))).trim()}};` : ""; return { method: "PATCH", body: `${deleteStatement} ${insertStatement}`, headers: { "Content-Type": "application/sparql-update", }, }; } /** * Create a Put request to write a locally created [[SolidDataset]] to a Pod. * @param solidDataset the [[SolidDataset]] that has been locally updated, and that should be persisted. * @returns an HTTP PUT request configuration object, aligned with the [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters), containing a serialization of the [[SolidDataset]]. * @hidden */ async function prepareSolidDatasetCreation(solidDataset, options) { return { method: "PUT", body: await triplesToTurtle(toRdfJsQuads(solidDataset).map(getNamedNodesForLocalNodes), options), headers: { "Content-Type": "text/turtle", "If-None-Match": "*", Link: `<${ldp.Resource}>; rel="type"`, }, }; } /** * Given a SolidDataset, store it in a Solid Pod (overwriting the existing data at the given URL). * * A SolidDataset keeps track of the data changes compared to the data in the Pod; i.e., * the changelog tracks both the old value and new values of the property being modified. This * function applies the changes to the current SolidDataset. If the old value specified in the * changelog does not correspond to the value currently in the Pod, this function will throw an * error (common issues are listed in [the documentation](https://docs.inrupt.com/sdk/javascript-sdkreference/error-codes/)). * * The SolidDataset returned by this function will contain the data sent to the Pod, and a ChangeLog * up-to-date with the saved data. Note that if the data on the server was modified in between the * first fetch and saving it, the updated data will not be reflected in the returned SolidDataset. * To make sure you have the latest data, call [[getSolidDataset]] again after saving the data. * * The Solid server will create any intermediary Containers that do not exist yet, so they do not * need to be created in advance. For example, if the target URL is * https://example.pod/container/resource and https://example.pod/container/ does not exist yet, * it will exist after this function resolves successfully. * * @param url URL to save `solidDataset` to. * @param solidDataset The [[SolidDataset]] to save. * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * `options.prefixes`: A prefix map to customize the serialization. Only applied on resource creation if the serialization allows it. * @returns A Promise resolving to a [[SolidDataset]] containing the stored data, or rejecting if saving it failed. */ async function saveSolidDatasetAt(url, solidDataset, options) { var _a; const targetUrl = normalizeUrl(internal_toIriString(url)); const datasetWithChangelog = internal_withChangeLog(solidDataset); const requestInit = isUpdate(datasetWithChangelog, targetUrl) ? await prepareSolidDatasetUpdate(datasetWithChangelog) : await prepareSolidDatasetCreation(datasetWithChangelog, options); const response = await ((_a = options === null || options === void 0 ? void 0 : options.fetch) !== null && _a !== void 0 ? _a : fetch)(targetUrl, requestInit); if (internal_isUnsuccessfulResponse(response)) { const diagnostics = isUpdate(datasetWithChangelog, targetUrl) ? `The changes that were sent to the Pod are listed below.\n\n${changeLogAsMarkdown(datasetWithChangelog)}` : `The SolidDataset that was sent to the Pod is listed below.\n\n${solidDatasetAsMarkdown(datasetWithChangelog)}`; const errorBody = await response.clone().text(); throw new FetchError(`Storing the Resource at [${targetUrl}] failed: [${response.status}] [${response.statusText}] ${errorBody}.\n\n${diagnostics}`, response, errorBody); } const resourceInfo = { ...internal_parseResourceInfo(response), isRawData: false, }; const storedDataset = freeze({ ...solidDataset, internal_changeLog: { additions: [], deletions: [] }, internal_resourceInfo: resourceInfo, }); const storedDatasetWithResolvedIris = resolveLocalIrisInSolidDataset(storedDataset); return storedDatasetWithResolvedIris; } /** * Deletes the SolidDataset at a given URL. * * If operating on a container, the container must be empty otherwise a 409 CONFLICT will be raised. * * @param solidDataset The URL of the SolidDataset to delete or the SolidDataset itself (if it has ResourceInfo). * @since 0.6.0 */ async function deleteSolidDataset(solidDataset, options) { var _a; const url = hasResourceInfo(solidDataset) ? internal_toIriString(getSourceUrl(solidDataset)) : normalizeUrl(internal_toIriString(solidDataset)); const response = await ((_a = options === null || options === void 0 ? void 0 : options.fetch) !== null && _a !== void 0 ? _a : fetch)(url, { method: "DELETE" }); if (internal_isUnsuccessfulResponse(response)) { const errorBody = await response.clone().text(); throw new FetchError(`Deleting the SolidDataset at [${url}] failed: [${response.status}] [${response.statusText}] ${errorBody}.`, response, errorBody); } } /** * Create a Container at the given URL. Some content may optionally be specified, * e.g. to add metadata describing the container. * * Throws an error if creating the Container failed, e.g. because the current user does not have * permissions to, or because the Container already exists. * * Note that a Solid server will automatically create the necessary Containers when storing a * Resource; i.e. there is no need to call this function if it is immediately followed by * [[saveSolidDatasetAt]] or [[overwriteFile]]. * * @param url URL of the empty Container that is to be created. * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * @param solidDataset Optional parameter - if provided we use this dataset as the body of the HTT request, meaning it's data is included in the Container resource. * @since 0.2.0 */ async function createContainerAt(url, options = {}) { var _a, _b; const normalizedUrl = normalizeUrl(internal_toIriString(url), { trailingSlash: true, }); const response = await ((_a = options.fetch) !== null && _a !== void 0 ? _a : fetch)(normalizedUrl, { method: "PUT", body: options.initialContent ? await triplesToTurtle(toRdfJsQuads(options.initialContent).map(getNamedNodesForLocalNodes)) : undefined, headers: { Accept: "text/turtle", "Content-Type": "text/turtle", "If-None-Match": "*", // This header should not be required to create a Container, // but ESS currently expects it: Link: `<${ldp.BasicContainer}>; rel="type"`, }, }); if (internal_isUnsuccessfulResponse(response)) { const containerType = options.initialContent === undefined ? "empty" : "non-empty"; const errorBody = await response.clone().text(); throw new FetchError(`Creating the ${containerType} Container at [${url}] failed: [${response.status}] [${response.statusText}] ${errorBody}.`, response, errorBody); } const resourceInfo = internal_parseResourceInfo(response); const containerDataset = freeze({ ...((_b = options.initialContent) !== null && _b !== void 0 ? _b : createSolidDataset()), internal_changeLog: { additions: [], deletions: [] }, internal_resourceInfo: resourceInfo, }); return containerDataset; } function isSourceIriEqualTo(dataset, iri) { return (normalizeServerSideIri(dataset.internal_resourceInfo.sourceIri) === normalizeServerSideIri(iri)); } function isUpdate(solidDataset, url) { return (hasChangelog(solidDataset) && hasResourceInfo(solidDataset) && typeof solidDataset.internal_resourceInfo.sourceIri === "string" && isSourceIriEqualTo(solidDataset, url)); } /** * Given a SolidDataset, store it in a Solid Pod in a new Resource inside a Container. * * The Container at the given URL should already exist; if it does not, you can initialise it first * using [[createContainerAt]], or directly save the SolidDataset at the desired location using * [[saveSolidDatasetAt]]. * * This function is primarily useful if the current user does not have access to change existing files in * a Container, but is allowed to add new files; in other words, they have Append, but not Write * access to a Container. This is useful in situations where someone wants to allow others to, * for example, send notifications to their Pod, but not to view or delete existing notifications. * You can pass a suggestion for the new Resource's name, but the server may decide to give it * another name — for example, if a Resource with that name already exists inside the given * Container. * If the user does have access to write directly to a given location, [[saveSolidDatasetAt]] * will do the job just fine, and does not require the parent Container to exist in advance. * * @param containerUrl URL of the Container in which to create a new Resource. * @param solidDataset The [[SolidDataset]] to save to a new Resource in the given Container. * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * @returns A Promise resolving to a [[SolidDataset]] containing the saved data. The Promise rejects if the save failed. */ async function saveSolidDatasetInContainer(containerUrl, solidDataset, options) { var _a; const normalizedUrl = normalizeUrl(internal_toIriString(containerUrl), { trailingSlash: true, }); const rawTurtle = await triplesToTurtle(toRdfJsQuads(solidDataset).map(getNamedNodesForLocalNodes)); const headers = { "Content-Type": "text/turtle", Link: `<${ldp.Resource}>; rel="type"`, }; if (options === null || options === void 0 ? void 0 : options.slugSuggestion) { headers.slug = options.slugSuggestion; } const response = await ((_a = options === null || options === void 0 ? void 0 : options.fetch) !== null && _a !== void 0 ? _a : fetch)(normalizedUrl, { method: "POST", body: rawTurtle, headers, }); if (internal_isUnsuccessfulResponse(response)) { const errorBody = await response.clone().text(); throw new FetchError(`Storing the Resource in the Container at [${normalizedUrl}] failed: [${response.status}] [${response.statusText}] ${errorBody}.\n\n` + `The SolidDataset that was sent to the Pod is listed below.\n\n${solidDatasetAsMarkdown(solidDataset)}`, response, errorBody); } const internalResourceInfo = internal_parseResourceInfo(response); if (!internalResourceInfo.location) { throw new Error("Could not determine the location of the newly saved SolidDataset."); } let resourceIri; try { // Try to parse the location header as a URL (safe if it's an absolute URL)`` // This should help determine the container URL if normalisation happened on the server side. resourceIri = new URL(internalResourceInfo.location).href; } catch (_b) { // If it's a relative URL then, rely on the response.url to construct the sourceIri resourceIri = new URL(internalResourceInfo.location, response.url).href; } const resourceInfo = { internal_resourceInfo: { isRawData: false, sourceIri: resourceIri, }, }; const resourceWithResourceInfo = freeze({ ...solidDataset, ...resourceInfo, }); const resourceWithResolvedIris = resolveLocalIrisInSolidDataset(resourceWithResourceInfo); return resourceWithResolvedIris; } /** * Create an empty Container inside the Container at the given URL. * * Throws an error if creating the Container failed, e.g. because the current user does not have * permissions to. * * The Container in which to create the new Container should itself already exist. * * This function is primarily useful if the current user does not have access to change existing files in * a Container, but is allowed to add new files; in other words, they have Append, but not Write * access to a Container. This is useful in situations where someone wants to allow others to, * for example, send notifications to their Pod, but not to view or delete existing notifications. * You can pass a suggestion for the new Resource's name, but the server may decide to give it * another name — for example, if a Resource with that name already exists inside the given * Container. * If the user does have access to write directly to a given location, [[createContainerAt]] * will do the job just fine, and does not require the parent Container to exist in advance. * * @param containerUrl URL of the Container in which the empty Container is to * be created. * @param options Optional parameter `options.fetch`: An alternative `fetch` * function to make the HTTP request, compatible with the browser-native [fetch * API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).`options.slugSuggestion` * accepts a string for your new Container's name. * @returns A promise that resolves to a SolidDataset with ResourceInfo if * successful, and that rejects otherwise. * @since 0.2.0 */ async function createContainerInContainer(containerUrl, options) { var _a; const normalizedUrl = normalizeUrl(internal_toIriString(containerUrl), { trailingSlash: true, }); const headers = { "Content-Type": "text/turtle", Link: `<${ldp.BasicContainer}>; rel="type"`, }; if (options === null || options === void 0 ? void 0 : options.slugSuggestion) { headers.slug = options.slugSuggestion; } const response = await ((_a = options === null || options === void 0 ? void 0 : options.fetch) !== null && _a !== void 0 ? _a : fetch)(normalizedUrl, { method: "POST", headers, }); if (internal_isUnsuccessfulResponse(response)) { const errorBody = await response.clone().text(); throw new FetchError(`Creating an empty Container in the Container at [${normalizedUrl}] failed: [${response.status}] [${response.statusText}] ${errorBody}.`, response, errorBody); } const internalResourceInfo = internal_parseResourceInfo(response); if (!internalResourceInfo.location) { throw new Error("Could not determine the location of the newly created Container."); } try { // Try to parse the location header as a URL (safe if it's an absolute URL)`` // This should help determine the container URL if normalisation happened on the server side. const sourceIri = new URL(internalResourceInfo.location).toString(); return freeze({ ...createSolidDataset(), internal_resourceInfo: { ...internalResourceInfo, sourceIri, }, }); } catch (_b) { // If it's a relative URL then, rely on the response.url to construct the sourceIri } return freeze({ ...createSolidDataset(), internal_resourceInfo: { ...internalResourceInfo, sourceIri: new URL(internalResourceInfo.location, response.url).href, }, }); } /** * Deletes the Container at a given URL. * * @param container The URL of the Container to delete or the Container Dataset itself (if it has ResourceInfo). * @since 0.6.0 */ async function deleteContainer(container, options) { var _a; const normalizedUrl = hasResourceInfo(container) ? internal_toIriString(getSourceUrl(container)) : normalizeUrl(internal_toIriString(container)); if (!isContainer(container)) { throw new Error(`You're trying to delete the Container at [${normalizedUrl}], but Container URLs should end in a \`/\`. Are you sure this is a Container?`); } const response = await ((_a = options === null || options === void 0 ? void 0 : options.fetch) !== null && _a !== void 0 ? _a : fetch)(normalizedUrl, { method: "DELETE", }); if (internal_isUnsuccessfulResponse(response)) { const errorBody = await response.clone().text(); throw new FetchError(`Deleting the Container at [${normalizedUrl}] failed: [${response.status}] [${response.statusText}] ${errorBody}.`, response, errorBody); } } function isChildResource(a, b) { const parent = new URL(b); const child = new URL(a); // Explicitly test on the whole URL to enforce similar origins. const isAncestor = child.href.startsWith(parent.href); const relativePath = child.pathname .substring(parent.pathname.length, child.pathname.length) .replace(/(^\/)|(\/$)/g, ""); // The child path component that isn't present in the parent should only // potentially include slashes at the end (if it is a container). return isAncestor && relativePath.length >= 1 && !relativePath.includes("/"); } /** * Given a [[SolidDataset]] representing a Container (see [[isContainer]]), fetch the URLs of all * contained resources that respect [slash semantics](https://solidproject.org/TR/protocol#uri-slash-semantics) * (see {@link validateContainedResourceAll}). * If the solidDataset given is not a container, or is missing resourceInfo, throw an error. * * @param solidDataset The container from which to fetch all contained Resource URLs. * @returns A list of URLs, each of which points to a contained Resource of the given SolidDataset. * @since 1.3.0 */ function getContainedResourceUrlAll(solidDataset) { const containerUrl = getSourceUrl(solidDataset); const container = getThing(solidDataset, containerUrl); if (container === null) { return []; } // See https://www.w3.org/TR/2015/REC-ldp-20150226/#h-ldpc-http_post: // > a containment triple MUST be added to the state of the LDPC whose subject is the LDPC URI, // > whose predicate is ldp:contains and whose object is the URI for the newly created document return (getIriAll(container, ldp.contains) // See https://solidproject.org/TR/protocol#resource-containment .filter((childUrl) => isChildResource(childUrl, containerUrl))); } /** * Given a {@link SolidDataset} representing a [Container](https://solidproject.org/TR/protocol#resource-containment) * (see {@link isContainer}), verify that all its containment claims are valid. Containment of a resource is valid if * it respects [slash semantics](https://solidproject.org/TR/protocol#uri-slash-semantics). * * For the example, given a container at https://example.org/container/: * - The following resources are valid: * - https://example.org/container/resource * - https://example.org/container/subcontainer/ * - The following resources are invalid: * - https://example.org/container/resource/invalid (not a direct child resource) * - https://example.org/container2 (not a child resource) * - https://domain2.example.org/container/resource (not a direct child resource) * * If a component claim is invalid, {@link validateContainedResourceAll} returns the invalid component's URL * as part of its return object. * * Note: It is recommended that this function always be used before calling * {@link getContainedResourceUrlAll} since {@link getContainedResourceUrlAll} does not * return Resources for which containment is invalid. Using the function in conjunction * with {@link getContainedResourceUrlAll} allows for the detection of unexpected behaviour from servers, * including malicious containment triples that could appear. Because ESS conforms to the Solid Protocol, * i.e., respects slash semantics for its containment triples, validateContainedResourceAll returns true for * containers fetched from ESS. * * @param solidDataset The container from which containment claims are validated. * @returns A validation report, including the offending contained resources URL if any. * @since 1.30.1 */ function validateContainedResourceAll(solidDataset) { const containerUrl = getSourceUrl(solidDataset); const container = getThing(solidDataset, containerUrl); if (container === null) { return { isValid: true, invalidContainedResources: [] }; } // See https://www.w3.org/TR/2015/REC-ldp-20150226/#h-ldpc-http_post: // > a containment triple MUST be added to the state of the LDPC whose subject is the LDPC URI, // > whose predicate is ldp:contains and whose object is the URI for the newly created document const invalidChildren = getIriAll(container, ldp.contains) // See https://solidproject.org/TR/protocol#resource-containment .filter((childUrl) => !isChildResource(childUrl, containerUrl)); if (invalidChildren.length > 0) { return { isValid: false, invalidContainedResources: invalidChildren }; } return { isValid: true, invalidContainedResources: [] }; } /** * Gets a human-readable representation of the given SolidDataset 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 solidDataset The [[SolidDataset]] to get a human-readable representation of. * @since 0.3.0 */ function solidDatasetAsMarkdown(solidDataset) { let readableSolidDataset = ""; if (hasResourceInfo(solidDataset)) { readableSolidDataset += `# SolidDataset: ${getSourceUrl(solidDataset)}\n`; } else { readableSolidDataset += `# SolidDataset (no URL yet)\n`; } const things = getThingAll(solidDataset); if (things.length === 0) { readableSolidDataset += "\n<empty>\n"; } else { things.forEach((thing) => { readableSolidDataset += `\n${thingAsMarkdown(thing)}`; if (hasChangelog(solidDataset)) { readableSolidDataset += `\n${getReadableChangeLogSummary(solidDataset, thing)}\n`; } }); } return readableSolidDataset; } /** * Gets a human-readable representation of the local changes to a Resource 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 solidDataset The Resource of which to get a human-readable representation of the changes applied to it locally. * @since 0.3.0 */ function changeLogAsMarkdown(solidDataset) { if (!hasResourceInfo(solidDataset)) { return "This is a newly initialized SolidDataset, so there is no source to compare it to."; } if (!hasChangelog(solidDataset) || (solidDataset.internal_changeLog.additions.length === 0 && solidDataset.internal_changeLog.deletions.length === 0)) { return (`## Changes compared to ${getSourceUrl(solidDataset)}\n\n` + `This SolidDataset has not been modified since it was fetched from ${getSourceUrl(solidDataset)}.\n`); } let readableChangeLog = `## Changes compared to ${getSourceUrl(solidDataset)}\n`; const changeLogsByThingAndProperty = sortChangeLogByThingAndProperty(solidDataset); Object.keys(changeLogsByThingAndProperty).forEach((thingUrl) => { readableChangeLog += `\n### Thing: ${thingUrl}\n`; const changeLogByProperty = changeLogsByThingAndProperty[thingUrl]; Object.keys(changeLogByProperty).forEach((propertyUrl) => { readableChangeLog += `\nProperty: ${propertyUrl}\n`; const { deleted } = changeLogByProperty[propertyUrl]; const { added } = changeLogByProperty[propertyUrl]; if (deleted.length > 0) { readableChangeLog += "- Removed:\n"; readableChangeLog += deleted.reduce((acc, deletedValue) => { return `${acc} - ${internal_getReadableValue(deletedValue)}\n`; }, ""); } if (added.length > 0) { readableChangeLog += "- Added:\n"; readableChangeLog += added.reduce((acc, addedValue) => { return `${acc} - ${internal_getReadableValue(addedValue)}\n`; }, ""); } }); }); return readableChangeLog; } function sortChangeLogByThingAndProperty(solidDataset) { const changeLogsByThingAndProperty = Object.create(null); solidDataset.internal_changeLog.deletions.forEach((deletion) => { var _a, _b; var _c; const subjectNode = isLocalNode(deletion.subject) ? /* istanbul ignore next: Unsaved deletions should be removed from the additions list instead, so this code path shouldn't be hit: */ resolveIriForLocalNode(deletion.subject, getSourceUrl(solidDataset)) : deletion.subject; if (!isNamedNode(subjectNode) || !isNamedNode(deletion.predicate)) { return; } const thingUrl = internal_toIriString(subjectNode); const propertyUrl = internal_toIriString(deletion.predicate); (_a = changeLogsByThingAndProperty[thingUrl]) !== null && _a !== void 0 ? _a : (changeLogsByThingAndProperty[thingUrl] = Object.create(null)); (_b = (_c = changeLogsByThingAndProperty[thingUrl])[propertyUrl]) !== null && _b !== void 0 ? _b : (_c[propertyUrl] = { added: [], deleted: [], }); changeLogsByThingAndProperty[thingUrl][propertyUrl].deleted.push(deletion.object); }); solidDataset.internal_changeLog.additions.forEach((addition) => { var _a, _b; var _c; const subjectNode = isLocalNode(addition.subject) ? /* istanbul ignore next: setThing already resolves local Subjects when adding them, so this code path should never be hit. */ resolveIriForLocalNode(addition.subject, getSourceUrl(solidDataset)) : addition.subject; if (!isNamedNode(subjectNode) || !isNamedNode(addition.predicate)) { return; } const thingUrl = internal_toIriString(subjectNode); const propertyUrl = internal_toIriString(addition.predicate); (_a = changeLogsByThingAndProperty[thingUrl]) !== null && _a !== void 0 ? _a : (changeLogsByThingAndProperty[thingUrl] = Object.create(null)); (_b = (_c = changeLogsByThingAndProperty[thingUrl])[propertyUrl]) !== null && _b !== void 0 ? _b : (_c[propertyUrl] = { added: [], deleted: [], }); changeLogsByThingAndProperty[thingUrl][propertyUrl].added.push(addition.object); }); return changeLogsByThingAndProperty; } function getReadableChangeLogSummary(solidDataset, thing) { const subject = DataFactory.namedNode(thing.url); const nrOfAdditions = solidDataset.internal_changeLog.additions.reduce((count, addition) => (addition.subject.equals(subject) ? count + 1 : count), 0); const nrOfDeletions = solidDataset.internal_changeLog.deletions.reduce((count, deletion) => (deletion.subject.equals(subject) ? count + 1 : count), 0); const additionString = nrOfAdditions === 1 ? "1 new value added" : `${nrOfAdditions} new values added`; const deletionString = nrOfDeletions === 1 ? "1 value removed" : `${nrOfDeletions} values removed`; return `(${additionString} / ${deletionString})`; } function getNamedNodesForLocalNodes(quad) { const subject = isNamedNode(quad.subject) ? getNamedNodeFromLocalNode(quad.subject) : /* istanbul ignore next: We don't allow non-NamedNodes as the Subject, so this code path should never be hit: */ quad.subject; const object = isNamedNode(quad.object) ? getNamedNodeFromLocalNode(quad.object) : quad.object; return DataFactory.quad(subject, quad.predicate, object, quad.graph); } function getNamedNodeFromLocalNode(node) { if (isLocalNodeIri(node.value)) { return DataFactory.namedNode(`#${getLocalNodeName(node.value)}`); } return node; } function resolveLocalIrisInSolidDataset(solidDataset) { const resourceIri = getSourceUrl(solidDataset); const defaultGraph = solidDataset.graphs.default; const thingIris = Object.keys(defaultGraph); const updatedDefaultGraph = thingIris.reduce((graphAcc, thingIri) => { const resolvedThing = resolveLocalIrisInThing(graphAcc[thingIri], resourceIri); const resolvedThingIri = isLocalNodeIri(thingIri) ? `${resourceIri}#${getLocalNodeName(thingIri)}` : thingIri; const updatedGraph = { ...graphAcc }; delete updatedGraph[thingIri]; updatedGraph[resolvedThingIri] = resolvedThing; return freeze(updatedGraph); }, defaultGraph); const updatedGraphs = freeze({ ...solidDataset.graphs, default: updatedDefaultGraph, }); return freeze({ ...solidDataset, graphs: updatedGraphs, }); } function resolveLocalIrisInThing(thing, baseIri) { const predicateIris = Object.keys(thing.predicates); const updatedPredicates = predicateIris.reduce((predicatesAcc, predicateIri) => { var _a; const namedNodes = (_a = predicatesAcc[predicateIri].namedNodes) !== null && _a !== void 0 ? _a : []; if (namedNodes.every((namedNode) => !isLocalNodeIri(namedNode))) { // This Predicate has no local node Objects, so return it unmodified: return predicatesAcc; } const updatedNamedNodes = freeze(namedNodes.map((namedNode) => isLocalNodeIri(namedNode) ? `${baseIri}#${getLocalNodeName(namedNode)}` : namedNode)); const updatedPredicate = freeze({ ...predicatesAcc[predicateIri], namedNodes: updatedNamedNodes, }); return freeze({ ...predicatesAcc, [predicateIri]: updatedPredicate, }); }, thing.predicates); return freeze({ ...thing, predicates: updatedPredicates, url: isLocalNodeIri(thing.url) ? `${baseIri}#${getLocalNodeName(thing.url)}` : thing.url, }); } /** * @hidden * * Fetch a SolidDataset containing information about the capabilities of the * storage server that hosts the given resource URL. For more information, * please see the [ESS * Documentation](https://docs.inrupt.com/ess/latest/services/discovery-endpoint/#well-known-solid). * * ```typescript * const wellKnown = await getWellKnownSolid(resource); * * // The wellKnown dataset uses a blank node for the subject all of its predicates, * // such that we need to call getThingAll with acceptBlankNodes set to true to * // retrieve back predicates contained within the dataset * const wellKnownSubjects = getThingAll(wellKnown, { * acceptBlankNodes: true, * }); * const wellKnownSubject = wellKnownSubjects[0]; * * // Retrieve a value from the wellKnown dataset: * let notificationGateway = getIri( * wellKnownSubject, * "http://www.w3.org/ns/solid/terms#notificationGateway" * ); * ``` * * * @param url URL of a Resource. * @returns Promise resolving to a [[SolidDataset]] containing the data at * '.well-known/solid' for the given Resource, or rejecting if fetching it * failed. * @since 1.12.0 */ async function getWellKnownSolid(url) { const urlString = internal_toIriString(url); // Try to fetch the well-known solid dataset from the server's root try { const wellKnownSolidUrl = new URL("/.well-known/solid", new URL(urlString).origin).href; return await getSolidDataset(wellKnownSolidUrl); } catch (_a) { throw new Error("Could not determine storage root or well-known solid resource."); } } export { changeLogAsMarkdown, createContainerAt, createContainerInContainer, createSolidDataset, deleteContainer, deleteSolidDataset, getContainedResourceUrlAll, getSolidDataset, getWellKnownSolid, responseToSolidDataset, saveSolidDatasetAt, saveSolidDatasetInContainer, solidDatasetAsMarkdown, validateContainedResourceAll };