import type { DatasetChanges } from "@ldo/rdf-utils"; import type { Quad } from "@rdfjs/types"; import { LeafBatchedRequester } from "../requester/LeafBatchedRequester.js"; import type { CheckRootResultError } from "../requester/requests/checkRootContainer.js"; import type { LeafCreateAndOverwriteResult, LeafCreateIfAbsentResult, } from "../requester/requests/createDataResource.js"; import type { DeleteResult } from "../requester/requests/deleteResource.js"; import type { ReadLeafResult } from "../requester/requests/readResource.js"; import type { UpdateResult } from "../requester/requests/updateDataResource.js"; import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess.js"; import { DataReadSuccess } from "../requester/results/success/SolidReadSuccess.js"; import { BinaryReadSuccess } from "../requester/results/success/SolidReadSuccess.js"; import { getParentUri } from "../util/rdfUtils.js"; import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError.js"; import type { SharedStatuses } from "./SolidResource.js"; import { SolidResource } from "./SolidResource.js"; import type { SolidLeafUri } from "../types.js"; import type { ResourceSuccess } from "@ldo/connected"; import { AbsentReadSuccess, Unfetched, type ConnectedContext, } from "@ldo/connected"; import type { SolidConnectedPlugin } from "../SolidConnectedPlugin.js"; import type { SolidContainer } from "./SolidContainer.js"; /** * 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; /** * @internal * Batched Requester for the Leaf */ protected requester: LeafBatchedRequester; /** * Indicates that this resource is a leaf resource */ readonly type = "SolidLeaf" 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 new AbsentReadSuccess(this, true); } else if (this.isBinary()) { return new BinaryReadSuccess( this, true, this.binaryData!.blob, this.binaryData!.mimeType, ); } else { return new DataReadSuccess(this, true); } } /** * 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(); } /** * @internal */ 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 }; } }