UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

475 lines (443 loc) 15.2 kB
// 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. // import { v4 as uuidv4 } from "uuid"; import { isNamedNode, resolveLocalIri, internal_isValidUrl, } from "../datatypes"; import type { UrlString, Url, Thing, ThingLocal, ThingPersisted, SolidDataset, WithChangeLog, IriString, } from "../interfaces"; import { SolidClientError, hasServerResourceInfo } from "../interfaces"; import { DataFactory, subjectToRdfJsQuads } from "../rdfjs.internal"; import { getSourceUrl } from "../resource/resource"; import { internal_addAdditionsToChangeLog, internal_addDeletionsToChangeLog, internal_getReadableValue, } from "./thing.internal"; import type { BlankNodeId, LocalNodeIri } from "../rdf.internal"; import { freeze, getLocalNodeIri, getLocalNodeName, isBlankNodeId, isLocalNodeIri, } from "../rdf.internal"; import { internal_toIriString } from "../interfaces.internal"; import { getTermAll } from "./get"; /** * @hidden Scopes are not yet consistently used in Solid and hence not properly implemented in this library yet (the add*() and set*() functions do not respect it yet), so we're not exposing these to developers at this point in time. */ export interface GetThingOptions { /** * Which Named Graph to extract the Thing from. * * If not specified, the Thing will include Quads from all Named Graphs in the given * [[SolidDataset]]. * */ scope?: Url | UrlString; } export function getThing( solidDataset: SolidDataset, thingUrl: UrlString | Url, options?: GetThingOptions, ): ThingPersisted | null; export function getThing( solidDataset: SolidDataset, thingUrl: LocalNodeIri, options?: GetThingOptions, ): ThingLocal | null; export function getThing( solidDataset: SolidDataset, thingUrl: UrlString | Url | LocalNodeIri | BlankNodeId, options?: GetThingOptions, ): Thing | null; /** * Extract Quads with a given Subject from a [[SolidDataset]] into a [[Thing]]. * * @param solidDataset The [[SolidDataset]] to extract the [[Thing]] from. * @param thingUrl The identifier of the desired [[Thing]] (URL or Blank Node identifier). * @param options Not yet implemented. */ export function getThing( solidDataset: SolidDataset, thingUrl: UrlString | Url | LocalNodeIri | BlankNodeId, options: GetThingOptions = {}, ): Thing | null { if (!internal_isValidUrl(thingUrl) && !thingUrl.match(/^_:/)) { throw new ValidThingUrlExpectedError(thingUrl); } const graph = typeof options.scope !== "undefined" ? internal_toIriString(options.scope) : "default"; const thingsByIri = solidDataset.graphs[graph] ?? {}; const thingIri = internal_toIriString(thingUrl); const resolvedThingIri = isLocalNodeIri(thingIri) && hasServerResourceInfo(solidDataset) ? resolveLocalIri(getLocalNodeName(thingIri), getSourceUrl(solidDataset)) : thingIri; const thing = thingsByIri[resolvedThingIri]; if (typeof thing === "undefined") { return null; } return thing; } /** * Get all [[Thing]]s in a [[SolidDataset]]. * * @param solidDataset The [[SolidDataset]] to extract the [[Thing]]s from. * @param options Not yet implemented. */ export function getThingAll( solidDataset: SolidDataset, options: GetThingOptions & { /** * Can Things local to the current dataset, and having no IRI, be returned ? */ acceptBlankNodes?: boolean; } = { acceptBlankNodes: false }, ): Thing[] { const graph = typeof options.scope !== "undefined" ? internal_toIriString(options.scope) : "default"; const thingsByIri = solidDataset.graphs[graph] ?? {}; return Object.values(thingsByIri).filter( (thing) => !isBlankNodeId(thing.url) || options.acceptBlankNodes, ); } /** * 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. */ export function setThing<Dataset extends SolidDataset>( solidDataset: Dataset, thing: Thing, ): Dataset & WithChangeLog { const thingIri = isThingLocal(thing) && hasServerResourceInfo(solidDataset) ? resolveLocalIri(getLocalNodeName(thing.url), getSourceUrl(solidDataset)) : thing.url; const defaultGraph = solidDataset.graphs.default; const updatedDefaultGraph = freeze({ ...defaultGraph, [thingIri]: freeze({ ...thing, url: thingIri }), }); const updatedGraphs = freeze({ ...solidDataset.graphs, default: updatedDefaultGraph, }); const subjectNode = DataFactory.namedNode(thingIri); const deletedThingPredicates = solidDataset.graphs.default[thingIri]?.predicates; const deletions = typeof deletedThingPredicates !== "undefined" ? subjectToRdfJsQuads( deletedThingPredicates, subjectNode, DataFactory.defaultGraph(), ) : []; const additions = subjectToRdfJsQuads( thing.predicates, subjectNode, DataFactory.defaultGraph(), ); return internal_addAdditionsToChangeLog( internal_addDeletionsToChangeLog( freeze({ ...solidDataset, graphs: updatedGraphs, }), deletions, ), additions, ); } /** * 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. */ export function removeThing<Dataset extends SolidDataset>( solidDataset: Dataset, thing: UrlString | Url | Thing, ): Dataset & WithChangeLog { let thingIri: IriString; if (isNamedNode(thing)) { thingIri = thing.value; } else if (typeof thing === "string") { thingIri = isLocalNodeIri(thing) && hasServerResourceInfo(solidDataset) ? resolveLocalIri(getLocalNodeName(thing), getSourceUrl(solidDataset)) : thing; } else if (isThingLocal(thing)) { thingIri = thing.url; } else { thingIri = asIri(thing); } const defaultGraph = solidDataset.graphs.default; const updatedDefaultGraph = { ...defaultGraph }; delete updatedDefaultGraph[thingIri]; const updatedGraphs = freeze({ ...solidDataset.graphs, default: freeze(updatedDefaultGraph), }); const subjectNode = DataFactory.namedNode(thingIri); const deletedThingPredicates = solidDataset.graphs.default[thingIri]?.predicates; const deletions = typeof deletedThingPredicates !== "undefined" ? subjectToRdfJsQuads( deletedThingPredicates, subjectNode, DataFactory.defaultGraph(), ) : []; return internal_addDeletionsToChangeLog( freeze({ ...solidDataset, graphs: updatedGraphs, }), deletions, ); } /** Pass these options to [[createThing]] to initialise a new [[Thing]] whose URL will be determined when it is saved. */ export type CreateThingLocalOptions = { /** * The name that should be used for this [[Thing]] when constructing its URL. * * If not provided, a random one will be generated. */ name?: string; }; /** Pass these options to [[createThing]] to initialise a new [[Thing]] whose URL is already known. */ export type CreateThingPersistedOptions = { /** * The URL of the newly created [[Thing]]. */ url: UrlString; }; /** The options you pass to [[createThing]]. * - To specify the URL for the initialised Thing, pass [[CreateThingPersistedOptions]]. * - To have the URL determined during the save, pass [[CreateThingLocalOptions]]. */ export type CreateThingOptions = | CreateThingLocalOptions | CreateThingPersistedOptions; /** * Initialise a new [[Thing]] in memory with a given URL. * * @param options See [[CreateThingPersistedOptions]] for how to specify the new [[Thing]]'s URL. */ export function createThing( options: CreateThingPersistedOptions, ): ThingPersisted; /** * Initialise a new [[Thing]] in memory. * * @param options Optional parameters that affect the final URL of this [[Thing]] when saved. */ export function createThing(options?: CreateThingLocalOptions): ThingLocal; export function createThing(options?: CreateThingOptions): Thing; export function createThing(options: CreateThingOptions = {}): Thing { if (typeof (options as CreateThingPersistedOptions).url !== "undefined") { const { url } = options as CreateThingPersistedOptions; if (!internal_isValidUrl(url)) { throw new ValidThingUrlExpectedError(url); } const thing: ThingPersisted = freeze({ type: "Subject", predicates: freeze({}), url, }); return thing; } const name = (options as CreateThingLocalOptions).name ?? generateName(); const localNodeIri = getLocalNodeIri(name); const thing: ThingLocal = freeze({ type: "Subject", predicates: freeze({}), url: localNodeIri, }); return thing; } /** * @param input An value that might be a [[Thing]]. * @returns Whether `input` is a Thing. * @since 0.2.0 */ export function isThing<X>(input: X | Thing): input is Thing { return ( typeof input === "object" && input !== null && typeof (input as Thing).type === "string" && (input as Thing).type === "Subject" ); } type IsNotThingLocal<T extends Thing> = T extends ThingLocal ? never : T; /** * Get the URL to a given [[Thing]]. * * @param thing The [[Thing]] you want to obtain the URL from. * @param baseUrl If `thing` is not persisted yet, the base URL that should be used to construct this [[Thing]]'s URL. */ export function asUrl(thing: ThingLocal, baseUrl: UrlString): UrlString; export function asUrl<T extends ThingPersisted>( thing: T & IsNotThingLocal<T>, ): UrlString; export function asUrl(thing: Thing, baseUrl: UrlString): UrlString; export function asUrl(thing: Thing, baseUrl?: UrlString): UrlString { 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(getLocalNodeName(thing.url), baseUrl); } return thing.url; } /** @hidden Alias of [[asUrl]] for those who prefer IRI terminology. */ export 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 */ export function thingAsMarkdown(thing: Thing): string { let thingAsMarkdown = ""; if (isThingLocal(thing)) { thingAsMarkdown += `## Thing (no URL yet — identifier: \`#${getLocalNodeName( thing.url, )}\`)\n`; } else { thingAsMarkdown += `## Thing: ${thing.url}\n`; } const predicateIris = Object.keys(thing.predicates); if (predicateIris.length === 0) { thingAsMarkdown += "\n<empty>\n"; } else { for (const predicate of predicateIris) { thingAsMarkdown += `\nProperty: ${predicate}\n`; const values = getTermAll(thing, predicate); thingAsMarkdown += values.reduce((acc, value) => { return `${acc}- ${internal_getReadableValue(value)}\n`; }, ""); } } return thingAsMarkdown; } /** * @param thing The [[Thing]] of which a URL might or might not be known. * @return `true` if `thing` has no known URL yet. * @since 1.7.0 */ export function isThingLocal( thing: ThingPersisted | ThingLocal, ): thing is ThingLocal { return isLocalNodeIri(thing.url); } /** * This error is thrown when a function expected to receive a [[Thing]] but received something else. * @since 1.2.0 */ export class ThingExpectedError extends SolidClientError { public readonly receivedValue: unknown; constructor(receivedValue: unknown) { 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. */ export class ValidPropertyUrlExpectedError extends SolidClientError { public readonly receivedProperty: unknown; constructor(receivedValue: unknown) { 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. */ export class ValidValueUrlExpectedError extends SolidClientError { public readonly receivedValue: unknown; constructor(receivedValue: unknown) { 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. */ export class ValidThingUrlExpectedError extends SolidClientError { public readonly receivedValue: unknown; constructor(receivedValue: unknown) { 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 uuidv4(); };