@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
1,055 lines (1,045 loc) • 438 kB
JavaScript
import LinkHeader from 'http-link-header';
import rdfjsDataset from '@rdfjs/dataset';
import * as crossFetch from 'cross-fetch';
/**
* 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.
*/
/**
* Verify whether a given SolidDataset includes metadata about where it was sent to.
*
* @param dataset A [[SolidDataset]] that may have metadata attached about the Resource it was retrieved from.
* @returns True if `dataset` includes metadata about the Resource it was sent to, false if not.
* @since 0.2.0
*/
function hasResourceInfo(resource) {
const potentialResourceInfo = resource;
return (typeof potentialResourceInfo === "object" &&
typeof potentialResourceInfo.internal_resourceInfo === "object");
}
/**
* Verify whether a given SolidDataset includes metadata about where it was retrieved from.
*
* @param dataset A [[SolidDataset]] that may have metadata attached about the Resource it was retrieved from.
* @returns True if `dataset` includes metadata about the Resource it was retrieved from, false if not.
* @since 0.6.0
*/
function hasServerResourceInfo(resource) {
const potentialResourceInfo = resource;
return (typeof potentialResourceInfo === "object" &&
typeof potentialResourceInfo.internal_resourceInfo === "object" &&
typeof potentialResourceInfo.internal_resourceInfo.linkedResources ===
"object");
}
/** @internal */
function hasChangelog(dataset) {
const potentialChangeLog = dataset;
return (typeof potentialChangeLog.internal_changeLog === "object" &&
Array.isArray(potentialChangeLog.internal_changeLog.additions) &&
Array.isArray(potentialChangeLog.internal_changeLog.deletions));
}
/**
* Errors thrown by solid-client extend this class, and can thereby be distinguished from errors
* thrown in lower-level libraries.
* @since 1.2.0
*/
class SolidClientError extends Error {
}
/**
* 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.
*/
/** @internal */
function internal_toIriString(iri) {
return typeof iri === "string" ? iri : iri.value;
}
/**
* 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.
*/
/**
* @ignore Internal fallback for when no fetcher is provided; not to be used downstream.
*/
const fetch = async (resource, init) => {
/* istanbul ignore if: `require` is always defined in the unit test environment */
if (typeof window === "object" && typeof require !== "function") {
return await window.fetch(resource, init);
}
/* istanbul ignore if: `require` is always defined in the unit test environment */
if (typeof require !== "function") {
// When using Node.js with ES Modules, require is not defined:
const crossFetchModule = await import('cross-fetch');
const fetch = crossFetchModule.default;
return fetch(resource, init);
}
// Implementation note: it's up to the client application to resolve these module names to the
// respective npm packages. At least one commonly used tool (Webpack) is only able to do that if
// the module names are literal strings.
// Additionally, Webpack throws a warning in a way that halts compilation for at least Next.js
// when using native Javascript dynamic imports (`import()`), whereas `require()` just logs a
// warning. Since the use of package names instead of file names requires a bundles anyway, this
// should not have any practical consequences. For more background, see:
// https://github.com/webpack/webpack/issues/7713
let fetch;
// Unfortunately solid-client-authn-browser does not support a default session yet.
// Once it does, we can auto-detect if it is available and use it as follows:
// try {
// fetch = require("solid-client-authn-browser").fetch;
// } catch (e) {
// When enabling the above, make sure to add a similar try {...} catch block using `import`
// statements in the elseif above.
// eslint-disable-next-line prefer-const
fetch = require("cross-fetch");
// }
return await fetch(resource, init);
};
/**
* 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.
*/
const dataset = rdfjsDataset.dataset;
const { quad, literal, namedNode, blankNode } = rdfjsDataset;
// TODO: Our code should be able to deal with switching the DataFactory
// implementation to that provided by '@rdfjs/dataset' (which is currently
// @rdfjs/data-model) - but currently it seems that implementation:
// - Doesn't treat capitalization of language tags correctly (i.e., according
// to the RDF specs, they should be case-insensitive).
// - Doesn't treat a string literal with an empty language tag of "" as an
// xsd:langString, instead treating it as a xsd:string.
// A fix for this would be here:
// https://github.com/rdfjs-base/data-model/blob/ed59e75132ee4d8a3a2f58443ff6a4f792a97033/lib/literal.js#L8
// ...changing this line to be:
// if (language || language == "") {
// But according to (https://w3c.github.io/rdf-dir-literal/langString.html)
// it seems the language tag should be non-empty.
// Our tests include specific checks for these behaviours (which is great), so
// until '@rdfjs/dataset' (or our tests!) are fixed, we need to avoid it's
// DataFactory.
// Currently (Feb 2021), only 4 tests fail now for the reasons above.
/**
* @internal
*/
const DataFactory = { quad, literal, namedNode, blankNode };
/**
* Clone a Dataset.
*
* Note that the Quads are not cloned, i.e. if you modify the Quads in the output Dataset, the Quads
* in the input Dataset will also be changed.
*
* @internal
* @param input Dataset to clone.
* @returns A new Dataset with the same Quads as `input`.
*/
function clone(input) {
const output = dataset();
for (const quad of input) {
output.add(quad);
}
return output;
}
/**
* @internal
* @param input Dataset to clone.
* @param callback Function that takes a Quad, and returns a boolean indicating whether that Quad should be included in the cloned Dataset.
* @returns A new Dataset with the same Quads as `input`, excluding the ones for which `callback` returned `false`.
*/
function filter(input, callback) {
const output = dataset();
for (const quad of input) {
if (callback(quad)) {
output.add(quad);
}
}
return output;
}
/**
* 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.
*/
/**
* @internal
*/
function internal_parseResourceInfo(response) {
var _a, _b, _c;
const contentTypeParts = (_b = (_a = response.headers.get("Content-Type")) === null || _a === void 0 ? void 0 : _a.split(";")) !== null && _b !== void 0 ? _b : [];
// If the server offers a Turtle or JSON-LD serialisation on its own accord,
// that tells us whether it is RDF data that the server can understand
// (and hence can be updated with a PATCH request with SPARQL INSERT and DELETE statements),
// in which case our SolidDataset-related functions should handle it.
// For more context, see https://github.com/inrupt/solid-client-js/pull/214.
const isSolidDataset = contentTypeParts.length > 0 &&
["text/turtle", "application/ld+json"].includes(contentTypeParts[0]);
const resourceInfo = {
sourceIri: response.url,
isRawData: !isSolidDataset,
contentType: (_c = response.headers.get("Content-Type")) !== null && _c !== void 0 ? _c : undefined,
linkedResources: {},
};
const linkHeader = response.headers.get("Link");
if (linkHeader) {
const parsedLinks = LinkHeader.parse(linkHeader);
// Set ACL link
const aclLinks = parsedLinks.get("rel", "acl");
if (aclLinks.length === 1) {
resourceInfo.aclUrl = new URL(aclLinks[0].uri, resourceInfo.sourceIri).href;
}
// Parse all link headers and expose them in a standard way
// (this can replace the parsing of the ACL link above):
resourceInfo.linkedResources = parsedLinks.refs.reduce((rels, ref) => {
var _a;
var _b;
(_a = rels[_b = ref.rel]) !== null && _a !== void 0 ? _a : (rels[_b] = []);
rels[ref.rel].push(new URL(ref.uri, resourceInfo.sourceIri).href);
return rels;
}, resourceInfo.linkedResources);
}
const wacAllowHeader = response.headers.get("WAC-Allow");
if (wacAllowHeader) {
resourceInfo.permissions = parseWacAllowHeader(wacAllowHeader);
}
return resourceInfo;
}
/**
* Parse a WAC-Allow header into user and public access booleans.
*
* @param wacAllowHeader A WAC-Allow header in the format `user="read append write control",public="read"`
* @see https://github.com/solid/solid-spec/blob/cb1373a369398d561b909009bd0e5a8c3fec953b/api-rest.md#wac-allow-headers
*/
function parseWacAllowHeader(wacAllowHeader) {
function parsePermissionStatement(permissionStatement) {
const permissions = permissionStatement.split(" ");
const writePermission = permissions.includes("write");
return writePermission
? {
read: permissions.includes("read"),
append: true,
write: true,
control: permissions.includes("control"),
}
: {
read: permissions.includes("read"),
append: permissions.includes("append"),
write: false,
control: permissions.includes("control"),
};
}
function getStatementFor(header, scope) {
const relevantEntries = header
.split(",")
.map((rawEntry) => rawEntry.split("="))
.filter((parts) => parts.length === 2 && parts[0].trim() === scope);
// There should only be one statement with the given scope:
if (relevantEntries.length !== 1) {
return "";
}
const relevantStatement = relevantEntries[0][1].trim();
// The given statement should be wrapped in double quotes to be valid:
if (relevantStatement.charAt(0) !== '"' ||
relevantStatement.charAt(relevantStatement.length - 1) !== '"') {
return "";
}
// Return the statment without the wrapping quotes, e.g.: read append write control
return relevantStatement.substring(1, relevantStatement.length - 1);
}
return {
user: parsePermissionStatement(getStatementFor(wacAllowHeader, "user")),
public: parsePermissionStatement(getStatementFor(wacAllowHeader, "public")),
};
}
/** @hidden Used to instantiate a separate instance from input parameters */
function internal_cloneResource(resource) {
let clonedResource;
if (typeof resource.slice === "function") {
// If given Resource is a File:
clonedResource = resource.slice();
}
else if (typeof resource.match === "function") {
// If given Resource is a SolidDataset:
// (We use the existince of a `match` method as a heuristic:)
clonedResource = clone(resource);
}
else {
// If it is just a plain object containing metadata:
clonedResource = Object.assign({}, resource);
}
return Object.assign(clonedResource,
// Although the RDF/JS data structures use classes and mutation,
// we only attach atomic properties that we never mutate.
// Hence, `copyNonClassProperties` is a heuristic that allows us to only clone our own data
// structures, rather than references to the same mutable instances of RDF/JS data structures:
copyNonClassProperties(resource));
}
function copyNonClassProperties(source) {
const copy = {};
Object.keys(source).forEach((key) => {
const value = source[key];
if (typeof value !== "object" || value === null) {
copy[key] = value;
return;
}
// Ignore properties that are Class methods, we don't want to copy those
// across (e.g., copying over an RDF/JS `.add()` method would result in the
// former instance's implementation of `.add()` being invoked).
if (typeof value.constructor === "undefined" ||
value.constructor.name !== "Object") {
return;
}
copy[key] = value;
});
return copy;
}
/** @internal */
function internal_isUnsuccessfulResponse(response) {
return !response.ok;
}
/**
* 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.
*/
/** @ignore For internal use only. */
const internal_defaultFetchOptions = {
fetch: fetch,
};
/**
* Retrieve the information about a resource (e.g. access permissions) without
* fetching the resource itself.
*
* @param url URL to fetch Resource metadata 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/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters).
* @returns Promise resolving to the metadata describing the given Resource, or rejecting if fetching it failed.
* @since 0.4.0
*/
async function getResourceInfo(url, options = internal_defaultFetchOptions) {
const config = Object.assign(Object.assign({}, internal_defaultFetchOptions), options);
const response = await config.fetch(url, { method: "HEAD" });
if (internal_isUnsuccessfulResponse(response)) {
throw new FetchError(`Fetching the metadata of the Resource at [${url}] failed: [${response.status}] [${response.statusText}].`, response);
}
const resourceInfo = internal_parseResourceInfo(response);
return { internal_resourceInfo: resourceInfo };
}
/**
* @param resource Resource for which to check whether it is a Container.
* @returns Whether `resource` is a Container.
*/
function isContainer(resource) {
const containerUrl = hasResourceInfo(resource)
? getSourceUrl(resource)
: internal_toIriString(resource);
return containerUrl.endsWith("/");
}
/**
* This function will tell you whether a given Resource contains raw data, or a SolidDataset.
*
* @param resource Resource for which to check whether it contains raw data.
* @return Whether `resource` contains raw data.
*/
function isRawData(resource) {
return resource.internal_resourceInfo.isRawData;
}
/**
* @param resource Resource for which to determine the Content Type.
* @returns The Content Type, if known, or null if not known.
*/
function getContentType$1(resource) {
var _a;
return (_a = resource.internal_resourceInfo.contentType) !== null && _a !== void 0 ? _a : null;
}
function getSourceUrl(resource) {
if (hasResourceInfo(resource)) {
return resource.internal_resourceInfo.sourceIri;
}
return null;
}
/** @hidden Alias of getSourceUrl for those who prefer to use IRI terminology. */
const getSourceIri = getSourceUrl;
/**
* Given a Resource that exposes information about the owner of the Pod it is in, returns the WebID of that owner.
*
* Data about the owner of the Pod is exposed when the following conditions hold:
* - The Pod server supports exposing the Pod owner
* - The current user is allowed to see who the Pod owner is.
*
* If one or more of those conditions are false, this function will return `null`.
*
* @param resource A Resource that contains information about the owner of the Pod it is in.
* @returns The WebID of the owner of the Pod the Resource is in, if provided, or `null` if not.
* @since 0.6.0
*/
function getPodOwner(resource) {
var _a;
if (!hasServerResourceInfo(resource)) {
return null;
}
const podOwners = (_a = resource.internal_resourceInfo.linkedResources["http://www.w3.org/ns/solid/terms#podOwner"]) !== null && _a !== void 0 ? _a : [];
return podOwners.length === 1 ? podOwners[0] : null;
}
/**
* Given a WebID and a Resource that exposes information about the owner of the Pod it is in, returns whether the given WebID is the owner of the Pod.
*
* Data about the owner of the Pod is exposed when the following conditions hold:
* - The Pod server supports exposing the Pod owner
* - The current user is allowed to see who the Pod owner is.
*
* If one or more of those conditions are false, this function will return `null`.
*
* @param webId The WebID of which to check whether it is the Pod Owner's.
* @param resource A Resource that contains information about the owner of the Pod it is in.
* @returns Whether the given WebID is the Pod Owner's, if the Pod Owner is exposed, or `null` if it is not exposed.
* @since 0.6.0
*/
function isPodOwner(webId, resource) {
const podOwner = getPodOwner(resource);
if (typeof podOwner !== "string") {
return null;
}
return podOwner === webId;
}
/**
* Extends the regular JavaScript error object with access to the status code and status message.
* @since 1.2.0
*/
class FetchError extends SolidClientError {
constructor(message, errorResponse) {
super(message);
this.response = errorResponse;
}
get statusCode() {
return this.response.status;
}
get statusText() {
return this.response.statusText;
}
}
/**
* 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.
*/
const defaultGetFileOptions = {
fetch: fetch,
};
const RESERVED_HEADERS = ["Slug", "If-None-Match", "Content-Type"];
/**
* Some of the headers must be set by the library, rather than directly.
*/
function containsReserved(header) {
return RESERVED_HEADERS.some((reserved) => header[reserved] !== undefined);
}
/**
* ```{note} This function is still experimental and subject to change, even in a non-major release.
* ```
*
* Retrieves a file from a URL and returns the file as a blob.
*
* @param url The URL of the file to return
* @param options Fetching options: a custom fetcher and/or headers.
* @returns The file as a blob.
*/
async function getFile(input, options = defaultGetFileOptions) {
const config = Object.assign(Object.assign({}, defaultGetFileOptions), options);
const url = internal_toIriString(input);
const response = await config.fetch(url, config.init);
if (internal_isUnsuccessfulResponse(response)) {
throw new FetchError(`Fetching the File failed: [${response.status}] [${response.statusText}].`, response);
}
const resourceInfo = internal_parseResourceInfo(response);
const data = await response.blob();
const fileWithResourceInfo = Object.assign(data, {
internal_resourceInfo: resourceInfo,
});
return fileWithResourceInfo;
}
/**
* ```{note} This function is still experimental and subject to change, even in a non-major release.
* ```
* Deletes a file at a given URL.
*
* @param file The URL of the file to delete
*/
async function deleteFile(file, options = defaultGetFileOptions) {
const config = Object.assign(Object.assign({}, defaultGetFileOptions), options);
const url = hasResourceInfo(file)
? internal_toIriString(getSourceIri(file))
: internal_toIriString(file);
const response = await config.fetch(url, Object.assign(Object.assign({}, config.init), { method: "DELETE" }));
if (internal_isUnsuccessfulResponse(response)) {
throw new FetchError(`Deleting the file at [${url}] failed: [${response.status}] [${response.statusText}].`, response);
}
}
/**
* ```{note} This function is still experimental and subject to change, even in a non-major release.
* ```
*
* Saves a file in a folder associated with the given URL. The final filename may or may
* not be the given `slug`.
*
* If you know the [media type](https://developer.mozilla.org/en-US/docs/Glossary/MIME_type)
* of the file you are attempting to save, then you should provide this in the
* `options` parameter. For example, if you know your file is a JPEG image,
* then you should provide the media type `image/jpeg`. If you don't know, or
* don't provide a media type, a default type of `application/octet-stream` will
* be applied (which indicates that the file should be regarded as pure binary
* data).
*
* The Container at the given URL should already exist; if it does not, the returned Promise will
* be rejected. You can initialise it first using [[createContainerAt]], or directly save the file
* at the desired location using [[overwriteFile]].
*
* 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, [[overwriteFile]]
* will do the job just fine, and does not require the parent Container to exist in advance.
*
* @param folderUrl The URL of the folder where the new file is saved.
* @param file The file to be written.
* @param options Additional parameters for file creation (e.g. a slug).
* @returns A Promise that resolves to the saved file, if available, or `null` if the current user does not have Read access to the newly-saved file. It rejects if saving fails.
*/
async function saveFileInContainer(folderUrl, file, options = defaultGetFileOptions) {
const folderUrlString = internal_toIriString(folderUrl);
const response = await writeFile(folderUrlString, file, "POST", options);
if (internal_isUnsuccessfulResponse(response)) {
throw new FetchError(`Saving the file in [${folderUrl}] failed: [${response.status}] [${response.statusText}].`, response);
}
const locationHeader = response.headers.get("Location");
if (locationHeader === null) {
throw new Error("Could not determine the location of the newly saved file.");
}
const fileIri = new URL(locationHeader, new URL(folderUrlString).origin).href;
const blobClone = internal_cloneResource(file);
const resourceInfo = {
internal_resourceInfo: {
isRawData: true,
sourceIri: fileIri,
contentType: getContentType(file, options.contentType),
},
};
return Object.assign(blobClone, resourceInfo);
}
/**
* ```{note} This function is still experimental and subject to change, even in a non-major release.
* ```
*
* Saves a file at a given URL, replacing any previous content.
*
* 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.
*
* If you know the [media type](https://developer.mozilla.org/en-US/docs/Glossary/MIME_type)
* of the file you are attempting to write, then you should provide this in the
* `options` parameter. For example, if you know your file is a JPEG image,
* then you should provide the media type `image/jpeg`. If you don't know, or
* don't provide a media type, a default type of `application/octet-stream` will
* be applied (which indicates that the file should be regarded as pure binary
* data).
*
* @param fileUrl The URL where the file is saved.
* @param file The file to be written.
* @param options Additional parameters for file creation (e.g. a slug, or media type).
*/
async function overwriteFile(fileUrl, file, options = defaultGetFileOptions) {
const fileUrlString = internal_toIriString(fileUrl);
const response = await writeFile(fileUrlString, file, "PUT", options);
if (internal_isUnsuccessfulResponse(response)) {
throw new FetchError(`Overwriting the file at [${fileUrlString}] failed: [${response.status}] [${response.statusText}].`, response);
}
const blobClone = internal_cloneResource(file);
const resourceInfo = internal_parseResourceInfo(response);
resourceInfo.sourceIri = fileUrlString;
resourceInfo.isRawData = true;
return Object.assign(blobClone, { internal_resourceInfo: resourceInfo });
}
function isHeadersArray(headers) {
return Array.isArray(headers);
}
/**
* The return type of this function is misleading: it should ONLY be used to check
* whether an object has a forEach method that returns <key, value> pairs.
*
* @param headers A headers object that might have a forEach
*/
function hasHeadersObjectForEach(headers) {
return typeof headers.forEach === "function";
}
/**
* @hidden
* This function feels unnecessarily complicated, but is required in order to
* have Headers according to type definitions in both Node and browser environments.
* This might require a fix upstream to be cleaned up.
*
* @param headersToFlatten A structure containing headers potentially in several formats
*/
function flattenHeaders(headersToFlatten) {
if (typeof headersToFlatten === "undefined") {
return {};
}
let flatHeaders = {};
if (isHeadersArray(headersToFlatten)) {
headersToFlatten.forEach(([key, value]) => {
flatHeaders[key] = value;
});
// Note that the following line must be a elsif, because string[][] has a forEach,
// but it returns string[] instead of <key, value>
}
else if (hasHeadersObjectForEach(headersToFlatten)) {
headersToFlatten.forEach((value, key) => {
flatHeaders[key] = value;
});
}
else {
// If the headers are already a Record<string, string>,
// they can directly be returned.
flatHeaders = headersToFlatten;
}
return flatHeaders;
}
/**
* Internal function that performs the actual write HTTP query, either POST
* or PUT depending on the use case.
*
* @param fileUrl The URL where the file is saved
* @param file The file to be written
* @param method The HTTP method
* @param options Additional parameters for file creation (e.g. a slug, or media type)
*/
async function writeFile(targetUrl, file, method, options) {
var _a, _b;
const config = Object.assign(Object.assign({}, defaultGetFileOptions), options);
const headers = flattenHeaders((_b = (_a = config.init) === null || _a === void 0 ? void 0 : _a.headers) !== null && _b !== void 0 ? _b : {});
if (containsReserved(headers)) {
throw new Error(`No reserved header (${RESERVED_HEADERS.join(", ")}) should be set in the optional RequestInit.`);
}
// If a slug is in the parameters, set the request headers accordingly
if (config.slug !== undefined) {
headers["Slug"] = config.slug;
}
headers["Content-Type"] = getContentType(file, options.contentType);
const targetUrlString = internal_toIriString(targetUrl);
return await config.fetch(targetUrlString, Object.assign(Object.assign({}, config.init), { headers,
method, body: file }));
}
function getContentType(file, contentTypeOverride) {
if (typeof contentTypeOverride === "string") {
return contentTypeOverride;
}
const fileType = typeof file === "object" &&
file !== null &&
typeof file.type === "string" &&
file.type.length > 0
? file.type
: undefined;
return fileType !== null && fileType !== void 0 ? fileType : "application/octet-stream";
}
/**
* 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.
*/
// TODO: These should be replaced by auto-generated constants,
// if we can ensure that unused constants will be excluded from bundles.
/** @hidden */
const acl = {
Authorization: "http://www.w3.org/ns/auth/acl#Authorization",
AuthenticatedAgent: "http://www.w3.org/ns/auth/acl#AuthenticatedAgent",
accessTo: "http://www.w3.org/ns/auth/acl#accessTo",
agent: "http://www.w3.org/ns/auth/acl#agent",
agentGroup: "http://www.w3.org/ns/auth/acl#agentGroup",
agentClass: "http://www.w3.org/ns/auth/acl#agentClass",
default: "http://www.w3.org/ns/auth/acl#default",
defaultForNew: "http://www.w3.org/ns/auth/acl#defaultForNew",
mode: "http://www.w3.org/ns/auth/acl#mode",
origin: "http://www.w3.org/ns/auth/acl#origin",
};
/** @hidden */
const rdf = {
type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
};
/** @hidden */
const ldp = {
BasicContainer: "http://www.w3.org/ns/ldp#BasicContainer",
Container: "http://www.w3.org/ns/ldp#Container",
Resource: "http://www.w3.org/ns/ldp#Resource",
contains: "http://www.w3.org/ns/ldp#contains",
};
/** @hidden */
const foaf = {
Agent: "http://xmlns.com/foaf/0.1/Agent",
};
/** @hidden */
const acp = {
Policy: "http://www.w3.org/ns/solid/acp#Policy",
AccessControl: "http://www.w3.org/ns/solid/acp#AccessControl",
Read: "http://www.w3.org/ns/solid/acp#Read",
Append: "http://www.w3.org/ns/solid/acp#Append",
Write: "http://www.w3.org/ns/solid/acp#Write",
Rule: "http://www.w3.org/ns/solid/acp#Rule",
accessControl: "http://www.w3.org/ns/solid/acp#accessControl",
apply: "http://www.w3.org/ns/solid/acp#apply",
applyMembers: "http://www.w3.org/ns/solid/acp#applyMembers",
allow: "http://www.w3.org/ns/solid/acp#allow",
deny: "http://www.w3.org/ns/solid/acp#deny",
allOf: "http://www.w3.org/ns/solid/acp#allOf",
anyOf: "http://www.w3.org/ns/solid/acp#anyOf",
noneOf: "http://www.w3.org/ns/solid/acp#noneOf",
access: "http://www.w3.org/ns/solid/acp#access",
accessMembers: "http://www.w3.org/ns/solid/acp#accessMembers",
agent: "http://www.w3.org/ns/solid/acp#agent",
group: "http://www.w3.org/ns/solid/acp#group",
client: "http://www.w3.org/ns/solid/acp#client",
PublicAgent: "http://www.w3.org/ns/solid/acp#PublicAgent",
AuthenticatedAgent: "http://www.w3.org/ns/solid/acp#AuthenticatedAgent",
CreatorAgent: "http://www.w3.org/ns/solid/acp#CreatorAgent",
};
/** @hidden */
const solid = {
PublicOidcClient: "http://www.w3.org/ns/solid/terms#PublicOidcClient",
};
/**
* 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.
*/
/**
* @param quads Triples that should be serialised to Turtle
* @internal Utility method for internal use; not part of the public API.
*/
async function triplesToTurtle(quads) {
const n3 = await loadN3();
const format = "text/turtle";
const writer = new n3.Writer({ format: format });
// Remove any potentially lingering references to Named Graphs in Quads;
// they'll be determined by the URL the Turtle will be sent to:
const triples = quads.map((quad) => DataFactory.quad(quad.subject, quad.predicate, quad.object, undefined));
writer.addQuads(triples);
const writePromise = new Promise((resolve, reject) => {
writer.end((error, result) => {
/* istanbul ignore if [n3.js doesn't actually pass an error nor a result, apparently: https://github.com/rdfjs/N3.js/blob/62682e48c02d8965b4d728cb5f2cbec6b5d1b1b8/src/N3Writer.js#L290] */
if (error) {
return reject(error);
}
resolve(result);
});
});
const rawTurtle = await writePromise;
return rawTurtle;
}
/**
* @param raw Turtle that should be parsed into Triples
* @internal Utility method for internal use; not part of the public API.
*/
async function turtleToTriples(raw, resourceIri) {
const format = "text/turtle";
const n3 = await loadN3();
const parser = new n3.Parser({ format: format, baseIRI: resourceIri });
const parsingPromise = new Promise((resolve, reject) => {
const parsedTriples = [];
parser.parse(raw, (error, triple, _prefixes) => {
if (error) {
return reject(error);
}
if (triple) {
parsedTriples.push(triple);
}
else {
resolve(parsedTriples);
}
});
});
return parsingPromise;
}
async function loadN3() {
// When loaded via Webpack or another bundler that looks at the `modules` field in package.json,
// N3 serves up ES modules with named exports.
// However, when it is loaded in Node, it serves up a CommonJS module, which, when imported from
// a Node ES module, is in the shape of a default export that is an object with all the named
// exports as its properties.
// This means that if we were to import the default module, our code would fail in Webpack,
// whereas if we imported the named exports, our code would fail in Node.
// As a workaround, we use a dynamic import. This way, we can use the same syntax in every
// environment, where the differences between the environments are in whether the returned object
// includes a `default` property that contains all exported functions, or whether those functions
// are available on the returned object directly. We can then respond to those different
// situations at runtime.
// Unfortunately, that does mean that tree shaking will not work until N3 also provides ES modules
// for Node, or adds a default export for Webpack. See
// https://github.com/rdfjs/N3.js/issues/196
const n3Module = await import('n3');
/* istanbul ignore if: the package provides named exports in the unit test environment */
if (typeof n3Module.default !== "undefined") {
return n3Module.default;
}
return n3Module;
}
/**
* 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.
*/
/**
* IRIs of the XML Schema data types we support
* @internal
*/
const xmlSchemaTypes = {
boolean: "http://www.w3.org/2001/XMLSchema#boolean",
dateTime: "http://www.w3.org/2001/XMLSchema#dateTime",
decimal: "http://www.w3.org/2001/XMLSchema#decimal",
integer: "http://www.w3.org/2001/XMLSchema#integer",
string: "http://www.w3.org/2001/XMLSchema#string",
langString: "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString",
};
/**
* @internal
* @param value Value to serialise.
* @returns String representation of `value`.
* @see https://www.w3.org/TR/xmlschema-2/#boolean-lexical-representation
*/
function serializeBoolean(value) {
return value ? "true" : "false";
}
/**
* @internal
* @param value Value to deserialise.
* @returns Deserialized boolean, or null if the given value is not a valid serialised boolean.
* @see https://www.w3.org/TR/xmlschema-2/#boolean-lexical-representation
*/
function deserializeBoolean(value) {
if (value === "true" || value === "1") {
return true;
}
else if (value === "false" || value === "0") {
return false;
}
else {
return null;
}
}
/**
* @internal
* @param value Value to serialise.
* @returns String representation of `value`.
* @see https://www.w3.org/TR/xmlschema-2/#dateTime-lexical-representation
*/
function serializeDatetime(value) {
// Although the XML Schema DateTime is not _exactly_ an ISO 8601 string
// (see https://www.w3.org/TR/xmlschema-2/#deviantformats),
// the deviations only affect the parsing, not the serialisation.
// Therefore, we can just use .toISOString():
return value.toISOString();
}
/**
* @internal
* @param value Value to deserialise.
* @returns Deserialized datetime, or null if the given value is not a valid serialised datetime.
* @see https://www.w3.org/TR/xmlschema-2/#dateTime-lexical-representation
*/
function deserializeDatetime(literalString) {
// DateTime in the format described at
// https://www.w3.org/TR/xmlschema-2/#dateTime-lexical-representation
// (without constraints on the value).
// -? - An optional leading `-`.
// \d{4,}- - Four or more digits followed by a `-` representing the year. Example: "3000-".
// \d\d-\d\d - Two digits representing the month and two representing the day of the month,
// separated by a `-`. Example: "11-03".
// T - The letter T, separating the date from the time.
// \d\d:\d\d:\d\d - Two digits for the hour, minute and second, respectively, separated by a `:`.
// Example: "13:37:42".
// (\.\d+)? - Optionally a `.` followed by one or more digits representing milliseconds.
// Example: ".1337".
// (Z|(\+|-)\d\d:\d\d) - The letter Z indicating UTC, or a `+` or `-` followed by two digits for
// the hour offset and two for the minute offset, separated by a `:`.
// Example: "+13:37".
const datetimeRegEx = /-?\d{4,}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|(\+|-)\d\d:\d\d)/;
if (!datetimeRegEx.test(literalString)) {
return null;
}
const [signedDateString, rest] = literalString.split("T");
// The date string can optionally be prefixed with `-`,
// in which case the year is negative:
const [yearMultiplier, dateString] = signedDateString.charAt(0) === "-"
? [-1, signedDateString.substring(1)]
: [1, signedDateString];
const [yearString, monthString, dayString] = dateString.split("-");
const utcFullYear = Number.parseInt(yearString, 10) * yearMultiplier;
const utcMonth = Number.parseInt(monthString, 10) - 1;
const utcDate = Number.parseInt(dayString, 10);
const [timeString, timezoneString] = splitTimeFromTimezone(rest);
const [hourOffset, minuteOffset] = getTimezoneOffsets(timezoneString);
const [hourString, minuteString, timeRest] = timeString.split(":");
const utcHours = Number.parseInt(hourString, 10) + hourOffset;
const utcMinutes = Number.parseInt(minuteString, 10) + minuteOffset;
const [secondString, optionalMillisecondString] = timeRest.split(".");
const utcSeconds = Number.parseInt(secondString, 10);
const utcMilliseconds = optionalMillisecondString
? Number.parseInt(optionalMillisecondString, 10)
: 0;
const date = new Date(Date.UTC(utcFullYear, utcMonth, utcDate, utcHours, utcMinutes, utcSeconds, utcMilliseconds));
// For the year, values from 0 to 99 map to the years 1900 to 1999. Since the serialisation
// always writes out the years fully, we should correct this to actually map to the years 0 to 99.
// See
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#Individual_date_and_time_component_values
if (utcFullYear >= 0 && utcFullYear < 100) {
// Note that we base it on the calculated year