Added some documentation

main
jaxoncreed 2 years ago
parent b955d3bdec
commit a866df62ed
  1. 13
      packages/demo-react/src/App-old.tsx
  2. 64
      packages/demo-react/src/App.tsx
  3. 10
      packages/solid-react/src/util/TrackingProxyContext.ts
  4. 7
      packages/solid-react/src/util/createWrapperProxy.ts
  5. 6
      packages/solid-react/src/util/useForceReload.ts
  6. 222
      packages/solid/README.md
  7. 33
      packages/solid/src/ResourceStore.ts
  8. 94
      packages/solid/src/SolidLdoDataset.ts
  9. 20
      packages/solid/src/SolidLdoDatasetContext.ts
  10. 28
      packages/solid/src/createSolidLdoDataset.ts
  11. 44
      packages/solid/src/methods.ts
  12. 49
      packages/solid/src/requester/BatchedRequester.ts
  13. 29
      packages/solid/src/requester/ContainerBatchedRequester.ts
  14. 41
      packages/solid/src/requester/LeafBatchedRequester.ts
  15. 36
      packages/solid/src/requester/requests/checkRootContainer.ts
  16. 92
      packages/solid/src/requester/requests/createDataResource.ts
  17. 35
      packages/solid/src/requester/requests/deleteResource.ts
  18. 44
      packages/solid/src/requester/requests/readResource.ts
  19. 12
      packages/solid/src/requester/requests/requestOptions.ts
  20. 48
      packages/solid/src/requester/requests/updateDataResource.ts
  21. 32
      packages/solid/src/requester/requests/uploadResource.ts
  22. 3
      packages/solid/src/requester/results/RequesterResult.ts
  23. 8
      packages/solid/src/requester/results/error/AccessControlError.ts
  24. 54
      packages/solid/src/requester/results/error/ErrorResult.ts
  25. 56
      packages/solid/src/requester/results/error/HttpErrorResult.ts
  26. 4
      packages/solid/src/requester/results/error/InvalidUriError.ts
  27. 9
      packages/solid/src/requester/results/error/NoncompliantPodError.ts
  28. 7
      packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts
  29. 7
      packages/solid/src/requester/results/success/CreateSuccess.ts
  30. 8
      packages/solid/src/requester/results/success/DeleteSuccess.ts
  31. 38
      packages/solid/src/requester/results/success/ReadSuccess.ts
  32. 16
      packages/solid/src/requester/results/success/SuccessResult.ts
  33. 3
      packages/solid/src/requester/results/success/Unfetched.ts
  34. 7
      packages/solid/src/requester/results/success/UpdateSuccess.ts
  35. 10
      packages/solid/src/requester/util/modifyQueueFuntions.ts
  36. 318
      packages/solid/src/resource/Container.ts
  37. 404
      packages/solid/src/resource/Leaf.ts
  38. 340
      packages/solid/src/resource/Resource.ts
  39. 6
      packages/solid/src/resource/resourceResult/ResourceResult.ts
  40. 63
      packages/solid/src/util/RequestBatcher.ts
  41. 7
      packages/solid/src/util/guaranteeFetch.ts
  42. 36
      packages/solid/src/util/rdfUtils.ts
  43. 19
      packages/solid/src/util/splitChangesByGraph.ts
  44. 26
      packages/solid/src/util/uriTypes.ts
  45. 3
      packages/solid/typedoc.json

@ -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 (
<BrowserSolidLdoProvider>
<Router />
</BrowserSolidLdoProvider>
);
};
export default ProfileApp;

@ -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 */
<BrowserSolidLdoProvider>
<Router />
<Login />
</BrowserSolidLdoProvider>
);
};
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 (
<div>
{/* Get the user's webId from session.webId */}
<p>Logged in as {session.webId}</p>
{/* Use the logout function to log out */}
<button onClick={logout}>Log Out</button>
<Profile />
</div>
);
}
return <button onClick={onLogin}>Log In</button>;
};
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 <input type="text" value={profile?.name} onChange={onNameChange} />;
};
export default App;

@ -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<Quad>;
}
/**
* @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<Quad>;

@ -1,7 +0,0 @@
import type { Resource } from "@ldo/solid";
export function createWrapperProxy<ResourceType extends Resource>(
target: ResourceType,
): ResourceType {
return new Proxy(target, {});
}

@ -1,6 +0,0 @@
import { useState, useCallback } from "react";
export function useForceReload() {
const [, setValue] = useState(0);
return useCallback(() => setValue((value) => value + 1), []);
}

@ -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([`<svg><circle r="9" /></svg>`]),
// 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/).

@ -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<string, Leaf | Container>;
/**
* @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;

@ -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<Quad>,
@ -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<Type extends LdoBase>(
shapeType: ShapeType<Type>,

@ -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;
}

@ -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 {

@ -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<Type extends LdoBase>(
input: Type,
@ -36,6 +56,24 @@ export function changeData<Type extends LdoBase>(
/**
* 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,

@ -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<ReadLeafResult | ReadContainerResult> {
const transaction = this.context.solidLdoDataset.startTransaction();
@ -65,6 +105,7 @@ export abstract class Requester {
/**
* Delete this resource
* @returns A DeleteResult
*/
async delete(): Promise<DeleteResult> {
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,

@ -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<ReadContainerResult> {
return super.read() as Promise<ReadContainerResult>;
}
/**
* Creates the container
* @param overwrite - If true, this will orverwrite the resource if it already
* exists
*/
createDataResource(
overwrite: true,
): Promise<ContainerCreateAndOverwriteResult>;
@ -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<CheckRootResult> {
return this.requestBatcher.queueProcess({
name: IS_ROOT_CONTAINER_KEY,

@ -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<ReadLeafResult> {
return super.read() as Promise<ReadLeafResult>;
}
/**
* Creates the leaf as a data resource
* @param overwrite - If true, this will orverwrite the resource if it already
* exists
*/
createDataResource(overwrite: true): Promise<LeafCreateAndOverwriteResult>;
createDataResource(overwrite?: false): Promise<LeafCreateIfAbsentResult>;
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<Quad>,
@ -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(

@ -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,

@ -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<ReadContainerResult, AbsentReadSuccess>
| CreateIfAbsentResultErrors;
/**
* All possible return values when creating a leaf if absent
*/
export type LeafCreateIfAbsentResult =
| CreateSuccess
| Exclude<ReadLeafResult, AbsentReadSuccess>
| 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,

@ -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,

@ -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,

@ -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<Quad>;
}

@ -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<Quad>,

@ -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,

@ -1,3 +1,6 @@
/**
* A type returned by all request functions
*/
export interface RequesterResult {
type: string;
isError: boolean;

@ -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.`);
}

@ -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<ErrorType extends ErrorResult> 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<ErrorType>)[],
message?: string,
@ -47,15 +81,35 @@ export class AggregateError<ErrorType extends ErrorResult> 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);

@ -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;
}

@ -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;

@ -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,

@ -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;
}

@ -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;
}

@ -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;
}

@ -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" ||

@ -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<SuccessType extends SuccessResult>
extends SuccessResult {
type: "aggregateSuccess";
/**
* An array of all successesses
*/
results: SuccessType[];
}

@ -1,5 +1,8 @@
import type { ResourceSuccess } from "./SuccessResult";
/**
* Indicates that a specific resource is unfetched
*/
export interface Unfetched extends ResourceSuccess {
type: "unfetched";
}

@ -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";
}

@ -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<any[], any>[],

@ -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<ResourceResult<ReadContainerResult, Container>> {
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<ReadContainerResult, Container> {
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<ReadContainerResult, Container>
> {
@ -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<CheckRootResult, Container>
> {
@ -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<Container | CheckRootResultError> {
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<ResourceResult<ContainerCreateAndOverwriteResult, Container>>;
@ -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<ResourceResult<ContainerCreateIfAbsentResult, Container>>;
@ -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<ResourceResult<DeleteSuccess, Container | Leaf>>
@ -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<DeleteResultError | ReadResultError>,
@ -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<ContainerCreateAndOverwriteResult, Container>
> {
@ -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<ContainerCreateIfAbsentResult, Container>
> {

@ -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<ResourceResult<ReadLeafResult, Leaf>> {
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<ReadLeafResult, Leaf> {
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<ResourceResult<ReadLeafResult, Leaf>> {
return super.readIfUnfetched() as Promise<
ResourceResult<ReadLeafResult, Leaf>
>;
}
// 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<Container | CheckRootResultError> {
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<DeleteResult> {
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<LeafCreateAndOverwriteResult, Leaf>
> {
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<LeafCreateIfAbsentResult, Leaf>
> {
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<Quad>,
): Promise<ResourceResult<UpdateResult, Leaf>> {
@ -183,26 +547,4 @@ export class Leaf extends Resource {
this.emitThisAndParent();
return { ...result, resource: this };
}
async delete(): Promise<DeleteResult> {
return this.handleDelete();
}
async createAndOverwrite(): Promise<
ResourceResult<LeafCreateAndOverwriteResult, Leaf>
> {
const createResult =
(await this.handleCreateAndOverwrite()) as LeafCreateAndOverwriteResult;
if (createResult.isError) return createResult;
return { ...createResult, resource: this };
}
async createIfAbsent(): Promise<
ResourceResult<LeafCreateIfAbsentResult, Leaf>
> {
const createResult =
(await this.handleCreateIfAbsent()) as LeafCreateIfAbsentResult;
if (createResult.isError) return createResult;
return { ...createResult, resource: this };
}
}

@ -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<ReadContainerResult | ReadLeafResult> {
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<ReadLeafResult | ReadContainerResult, Container | Leaf>
>;
/**
* Reads the resource if it isn't fetched yet
* @returns a ReadResult
*/
async readIfUnfetched(): Promise<
ResourceResult<ReadLeafResult | ReadContainerResult, Container | Leaf>
> {
@ -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<DeleteResult> {
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<Container | CheckRootResultError>;
// Access Rules Methods

@ -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,

@ -10,16 +10,32 @@ export interface WaitingProcess<Args extends any[], Return> {
export const ANY_KEY = "any";
/**
* Options for processes that are waiting to execute
*/
export interface WaitingProcessOptions<Args extends any[], Return> {
/**
* 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<Return>;
/**
*
* @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<any[], any>[],
@ -30,15 +46,35 @@ export interface WaitingProcessOptions<Args extends any[], Return> {
}
/**
* 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<string, number> = {};
/**
* A pointer to the current process the batcher is working on
*/
private currentlyProcessing: WaitingProcess<any[], any> | undefined =
undefined;
/**
* A queue of upcoming processes
*/
private processQueue: WaitingProcess<any[], any>[] = [];
/**
* 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<Args extends any[], ReturnType>(
options: WaitingProcessOptions<Args, ReturnType>,
): Promise<ReturnType> {

@ -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;
}

@ -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,

@ -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<Quad>,
): Map<GraphNode, DatasetChanges<Quad>> {

@ -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"

@ -2,5 +2,6 @@
"entryPoints": ["src/index.ts"],
"out": "docs",
"allReflectionsHaveOwnDocument": true,
"hideInPageTOC": true
"hideInPageTOC": true,
"hideBreadcrumbs": true,
}
Loading…
Cancel
Save