You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

580 lines
17 KiB

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";
import type { HttpErrorResultType } from "../requester/results/error/HttpErrorResult";
/**
* 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 is HttpErrorResultType<this> => 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 };
}
}