From a866df62edfb8555f8a3ad9490947a06b8660a31 Mon Sep 17 00:00:00 2001 From: jaxoncreed Date: Fri, 5 Jan 2024 15:32:09 -0500 Subject: [PATCH] Added some documentation --- packages/demo-react/src/App-old.tsx | 13 + packages/demo-react/src/App.tsx | 64 ++- .../src/util/TrackingProxyContext.ts | 10 + .../src/util/createWrapperProxy.ts | 7 - .../solid-react/src/util/useForceReload.ts | 6 - packages/solid/README.md | 222 ++++++++++ packages/solid/src/ResourceStore.ts | 33 ++ packages/solid/src/SolidLdoDataset.ts | 94 +++- packages/solid/src/SolidLdoDatasetContext.ts | 20 +- packages/solid/src/createSolidLdoDataset.ts | 28 ++ packages/solid/src/methods.ts | 44 +- .../{Requester.ts => BatchedRequester.ts} | 49 ++- ...uester.ts => ContainerBatchedRequester.ts} | 29 +- ...afRequester.ts => LeafBatchedRequester.ts} | 41 +- .../requester/requests/checkRootContainer.ts | 36 ++ .../requester/requests/createDataResource.ts | 92 ++++ .../src/requester/requests/deleteResource.ts | 35 ++ .../src/requester/requests/readResource.ts | 44 ++ .../src/requester/requests/requestOptions.ts | 12 + .../requester/requests/updateDataResource.ts | 48 +++ .../src/requester/requests/uploadResource.ts | 32 ++ .../src/requester/results/RequesterResult.ts | 3 + .../results/error/AccessControlError.ts | 8 + .../requester/results/error/ErrorResult.ts | 54 +++ .../results/error/HttpErrorResult.ts | 56 +++ .../results/error/InvalidUriError.ts | 4 + .../results/error/NoncompliantPodError.ts | 9 + .../success/CheckRootContainerSuccess.ts | 7 + .../results/success/CreateSuccess.ts | 7 + .../results/success/DeleteSuccess.ts | 8 + .../requester/results/success/ReadSuccess.ts | 38 ++ .../results/success/SuccessResult.ts | 16 + .../requester/results/success/Unfetched.ts | 3 + .../results/success/UpdateSuccess.ts | 7 + .../src/requester/util/modifyQueueFuntions.ts | 10 + packages/solid/src/resource/Container.ts | 318 +++++++++++++- packages/solid/src/resource/Leaf.ts | 404 ++++++++++++++++-- packages/solid/src/resource/Resource.ts | 340 ++++++++++++++- .../resource/resourceResult/ResourceResult.ts | 6 + packages/solid/src/util/RequestBatcher.ts | 63 ++- packages/solid/src/util/guaranteeFetch.ts | 7 + packages/solid/src/util/rdfUtils.ts | 36 ++ .../solid/src/util/splitChangesByGraph.ts | 19 + packages/solid/src/util/uriTypes.ts | 26 ++ packages/solid/typedoc.json | 3 +- 45 files changed, 2296 insertions(+), 115 deletions(-) create mode 100644 packages/demo-react/src/App-old.tsx delete mode 100644 packages/solid-react/src/util/createWrapperProxy.ts delete mode 100644 packages/solid-react/src/util/useForceReload.ts rename packages/solid/src/requester/{Requester.ts => BatchedRequester.ts} (76%) rename packages/solid/src/requester/{ContainerRequester.ts => ContainerBatchedRequester.ts} (72%) rename packages/solid/src/requester/{LeafRequester.ts => LeafBatchedRequester.ts} (79%) diff --git a/packages/demo-react/src/App-old.tsx b/packages/demo-react/src/App-old.tsx new file mode 100644 index 0000000..721b93e --- /dev/null +++ b/packages/demo-react/src/App-old.tsx @@ -0,0 +1,13 @@ +import type { FunctionComponent } from "react"; +import React from "react"; +import { Router } from "./Layout"; +import { BrowserSolidLdoProvider } from "@ldo/solid-react"; + +const ProfileApp: FunctionComponent = () => { + return ( + + + + ); +}; +export default ProfileApp; diff --git a/packages/demo-react/src/App.tsx b/packages/demo-react/src/App.tsx index 721b93e..4d00635 100644 --- a/packages/demo-react/src/App.tsx +++ b/packages/demo-react/src/App.tsx @@ -1,13 +1,65 @@ import type { FunctionComponent } from "react"; -import React from "react"; -import { Router } from "./Layout"; -import { BrowserSolidLdoProvider } from "@ldo/solid-react"; +import React, { useCallback } from "react"; +import { + BrowserSolidLdoProvider, + useResource, + useSolidAuth, + useSubject, +} from "@ldo/solid-react"; +import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes"; +import { changeData, commitData } from "@ldo/solid"; -const ProfileApp: FunctionComponent = () => { +// The base component for the app +const App: FunctionComponent = () => { return ( + /* The application should be surrounded with the BrowserSolidLdoProvider + this will set up all the underlying infrastructure for the application */ - + ); }; -export default ProfileApp; + +// A component that handles login +const Login: FunctionComponent = () => { + // Get login information using the "useSolidAuth" hook + const { login, logout, session } = useSolidAuth(); + + const onLogin = useCallback(() => { + const issuer = prompt("What is your Solid IDP?"); + // Call the "login" function to initiate login + if (issuer) login(issuer); + }, []); + + // You can use session.isLoggedIn to check if the user is logged in + if (session.isLoggedIn) { + return ( +
+ {/* Get the user's webId from session.webId */} +

Logged in as {session.webId}

+ {/* Use the logout function to log out */} + + +
+ ); + } + return ; +}; + +const Profile: FunctionComponent = () => { + const { session } = useSolidAuth(); + const resource = useResource(session.webId); + const profile = useSubject(SolidProfileShapeShapeType, session.webId); + + const onNameChange = useCallback(async (e) => { + // Ensure that the + if (!profile || !resource) return; + const cProfile = changeData(profile, resource); + cProfile.name = e.target.value; + await commitData(cProfile); + }, []); + + return ; +}; + +export default App; diff --git a/packages/solid-react/src/util/TrackingProxyContext.ts b/packages/solid-react/src/util/TrackingProxyContext.ts index d354a13..de94629 100644 --- a/packages/solid-react/src/util/TrackingProxyContext.ts +++ b/packages/solid-react/src/util/TrackingProxyContext.ts @@ -9,10 +9,20 @@ import type { SubscribableDataset } from "@ldo/subscribable-dataset"; import { namedNode } from "@rdfjs/data-model"; import type { Quad } from "@rdfjs/types"; +/** + * @internal + * Options to be passed to the tracking proxy + */ export interface TrackingProxyContextOptions extends ProxyContextOptions { dataset: SubscribableDataset; } +/** + * @internal + * This proxy exists to ensure react components rerender at the right time. It + * keeps track of every key accessed in a Linked Data Object and only when the + * dataset is updated with that key does it rerender the react component. + */ export class TrackingProxyContext extends ProxyContext { private listener: () => void; private subscribableDataset: SubscribableDataset; diff --git a/packages/solid-react/src/util/createWrapperProxy.ts b/packages/solid-react/src/util/createWrapperProxy.ts deleted file mode 100644 index 244c891..0000000 --- a/packages/solid-react/src/util/createWrapperProxy.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Resource } from "@ldo/solid"; - -export function createWrapperProxy( - target: ResourceType, -): ResourceType { - return new Proxy(target, {}); -} diff --git a/packages/solid-react/src/util/useForceReload.ts b/packages/solid-react/src/util/useForceReload.ts deleted file mode 100644 index 6082c84..0000000 --- a/packages/solid-react/src/util/useForceReload.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useState, useCallback } from "react"; - -export function useForceReload() { - const [, setValue] = useState(0); - return useCallback(() => setValue((value) => value + 1), []); -} diff --git a/packages/solid/README.md b/packages/solid/README.md index 1670088..ecdeab6 100644 --- a/packages/solid/README.md +++ b/packages/solid/README.md @@ -1,6 +1,228 @@ # @ldo/solid +@ldo/solid is a client that implements the Solid specification with the use of Linked Data Objects. +## Installation + +Navigate into your project's root folder and run the following command: +``` +cd my_project/ +npx run @ldo/cli init +``` + +Now install the @ldo/solid library + +``` +npm i @ldo/solid +``` + +### Manual Installation + +If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently. + +``` +npm i @ldo/ldo @ldo/solid +``` + +## Simple Examples + +Below is a simple example of @ldo/solid. Assume that a ShapeType was previously generated and placed at `./.ldo/foafProfile.shapeTypes`. Also assume we have a shape type for social media at `./.ldo/socialMediaPost.shapeTypes` + +```typescript +import { changeData, commitData, createSolidLdoDataset } from "@ldo/solid"; +import { fetch, getDefaultSession } from "@inrupt/solid-client-authn-browser"; +import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes"; +import { SocialMediaPostShapeType } from "./.ldo/socialMediaPost.shapeTypes"; + +async function main() { + /** + * =========================================================================== + * READING DATA FROM A POD + * =========================================================================== + */ + + // Before we begin using @ldo/solid. Let's get the WebId of the current user + const webIdUri = getDefaultSession().info.webId; + if (!webIdUri) throw new Error("User is not logged in"); + + // Now let's proceed with @ldo/solid. Our first step is setting up a + // SolidLdoDataset. You can think of this dataset as a local store for all the + // information in the Solidverse. Don't forget to pass the authenticated fetch + // function to do your queries! + const solidLdoDataset = createSolidLdoDataset({ fetch }); + + // We'll start with getting a representation of our WebId's resource + const webIdResource = solidLdoDataset.getResource(webIdUri); + + // This resource is currently unfetched + console.log(webIdResource.isUnfetched()); // Logs true + + // So let's fetch it! Running the `read` command will make a request to get + // the WebId. + const readResult = await webIdResource.read(); + + // @ldo/solid will never throw an error. Instead, it will return errors. This + // design decision was made to force you to handle any errors. It may seem a + // bit annoying at first, but it will result in more resiliant code. You can + // easily follow intellisense tooltips to see what kinds of errors each action + // can throw. + if (readResult.isError) { + switch (readResult.type) { + case "serverError": + console.error("The solid server had an error:", readResult.message); + return; + case "noncompliantPodError": + console.error("The Pod responded in a way not compliant with the spec"); + return; + default: + console.error("Some other error was detected:", readResult.message); + } + } + + // When fetching a data resource, read triples will automatically be added to + // the solidLdoDataset. You can access them using Linked Data Objects. In + // the following example we're using a Profile Linked Data Object that was + // generated with the init step. + const profile = solidLdoDataset + .usingType(FoafProfileShapeType) + .fromSubject(webIdUri); + + // Now you can read "profile" like any JSON. + console.log(profile.name); + + /** + * =========================================================================== + * MODIFYING DATA + * =========================================================================== + */ + + // When we want to modify data the first step is to use the `changeData` + // function. We pass in an object that we want to change (in this case, + // "profile") as well an a list of any resources to which we want those + // changes to be applied (in this case, just the webIdResource). This gives + // us a new variable (conventionally named with a c for "changed") that we can + // write changes to. + const cProfile = changeData(profile, webIdResource); + + // We can make changes just like it's regular JSON + cProfile.name = "Captain Cool Dude"; + + // Committing data is as easy as running the "commitData" function. + const commitResult = await commitData(cProfile); + + // Remember to check for and handle errors! We'll keep it short this time. + if (commitResult.isError) throw commitResult; + + /** + * =========================================================================== + * CREATING NEW RESOURCES + * =========================================================================== + */ + + // Let's create some social media posts to be stored on the Solid Pod! + // Our first step is going to be finding where to place these posts. In the + // future, there will be advanced ways to determine the location of resources + // but for now, let's throw it in the root folder. + + // But, first, let's find out where the root folder is. We can take our WebId + // resource and call `getRootContainer`. Let's assume the root container has + // a URI "https://example.com/" + const rootContainer = await webIdResource.getRootContainer(); + if (rootContainer.isError) throw rootContainer; + + // Now, let's create a container for our posts + const createPostContainerResult = + await rootContainer.createChildIfAbsent("social-posts/"); + if (createPostContainerResult.isError) throw createPostContainerResult; + + // Most results store the affected resource in the "resource" field. This + // container has the URI "https://example.com/social-posts/" + const postContainer = createPostContainerResult.resource; + + // Now that we have our container, let's make a Post resource! This is a data + // resource, which means we can put raw Solid Data (RDF) into it. + const postResourceResult = + await postContainer.createChildAndOverwrite("post1.ttl"); + if (postResourceResult.isError) throw postResourceResult; + const postResource = postResourceResult.resource; + + // We can also create binary resources with things like images + const imageResourceResult = await postContainer.uploadChildAndOverwrite( + // name of the binary + "image1.svg", + // A blob for the binary + new Blob([``]), + // mime type of the binary + "image/svg+xml", + ); + if (imageResourceResult.isError) throw imageResourceResult; + const imageResource = imageResourceResult.resource; + + /** + * =========================================================================== + * CREATING NEW DATA + * =========================================================================== + */ + + // We create data in a similar way to the way we modify data. We can use the + // "createData" method. + const cPost = solidLdoDataset.createData( + // An LDO ShapeType saying that this is a social media psot + SocialMediaPostShapeType, + // The URI of the post (in this case we'll make it the same as the resource) + postResource.uri, + // The resource we should write it to + postResource, + ); + + // We can add new data + cPost.text = "Check out this bad svg:"; + cPost.image = { "@id": imageResource.uri }; + + // And now we commit data + const newDataResult = await commitData(cPost); + if (newDataResult.isError) throw newDataResult; + + /** + * =========================================================================== + * DELETING RESOURCES + * =========================================================================== + */ + + // Deleting resources can be done with a single method call. In this case, + // the container will be deleted along with all its contained resources + const deleteResult = await postContainer.delete(); + if (deleteResult.isError) throw deleteResult; +} +main(); +``` + +## API Details + +SolidLdoDataset + + - [createSolidLdoDataset](https://ldo.js.org/api/solid/functions/createSolidLdoDataset/) + - [SolidLdoDataset](https://ldo.js.org/api/solid/classes/SolidLdoDataset/) + +Resources (Manage batching requests) + + - [LeafUri](https://ldo.js.org/api/solid/types/LeafUri/) + - [ContainerUri](https://ldo.js.org/api/solid/types/ContainerUri/) + - [Leaf](https://ldo.js.org/api/solid/classes/Leaf/) + - [Container](https://ldo.js.org/api/solid/classes/Container/) + +Standalone Functions + + - [checkRootContainter](https://ldo.js.org/api/solid/functions/checkRootContainer/) + - [createDataResource](https://ldo.js.org/api/solid/functions/createDataResource/) + - [deleteResource](https://ldo.js.org/api/solid/functions/deleteResource/) + - [readResource](https://ldo.js.org/api/solid/functions/readResource/) + - [updateResource](https://ldo.js.org/api/solid/functions/updateResource/) + - [uploadResource](https://ldo.js.org/api/solid/functions/uploadResource/) + +Data Functions + - [changeData](https://ldo.js.org/api/solid/functions/changeData/) + - [commitData](https://ldo.js.org/api/solid/functions/commitData/) ## Sponsorship This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/). diff --git a/packages/solid/src/ResourceStore.ts b/packages/solid/src/ResourceStore.ts index b3fba0f..5ecbafe 100644 --- a/packages/solid/src/ResourceStore.ts +++ b/packages/solid/src/ResourceStore.ts @@ -4,19 +4,52 @@ import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; import type { ContainerUri, LeafUri } from "./util/uriTypes"; import { isContainerUri } from "./util/uriTypes"; +/** + * Options for getting a resource + */ export interface ResourceGetterOptions { + /** + * If autoLoad is set to true and the resource is unfetched, `read` will be called. + * + * @default false + */ autoLoad?: boolean; } +/** + * @internal + * A store of Solid resources + */ export class ResourceStore { + /** + * @internal + * + * A mapping between a resource URI and a Solid resource + */ protected resourceMap: Map; + /** + * @internal + * + * Context about the SolidLdoDataset + */ protected context: SolidLdoDatasetContext; + /** + * @param context - A SolidLdoDatasetContext of the parent SolidLdoDataset + */ constructor(context: SolidLdoDatasetContext) { this.resourceMap = new Map(); this.context = context; } + /** + * Gets a resource representation + * + * @param uri - The URI of the resource + * @param options - ResourceGetterOptions + * + * @returns The resource representation + */ get(uri: ContainerUri, options?: ResourceGetterOptions): Container; get(uri: LeafUri, options?: ResourceGetterOptions): Leaf; get(uri: string, options?: ResourceGetterOptions): Leaf | Container; diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index 61ef235..53c2e02 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -22,11 +22,43 @@ import { splitChangesByGraph } from "./util/splitChangesByGraph"; import type { ContainerUri, LeafUri } from "./util/uriTypes"; import { isContainerUri } from "./util/uriTypes"; import type { Resource } from "./resource/Resource"; -import { quad as createQuad } from "@rdfjs/data-model"; +/** + * A SolidLdoDataset has all the functionality of an LdoDataset with the added + * functionality of keeping track of fetched Solid Resources. + * + * It is recommended to use the { @link createSolidLdoDataset } to initialize + * this class + * + * @example + * ```typescript + * import { createSolidLdoDataset } from "@ldo/solid"; + * import { ProfileShapeType } from "./.ldo/profile.shapeTypes.ts" + * + * // ... + * + * const solidLdoDataset = createSolidLdoDataset(); + * + * const profileDocument = solidLdoDataset + * .getResource("https://example.com/profile"); + * await profileDocument.read(); + * + * const profile = solidLdoDataset + * .using(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * ``` + */ export class SolidLdoDataset extends LdoDataset { + /** + * @internal + */ public context: SolidLdoDatasetContext; + /** + * @param context - SolidLdoDatasetContext + * @param datasetFactory - An optional dataset factory + * @param initialDataset - A set of triples to initialize this dataset + */ constructor( context: SolidLdoDatasetContext, datasetFactory: DatasetFactory, @@ -36,6 +68,23 @@ export class SolidLdoDataset extends LdoDataset { this.context = context; } + /** + * Retireves a representation (either a LeafResource or a ContainerResource) + * of a Solid Resource at the given URI. This resource represents the + * current state of the resource: whether it is currently fetched or in the + * process of fetching as well as some information about it. + * + * @param uri - the URI of the resource + * @param options - Special options for getting the resource + * + * @returns a Leaf or Container Resource + * + * @example + * ```typescript + * const profileDocument = solidLdoDataset + * .getResource("https://example.com/profile"); + * ``` + */ getResource(uri: ContainerUri, options?: ResourceGetterOptions): Container; getResource(uri: LeafUri, options?: ResourceGetterOptions): Leaf; getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container; @@ -44,7 +93,27 @@ export class SolidLdoDataset extends LdoDataset { } /** - * commitChangesToPod + * Given dataset changes, commit all changes made to the proper place + * on Solid Pods. + * + * @param changes - A set of changes that should be applied to Solid Pods + * + * @returns an AggregateSuccess if successful and an AggregateError if not + * + * @example + * ```typescript + * const result = await solidLdoDataset.commitChangesToPod({ + * added: createDataset([ + * quad(namedNode("a"), namedNode("b"), namedNode("d")); + * ]), + * removed: createDataset([ + * quad(namedNode("a"), namedNode("b"), namedNode("c")); + * ]) + * }); + * if (result.isError()) { + * // handle error + * } + * ``` */ async commitChangesToPod( changes: DatasetChanges, @@ -98,21 +167,6 @@ export class SolidLdoDataset extends LdoDataset { const errors = results.filter((result) => result[2].isError); if (errors.length > 0) { - // // Rollback errors - // errors.forEach((error) => { - // // Add the graph back to the quads - // const added = error[1].added?.map((quad) => - // createQuad(quad.subject, quad.predicate, quad.object, error[0]), - // ); - // const removed = error[1].removed?.map((quad) => - // createQuad(quad.subject, quad.predicate, quad.object, error[0]), - // ); - // this.bulk({ - // added: removed, - // removed: added, - // }); - // }); - return new AggregateError( errors.map( (result) => result[2] as UpdateResultError | InvalidUriError, @@ -137,9 +191,9 @@ export class SolidLdoDataset extends LdoDataset { * .usingType(shapeType) * .write(...resources.map((r) => r.uri)) * .fromSubject(subject); - * @param shapeType The shapetype to represent the data - * @param subject A subject URI - * @param resources The resources changes to should written to + * @param shapeType - The shapetype to represent the data + * @param subject - A subject URI + * @param resources - The resources changes to should written to */ createData( shapeType: ShapeType, diff --git a/packages/solid/src/SolidLdoDatasetContext.ts b/packages/solid/src/SolidLdoDatasetContext.ts index 2a31044..c78b50b 100644 --- a/packages/solid/src/SolidLdoDatasetContext.ts +++ b/packages/solid/src/SolidLdoDatasetContext.ts @@ -1,16 +1,20 @@ -// import type TypedEmitter from "typed-emitter"; import type { ResourceStore } from "./ResourceStore"; import type { SolidLdoDataset } from "./SolidLdoDataset"; -// import type { DocumentError } from "./document/errors/DocumentError"; - -// export type OnDocumentErrorCallback = (error: DocumentError) => void; - -// export type DocumentEventEmitter = TypedEmitter<{ -// documentError: OnDocumentErrorCallback; -// }>; +/** + * Context to be shared between aspects of a SolidLdoDataset + */ export interface SolidLdoDatasetContext { + /** + * A pointer to the parent SolidLdoDataset + */ solidLdoDataset: SolidLdoDataset; + /** + * The resource store of the SolidLdoDataset + */ resourceStore: ResourceStore; + /** + * Http fetch function + */ fetch: typeof fetch; } diff --git a/packages/solid/src/createSolidLdoDataset.ts b/packages/solid/src/createSolidLdoDataset.ts index f9df1c7..3a58a84 100644 --- a/packages/solid/src/createSolidLdoDataset.ts +++ b/packages/solid/src/createSolidLdoDataset.ts @@ -6,12 +6,40 @@ import { createDataset, createDatasetFactory } from "@ldo/dataset"; import { ResourceStore } from "./ResourceStore"; import { guaranteeFetch } from "./util/guaranteeFetch"; +/** + * Options for createSolidDataset + */ export interface CreateSolidLdoDatasetOptions { + /** + * A fetch function. Most often, this is the fetch function from @inrupt/solid-clieht-authn-js + */ fetch?: typeof fetch; + /** + * An initial dataset + * @default A blank dataset + */ dataset?: Dataset; + /** + * An RDFJS DatasetFactory + * @default An extended RDFJS DatasetFactory + */ datasetFactory?: DatasetFactory; } +/** + * Creates a SolidLdoDataset + * + * @param options - CreateSolidLdoDatasetOptions + * @returns A SolidLdoDataset + * + * @example + * ```typescript + * import { createSolidLdoDataset } from "@ldo/solid"; + * import { fetch } from "@inrupt/solid-client-authn-browswer"; + * + * const solidLdoDataset = createSolidLdoDataset({ fetch }); + * ``` + */ export function createSolidLdoDataset( options?: CreateSolidLdoDatasetOptions, ): SolidLdoDataset { diff --git a/packages/solid/src/methods.ts b/packages/solid/src/methods.ts index 8c94b64..5c4333d 100644 --- a/packages/solid/src/methods.ts +++ b/packages/solid/src/methods.ts @@ -13,9 +13,29 @@ import { _proxyContext, getProxyFromObject } from "@ldo/jsonld-dataset-proxy"; import type { SubscribableDataset } from "@ldo/subscribable-dataset"; /** - * Begins tracking changes to eventually commit - * @param input A linked data object to track changes on - * @param resources + * Begins tracking changes to eventually commit. + * + * @param input - A linked data object to track changes on + * @param resource - A resource that all additions will eventually be committed to + * @param additionalResources - Any additional resources that changes will eventually be committed to + * + * @returns A transactable Linked Data Object + * + * @example + * ```typescript + * import { changeData } from "@ldo/solid"; + * + * // ... + * + * const profile = solidLdoDataset + * .using(ProfileShapeType) + * .fromSubject("https://example.com/proifle#me"); + * const resource = solidLdoDataset.getResource("https://example.com/profile"); + * + * const cProfile = changeData(profile, resource); + * cProfile.name = "My New Name"; + * await commitData(cProfile); + * ``` */ export function changeData( input: Type, @@ -36,6 +56,24 @@ export function changeData( /** * Commits the transaction to the global dataset, syncing all subscribing * components and Solid Pods + * + * @param input - A transactable linked data object + * + * @example + * ```typescript + * import { changeData } from "@ldo/solid"; + * + * // ... + * + * const profile = solidLdoDataset + * .using(ProfileShapeType) + * .fromSubject("https://example.com/proifle#me"); + * const resource = solidLdoDataset.getResource("https://example.com/profile"); + * + * const cProfile = changeData(profile, resource); + * cProfile.name = "My New Name"; + * await commitData(cProfile); + * ``` */ export function commitData( input: LdoBase, diff --git a/packages/solid/src/requester/Requester.ts b/packages/solid/src/requester/BatchedRequester.ts similarity index 76% rename from packages/solid/src/requester/Requester.ts rename to packages/solid/src/requester/BatchedRequester.ts index 3d9229a..7c0b5a8 100644 --- a/packages/solid/src/requester/Requester.ts +++ b/packages/solid/src/requester/BatchedRequester.ts @@ -20,32 +20,72 @@ const READ_KEY = "read"; const CREATE_KEY = "createDataResource"; const DELETE_KEY = "delete"; -export abstract class Requester { +/** + * @internal + * + * A singleton for handling batched requests + */ +export abstract class BatchedRequester { + /** + * @internal + * A request batcher to maintain state for ongoing requests + */ protected readonly requestBatcher = new RequestBatcher(); - // All intance variables + /** + * The uri of the resource + */ abstract readonly uri: string; + + /** + * @internal + * SolidLdoDatasetContext for the parent SolidLdoDataset + */ protected context: SolidLdoDatasetContext; + /** + * @param context - SolidLdoDatasetContext for the parent SolidLdoDataset + */ constructor(context: SolidLdoDatasetContext) { this.context = context; } + /** + * Checks if the resource is currently making any request + * @returns true if the resource is making any requests + */ isLoading(): boolean { return this.requestBatcher.isLoading(ANY_KEY); } + + /** + * Checks if the resource is currently executing a create request + * @returns true if the resource is currently executing a create request + */ isCreating(): boolean { return this.requestBatcher.isLoading(CREATE_KEY); } + + /** + * Checks if the resource is currently executing a read request + * @returns true if the resource is currently executing a read request + */ isReading(): boolean { return this.requestBatcher.isLoading(READ_KEY); } + + /** + * Checks if the resource is currently executing a delete request + * @returns true if the resource is currently executing a delete request + */ isDeletinng(): boolean { return this.requestBatcher.isLoading(DELETE_KEY); } /** * Read this resource. + * @returns A ReadLeafResult or a ReadContainerResult depending on the uri of + * this resource */ async read(): Promise { const transaction = this.context.solidLdoDataset.startTransaction(); @@ -65,6 +105,7 @@ export abstract class Requester { /** * Delete this resource + * @returns A DeleteResult */ async delete(): Promise { const transaction = this.context.solidLdoDataset.startTransaction(); @@ -84,8 +125,10 @@ export abstract class Requester { /** * Creates a Resource - * @param overwrite: If true, this will orverwrite the resource if it already + * @param overwrite - If true, this will orverwrite the resource if it already * exists + * @returns A ContainerCreateAndOverwriteResult or a + * LeafCreateAndOverwriteResult depending on this resource's URI */ createDataResource( overwrite: true, diff --git a/packages/solid/src/requester/ContainerRequester.ts b/packages/solid/src/requester/ContainerBatchedRequester.ts similarity index 72% rename from packages/solid/src/requester/ContainerRequester.ts rename to packages/solid/src/requester/ContainerBatchedRequester.ts index 8adc6a7..42d744b 100644 --- a/packages/solid/src/requester/ContainerRequester.ts +++ b/packages/solid/src/requester/ContainerBatchedRequester.ts @@ -1,6 +1,6 @@ import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import type { ContainerUri } from "../util/uriTypes"; -import { Requester } from "./Requester"; +import { BatchedRequester } from "./BatchedRequester"; import type { CheckRootResult } from "./requests/checkRootContainer"; import { checkRootContainer } from "./requests/checkRootContainer"; import type { @@ -12,18 +12,39 @@ import { modifyQueueByMergingEventsWithTheSameKeys } from "./util/modifyQueueFun export const IS_ROOT_CONTAINER_KEY = "isRootContainer"; -export class ContainerRequester extends Requester { +/** + * @internal + * + * A singleton to handle batched requests for containers + */ +export class ContainerBatchedRequester extends BatchedRequester { + /** + * The URI of the container + */ readonly uri: ContainerUri; + /** + * @param uri - The URI of the container + * @param context - SolidLdoDatasetContext of the parent dataset + */ constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { super(context); this.uri = uri; } + /** + * Reads the container + * @returns A ReadContainerResult + */ read(): Promise { return super.read() as Promise; } + /** + * Creates the container + * @param overwrite - If true, this will orverwrite the resource if it already + * exists + */ createDataResource( overwrite: true, ): Promise; @@ -41,6 +62,10 @@ export class ContainerRequester extends Requester { >; } + /** + * Checks to see if this container is a root container + * @returns A CheckRootResult + */ async isRootContainer(): Promise { return this.requestBatcher.queueProcess({ name: IS_ROOT_CONTAINER_KEY, diff --git a/packages/solid/src/requester/LeafRequester.ts b/packages/solid/src/requester/LeafBatchedRequester.ts similarity index 79% rename from packages/solid/src/requester/LeafRequester.ts rename to packages/solid/src/requester/LeafBatchedRequester.ts index 284ee23..c40b63a 100644 --- a/packages/solid/src/requester/LeafRequester.ts +++ b/packages/solid/src/requester/LeafBatchedRequester.ts @@ -3,7 +3,7 @@ import { mergeDatasetChanges } from "@ldo/subscribable-dataset"; import type { Quad } from "@rdfjs/types"; import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import type { LeafUri } from "../util/uriTypes"; -import { Requester } from "./Requester"; +import { BatchedRequester } from "./BatchedRequester"; import type { LeafCreateAndOverwriteResult, LeafCreateIfAbsentResult, @@ -16,26 +16,55 @@ import { uploadResource } from "./requests/uploadResource"; export const UPDATE_KEY = "update"; export const UPLOAD_KEY = "upload"; -export class LeafRequester extends Requester { +/** + * @internal + * + * A singleton to handle batched requests for leafs + */ +export class LeafBatchedRequester extends BatchedRequester { + /** + * The URI of the leaf + */ readonly uri: LeafUri; + /** + * @param uri - the URI of the leaf + * @param context - SolidLdoDatasetContext of the parent dataset + */ constructor(uri: LeafUri, context: SolidLdoDatasetContext) { super(context); this.uri = uri; } + /** + * Checks if the resource is currently executing an update request + * @returns true if the resource is currently executing an update request + */ isUpdating(): boolean { return this.requestBatcher.isLoading(UPDATE_KEY); } + /** + * Checks if the resource is currently executing an upload request + * @returns true if the resource is currently executing an upload request + */ isUploading(): boolean { return this.requestBatcher.isLoading(UPLOAD_KEY); } + /** + * Reads the leaf + * @returns A ReadLeafResult + */ async read(): Promise { return super.read() as Promise; } + /** + * Creates the leaf as a data resource + * @param overwrite - If true, this will orverwrite the resource if it already + * exists + */ createDataResource(overwrite: true): Promise; createDataResource(overwrite?: false): Promise; createDataResource( @@ -51,7 +80,7 @@ export class LeafRequester extends Requester { /** * Update the data on this resource - * @param changes + * @param changes - DatasetChanges that should be applied to the Pod */ async updateDataResource( changes: DatasetChanges, @@ -78,9 +107,9 @@ export class LeafRequester extends Requester { } /** - * Upload a binary - * @param blob - * @param mimeType + * Upload a binary at this resource's URI + * @param blob - A binary blob + * @param mimeType - the mime type of the blob * @param overwrite: If true, will overwrite an existing file */ upload( diff --git a/packages/solid/src/requester/requests/checkRootContainer.ts b/packages/solid/src/requester/requests/checkRootContainer.ts index 8900f05..18f4988 100644 --- a/packages/solid/src/requester/requests/checkRootContainer.ts +++ b/packages/solid/src/requester/requests/checkRootContainer.ts @@ -11,13 +11,29 @@ import { UnexpectedResourceError } from "../results/error/ErrorResult"; import { guaranteeFetch } from "../../util/guaranteeFetch"; import type { ContainerUri } from "../../util/uriTypes"; +/** + * checkRootContainer result + */ export type CheckRootResult = CheckRootContainerSuccess | CheckRootResultError; + +/** + * All possible errors checkRootResult can return + */ export type CheckRootResultError = | HttpErrorResultType | NoncompliantPodError | UnexpectedHttpError | UnexpectedResourceError; +/** + * @internal + * Checks provided headers to see if a given URI is a root container as defined + * in the [solid specification section 4.1](https://solidproject.org/TR/protocol#storage-resource) + * + * @param uri - the URI of the container resource + * @param headers - headers returned when making a GET request to the resource + * @returns CheckRootContainerSuccess if there is not error + */ export function checkHeadersForRootContainer( uri: ContainerUri, headers: Headers, @@ -39,6 +55,26 @@ export function checkHeadersForRootContainer( }; } +/** + * Performs a request to the Pod to check if the given URI is a root container + * as defined in the [solid specification section 4.1](https://solidproject.org/TR/protocol#storage-resource) + * + * @param uri - the URI of the container resource + * @param options - options variable to pass a fetch function + * @returns CheckResourceSuccess if there is no error + * + * @example + * ```typescript + * import { checkRootContainer } from "@ldo/solid"; + * import { fetch } from "@inrupt/solid-client-authn-browser"; + * + * const result = await checkRootContainer("https://example.com/", { fetch }); + * if (!result.isError) { + * // true if the container is a root container + * console.log(result.isRootContainer); + * } + * ``` + */ export async function checkRootContainer( uri: ContainerUri, options?: BasicRequestOptions, diff --git a/packages/solid/src/requester/requests/createDataResource.ts b/packages/solid/src/requester/requests/createDataResource.ts index b75386f..4c5bd5c 100644 --- a/packages/solid/src/requester/requests/createDataResource.ts +++ b/packages/solid/src/requester/requests/createDataResource.ts @@ -21,25 +21,117 @@ import type { import { readResource } from "./readResource"; import type { DatasetRequestOptions } from "./requestOptions"; +/** + * All possible return values when creating and overwriting a container + */ export type ContainerCreateAndOverwriteResult = | CreateSuccess | CreateAndOverwriteResultErrors; + +/** + * All possible return values when creating and overwriting a leaf + */ export type LeafCreateAndOverwriteResult = | CreateSuccess | CreateAndOverwriteResultErrors; + +/** + * All possible return values when creating a container if absent + */ export type ContainerCreateIfAbsentResult = | CreateSuccess | Exclude | CreateIfAbsentResultErrors; + +/** + * All possible return values when creating a leaf if absent + */ export type LeafCreateIfAbsentResult = | CreateSuccess | Exclude | CreateIfAbsentResultErrors; +/** + * All possible errors returned by creating and overwriting a resource + */ export type CreateAndOverwriteResultErrors = DeleteResultError | CreateErrors; + +/** + * All possible errors returned by creating a resource if absent + */ export type CreateIfAbsentResultErrors = ReadResultError | CreateErrors; + +/** + * All possible errors returned by creating a resource + */ export type CreateErrors = HttpErrorResultType | UnexpectedResourceError; +/** + * Creates a data resource (RDF resource) at the provided URI. This resource + * could also be a container. + * + * @param uri - The URI of the resource + * @param overwrite - If true, the request will overwrite any previous resource + * at this URI. + * @param options - Options to provide a fetch function and a local dataset to + * update. + * @returns One of many create results depending on the input + * + * @example + * `createDataResource` can be used to create containers. + * + * ```typescript + * import { createDataResource } from "@ldo/solid"; + * import { fetch } from "@inrupt/solid-client-autn-js"; + * + * const result = await createDataResource( + * "https://example.com/container/", + * true, + * { fetch }, + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + * + * @example + * `createDataResource` can also create a blank data resource at the provided + * URI. + * + * ```typescript + * import { createDataResource } from "@ldo/solid"; + * import { fetch } from "@inrupt/solid-client-autn-js"; + * + * const result = await createDataResource( + * "https://example.com/container/someResource.ttl", + * true, + * { fetch }, + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + * + * @example + * Any local RDFJS dataset passed to the `options` field will be updated with + * any new RDF data from the create process. + * + * ```typescript + * import { createDataResource } from "@ldo/solid"; + * import { createDataset } from "@ldo/dataset" + * import { fetch } from "@inrupt/solid-client-autn-js"; + * + * const localDataset = createDataset(); + * const result = await createDataResource( + * "https://example.com/container/someResource.ttl", + * true, + * { fetch, dataset: localDataset }, + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ export function createDataResource( uri: ContainerUri, overwrite: true, diff --git a/packages/solid/src/requester/requests/deleteResource.ts b/packages/solid/src/requester/requests/deleteResource.ts index 0479d05..d3de47e 100644 --- a/packages/solid/src/requester/requests/deleteResource.ts +++ b/packages/solid/src/requester/requests/deleteResource.ts @@ -8,9 +8,44 @@ import { HttpErrorResult } from "../results/error/HttpErrorResult"; import type { DeleteSuccess } from "../results/success/DeleteSuccess"; import type { DatasetRequestOptions } from "./requestOptions"; +/** + * All possible return values for deleteResource + */ export type DeleteResult = DeleteSuccess | DeleteResultError; + +/** + * All possible errors that can be returned by deleteResource + */ export type DeleteResultError = HttpErrorResultType | UnexpectedResourceError; +/** + * Deletes a resource on a Pod at a given URL. + * + * @param uri - The URI for the resource that should be deleted + * @param options - Options to provide a fetch function and a local dataset to + * update. + * @returns a DeleteResult + * + * @example + * `deleteResource` will send a request to a Solid Pod using the provided fetch + * function. A local dataset can also be provided. It will be updated with any + * new information from the delete. + * + * ```typescript + * import { deleteResource } from "@ldo/solid"; + * import { createDataset } from "@ldo/dataset" + * import { fetch } from "@inrupt/solid-client-autn-js"; + * + * const localDataset = createDataset(); + * const result = await deleteResource( + * "https://example.com/container/someResource.ttl", + * { fetch, dataset: localDataset }, + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ export async function deleteResource( uri: string, options?: DatasetRequestOptions, diff --git a/packages/solid/src/requester/requests/readResource.ts b/packages/solid/src/requester/requests/readResource.ts index 1643ff3..fbfb4db 100644 --- a/packages/solid/src/requester/requests/readResource.ts +++ b/packages/solid/src/requester/requests/readResource.ts @@ -21,21 +21,65 @@ import { guaranteeFetch } from "../../util/guaranteeFetch"; import { UnexpectedResourceError } from "../results/error/ErrorResult"; import { checkHeadersForRootContainer } from "./checkRootContainer"; +/** + * All possible return values for reading a leaf + */ export type ReadLeafResult = | BinaryReadSuccess | DataReadSuccess | AbsentReadSuccess | ReadResultError; + +/** + * All possible return values for reading a container + */ export type ReadContainerResult = | ContainerReadSuccess | AbsentReadSuccess | ReadResultError; + +/** + * All possible errors the readResource function can return + */ export type ReadResultError = | HttpErrorResultType | NoncompliantPodError | UnexpectedHttpError | UnexpectedResourceError; +/** + * Reads resource at a provided URI and returns the result + * + * @param uri - The URI of the resource + * @param options - Options to provide a fetch function and a local dataset to + * update. + * @returns ReadResult + * + * @example + * ```typescript + * import { deleteResource } from "@ldo/solid"; + * import { createDataset } from "@ldo/dataset" + * import { fetch } from "@inrupt/solid-client-autn-js"; + * + * const dataset = createDataset(); + * const result = await readResource( + * "https://example.com/container/someResource.ttl", + * { fetch, dataset }, + * ); + * if (!result.isError) { + * if (result.type === "absentReadSuccess") { + * // There was no problem reading the resource, but it doesn't exist + * } else if (result.type === "dataReadSuccess") { + * // The resource was read and it is an RDF resource. The dataset provided + * // dataset will also be loaded with the data from the resource + * } else if (result.type === "binaryReadSuccess") { + * // The resource is a binary + * console.log(result.blob); + * console.log(result.mimeType); + * } + * } + * ``` + */ export async function readResource( uri: LeafUri, options?: DatasetRequestOptions, diff --git a/packages/solid/src/requester/requests/requestOptions.ts b/packages/solid/src/requester/requests/requestOptions.ts index 830e04d..3232911 100644 --- a/packages/solid/src/requester/requests/requestOptions.ts +++ b/packages/solid/src/requester/requests/requestOptions.ts @@ -1,10 +1,22 @@ import type { BulkEditableDataset } from "@ldo/subscribable-dataset"; import type { Quad } from "@rdfjs/types"; +/** + * Request Options to be passed to request functions + */ export interface BasicRequestOptions { + /** + * A fetch function usually imported from @inrupt/solid-client-authn-js + */ fetch?: typeof fetch; } +/** + * Request options with a dataset component + */ export interface DatasetRequestOptions extends BasicRequestOptions { + /** + * A dataset to be modified with any new information obtained from a request + */ dataset?: BulkEditableDataset; } diff --git a/packages/solid/src/requester/requests/updateDataResource.ts b/packages/solid/src/requester/requests/updateDataResource.ts index a053fde..9b0237f 100644 --- a/packages/solid/src/requester/requests/updateDataResource.ts +++ b/packages/solid/src/requester/requests/updateDataResource.ts @@ -9,9 +9,57 @@ import { HttpErrorResult } from "../results/error/HttpErrorResult"; import type { UpdateSuccess } from "../results/success/UpdateSuccess"; import type { DatasetRequestOptions } from "./requestOptions"; +/** + * All return values for updateDataResource + */ export type UpdateResult = UpdateSuccess | UpdateResultError; + +/** + * All errors updateDataResource can return + */ export type UpdateResultError = HttpErrorResultType | UnexpectedResourceError; +/** + * Updates a specific data resource with the provided dataset changes + * + * @param uri - the URI of the data resource + * @param datasetChanges - A set of triples added and removed from this dataset + * @param options - Options to provide a fetch function and a local dataset to + * update. + * @returns An UpdateResult + * + * @example + * ```typescript + * import { + * updateDataResource, + * transactionChanges, + * changeData, + * createSolidLdoDataset, + * } from "@ldo/solid"; + * import { fetch } from "@inrupt/solid-client-authn-browser"; + * + * // Initialize an LDO dataset + * const solidLdoDataset = createSolidLdoDataset(); + * // Get a Linked Data Object + * const profile = solidLdoDataset + * .usingType(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * // Create a transaction to change data + * const cProfile = changeData( + * profile, + * solidLdoDataset.getResource("https://example.com/profile"), + * ); + * cProfile.name = "John Doe"; + * // Get data in "DatasetChanges" form + * const datasetChanges = transactionChanges(someLinkedDataObject); + * // Use "updateDataResource" to apply the changes + * const result = await updateDataResource( + * "https://example.com/profile", + * datasetChanges, + * { fetch, dataset: solidLdoDataset }, + * ); + * ``` + */ export async function updateDataResource( uri: LeafUri, datasetChanges: DatasetChanges, diff --git a/packages/solid/src/requester/requests/uploadResource.ts b/packages/solid/src/requester/requests/uploadResource.ts index 742ba7b..fcce355 100644 --- a/packages/solid/src/requester/requests/uploadResource.ts +++ b/packages/solid/src/requester/requests/uploadResource.ts @@ -15,6 +15,38 @@ import { deleteResource } from "./deleteResource"; import { readResource } from "./readResource"; import type { DatasetRequestOptions } from "./requestOptions"; +/** + * Uploads a binary resource at the provided URI + * + * @param uri - The URI of the resource + * @param overwrite - If true, the request will overwrite any previous resource + * at this URI. + * @param options - Options to provide a fetch function and a local dataset to + * update. + * @returns One of many create results depending on the input + * + * @example + * Any local RDFJS dataset passed to the `options` field will be updated with + * any new RDF data from the create process. + * + * ```typescript + * import { createDataResource } from "@ldo/solid"; + * import { createDataset } from "@ldo/dataset" + * import { fetch } from "@inrupt/solid-client-autn-js"; + * + * const localDataset = createDataset(); + * const result = await uploadResource( + * "https://example.com/container/someResource.txt", + * new Blob("some text."), + * "text/txt", + * true, + * { fetch, dataset: localDataset }, + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ export function uploadResource( uri: LeafUri, blob: Blob, diff --git a/packages/solid/src/requester/results/RequesterResult.ts b/packages/solid/src/requester/results/RequesterResult.ts index 978ad6d..2c51cfb 100644 --- a/packages/solid/src/requester/results/RequesterResult.ts +++ b/packages/solid/src/requester/results/RequesterResult.ts @@ -1,3 +1,6 @@ +/** + * A type returned by all request functions + */ export interface RequesterResult { type: string; isError: boolean; diff --git a/packages/solid/src/requester/results/error/AccessControlError.ts b/packages/solid/src/requester/results/error/AccessControlError.ts index ba08ccf..a4c5a18 100644 --- a/packages/solid/src/requester/results/error/AccessControlError.ts +++ b/packages/solid/src/requester/results/error/AccessControlError.ts @@ -1,9 +1,17 @@ /* istanbul ignore file */ import { ResourceError } from "./ErrorResult"; +/** + * An error: Could not fetch access rules + */ export class AccessRuleFetchError extends ResourceError { readonly type = "accessRuleFetchError" as const; + /** + * @param uri - The uri of the resource for which access rules couldn't be + * fetched + * @param message - A custom message for the error + */ constructor(uri: string, message?: string) { super(uri, message || `${uri} had trouble fetching access rules.`); } diff --git a/packages/solid/src/requester/results/error/ErrorResult.ts b/packages/solid/src/requester/results/error/ErrorResult.ts index 2efaa56..84a29ad 100644 --- a/packages/solid/src/requester/results/error/ErrorResult.ts +++ b/packages/solid/src/requester/results/error/ErrorResult.ts @@ -1,27 +1,61 @@ import type { RequesterResult } from "../RequesterResult"; +/** + * A result indicating that the request failed in some kind of way + */ export abstract class ErrorResult extends Error implements RequesterResult { + /** + * Indicates the specific type of error + */ abstract type: string; + + /** + * Always true + */ readonly isError = true as const; + /** + * @param message - a custom message for the error + */ constructor(message?: string) { super(message || "An unkown error was encountered."); } } +/** + * An error for a specific resource + */ export abstract class ResourceError extends ErrorResult { + /** + * The URI of the resource + */ readonly uri: string; + /** + * @param uri - The URI of the resource + * @param message - A custom message for the error + */ constructor(uri: string, message?: string) { super(message || `An unkown error for ${uri}`); this.uri = uri; } } +/** + * An error that aggregates many errors + */ export class AggregateError extends ErrorResult { readonly type = "aggregateError" as const; + + /** + * A list of all errors returned + */ readonly errors: ErrorType[]; + /** + * @param errors - List of all errors returned + * @param message - A custom message for the error + */ constructor( errors: (ErrorType | AggregateError)[], message?: string, @@ -47,15 +81,35 @@ export class AggregateError extends ErrorResult { } } +/** + * Represents some error that isn't handled under other errors. This is usually + * returned when something threw an error that LDO did not expect. + */ export class UnexpectedResourceError extends ResourceError { readonly type = "unexpectedResourceError" as const; + + /** + * The error that was thrown + */ error: Error; + /** + * @param uri - URI of the resource + * @param error - The error that was thrown + */ constructor(uri: string, error: Error) { super(uri, error.message); this.error = error; } + /** + * @internal + * + * Creates an UnexpectedResourceError from a thrown error + * @param uri - The URI of the resource + * @param err - The thrown error + * @returns an UnexpectedResourceError + */ static fromThrown(uri: string, err: unknown) { if (err instanceof Error) { return new UnexpectedResourceError(uri, err); diff --git a/packages/solid/src/requester/results/error/HttpErrorResult.ts b/packages/solid/src/requester/results/error/HttpErrorResult.ts index 94653c5..283bfe9 100644 --- a/packages/solid/src/requester/results/error/HttpErrorResult.ts +++ b/packages/solid/src/requester/results/error/HttpErrorResult.ts @@ -1,15 +1,37 @@ import { ResourceError } from "./ErrorResult"; +/** + * A set of standard errors that can be returned as a result of an HTTP request + */ export type HttpErrorResultType = | ServerHttpError | UnexpectedHttpError | UnauthenticatedHttpError; +/** + * An error caused by an HTTP request + */ export abstract class HttpErrorResult extends ResourceError { + /** + * The status of the HTTP request + */ public readonly status: number; + + /** + * Headers returned by the HTTP request + */ public readonly headers: Headers; + + /** + * Response returned by the HTTP request + */ public readonly response: Response; + /** + * @param uri - URI of the resource + * @param response - The response returned by the HTTP requests + * @param message - A custom message for the error + */ constructor(uri: string, response: Response, message?: string) { super( uri, @@ -21,6 +43,11 @@ export abstract class HttpErrorResult extends ResourceError { this.response = response; } + /** + * Checks to see if a given response does not constitute an HTTP Error + * @param response - The response of the request + * @returns true if the response does not constitute an HTTP Error + */ static isnt(response: Response) { return ( !(response.status >= 200 && response.status < 300) && @@ -29,6 +56,13 @@ export abstract class HttpErrorResult extends ResourceError { ); } + /** + * Checks a given response to see if it is a ServerHttpError, an + * UnauthenticatedHttpError or a some unexpected error. + * @param uri - The uri of the request + * @param response - The response of the request + * @returns An error if the response calls for it. Undefined if not. + */ static checkResponse(uri: string, response: Response) { if (ServerHttpError.is(response)) { return new ServerHttpError(uri, response); @@ -43,21 +77,43 @@ export abstract class HttpErrorResult extends ResourceError { } } +/** + * An unexpected error as a result of an HTTP request. This is usually returned + * when the HTTP request returns a status code LDO does not recognize. + */ export class UnexpectedHttpError extends HttpErrorResult { readonly type = "unexpectedHttpError" as const; } +/** + * An UnauthenticatedHttpError triggers when a Solid server returns a 401 status + * indicating that the request is not authenticated. + */ export class UnauthenticatedHttpError extends HttpErrorResult { readonly type = "unauthenticatedError" as const; + /** + * Indicates if a specific response constitutes an UnauthenticatedHttpError + * @param response - The request response + * @returns true if this response constitutes an UnauthenticatedHttpError + */ static is(response: Response) { return response.status === 401; } } +/** + * A ServerHttpError triggers when a Solid server returns a 5XX status, + * indicating that an error happened on the server. + */ export class ServerHttpError extends HttpErrorResult { readonly type = "serverError" as const; + /** + * Indicates if a specific response constitutes a ServerHttpError + * @param response - The request response + * @returns true if this response constitutes a ServerHttpError + */ static is(response: Response) { return response.status >= 500 && response.status < 600; } diff --git a/packages/solid/src/requester/results/error/InvalidUriError.ts b/packages/solid/src/requester/results/error/InvalidUriError.ts index 9d4ae6d..e1c3201 100644 --- a/packages/solid/src/requester/results/error/InvalidUriError.ts +++ b/packages/solid/src/requester/results/error/InvalidUriError.ts @@ -1,5 +1,9 @@ import { ResourceError } from "./ErrorResult"; +/** + * An InvalidUriError is returned when a URI was provided that is not a valid + * URI. + */ export class InvalidUriError extends ResourceError { readonly type = "invalidUriError" as const; diff --git a/packages/solid/src/requester/results/error/NoncompliantPodError.ts b/packages/solid/src/requester/results/error/NoncompliantPodError.ts index df87f5b..b981414 100644 --- a/packages/solid/src/requester/results/error/NoncompliantPodError.ts +++ b/packages/solid/src/requester/results/error/NoncompliantPodError.ts @@ -1,7 +1,16 @@ import { ResourceError } from "./ErrorResult"; +/** + * A NoncompliantPodError is returned when the server responded in a way that is + * not compliant with the Solid specification. + */ export class NoncompliantPodError extends ResourceError { readonly type = "noncompliantPodError" as const; + + /** + * @param uri - the URI of the requested resource + * @param message - a custom message for the error + */ constructor(uri: string, message?: string) { super( uri, diff --git a/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts b/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts index 5f75605..e2e12cb 100644 --- a/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts +++ b/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts @@ -1,6 +1,13 @@ import type { ResourceSuccess } from "./SuccessResult"; +/** + * Indicates that the request to check if a resource is the root container was + * a success. + */ export interface CheckRootContainerSuccess extends ResourceSuccess { type: "checkRootContainerSuccess"; + /** + * True if this resoure is the root container + */ isRootContainer: boolean; } diff --git a/packages/solid/src/requester/results/success/CreateSuccess.ts b/packages/solid/src/requester/results/success/CreateSuccess.ts index 190209f..3d83b9f 100644 --- a/packages/solid/src/requester/results/success/CreateSuccess.ts +++ b/packages/solid/src/requester/results/success/CreateSuccess.ts @@ -1,6 +1,13 @@ import type { ResourceSuccess } from "./SuccessResult"; +/** + * Indicates that the request to create the resource was a success. + */ export interface CreateSuccess extends ResourceSuccess { type: "createSuccess"; + /** + * True if there was a resource that existed before at the given URI that was + * overwritten + */ didOverwrite: boolean; } diff --git a/packages/solid/src/requester/results/success/DeleteSuccess.ts b/packages/solid/src/requester/results/success/DeleteSuccess.ts index 4cb131f..0345a1c 100644 --- a/packages/solid/src/requester/results/success/DeleteSuccess.ts +++ b/packages/solid/src/requester/results/success/DeleteSuccess.ts @@ -1,6 +1,14 @@ import type { ResourceSuccess } from "./SuccessResult"; +/** + * Indicates that the request to delete a resource was a success. + */ export interface DeleteSuccess extends ResourceSuccess { type: "deleteSuccess"; + + /** + * True if there was a resource at the provided URI that was deleted. False if + * a resource didn't exist. + */ resourceExisted: boolean; } diff --git a/packages/solid/src/requester/results/success/ReadSuccess.ts b/packages/solid/src/requester/results/success/ReadSuccess.ts index 419c984..756642a 100644 --- a/packages/solid/src/requester/results/success/ReadSuccess.ts +++ b/packages/solid/src/requester/results/success/ReadSuccess.ts @@ -1,28 +1,66 @@ import type { ResourceSuccess, SuccessResult } from "./SuccessResult"; +/** + * Indicates that the request to read a resource was a success + */ export interface ReadSuccess extends ResourceSuccess { + /** + * True if the resource was recalled from local memory rather than a recent + * request + */ recalledFromMemory: boolean; } +/** + * Indicates that the read request was successful and that the resource + * retrieved was a binary resource. + */ export interface BinaryReadSuccess extends ReadSuccess { type: "binaryReadSuccess"; + /** + * The raw data for the binary resource + */ blob: Blob; + /** + * The mime type of the binary resource + */ mimeType: string; } +/** + * Indicates that the read request was successful and that the resource + * retrieved was a data (RDF) resource. + */ export interface DataReadSuccess extends ReadSuccess { type: "dataReadSuccess"; } +/** + * Indicates that the read request was successful and that the resource + * retrieved was a container resource. + */ export interface ContainerReadSuccess extends ReadSuccess { type: "containerReadSuccess"; + /** + * True if this container is a root container + */ isRootContainer: boolean; } +/** + * Indicates that the read request was successful, but no resource exists at + * the provided URI. + */ export interface AbsentReadSuccess extends ReadSuccess { type: "absentReadSuccess"; } +/** + * A helper function that checks to see if a result is a ReadSuccess result + * + * @param result - the result to check + * @returns true if the result is a ReadSuccessResult result + */ export function isReadSuccess(result: SuccessResult): result is ReadSuccess { return ( result.type === "binaryReadSuccess" || diff --git a/packages/solid/src/requester/results/success/SuccessResult.ts b/packages/solid/src/requester/results/success/SuccessResult.ts index f79275c..35b891c 100644 --- a/packages/solid/src/requester/results/success/SuccessResult.ts +++ b/packages/solid/src/requester/results/success/SuccessResult.ts @@ -1,15 +1,31 @@ import type { RequesterResult } from "../RequesterResult"; +/** + * Indicates that some action taken by LDO was a success + */ export interface SuccessResult extends RequesterResult { isError: false; } +/** + * Indicates that a request to a resource was aa success + */ export interface ResourceSuccess extends SuccessResult { + /** + * The URI of the resource + */ uri: string; } +/** + * A grouping of multiple successes as a result of an action + */ export interface AggregateSuccess extends SuccessResult { type: "aggregateSuccess"; + + /** + * An array of all successesses + */ results: SuccessType[]; } diff --git a/packages/solid/src/requester/results/success/Unfetched.ts b/packages/solid/src/requester/results/success/Unfetched.ts index 6eacacb..9d8db38 100644 --- a/packages/solid/src/requester/results/success/Unfetched.ts +++ b/packages/solid/src/requester/results/success/Unfetched.ts @@ -1,5 +1,8 @@ import type { ResourceSuccess } from "./SuccessResult"; +/** + * Indicates that a specific resource is unfetched + */ export interface Unfetched extends ResourceSuccess { type: "unfetched"; } diff --git a/packages/solid/src/requester/results/success/UpdateSuccess.ts b/packages/solid/src/requester/results/success/UpdateSuccess.ts index 9bbea45..a22a06c 100644 --- a/packages/solid/src/requester/results/success/UpdateSuccess.ts +++ b/packages/solid/src/requester/results/success/UpdateSuccess.ts @@ -1,9 +1,16 @@ import type { ResourceSuccess } from "./SuccessResult"; +/** + * Indicates that an update request to a resource was successful + */ export interface UpdateSuccess extends ResourceSuccess { type: "updateSuccess"; } +/** + * Indicates that an update request to the default graph was successful. This + * data was not written to a Pod. It was only written locally. + */ export interface UpdateDefaultGraphSuccess extends ResourceSuccess { type: "updateDefaultGraphSuccess"; } diff --git a/packages/solid/src/requester/util/modifyQueueFuntions.ts b/packages/solid/src/requester/util/modifyQueueFuntions.ts index 8304934..780b5d1 100644 --- a/packages/solid/src/requester/util/modifyQueueFuntions.ts +++ b/packages/solid/src/requester/util/modifyQueueFuntions.ts @@ -1,6 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { WaitingProcess } from "../../util/RequestBatcher"; +/** + * @internal + * + * A helper function for a common way to modify the batch queue. This merges + * the incoming request with the currently executing request or the last request + * in the queue if its keys are the same. + * + * @param key - the key of the incoming request + * @returns a modifyQueue function + */ export function modifyQueueByMergingEventsWithTheSameKeys(key: string) { return ( queue: WaitingProcess[], diff --git a/packages/solid/src/resource/Container.ts b/packages/solid/src/resource/Container.ts index f6ecc2b..e21fcde 100644 --- a/packages/solid/src/resource/Container.ts +++ b/packages/solid/src/resource/Container.ts @@ -1,5 +1,5 @@ import { namedNode } from "@rdfjs/data-model"; -import { ContainerRequester } from "../requester/ContainerRequester"; +import { ContainerBatchedRequester } from "../requester/ContainerBatchedRequester"; import type { CheckRootResult, CheckRootResultError, @@ -32,12 +32,47 @@ import type { SharedStatuses } from "./Resource"; import { Resource } from "./Resource"; import type { ResourceResult } from "./resourceResult/ResourceResult"; +/** + * Represents the current status of a specific container on a Pod as known by + * LDO. + * + * @example + * ```typescript + * const container = solidLdoDataset + * .getResource("https://example.com/container/"); + * ``` + */ export class Container extends Resource { + /** + * The URI of the container + */ readonly uri: ContainerUri; - protected requester: ContainerRequester; + + /** + * @internal + * Batched Requester for the Container + */ + protected requester: ContainerBatchedRequester; + + /** + * @internal + * True if this is the root container, false if not, undefined if unknown + */ protected rootContainer: boolean | undefined; + + /** + * Indicates that this resource is a container resource + */ readonly type = "container" as const; + + /** + * Indicates that this resource is not an error + */ readonly isError = false as const; + + /** + * The status of the last request made for this container + */ status: | SharedStatuses | ReadContainerResult @@ -45,18 +80,48 @@ export class Container extends Resource { | ContainerCreateIfAbsentResult | CheckRootResult; + /** + * @param uri - The uri of the container + * @param context - SolidLdoDatasetContext for the parent dataset + */ constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { super(context); this.uri = uri; - this.requester = new ContainerRequester(uri, context); + this.requester = new ContainerBatchedRequester(uri, context); this.status = { isError: false, type: "unfetched", uri }; } + /** + * Checks if this container is a root container + * @returns true if this container is a root container, false if not, and + * undefined if this is unknown at the moment. + * + * @example + * ```typescript + * // Returns "undefined" when the container is unfetched + * console.log(container.isRootContainer()); + * const result = await container.read(); + * if (!result.isError) { + * // Returns true or false + * console.log(container.isRootContainer()); + * } + * ``` + */ isRootContainer(): boolean | undefined { return this.rootContainer; } - // Read Methods + /** + * =========================================================================== + * READ METHODS + * =========================================================================== + */ + + /** + * @internal + * A helper method updates this container's internal state upon read success + * @param result - the result of the read success + */ protected updateWithReadSuccess( result: ContainerReadSuccess | AbsentReadSuccess, ): void { @@ -66,12 +131,29 @@ export class Container extends Resource { } } + /** + * Reads the container + * @returns A read result + * + * @example + * ```typescript + * const result = await container.read(); + * if (result.isError) { + * // Do something + * } + * ``` + */ async read(): Promise> { const result = (await this.handleRead()) as ReadContainerResult; if (result.isError) return result; return { ...result, resource: this }; } + /** + * @internal + * Converts the current state of this container to a readResult + * @returns a ReadContainerResult + */ protected toReadResult(): ResourceResult { if (this.isAbsent()) { return { @@ -93,6 +175,20 @@ export class Container extends Resource { } } + /** + * Makes a request to read this container if it hasn't been fetched yet. If it + * has, return the cached informtation + * @returns a ReadContainerResult + * + * @example + * ```typescript + * const result = await container.read(); + * if (!result.isError) { + * // Will execute without making a request + * const result2 = await container.readIfUnfetched(); + * } + * ``` + */ async readIfUnfetched(): Promise< ResourceResult > { @@ -101,7 +197,17 @@ export class Container extends Resource { >; } - // Parent Container Methods + /** + * =========================================================================== + * PARENT CONTAINER METHODS + * =========================================================================== + */ + + /** + * @internal + * Checks if this container is a root container by making a request + * @returns CheckRootResult + */ private async checkIfIsRootContainer(): Promise< ResourceResult > { @@ -113,6 +219,24 @@ export class Container extends Resource { return { ...rootContainerResult, resource: this }; } + /** + * Gets the root container of this container. If this container is the root + * container, this function returns itself. + * @returns The root container for this container + * + * @example + * Suppose the root container is at `https://example.com/` + * + * ```typescript + * const container = ldoSolidDataset + * .getResource("https://example.com/container/"); + * const rootContainer = await container.getRootContainer(); + * if (!rootContainer.isError) { + * // logs "https://example.com/" + * console.log(rootContainer.uri); + * } + * ``` + */ async getRootContainer(): Promise { const parentContainerResult = await this.getParentContainer(); if (parentContainerResult?.isError) return parentContainerResult; @@ -122,6 +246,27 @@ export class Container extends Resource { return parentContainerResult.getRootContainer(); } + /** + * Gets the parent container for this container by making a request + * @returns The parent container or undefined if there is no parent container + * because this container is the root container + * + * @example + * Suppose the root container is at `https://example.com/` + * + * ```typescript + * const root = solidLdoDataset.getResource("https://example.com/"); + * const container = solidLdoDataset + * .getResource("https://example.com/container"); + * const rootParent = await root.getParentContainer(); + * console.log(rootParent); // Logs "undefined" + * const containerParent = await container.getParentContainer(); + * if (!containerParent.isError) { + * // Logs "https://example.com/" + * console.log(containerParent.uri); + * } + * ``` + */ async getParentContainer(): Promise< Container | CheckRootResultError | undefined > { @@ -140,6 +285,20 @@ export class Container extends Resource { return this.context.resourceStore.get(parentUri); } + /** + * Lists the currently cached children of this container (no request is made) + * @returns An array of children + * + * ```typescript + * const result = await container.read(); + * if (!result.isError) { + * const children = container.children(); + * children.forEach((child) => { + * console.log(child.uri); + * }); + * } + * ``` + */ children(): (Leaf | Container)[] { const childQuads = this.context.solidLdoDataset.match( namedNode(this.uri), @@ -152,6 +311,23 @@ export class Container extends Resource { }); } + /** + * Returns a child resource with a given name (slug) + * @param slug - the given name for that child resource + * @returns the child resource (either a Leaf or Container depending on the + * name) + * + * @example + * ```typescript + * const root = solidLdoDataset.getResource("https://example.com/"); + * const container = solidLdoDataset.child("container/"); + * // Logs "https://example.com/container/" + * console.log(container.uri); + * const resource = container.child("resource.ttl"); + * // Logs "https://example.com/container/resource.ttl" + * console.log(resource.uri); + * ``` + */ child(slug: ContainerUri): Container; child(slug: LeafUri): Leaf; child(slug: string): Leaf | Container; @@ -159,7 +335,29 @@ export class Container extends Resource { return this.context.resourceStore.get(`${this.uri}${slug}`); } - // Child Creators + /** + * =========================================================================== + * CHILD CREATORS + * =========================================================================== + */ + + /** + * Creates a resource and overwrites any existing resource that existed at the + * URI + * + * @param slug - the name of the resource + * @return the result of creating that resource + * + * @example + * ```typescript + * const container = solidLdoDataset + * .getResource("https://example.com/container/"); + * cosnt result = await container.createChildAndOverwrite("resource.ttl"); + * if (!result.isError) { + * // Do something + * } + * ``` + */ createChildAndOverwrite( slug: ContainerUri, ): Promise>; @@ -185,6 +383,23 @@ export class Container extends Resource { return this.child(slug).createAndOverwrite(); } + /** + * Creates a resource only if that resource doesn't already exist on the Solid + * Pod + * + * @param slug - the name of the resource + * @return the result of creating that resource + * + * @example + * ```typescript + * const container = solidLdoDataset + * .getResource("https://example.com/container/"); + * cosnt result = await container.createChildIfAbsent("resource.ttl"); + * if (!result.isError) { + * // Do something + * } + * ``` + */ createChildIfAbsent( slug: ContainerUri, ): Promise>; @@ -210,6 +425,27 @@ export class Container extends Resource { return this.child(slug).createIfAbsent(); } + /** + * Creates a new binary resource and overwrites any existing resource that + * existed at the URI + * + * @param slug - the name of the resource + * @return the result of creating that resource + * + * @example + * ```typescript + * const container = solidLdoDataset + * .getResource("https://example.com/container/"); + * cosnt result = await container.uploadChildAndOverwrite( + * "resource.txt", + * new Blob("some text."), + * "text/txt", + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async uploadChildAndOverwrite( slug: LeafUri, blob: Blob, @@ -218,6 +454,27 @@ export class Container extends Resource { return this.child(slug).uploadAndOverwrite(blob, mimeType); } + /** + * Creates a new binary resource and overwrites any existing resource that + * existed at the URI + * + * @param slug - the name of the resource + * @return the result of creating that resource + * + * @example + * ```typescript + * const container = solidLdoDataset + * .getResource("https://example.com/container/"); + * cosnt result = await container.uploadChildIfAbsent( + * "resource.txt", + * new Blob("some text."), + * "text/txt", + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async uploadChildIfAbsent( slug: LeafUri, blob: Blob, @@ -226,6 +483,20 @@ export class Container extends Resource { return this.child(slug).uploadIfAbsent(blob, mimeType); } + /** + * Deletes all contents in this container + * @returns An AggregateSuccess or Aggregate error corresponding with all the + * deleted resources + * + * @example + * ```typescript + * const result = container.clear(); + * if (!result.isError) { + * console.log("All deleted resources:"); + * result.results.forEach((result) => console.log(result.uri)); + * } + * ``` + */ async clear(): Promise< ResourceResult< | AggregateSuccess> @@ -260,6 +531,17 @@ export class Container extends Resource { }; } + /** + * Deletes this container and all its contents + * @returns A Delete result for this container + * + * ```typescript + * const result = await container.delete(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async delete(): Promise< ResourceResult< DeleteResult | AggregateError, @@ -273,6 +555,18 @@ export class Container extends Resource { return { ...deleteResult, resource: this }; } + /** + * Creates a container at this URI and overwrites any that already exists + * @returns ContainerCreateAndOverwriteResult + * + * @example + * ```typescript + * const result = await container.createAndOverwrite(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async createAndOverwrite(): Promise< ResourceResult > { @@ -282,6 +576,18 @@ export class Container extends Resource { return { ...createResult, resource: this }; } + /** + * Creates a container at this URI if the container doesn't already exist + * @returns ContainerCreateIfAbsentResult + * + * @example + * ```typescript + * const result = await container.createIfAbsent(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async createIfAbsent(): Promise< ResourceResult > { diff --git a/packages/solid/src/resource/Leaf.ts b/packages/solid/src/resource/Leaf.ts index 11ca125..91b5329 100644 --- a/packages/solid/src/resource/Leaf.ts +++ b/packages/solid/src/resource/Leaf.ts @@ -1,6 +1,6 @@ import type { DatasetChanges } from "@ldo/rdf-utils"; import type { Quad } from "@rdfjs/types"; -import { LeafRequester } from "../requester/LeafRequester"; +import { LeafBatchedRequester } from "../requester/LeafBatchedRequester"; import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; import type { LeafCreateAndOverwriteResult, @@ -24,11 +24,40 @@ import type { SharedStatuses } from "./Resource"; import { Resource } from "./Resource"; import type { ResourceResult } from "./resourceResult/ResourceResult"; +/** + * Represents the current status of a specific Leaf on a Pod as known by LDO. + * + * @example + * ```typescript + * const leaf = solidLdoDataset + * .getResource("https://example.com/container/resource.ttl"); + * ``` + */ export class Leaf extends Resource { + /** + * The URI of the leaf + */ readonly uri: LeafUri; - protected requester: LeafRequester; + + /** + * @internal + * Batched Requester for the Leaf + */ + protected requester: LeafBatchedRequester; + + /** + * Indicates that this resource is a leaf resource + */ readonly type = "leaf" as const; + + /** + * Indicates that this resource is not an error + */ readonly isError = false as const; + + /** + * The status of the last request made for this leaf + */ status: | SharedStatuses | ReadLeafResult @@ -36,34 +65,134 @@ export class Leaf extends Resource { | LeafCreateIfAbsentResult | UpdateResult; + /** + * @internal + * The raw binary data if this leaf is a Binary resource + */ protected binaryData: { blob: Blob; mimeType: string } | undefined; + /** + * @param uri - The uri of the leaf + * @param context - SolidLdoDatasetContext for the parent dataset + */ constructor(uri: LeafUri, context: SolidLdoDatasetContext) { super(context); this.uri = uri; - this.requester = new LeafRequester(uri, context); + this.requester = new LeafBatchedRequester(uri, context); this.status = { isError: false, type: "unfetched", uri }; } - // Getters + /** + * =========================================================================== + * GETTERS + * =========================================================================== + */ + + /** + * Checks to see if the resource is currently uploading data + * @returns true if the current resource is uploading + * + * @example + * ```typescript + * leaf.uploadAndOverwrite(new Blob("some text"), "text/txt").then(() => { + * // Logs "false" + * console.log(leaf.isUploading()) + * }); + * // Logs "true" + * console.log(leaf.isUploading()); + * ``` + */ isUploading(): boolean { return this.requester.isUploading(); } + + /** + * Checks to see if the resource is currently updating data + * @returns true if the current resource is updating + * + * @example + * ```typescript + * leaf.update(datasetChanges).then(() => { + * // Logs "false" + * console.log(leaf.isUpdating()) + * }); + * // Logs "true" + * console.log(leaf.isUpdating()); + * ``` + */ isUpdating(): boolean { return this.requester.isUpdating(); } + + /** + * If this resource is a binary resource, returns the mime type + * @returns The mime type if this resource is a binary resource, undefined + * otherwise + * + * @example + * ```typescript + * // Logs "text/txt" + * console.log(leaf.getMimeType()); + * ``` + */ getMimeType(): string | undefined { return this.binaryData?.mimeType; } + + /** + * If this resource is a binary resource, returns the Blob + * @returns The Blob if this resource is a binary resource, undefined + * otherwise + * + * @example + * ```typescript + * // Logs "some text." + * console.log(leaf.getBlob()?.toString()); + * ``` + */ getBlob(): Blob | undefined { return this.binaryData?.blob; } + + /** + * Check if this resource is a binary resource + * @returns True if this resource is a binary resource, false if not, + * undefined if unknown + * + * @example + * ```typescript + * // Logs "undefined" + * console.log(leaf.isBinary()); + * const result = await leaf.read(); + * if (!result.isError) { + * // Logs "true" + * console.log(leaf.isBinary()); + * } + * ``` + */ isBinary(): boolean | undefined { if (!this.didInitialFetch) { return undefined; } return !!this.binaryData; } + + /** + * Check if this resource is a data (RDF) resource + * @returns True if this resource is a data resource, false if not, undefined + * if unknown + * + * @example + * ```typescript + * // Logs "undefined" + * console.log(leaf.isDataResource()); + * const result = await leaf.read(); + * if (!result.isError) { + * // Logs "true" + * console.log(leaf.isDataResource()); + * } + * ``` + */ isDataResource(): boolean | undefined { if (!this.didInitialFetch) { return undefined; @@ -71,7 +200,17 @@ export class Leaf extends Resource { return !this.binaryData; } - // Read Methods + /** + * =========================================================================== + * READ METHODS + * =========================================================================== + */ + + /** + * @internal + * A helper method updates this leaf's internal state upon read success + * @param result - the result of the read success + */ protected updateWithReadSuccess( result: BinaryReadSuccess | DataReadSuccess | AbsentReadSuccess, ): void { @@ -83,12 +222,29 @@ export class Leaf extends Resource { } } + /** + * Reads the leaf by making a request + * @returns A read result + * + * @example + * ```typescript + * const result = await leaf.read(); + * if (result.isError) { + * // Do something + * } + * ``` + */ async read(): Promise> { const result = (await this.handleRead()) as ReadLeafResult; if (result.isError) return result; return { ...result, resource: this }; } + /** + * @internal + * Converts the current state of this leaf to a readResult + * @returns a ReadLeafResult + */ protected toReadResult(): ResourceResult { if (this.isAbsent()) { return { @@ -119,33 +275,185 @@ export class Leaf extends Resource { } } + /** + * Makes a request to read this leaf if it hasn't been fetched yet. If it has, + * return the cached informtation + * @returns a ReadLeafResult + * + * @example + * ```typescript + * const result = await leaf.read(); + * if (!result.isError) { + * // Will execute without making a request + * const result2 = await leaf.readIfUnfetched(); + * } + * ``` + */ async readIfUnfetched(): Promise> { return super.readIfUnfetched() as Promise< ResourceResult >; } - // Parent Container Methods + /** + * =========================================================================== + * PARENT CONTAINER METHODS + * =========================================================================== + */ + + /** + * Gets the parent container for this leaf by making a request + * @returns The parent container + * + * @example + * ```typescript + * const leaf = solidLdoDataset + * .getResource("https://example.com/container/resource.ttl"); + * const leafParent = leaf.getParentContainer(); + * if (!leafParent.isError) { + * // Logs "https://example.com/container/" + * console.log(leafParent.uri); + * } + * ``` + */ getParentContainer(): Container { const parentUri = getParentUri(this.uri)!; return this.context.resourceStore.get(parentUri); } + + /** + * Gets the root container for this leaf. + * @returns The root container for this leaf + * + * @example + * Suppose the root container is at `https://example.com/` + * + * ```typescript + * const leaf = ldoSolidDataset + * .getResource("https://example.com/container/resource.ttl"); + * const rootContainer = await leaf.getRootContainer(); + * if (!rootContainer.isError) { + * // logs "https://example.com/" + * console.log(rootContainer.uri); + * } + * ``` + */ getRootContainer(): Promise { const parent = this.getParentContainer(); return parent.getRootContainer(); } - // Delete Methods + /** + * =========================================================================== + * DELETE METHODS + * =========================================================================== + */ + + /** + * @internal + * A helper method updates this leaf's internal state upon delete success + * @param result - the result of the delete success + */ protected updateWithDeleteSuccess(_result: DeleteSuccess) { this.binaryData = undefined; } - // Create Methods + /** + * Deletes this leaf and all its contents + * @returns A Delete result for this leaf + * + * ```typescript + * const result = await container.leaf(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ + async delete(): Promise { + return this.handleDelete(); + } + + /** + * =========================================================================== + * CREATE METHODS + * =========================================================================== + */ + + /** + * A helper method updates this leaf's internal state upon create success + * @param _result - the result of the create success + */ protected updateWithCreateSuccess(_result: ResourceSuccess): void { this.binaryData = undefined; } - // Upload Methods + /** + * Creates a leaf at this URI and overwrites any that already exists + * @returns LeafCreateAndOverwriteResult + * + * @example + * ```typescript + * const result = await leaf.createAndOverwrite(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ + async createAndOverwrite(): Promise< + ResourceResult + > { + const createResult = + (await this.handleCreateAndOverwrite()) as LeafCreateAndOverwriteResult; + if (createResult.isError) return createResult; + return { ...createResult, resource: this }; + } + + /** + * Creates a leaf at this URI if the leaf doesn't already exist + * @returns LeafCreateIfAbsentResult + * + * @example + * ```typescript + * const result = await leaf.createIfAbsent(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ + async createIfAbsent(): Promise< + ResourceResult + > { + const createResult = + (await this.handleCreateIfAbsent()) as LeafCreateIfAbsentResult; + if (createResult.isError) return createResult; + return { ...createResult, resource: this }; + } + + /** + * =========================================================================== + * UPLOAD METHODS + * =========================================================================== + */ + + /** + * Uploads a binary resource to this URI. If there is already a resource + * present at this URI, it will be overwritten + * + * @param blob - the Blob of the binary + * @param mimeType - the MimeType of the binary + * @returns A LeafCreateAndOverwriteResult + * + * @example + * ```typescript + * const result = await leaf.uploadAndOverwrite( + * new Blob("some text."), + * "text/txt", + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async uploadAndOverwrite( blob: Blob, mimeType: string, @@ -159,6 +467,25 @@ export class Leaf extends Resource { return { ...result, resource: this }; } + /** + * Uploads a binary resource to this URI tf there not is already a resource + * present at this URI. + * + * @param blob - the Blob of the binary + * @param mimeType - the MimeType of the binary + * @returns A LeafCreateIfAbsentResult + * + * @example + * ```typescript + * const result = await leaf.uploadIfAbsent( + * new Blob("some text."), + * "text/txt", + * ); + * if (!result.isError) { + * // Do something + * } + * ``` + */ async uploadIfAbsent( blob: Blob, mimeType: string, @@ -172,6 +499,43 @@ export class Leaf extends Resource { return { ...result, resource: this }; } + /** + * =========================================================================== + * UPDATE METHODS + * =========================================================================== + */ + + /** + * Updates a data resource with the changes provided + * @param changes - Dataset changes that will be applied to the resoruce + * @returns An UpdateResult + * + * @example + * ```typescript + * import { + * updateDataResource, + * transactionChanges, + * changeData, + * createSolidLdoDataset, + * } from "@ldo/solid"; + * + * //... + * + * // Get a Linked Data Object + * const profile = solidLdoDataset + * .usingType(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * cosnt resource = solidLdoDataset + * .getResource("https://example.com/profile"); + * // Create a transaction to change data + * const cProfile = changeData(profile, resource); + * cProfile.name = "John Doe"; + * // Get data in "DatasetChanges" form + * const datasetChanges = transactionChanges(someLinkedDataObject); + * // Use "update" to apply the changes + * cosnt result = resource.update(datasetChanges); + * ``` + */ async update( changes: DatasetChanges, ): Promise> { @@ -183,26 +547,4 @@ export class Leaf extends Resource { this.emitThisAndParent(); return { ...result, resource: this }; } - - async delete(): Promise { - return this.handleDelete(); - } - - async createAndOverwrite(): Promise< - ResourceResult - > { - const createResult = - (await this.handleCreateAndOverwrite()) as LeafCreateAndOverwriteResult; - if (createResult.isError) return createResult; - return { ...createResult, resource: this }; - } - - async createIfAbsent(): Promise< - ResourceResult - > { - const createResult = - (await this.handleCreateIfAbsent()) as LeafCreateIfAbsentResult; - if (createResult.isError) return createResult; - return { ...createResult, resource: this }; - } } diff --git a/packages/solid/src/resource/Resource.ts b/packages/solid/src/resource/Resource.ts index 2b3e87b..e03f91c 100644 --- a/packages/solid/src/resource/Resource.ts +++ b/packages/solid/src/resource/Resource.ts @@ -9,7 +9,7 @@ import type { ReadContainerResult, ReadLeafResult, } from "../requester/requests/readResource"; -import type { Requester } from "../requester/Requester"; +import type { BatchedRequester } from "../requester/BatchedRequester"; import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; import type { AccessRule } from "../requester/results/success/AccessRule"; import type { SetAccessRulesResult } from "../requester/requests/setAccessRules"; @@ -29,60 +29,271 @@ import type { ResourceResult } from "./resourceResult/ResourceResult"; import type { Container } from "./Container"; import type { Leaf } from "./Leaf"; +/** + * Statuses shared between both Leaf and Container + */ export type SharedStatuses = Unfetched | DeleteResult | CreateSuccess; +/** + * Represents the current status of a specific Resource on a Pod as known by LDO. + */ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ update: () => void; }>) { - // All intance variables + /** + * @internal + * The SolidLdoDatasetContext from the Parent Dataset + */ protected readonly context: SolidLdoDatasetContext; + + /** + * The uri of the resource + */ abstract readonly uri: string; + + /** + * The type of resource (leaf or container) + */ abstract readonly type: string; + + /** + * The status of the last request made for this resource + */ abstract status: RequesterResult; - protected abstract readonly requester: Requester; + + /** + * @internal + * Batched Requester for the Resource + */ + protected abstract readonly requester: BatchedRequester; + + /** + * @internal + * True if this resource has been fetched at least once + */ protected didInitialFetch: boolean = false; + + /** + * @internal + * True if this resource has been fetched but does not exist + */ protected absent: boolean | undefined; + /** + * @param context - SolidLdoDatasetContext for the parent dataset + */ constructor(context: SolidLdoDatasetContext) { super(); this.context = context; } - // Loading Methods + /** + * =========================================================================== + * GETTERS + * =========================================================================== + */ + + /** + * Checks to see if this resource is loading in any way + * @returns true if the resource is currently loading + * + * @example + * ```typescript + * resource.read().then(() => { + * // Logs "false" + * console.log(resource.isLoading()) + * }); + * // Logs "true" + * console.log(resource.isLoading()); + * ``` + */ isLoading(): boolean { return this.requester.isLoading(); } + + /** + * Checks to see if this resource is being created + * @returns true if the resource is currently being created + * + * @example + * ```typescript + * resource.read().then(() => { + * // Logs "false" + * console.log(resource.isCreating()) + * }); + * // Logs "true" + * console.log(resource.isCreating()); + * ``` + */ isCreating(): boolean { return this.requester.isCreating(); } + + /** + * Checks to see if this resource is being read + * @returns true if the resource is currently being read + * + * @example + * ```typescript + * resource.read().then(() => { + * // Logs "false" + * console.log(resource.isReading()) + * }); + * // Logs "true" + * console.log(resource.isReading()); + * ``` + */ isReading(): boolean { return this.requester.isReading(); } + + /** + * Checks to see if this resource is being deleted + * @returns true if the resource is currently being deleted + * + * @example + * ```typescript + * resource.read().then(() => { + * // Logs "false" + * console.log(resource.isDeleting()) + * }); + * // Logs "true" + * console.log(resource.isDeleting()); + * ``` + */ isDeleting(): boolean { return this.requester.isDeletinng(); } + + /** + * Checks to see if this resource is being read for the first time + * @returns true if the resource is currently being read for the first time + * + * @example + * ```typescript + * resource.read().then(() => { + * // Logs "false" + * console.log(resource.isDoingInitialFetch()) + * }); + * // Logs "true" + * console.log(resource.isDoingInitialFetch()); + * ``` + */ isDoingInitialFetch(): boolean { return this.isReading() && !this.isFetched(); } + + /** + * Checks to see if this resource is being read for a subsequent time + * @returns true if the resource is currently being read for a subsequent time + * + * @example + * ```typescript + * await resource.read(); + * resource.read().then(() => { + * // Logs "false" + * console.log(resource.isCreating()) + * }); + * // Logs "true" + * console.log(resource.isCreating()); + * ``` + */ isReloading(): boolean { return this.isReading() && this.isFetched(); } - // Checkers + /** + * =========================================================================== + * CHECKERS + * =========================================================================== + */ + + /** + * Check to see if this resource has been fetched + * @returns true if this resource has been fetched before + * + * @example + * ```typescript + * // Logs "false" + * console.log(resource.isFetched()); + * const result = await resource.read(); + * if (!result.isError) { + * // Logs "true" + * console.log(resource.isFetched()); + * } + * ``` + */ isFetched(): boolean { return this.didInitialFetch; } + + /** + * Check to see if this resource is currently unfetched + * @returns true if the resource is currently unfetched + * + * @example + * ```typescript + * // Logs "true" + * console.log(resource.isUnetched()); + * const result = await resource.read(); + * if (!result.isError) { + * // Logs "false" + * console.log(resource.isUnfetched()); + * } + * ``` + */ isUnfetched(): boolean { return !this.didInitialFetch; } + + /** + * Is this resource currently absent (it does not exist) + * @returns true if the resource is absent, false if not, undefined if unknown + * + * @example + * ```typescript + * // Logs "undefined" + * console.log(resource.isAbsent()); + * const result = resource.read(); + * if (!result.isError) { + * // False if the resource exists, true if it does not + * console.log(resource.isAbsent()); + * } + * ``` + */ isAbsent(): boolean | undefined { return this.absent; } + + /** + * Is this resource currently present on the Pod + * @returns false if the resource is absent, true if not, undefined if unknown + * + * @example + * ```typescript + * // Logs "undefined" + * console.log(resource.isPresent()); + * const result = resource.read(); + * if (!result.isError) { + * // True if the resource exists, false if it does not + * console.log(resource.isPresent()); + * } + * ``` + */ isPresent(): boolean | undefined { return this.absent === undefined ? undefined : !this.absent; } - // Helper Methods + /** + * =========================================================================== + * HELPER METHODS + * =========================================================================== + */ + + /** + * @internal + * Emits an update event for both this resource and the parent + */ protected emitThisAndParent() { this.emit("update"); const parentUri = getParentUri(this.uri); @@ -92,12 +303,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ } } - // Read Methods + /** + * =========================================================================== + * READ METHODS + * =========================================================================== + */ + + /** + * @internal + * A helper method updates this resource's internal state upon read success + * @param result - the result of the read success + */ protected updateWithReadSuccess(result: ReadSuccess) { this.absent = result.type === "absentReadSuccess"; this.didInitialFetch = true; } + /** + * @internal + * A helper method that handles the core functions for reading + * @returns ReadResult + */ protected async handleRead(): Promise { const result = await this.requester.read(); this.status = result; @@ -107,15 +333,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ return result; } + /** + * @internal + * Converts the current state of this resource to a readResult + * @returns a ReadResult + */ protected abstract toReadResult(): ResourceResult< ReadLeafResult | ReadContainerResult, Container | Leaf >; + /** + * Reads the resource + */ abstract read(): Promise< ResourceResult >; + /** + * Reads the resource if it isn't fetched yet + * @returns a ReadResult + */ async readIfUnfetched(): Promise< ResourceResult > { @@ -127,12 +365,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ return this.read(); } - // Delete Methods + /** + * =========================================================================== + * DELETE METHODS + * =========================================================================== + */ + + /** + * @internal + * A helper method updates this resource's internal state upon delete success + * @param result - the result of the delete success + */ protected updateWithDeleteSuccess(_result: DeleteSuccess) { this.absent = true; this.didInitialFetch = true; } + /** + * @internal + * Helper method that handles the core functions for deleting a resource + * @returns DeleteResult + */ protected async handleDelete(): Promise { const result = await this.requester.delete(); this.status = result; @@ -142,7 +395,16 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ return result; } - // Create Methods + /** + * =========================================================================== + * CREATE METHODS + * =========================================================================== + */ + + /** + * A helper method updates this resource's internal state upon create success + * @param _result - the result of the create success + */ protected updateWithCreateSuccess(result: ResourceSuccess) { this.absent = false; this.didInitialFetch = true; @@ -151,6 +413,18 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ } } + /** + * Creates a resource at this URI and overwrites any that already exists + * @returns CreateAndOverwriteResult + * + * @example + * ```typescript + * const result = await resource.createAndOverwrite(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ abstract createAndOverwrite(): Promise< ResourceResult< ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult, @@ -158,6 +432,12 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ > >; + /** + * @internal + * Helper method that handles the core functions for creating and overwriting + * a resource + * @returns DeleteResult + */ protected async handleCreateAndOverwrite(): Promise< ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult > { @@ -169,6 +449,18 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ return result; } + /** + * Creates a resource at this URI if the resource doesn't already exist + * @returns CreateIfAbsentResult + * + * @example + * ```typescript + * const result = await leaf.createIfAbsent(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ abstract createIfAbsent(): Promise< ResourceResult< ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult, @@ -176,6 +468,12 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ > >; + /** + * @internal + * Helper method that handles the core functions for creating a resource if + * absent + * @returns DeleteResult + */ protected async handleCreateIfAbsent(): Promise< ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult > { @@ -187,7 +485,29 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ return result; } - // Parent Container Methods -- Remember to change for Container + /** + * =========================================================================== + * PARENT CONTAINER METHODS + * =========================================================================== + */ + + /** + * Gets the root container for this resource. + * @returns The root container for this resource + * + * @example + * Suppose the root container is at `https://example.com/` + * + * ```typescript + * const resource = ldoSolidDataset + * .getResource("https://example.com/container/resource.ttl"); + * const rootContainer = await resource.getRootContainer(); + * if (!rootContainer.isError) { + * // logs "https://example.com/" + * console.log(rootContainer.uri); + * } + * ``` + */ abstract getRootContainer(): Promise; // Access Rules Methods diff --git a/packages/solid/src/resource/resourceResult/ResourceResult.ts b/packages/solid/src/resource/resourceResult/ResourceResult.ts index 4b411e8..3132fd1 100644 --- a/packages/solid/src/resource/resourceResult/ResourceResult.ts +++ b/packages/solid/src/resource/resourceResult/ResourceResult.ts @@ -2,11 +2,17 @@ import type { RequesterResult } from "../../requester/results/RequesterResult"; import type { Container } from "../Container"; import type { Leaf } from "../Leaf"; +/** + * Adds an additional field "resource" to SuccessResults. + */ export type ResourceSuccess< Result extends RequesterResult, ResourceType extends Leaf | Container, > = Result & { resource: ResourceType }; +/** + * Adds an additional field "resource" to Results. + */ export type ResourceResult< Result extends RequesterResult, ResourceType extends Leaf | Container, diff --git a/packages/solid/src/util/RequestBatcher.ts b/packages/solid/src/util/RequestBatcher.ts index d2b0fc0..cb78d66 100644 --- a/packages/solid/src/util/RequestBatcher.ts +++ b/packages/solid/src/util/RequestBatcher.ts @@ -10,16 +10,32 @@ export interface WaitingProcess { export const ANY_KEY = "any"; +/** + * Options for processes that are waiting to execute + */ export interface WaitingProcessOptions { + /** + * The name of the process like "read" or "delete" + */ name: string; + /** + * The arguements supplied to the process + */ args: Args; + /** + * A function that will be triggered when it's time to execute this process + * @param args - arguments supplied to the process + * @returns a return type + */ perform: (...args: Args) => Promise; /** - * - * @param processQueue The current process queue - * @param currentlyProcessing: The Process that is currently executing - * @param args provided args - * @returns A WaitingProcess that this request should listen to, or undefined if it should create its own + * A custom function to modify the queue based on the current state of the + * queue + * @param processQueue - The current process queue + * @param currentlyProcessing - The Process that is currently executing + * @param args - provided args + * @returns A WaitingProcess that this request should listen to, or undefined + * if it should create its own */ modifyQueue: ( processQueue: WaitingProcess[], @@ -30,15 +46,35 @@ export interface WaitingProcessOptions { } /** - * Request Batcher + * @internal + * A utility for batching a request */ export class RequestBatcher { + /** + * A mapping between a process key and the last time in UTC a process of that + * key was executed. + */ private lastRequestTimestampMap: Record = {}; + + /** + * A pointer to the current process the batcher is working on + */ private currentlyProcessing: WaitingProcess | undefined = undefined; + + /** + * A queue of upcoming processes + */ private processQueue: WaitingProcess[] = []; + + /** + * The amount of time (in milliseconds) between requests of the same key + */ public batchMillis: number; + /** + * @param options - options, including the value for batchMillis + */ constructor( options?: Partial<{ batchMillis: number; @@ -47,11 +83,21 @@ export class RequestBatcher { this.batchMillis = options?.batchMillis || 1000; } + /** + * Check if the request batcher is currently working on a process + * @param key - the key of the process to check + * @returns true if the batcher is currently working on the provided process + */ public isLoading(key: string): boolean { if (key === ANY_KEY) return !!this.currentlyProcessing; return this.currentlyProcessing?.name === key; } + /** + * Triggers the next process in the queue or triggers a timeout to wait to + * execute the next process in the queue if not enough time has passed since + * the last process was triggered. + */ private triggerOrWaitProcess() { if (!this.processQueue[0]) { return; @@ -103,6 +149,11 @@ export class RequestBatcher { } } + /** + * Adds a process to the queue and waits for the process to be complete + * @param options - WaitingProcessOptions + * @returns A promise that resolves when the process resolves + */ public async queueProcess( options: WaitingProcessOptions, ): Promise { diff --git a/packages/solid/src/util/guaranteeFetch.ts b/packages/solid/src/util/guaranteeFetch.ts index 3d210de..987b7b4 100644 --- a/packages/solid/src/util/guaranteeFetch.ts +++ b/packages/solid/src/util/guaranteeFetch.ts @@ -1,5 +1,12 @@ import crossFetch from "cross-fetch"; +/** + * @internal + * Guantees that some kind of fetch is available + * + * @param fetchInput - A potential fetch object + * @returns a proper fetch object. Cross-fetch is default + */ export function guaranteeFetch(fetchInput?: typeof fetch): typeof fetch { return fetchInput || crossFetch; } diff --git a/packages/solid/src/util/rdfUtils.ts b/packages/solid/src/util/rdfUtils.ts index f7f8d30..e458f55 100644 --- a/packages/solid/src/util/rdfUtils.ts +++ b/packages/solid/src/util/rdfUtils.ts @@ -16,6 +16,13 @@ export const ldpBasicContainer = namedNode( "http://www.w3.org/ns/ldp#BasicContainer", ); +/** + * @internal + * Gets the URI of a parent according the the Solid Spec + * + * @param uri - the child URI + * @returns A parent URI or undefined if not possible + */ export function getParentUri(uri: string): ContainerUri | undefined { const urlObject = new URL(uri); const pathItems = urlObject.pathname.split("/"); @@ -33,12 +40,26 @@ export function getParentUri(uri: string): ContainerUri | undefined { return urlObject.toString() as ContainerUri; } +/** + * @internal + * Gets the slug (last part of the path) for a given URI + * + * @param uri - the full URI + * @returns the slug of the URI + */ export function getSlug(uri: string): string { const urlObject = new URL(uri); const pathItems = urlObject.pathname.split("/"); return pathItems[pathItems.length - 1] || pathItems[pathItems.length - 2]; } +/** + * @internal + * Deletes mention of a resource from the provided dataset + * + * @param resourceUri - the resource to delete + * @param dataset - dataset to modify + */ export function deleteResourceRdfFromContainer( resourceUri: string, dataset: Dataset, @@ -54,6 +75,13 @@ export function deleteResourceRdfFromContainer( } } +/** + * @internal + * Adds a resource to a container in an RDF dataset + * + * @param resourceUri - the resource to add + * @param dataset - the dataset to modify + */ export function addResourceRdfToContainer( resourceUri: string, dataset: Dataset, @@ -74,6 +102,14 @@ export function addResourceRdfToContainer( } } +/** + * @internal + * Adds raw turtle to the provided dataset + * @param rawTurtle - String of raw turtle + * @param dataset - the dataset to modify + * @param baseUri - base URI to parsing turtle + * @returns Undefined if successful, noncompliantPodError if not + */ export async function addRawTurtleToDataset( rawTurtle: string, dataset: Dataset, diff --git a/packages/solid/src/util/splitChangesByGraph.ts b/packages/solid/src/util/splitChangesByGraph.ts index 326c292..007253f 100644 --- a/packages/solid/src/util/splitChangesByGraph.ts +++ b/packages/solid/src/util/splitChangesByGraph.ts @@ -3,16 +3,35 @@ import type { GraphNode, DatasetChanges } from "@ldo/rdf-utils"; import type { Quad } from "@rdfjs/types"; import { defaultGraph, namedNode, quad as createQuad } from "@rdfjs/data-model"; +/** + * @internal + * Converts an RDFJS Graph Node to a string hash + * @param graphNode - the node to convert + * @returns a unique string corresponding to the node + */ export function graphNodeToString(graphNode: GraphNode): string { return graphNode.termType === "DefaultGraph" ? "defaultGraph()" : graphNode.value; } +/** + * @internal + * Converts a unique string to a GraphNode + * @param input - the unique string + * @returns A graph node + */ export function stringToGraphNode(input: string): GraphNode { return input === "defaultGraph()" ? defaultGraph() : namedNode(input); } +/** + * Splits all changes in a DatasetChanges into individual DatasetChanges grouped + * by the quad graph. + * @param changes - Changes to split + * @returns A map between the quad graph and the changes associated with that + * graph + */ export function splitChangesByGraph( changes: DatasetChanges, ): Map> { diff --git a/packages/solid/src/util/uriTypes.ts b/packages/solid/src/util/uriTypes.ts index 24e4d58..aa15152 100644 --- a/packages/solid/src/util/uriTypes.ts +++ b/packages/solid/src/util/uriTypes.ts @@ -1,21 +1,47 @@ +/** + * A LeafUri is any URI that has a pahtname that ends in a "/". It represents a + * container. + */ // The & {} allows for alias preservation // eslint-disable-next-line @typescript-eslint/ban-types export type ContainerUri = `${string}/${NonPathnameEnding}` & {}; + +/** + * A LeafUri is any URI that does not have a pahtname that ends in a "/". It + * represents a data resource or a binary resource. Not a container. + */ export type LeafUri = // The & {} allows for alias preservation // eslint-disable-next-line @typescript-eslint/ban-types `${string}${EveryLegalPathnameCharacterOtherThanSlash}${NonPathnameEnding}` & {}; +/** + * Checks if a provided string is a Container URI + * @param uri - the string to check + * @returns true if the string is a container URI + */ export function isContainerUri(uri: string): uri is ContainerUri { const url = new URL(uri); return url.pathname.endsWith("/"); } +/** + * Checks if a provided string is a leaf URI + * @param uri - the string to check + * @returns true if the string is a leaf URI + */ export function isLeafUri(uri: string): uri is LeafUri { return !isContainerUri(uri); } +/** + * @internal + */ type NonPathnameEnding = "" | `?${string}` | `#${string}`; + +/** + * @internal + */ type EveryLegalPathnameCharacterOtherThanSlash = | "A" | "B" diff --git a/packages/solid/typedoc.json b/packages/solid/typedoc.json index 57a416c..c0e7b5d 100644 --- a/packages/solid/typedoc.json +++ b/packages/solid/typedoc.json @@ -2,5 +2,6 @@ "entryPoints": ["src/index.ts"], "out": "docs", "allReflectionsHaveOwnDocument": true, - "hideInPageTOC": true + "hideInPageTOC": true, + "hideBreadcrumbs": true, } \ No newline at end of file