From a879b00b44e03f067c33f992bd3c0d95fa37a5fd Mon Sep 17 00:00:00 2001 From: Jackson Morgan Date: Thu, 20 Mar 2025 11:46:05 -0400 Subject: [PATCH] Resource refactor for connected-solid complete --- .../src/SolidConnectedPlugin.ts | 2 +- .../src/requester/LeafBatchedRequester.ts | 3 +- .../requester/requests/createDataResource.ts | 7 +- .../src/requester/requests/readResource.ts | 2 +- .../src/resources/SolidContainer.ts | 576 +++++++++++++++++- .../src/resources/SolidLeaf.ts | 564 ++++++++++++++++- .../src/resources/SolidResource.ts | 78 ++- packages/connected-solid/src/test.ts | 3 +- packages/connected-solid/src/types.ts | 22 +- packages/connected/src/ConnectedLdoDataset.ts | 5 +- packages/connected/src/test.ts | 0 11 files changed, 1211 insertions(+), 51 deletions(-) create mode 100644 packages/connected/src/test.ts diff --git a/packages/connected-solid/src/SolidConnectedPlugin.ts b/packages/connected-solid/src/SolidConnectedPlugin.ts index d755b65..91af9db 100644 --- a/packages/connected-solid/src/SolidConnectedPlugin.ts +++ b/packages/connected-solid/src/SolidConnectedPlugin.ts @@ -32,7 +32,7 @@ export const solidConnectedPlugin: SolidConnectedPlugin = { context: ConnectedContext, ): SolidLeaf | SolidContainer { if (isSolidContainerUri(uri)) { - return new SolidContainer(uri, context); + return new SolidContainer(uri, context.solid); } else { return new SolidLeaf(uri, context); } diff --git a/packages/connected-solid/src/requester/LeafBatchedRequester.ts b/packages/connected-solid/src/requester/LeafBatchedRequester.ts index 86a6fbe..d462f38 100644 --- a/packages/connected-solid/src/requester/LeafBatchedRequester.ts +++ b/packages/connected-solid/src/requester/LeafBatchedRequester.ts @@ -146,7 +146,8 @@ export class LeafBatchedRequester extends BatchedRequester { this.resource, blob, mimeType, - overwrite, + // Hack: Something's up with these types. I can't be bothered to fix it + overwrite as false, { dataset: transaction, fetch: this.context.solid.fetch }, ], perform: uploadResource, diff --git a/packages/connected-solid/src/requester/requests/createDataResource.ts b/packages/connected-solid/src/requester/requests/createDataResource.ts index 852f077..6775188 100644 --- a/packages/connected-solid/src/requester/requests/createDataResource.ts +++ b/packages/connected-solid/src/requester/requests/createDataResource.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { guaranteeFetch } from "../../util/guaranteeFetch"; import { addResourceRdfToContainer, @@ -41,15 +42,15 @@ export type LeafCreateAndOverwriteResult = */ export type ContainerCreateIfAbsentResult = | CreateSuccess - | Exclude> - | CreateIfAbsentResultErrors; + | Exclude> + | CreateIfAbsentResultErrors; /** * All possible return values when creating a leaf if absent */ export type LeafCreateIfAbsentResult = | CreateSuccess - | Exclude> + | Exclude> | CreateIfAbsentResultErrors; /** diff --git a/packages/connected-solid/src/requester/requests/readResource.ts b/packages/connected-solid/src/requester/requests/readResource.ts index 11e6f6e..7880dc4 100644 --- a/packages/connected-solid/src/requester/requests/readResource.ts +++ b/packages/connected-solid/src/requester/requests/readResource.ts @@ -30,7 +30,7 @@ export type ReadLeafResult = | BinaryReadSuccess | DataReadSuccess | AbsentReadSuccess - | ReadResultError; + | ReadResultError; /** * All possible return values for reading a container diff --git a/packages/connected-solid/src/resources/SolidContainer.ts b/packages/connected-solid/src/resources/SolidContainer.ts index 2208c3c..0399e95 100644 --- a/packages/connected-solid/src/resources/SolidContainer.ts +++ b/packages/connected-solid/src/resources/SolidContainer.ts @@ -1,3 +1,577 @@ +import { namedNode } from "@rdfjs/data-model"; +import { ContainerBatchedRequester } from "../requester/ContainerBatchedRequester"; +import type { + CheckRootResult, + CheckRootResultError, +} from "../requester/requests/checkRootContainer"; +import type { + ContainerCreateAndOverwriteResult, + ContainerCreateIfAbsentResult, + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, +} from "../requester/requests/createDataResource"; +import type { + DeleteResult, + DeleteResultError, +} from "../requester/requests/deleteResource"; +import type { + ReadContainerResult, + ReadResultError, +} from "../requester/requests/readResource"; +import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; +import type { AbsentReadSuccess } from "../requester/results/success/ReadSuccess"; +import type { ContainerReadSuccess } from "../requester/results/success/ReadSuccess"; +import { getParentUri, ldpContains } from "../util/rdfUtils"; +import { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; +import type { SharedStatuses } from "./SolidResource"; import { SolidResource } from "./SolidResource"; +import type { + SolidContainerSlug, + SolidContainerUri, + SolidLeafSlug, +} from "../types"; +import { AggregateSuccess } from "@ldo/connected"; +import { + Unfetched, + type ConnectedContext, + AggregateError, +} from "@ldo/connected"; +import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; +import type { SolidLeaf } from "./SolidLeaf"; -export class SolidContainer extends SolidResource {} +/** + * 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 SolidContainer extends SolidResource { + /** + * The URI of the container + */ + readonly uri: SolidContainerUri; + + /** + * @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 + | ContainerCreateAndOverwriteResult + | ContainerCreateIfAbsentResult + | CheckRootResult; + + /** + * @param uri - The uri of the container + * @param context - SolidLdoDatasetContext for the parent dataset + */ + constructor( + uri: SolidContainerUri, + context: ConnectedContext, + ) { + super(context); + this.uri = uri; + this.requester = new ContainerBatchedRequester(this, context); + this.status = new Unfetched(this); + } + + /** + * 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 + * =========================================================================== + */ + + /** + * @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 { + super.updateWithReadSuccess(result); + if (result.type === "containerReadSuccess") { + this.rootContainer = result.isRootContainer; + } + } + + /** + * Reads the container + * @returns A read result + * + * @example + * ```typescript + * const result = await container.read(); + * if (result.isError) { + * // Do something + * } + * ``` + */ + async read(): Promise { + const result = (await this.handleRead()) as ReadContainerResult; + if (result.isError) return result; + return { ...result, resource: this }; + } + + /** + * @internal + * Converts the current state of this container to a readResult + * @returns a ReadContainerResult + */ + protected toReadResult(): ReadContainerResult { + if (this.isAbsent()) { + return { + isError: false, + type: "absentReadSuccess", + uri: this.uri, + recalledFromMemory: true, + resource: this, + }; + } else { + return { + isError: false, + type: "containerReadSuccess", + uri: this.uri, + recalledFromMemory: true, + isRootContainer: this.isRootContainer()!, + resource: this, + }; + } + } + + /** + * 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 { + return super.readIfUnfetched() as Promise; + } + + /** + * =========================================================================== + * PARENT CONTAINER METHODS + * =========================================================================== + */ + + /** + * @internal + * Checks if this container is a root container by making a request + * @returns CheckRootResult + */ + private async checkIfIsRootContainer(): Promise { + const rootContainerResult = await this.requester.isRootContainer(); + this.status = rootContainerResult; + if (rootContainerResult.isError) return rootContainerResult; + this.rootContainer = rootContainerResult.isRootContainer; + this.emit("update"); + 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 or undefined if there is no + * root 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< + SolidContainer | CheckRootResultError | NoRootContainerError + > { + const parentContainerResult = await this.getParentContainer(); + if (parentContainerResult?.isError) return parentContainerResult; + if (!parentContainerResult) { + return this.isRootContainer() ? this : new NoRootContainerError(this); + } + 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< + SolidContainer | CheckRootResultError | undefined + > { + if (this.rootContainer === undefined) { + const checkResult = await this.checkIfIsRootContainer(); + if (checkResult.isError) return checkResult; + } + if (this.rootContainer) return undefined; + const parentUri = getParentUri(this.uri); + if (!parentUri) { + return undefined; + } + return this.context.dataset.getResource(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(): (SolidContainer | SolidLeaf)[] { + const childQuads = this.context.dataset.match( + namedNode(this.uri), + ldpContains, + null, + namedNode(this.uri), + ); + return childQuads.toArray().map((childQuad) => { + return this.context.dataset.getResource(childQuad.object.value) as + | SolidContainer + | SolidLeaf; + }); + } + + /** + * 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: SolidContainerSlug): SolidContainer; + child(slug: SolidLeafSlug): SolidLeaf; + child(slug: string): SolidLeaf | SolidContainer; + child(slug: string): SolidLeaf | SolidContainer { + return this.context.dataset.getResource(`${this.uri}${slug}`) as + | SolidLeaf + | SolidContainer; + } + + /** + * =========================================================================== + * 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: SolidContainerSlug, + ): Promise; + createChildAndOverwrite( + slug: SolidLeafSlug, + ): Promise; + createChildAndOverwrite( + slug: string, + ): Promise; + createChildAndOverwrite( + slug: string, + ): Promise { + 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: SolidContainerSlug, + ): Promise; + createChildIfAbsent(slug: SolidLeafSlug): Promise; + createChildIfAbsent( + slug: string, + ): Promise; + createChildIfAbsent( + slug: string, + ): Promise { + 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: SolidLeafSlug, + blob: Blob, + mimeType: string, + ): Promise { + 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: SolidLeafSlug, + blob: Blob, + mimeType: string, + ): Promise { + 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< + | AggregateSuccess> + | AggregateError< + | DeleteResultError + | ReadResultError + > + > { + const readResult = await this.read(); + if (readResult.isError) return new AggregateError([readResult]); + const results = ( + await Promise.all( + this.children().map(async (child) => { + return child.delete(); + }), + ) + ).flat(); + const errors = results.filter((value) => value.isError); + if (errors.length > 0) { + return new AggregateError(errors); + } + return new AggregateSuccess( + results as DeleteSuccess[], + ); + } + + /** + * 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< + | DeleteResult + | AggregateError< + | DeleteResultError + | ReadResultError + > + > { + const clearResult = await this.clear(); + if (clearResult.isError) return clearResult; + const deleteResult = await this.handleDelete(); + if (deleteResult.isError) return deleteResult; + return { ...deleteResult, resource: this }; + } + + protected async handleDelete(): Promise> { + return super.handleDelete() as Promise>; + } + + /** + * 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 { + const createResult = + (await this.handleCreateAndOverwrite()) as ContainerCreateAndOverwriteResult; + if (createResult.isError) return createResult; + 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 { + const createResult = + (await this.handleCreateIfAbsent()) as ContainerCreateIfAbsentResult; + if (createResult.isError) return createResult; + return { ...createResult, resource: this }; + } +} diff --git a/packages/connected-solid/src/resources/SolidLeaf.ts b/packages/connected-solid/src/resources/SolidLeaf.ts index d5f42f3..254cef9 100644 --- a/packages/connected-solid/src/resources/SolidLeaf.ts +++ b/packages/connected-solid/src/resources/SolidLeaf.ts @@ -1,14 +1,560 @@ -import type { Resource } from "@ldo/connected"; +import type { DatasetChanges } from "@ldo/rdf-utils"; +import type { Quad } from "@rdfjs/types"; +import { LeafBatchedRequester } from "../requester/LeafBatchedRequester"; +import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; +import type { + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, +} from "../requester/requests/createDataResource"; +import type { DeleteResult } from "../requester/requests/deleteResource"; +import type { ReadLeafResult } from "../requester/requests/readResource"; +import type { UpdateResult } from "../requester/requests/updateDataResource"; +import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; +import type { AbsentReadSuccess } from "../requester/results/success/ReadSuccess"; +import type { + BinaryReadSuccess, + DataReadSuccess, +} from "../requester/results/success/ReadSuccess"; +import { getParentUri } from "../util/rdfUtils"; +import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; +import type { SharedStatuses } from "./SolidResource"; import { SolidResource } from "./SolidResource"; -import type { SolidContainerUri, SolidLeafUri } from "../types"; +import type { SolidLeafUri } from "../types"; +import type { ResourceSuccess } from "@ldo/connected"; +import { Unfetched, type ConnectedContext } from "@ldo/connected"; +import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; +import type { SolidContainer } from "./SolidContainer"; -export class SolidLeaf - extends SolidResource - implements Resource { - public uri: SolidLeafUri; +/** + * 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 SolidLeaf extends SolidResource { + /** + * The URI of the leaf + */ + readonly uri: SolidLeafUri; - constructor() { - super() + /** + * @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 + | LeafCreateAndOverwriteResult + | 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: SolidLeafUri, + context: ConnectedContext, + ) { + super(context); + const uriObject = new URL(uri); + uriObject.hash = ""; + this.uri = uriObject.toString() as SolidLeafUri; + this.requester = new LeafBatchedRequester(this, context); + this.status = new Unfetched(this); + } + + /** + * =========================================================================== + * 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; + } + return !this.binaryData; + } + + /** + * =========================================================================== + * 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 { + super.updateWithReadSuccess(result); + if (result.type === "binaryReadSuccess") { + this.binaryData = { blob: result.blob, mimeType: result.mimeType }; + } else { + this.binaryData = undefined; + } + } + + /** + * Reads the leaf by making a request + * @returns A read result + * + * @example + * ```typescript + * const result = await leaf.read(); + * if (result.isError) { + * // Do something + * } + * ``` + */ + async read(): Promise { + const result = (await this.handleRead()) as ReadLeafResult; + if (result.isError) return result; + return { ...result, resource: this }; + } + + /** + * @internal + * Converts the current state of this leaf to a readResult + * @returns a ReadLeafResult + */ + protected toReadResult(): ReadLeafResult { + if (this.isAbsent()) { + return { + isError: false, + type: "absentReadSuccess", + uri: this.uri, + recalledFromMemory: true, + resource: this, + }; + } else if (this.isBinary()) { + return { + isError: false, + type: "binaryReadSuccess", + uri: this.uri, + recalledFromMemory: true, + blob: this.binaryData!.blob, + mimeType: this.binaryData!.mimeType, + resource: this, + }; + } else { + return { + isError: false, + type: "dataReadSuccess", + uri: this.uri, + recalledFromMemory: true, + resource: this, + }; + } + } + + /** + * Makes a request to read this leaf if it hasn't been fetched yet. If it has, + * return the cached informtation + * @returns a ReadLeafResult + * + * @example + * ```typescript + * const result = await leaf.read(); + * if (!result.isError) { + * // Will execute without making a request + * const result2 = await leaf.readIfUnfetched(); + * } + * ``` + */ + async readIfUnfetched(): Promise { + return super.readIfUnfetched() as Promise; + } + + /** + * =========================================================================== + * 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 = await leaf.getParentContainer(); + * if (!leafParent.isError) { + * // Logs "https://example.com/container/" + * console.log(leafParent.uri); + * } + * ``` + */ + async getParentContainer(): Promise { + const parentUri = getParentUri(this.uri)!; + return this.context.dataset.getResource(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); + * } + * ``` + */ + async getRootContainer(): Promise< + SolidContainer | CheckRootResultError | NoRootContainerError + > { + // Check to see if this document has a pim:storage if so, use that + + // If not, traverse the tree + const parent = await this.getParentContainer(); + return parent.getRootContainer(); + } + + /** + * =========================================================================== + * DELETE METHODS + * =========================================================================== + */ + + /** + * @internal + * A helper method updates this leaf's internal state upon delete success + * @param result - the result of the delete success + */ + public updateWithDeleteSuccess(result: DeleteSuccess) { + super.updateWithDeleteSuccess(result); + this.binaryData = undefined; + } + + /** + * Deletes this leaf and all its contents + * @returns A Delete result for this leaf + * + * ```typescript + * const result = await container.leaf(); + * if (!result.isError) { + * // Do something + * } + * ``` + */ + async delete(): Promise> { + return this.handleDelete(); + } + + protected async handleDelete(): Promise> { + return super.handleDelete() as Promise>; + } + + /** + * =========================================================================== + * 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; + } + + /** + * 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 { + 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 { + 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, + ): Promise { + const result = await this.requester.upload(blob, mimeType, true); + this.status = result; + if (result.isError) return result; + super.updateWithCreateSuccess(result); + this.binaryData = { blob, mimeType }; + this.emitThisAndParent(); + 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, + ): Promise { + const result = await this.requester.upload(blob, mimeType); + this.status = result; + if (result.isError) return result; + super.updateWithCreateSuccess(result); + this.binaryData = { blob, mimeType }; + this.emitThisAndParent(); + return { ...result, resource: this }; + } + + /** + * =========================================================================== + * UPDATE METHODS + * =========================================================================== + */ + + /** + * Updates a data resource with the changes provided + * @param changes - Dataset changes that will be applied to the resoruce + * @returns An UpdateResult + * + * @example + * ```typescript + * import { + * updateDataResource, + * transactionChanges, + * changeData, + * createSolidLdoDataset, + * } from "@ldo/solid"; + * + * //... + * + * // Get a Linked Data Object + * const profile = solidLdoDataset + * .usingType(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * cosnt resource = solidLdoDataset + * .getResource("https://example.com/profile"); + * // Create a transaction to change data + * const cProfile = changeData(profile, resource); + * cProfile.name = "John Doe"; + * // Get data in "DatasetChanges" form + * const datasetChanges = transactionChanges(someLinkedDataObject); + * // Use "update" to apply the changes + * cosnt result = resource.update(datasetChanges); + * ``` + */ + async update( + changes: DatasetChanges, + ): Promise> { + const result = await this.requester.updateDataResource(changes); + this.status = result; + if (result.isError) return result; + this.binaryData = undefined; + this.absent = false; + this.emitThisAndParent(); + return { ...result, resource: this }; } - } diff --git a/packages/connected-solid/src/resources/SolidResource.ts b/packages/connected-solid/src/resources/SolidResource.ts index 76d49f2..9e62cbc 100644 --- a/packages/connected-solid/src/resources/SolidResource.ts +++ b/packages/connected-solid/src/resources/SolidResource.ts @@ -5,13 +5,17 @@ import type { ResourceEventEmitter, ResourceResult, ResourceSuccess, + Unfetched, } from "@ldo/connected"; import type { SolidContainerUri, SolidLeafUri } from "../types"; import EventEmitter from "events"; import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; import type { BatchedRequester } from "../requester/BatchedRequester"; import type { WacRule } from "../wac/WacRule"; -import type { SolidNotificationSubscription } from "../notifications/SolidNotificationSubscription"; +import type { + SolidNotificationSubscription, + SubscriptionCallbacks, +} from "../notifications/SolidNotificationSubscription"; import { Websocket2023NotificationSubscription } from "../notifications/Websocket2023NotificationSubscription"; import { getParentUri } from "../util/rdfUtils"; import { @@ -22,8 +26,11 @@ import type { ReadContainerResult, ReadLeafResult, } from "../requester/requests/readResource"; -import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; -import type { DeleteResult } from "../requester/requests/deleteResource"; +import { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; +import { + updateDatasetOnSuccessfulDelete, + type DeleteResult, +} from "../requester/requests/deleteResource"; import type { ContainerCreateAndOverwriteResult, ContainerCreateIfAbsentResult, @@ -37,8 +44,19 @@ import type { SolidLeaf } from "./SolidLeaf"; import type { GetWacUriError } from "../wac/getWacUri"; import { getWacUri, type GetWacUriResult } from "../wac/getWacUri"; import { getWacRuleWithAclUri, type GetWacRuleResult } from "../wac/getWacRule"; +import type { SetWacRuleResult } from "../wac/setWacRule"; import { setWacRuleForAclUri } from "../wac/setWacRule"; import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; +import type { SolidNotificationMessage } from "../notifications/SolidNotificationMessage"; +import type { CreateSuccess } from "../requester/results/success/CreateSuccess"; + +/** + * Statuses shared between both Leaf and Container + */ +export type SharedStatuses = + | Unfetched + | DeleteResult + | CreateSuccess; export abstract class SolidResource extends (EventEmitter as new () => ResourceEventEmitter) @@ -69,7 +87,9 @@ export abstract class SolidResource * @internal * Batched Requester for the Resource */ - protected abstract readonly requester: BatchedRequester; + protected abstract readonly requester: BatchedRequester< + SolidLeaf | SolidContainer + >; /** * @internal @@ -420,7 +440,7 @@ export abstract class SolidResource * A helper method updates this resource's internal state upon delete success * @param result - the result of the delete success */ - public updateWithDeleteSuccess(_result: DeleteSuccess) { + public updateWithDeleteSuccess(_result: DeleteSuccess) { this.absent = true; this.didInitialFetch = true; } @@ -430,7 +450,9 @@ export abstract class SolidResource * Helper method that handles the core functions for deleting a resource * @returns DeleteResult */ - protected async handleDelete(): Promise> { + protected async handleDelete(): Promise< + DeleteResult + > { const result = await this.requester.delete(); this.status = result; if (result.isError) return result; @@ -761,29 +783,27 @@ export abstract class SolidResource * @internal * Function that triggers whenever a notification is recieved. */ - protected async onNotification(message: NotificationMessage): Promise { - const objectResource = this.context.solidLdoDataset.getResource( - message.object, - ); - switch (message.type) { - case "Update": - case "Add": - await objectResource.read(); - return; - case "Delete": - case "Remove": - // Delete the resource without have to make an additional read request - updateDatasetOnSuccessfulDelete( - message.object, - this.context.solidLdoDataset, - ); - objectResource.updateWithDeleteSuccess({ - type: "deleteSuccess", - isError: false, - uri: message.object, - resourceExisted: true, - }); - return; + protected async onNotification( + message: SolidNotificationMessage, + ): Promise { + const objectResource = this.context.dataset.getResource(message.object); + // Do Nothing if the resource is invalid. + if (objectResource.type === "InvalidIdentifierResouce") return; + if (objectResource.type === "leaf") { + switch (message.type) { + case "Update": + case "Add": + await objectResource.read(); + return; + case "Delete": + case "Remove": + // Delete the resource without have to make an additional read request + updateDatasetOnSuccessfulDelete(message.object, this.context.dataset); + objectResource.updateWithDeleteSuccess( + new DeleteSuccess(objectResource, true), + ); + return; + } } } diff --git a/packages/connected-solid/src/test.ts b/packages/connected-solid/src/test.ts index 41550c0..42d5c41 100644 --- a/packages/connected-solid/src/test.ts +++ b/packages/connected-solid/src/test.ts @@ -2,10 +2,9 @@ import { ConnectedLdoDataset } from "@ldo/connected"; import { solidConnectedPlugin } from "./SolidConnectedPlugin"; import { createDatasetFactory } from "@ldo/dataset"; import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset"; -import { nextGraphConnectedPlugin } from "@ldo/connected-nextgraph"; const dataset = new ConnectedLdoDataset( - [solidConnectedPlugin, nextGraphConnectedPlugin], + [solidConnectedPlugin], createDatasetFactory(), createTransactionDatasetFactory(), ); diff --git a/packages/connected-solid/src/types.ts b/packages/connected-solid/src/types.ts index b7c4edb..6ab4636 100644 --- a/packages/connected-solid/src/types.ts +++ b/packages/connected-solid/src/types.ts @@ -5,14 +5,30 @@ export type SolidUriPrefix = `http${"s" | ""}://`; */ export type SolidUri = SolidContainerUri | SolidLeafUri; +/** + * A SolidContainerSlug is any string 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 SolidContainerSlug = `${string}/${NonPathnameEnding}` & {}; + /** * A SolidLeafUri 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 SolidContainerUri = - `${SolidUriPrefix}${string}/${NonPathnameEnding}` & {}; +export type SolidContainerUri = `${SolidUriPrefix}${SolidContainerSlug}` & {}; + +/** + * A SolidLeafSlug is any string that does not have a pahtname that ends in a + * "/". It represents a data resource or a binary resource. Not a container. + */ +export type SolidLeafSlug = + // The & {} allows for alias preservation + // eslint-disable-next-line @typescript-eslint/ban-types + `${string}${EveryLegalPathnameCharacterOtherThanSlash}${NonPathnameEnding}` & {}; /** * A LeafUri is any URI that does not have a pahtname that ends in a "/". It @@ -21,7 +37,7 @@ export type SolidContainerUri = export type SolidLeafUri = // The & {} allows for alias preservation // eslint-disable-next-line @typescript-eslint/ban-types - `${SolidUriPrefix}${string}${EveryLegalPathnameCharacterOtherThanSlash}${NonPathnameEnding}` & {}; + `${SolidUriPrefix}${SolidLeafSlug}` & {}; /** * @internal diff --git a/packages/connected/src/ConnectedLdoDataset.ts b/packages/connected/src/ConnectedLdoDataset.ts index 52aab40..7490a59 100644 --- a/packages/connected/src/ConnectedLdoDataset.ts +++ b/packages/connected/src/ConnectedLdoDataset.ts @@ -6,7 +6,10 @@ import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; import { InvalidIdentifierResource } from "./InvalidIdentifierResource"; import type { ConnectedContext } from "./ConnectedContext"; -type ReturnTypeFromArgs = Func extends (arg: Arg) => infer R +type ReturnTypeFromArgs = Func extends ( + arg: Arg, + context: any, +) => infer R ? R : never; diff --git a/packages/connected/src/test.ts b/packages/connected/src/test.ts new file mode 100644 index 0000000..e69de29