parent
3cb56a083f
commit
a879b00b44
@ -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 { 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<this> |
||||||
|
| ReadContainerResult |
||||||
|
| ContainerCreateAndOverwriteResult |
||||||
|
| ContainerCreateIfAbsentResult |
||||||
|
| CheckRootResult; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param uri - The uri of the container |
||||||
|
* @param context - SolidLdoDatasetContext for the parent dataset |
||||||
|
*/ |
||||||
|
constructor( |
||||||
|
uri: SolidContainerUri, |
||||||
|
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||||
|
) { |
||||||
|
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<this>, |
||||||
|
): 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<ReadContainerResult> { |
||||||
|
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<ReadContainerResult> { |
||||||
|
return super.readIfUnfetched() as Promise<ReadContainerResult>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* PARENT CONTAINER METHODS |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Checks if this container is a root container by making a request |
||||||
|
* @returns CheckRootResult |
||||||
|
*/ |
||||||
|
private async checkIfIsRootContainer(): Promise<CheckRootResult> { |
||||||
|
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<SolidContainer> |
||||||
|
> { |
||||||
|
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<ContainerCreateAndOverwriteResult>; |
||||||
|
createChildAndOverwrite( |
||||||
|
slug: SolidLeafSlug, |
||||||
|
): Promise<LeafCreateAndOverwriteResult>; |
||||||
|
createChildAndOverwrite( |
||||||
|
slug: string, |
||||||
|
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>; |
||||||
|
createChildAndOverwrite( |
||||||
|
slug: string, |
||||||
|
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult> { |
||||||
|
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<ContainerCreateIfAbsentResult>; |
||||||
|
createChildIfAbsent(slug: SolidLeafSlug): Promise<LeafCreateIfAbsentResult>; |
||||||
|
createChildIfAbsent( |
||||||
|
slug: string, |
||||||
|
): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult>; |
||||||
|
createChildIfAbsent( |
||||||
|
slug: string, |
||||||
|
): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult> { |
||||||
|
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<LeafCreateAndOverwriteResult> { |
||||||
|
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<LeafCreateIfAbsentResult> { |
||||||
|
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<DeleteSuccess<SolidContainer | SolidLeaf>> |
||||||
|
| AggregateError< |
||||||
|
| DeleteResultError<SolidContainer | SolidLeaf> |
||||||
|
| ReadResultError<SolidContainer | SolidLeaf> |
||||||
|
> |
||||||
|
> { |
||||||
|
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<SolidContainer | SolidLeaf>[], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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<this> |
||||||
|
| AggregateError< |
||||||
|
| DeleteResultError<SolidContainer | SolidLeaf> |
||||||
|
| ReadResultError<SolidContainer | SolidLeaf> |
||||||
|
> |
||||||
|
> { |
||||||
|
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<DeleteResult<this>> { |
||||||
|
return super.handleDelete() as Promise<DeleteResult<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<ContainerCreateAndOverwriteResult> { |
||||||
|
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<ContainerCreateIfAbsentResult> { |
||||||
|
const createResult = |
||||||
|
(await this.handleCreateIfAbsent()) as ContainerCreateIfAbsentResult; |
||||||
|
if (createResult.isError) return createResult; |
||||||
|
return { ...createResult, resource: this }; |
||||||
|
} |
||||||
|
} |
||||||
|
@ -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 { 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 |
* Represents the current status of a specific Leaf on a Pod as known by LDO. |
||||||
implements Resource<SolidLeafUri> { |
* |
||||||
public uri: SolidLeafUri; |
* @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<this> |
||||||
|
| ReadLeafResult |
||||||
|
| LeafCreateAndOverwriteResult |
||||||
|
| LeafCreateIfAbsentResult |
||||||
|
| UpdateResult<this>; |
||||||
|
|
||||||
|
/** |
||||||
|
* @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<SolidConnectedPlugin[]>, |
||||||
|
) { |
||||||
|
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<this>, |
||||||
|
): 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<ReadLeafResult> { |
||||||
|
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<ReadLeafResult> { |
||||||
|
return super.readIfUnfetched() as Promise<ReadLeafResult>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* 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<SolidContainer> { |
||||||
|
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<SolidContainer> |
||||||
|
> { |
||||||
|
// 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<this>) { |
||||||
|
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<DeleteResult<this>> { |
||||||
|
return this.handleDelete(); |
||||||
|
} |
||||||
|
|
||||||
|
protected async handleDelete(): Promise<DeleteResult<this>> { |
||||||
|
return super.handleDelete() as Promise<DeleteResult<this>>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* 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<this>): 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<LeafCreateAndOverwriteResult> { |
||||||
|
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<LeafCreateIfAbsentResult> { |
||||||
|
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<LeafCreateAndOverwriteResult> { |
||||||
|
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<LeafCreateIfAbsentResult> { |
||||||
|
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<Quad>, |
||||||
|
): Promise<UpdateResult<SolidLeaf>> { |
||||||
|
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 }; |
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue