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.
 
 
 

871 lines
25 KiB

import type {
ConnectedContext,
ConnectedResult,
IgnoredInvalidUpdateSuccess,
ReadSuccess,
Resource,
ResourceEventEmitter,
ResourceSuccess,
SubscriptionCallbacks,
Unfetched,
} from "@ldo/connected";
import type { SolidContainerUri, SolidLeafUri } from "../types.js";
import EventEmitter from "events";
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin.js";
import type { BatchedRequester } from "../requester/BatchedRequester.js";
import type { WacRule } from "../wac/WacRule.js";
import type { NotificationSubscription } from "@ldo/connected";
import { Websocket2023NotificationSubscription } from "../notifications/Websocket2023NotificationSubscription.js";
import { getParentUri } from "../util/rdfUtils.js";
import { isReadSuccess } from "../requester/results/success/SolidReadSuccess.js";
import type {
ReadContainerResult,
ReadLeafResult,
} from "../requester/requests/readResource.js";
import { DeleteSuccess } from "../requester/results/success/DeleteSuccess.js";
import {
updateDatasetOnSuccessfulDelete,
type DeleteResult,
} from "../requester/requests/deleteResource.js";
import type {
ContainerCreateAndOverwriteResult,
ContainerCreateIfAbsentResult,
LeafCreateAndOverwriteResult,
LeafCreateIfAbsentResult,
} from "../requester/requests/createDataResource.js";
import type { SolidContainer } from "./SolidContainer.js";
import type { CheckRootResultError } from "../requester/requests/checkRootContainer.js";
import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError.js";
import type { SolidLeaf } from "./SolidLeaf.js";
import type { GetWacUriError } from "../wac/getWacUri.js";
import { getWacUri, type GetWacUriResult } from "../wac/getWacUri.js";
import type { GetWacRuleError } from "../wac/getWacRule.js";
import { getWacRuleWithAclUri } from "../wac/getWacRule.js";
import type { SetWacRuleResult } from "../wac/setWacRule.js";
import { setWacRuleForAclUri } from "../wac/setWacRule.js";
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError.js";
import type { SolidNotificationMessage } from "../notifications/SolidNotificationMessage.js";
import type { CreateSuccess } from "../requester/results/success/CreateSuccess.js";
import { GetWacUriSuccess } from "../wac/results/GetWacUriSuccess.js";
import { GetWacRuleSuccess } from "../wac/results/GetWacRuleSuccess.js";
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { UpdateResult } from "../requester/requests/updateDataResource.js";
/**
* Statuses shared between both Leaf and Container
*/
export type SharedStatuses<ResourceType extends SolidLeaf | SolidContainer> =
| Unfetched<ResourceType>
| DeleteResult<ResourceType>
| CreateSuccess<ResourceType>;
export abstract class SolidResource
extends (EventEmitter as new () => ResourceEventEmitter)
implements Resource<SolidLeafUri | SolidContainerUri>
{
/**
* @internal
* The ConnectedContext from the Parent Dataset
*/
protected readonly context: ConnectedContext<SolidConnectedPlugin[]>;
/**
* The uri of the resource
*/
abstract readonly uri: SolidLeafUri | SolidContainerUri;
/**
* The type of resource (leaf or container)
*/
abstract readonly type: "SolidLeaf" | "SolidContainer";
/**
* The status of the last request made for this resource
*/
abstract status: ConnectedResult;
/**
* @internal
* Batched Requester for the Resource
*/
protected abstract readonly requester: BatchedRequester<
SolidLeaf | SolidContainer
>;
/**
* @internal
* True if this resource has been fetched at least once
*/
protected didInitialFetch: boolean = false;
/**
* @internal
* True if this resource has been fetched but does not exist
*/
protected absent: boolean | undefined;
/**
* @internal
* If a wac uri is fetched, it is cached here
*/
protected wacUri?: SolidLeafUri;
/**
* @internal
* If a wac rule was fetched, it is cached here
*/
protected wacRule?: WacRule;
/**
* @internal
* Handles notification subscriptions
*/
protected notificationSubscription: NotificationSubscription<
SolidConnectedPlugin,
SolidNotificationMessage
>;
/**
* Indicates that resources are not errors
*/
public readonly isError: false = false as const;
/**
* @param context - SolidLdoDatasetContext for the parent dataset
*/
constructor(context: ConnectedContext<SolidConnectedPlugin[]>) {
super();
this.context = context;
this.notificationSubscription = new Websocket2023NotificationSubscription(
this as unknown as SolidLeaf | SolidContainer,
this.onNotification.bind(this),
this.context,
);
}
/**
* ===========================================================================
* GETTERS
* ===========================================================================
*/
/**
* Checks to see if this resource is loading in any way
* @returns true if the resource is currently loading
*
* @example
* ```typescript
* resource.read().then(() => {
* // Logs "false"
* console.log(resource.isLoading())
* });
* // Logs "true"
* console.log(resource.isLoading());
* ```
*/
isLoading(): boolean {
return this.requester.isLoading();
}
/**
* Checks to see if this resource is being created
* @returns true if the resource is currently being created
*
* @example
* ```typescript
* resource.read().then(() => {
* // Logs "false"
* console.log(resource.isCreating())
* });
* // Logs "true"
* console.log(resource.isCreating());
* ```
*/
isCreating(): boolean {
return this.requester.isCreating();
}
/**
* Checks to see if this resource is being read
* @returns true if the resource is currently being read
*
* @example
* ```typescript
* resource.read().then(() => {
* // Logs "false"
* console.log(resource.isReading())
* });
* // Logs "true"
* console.log(resource.isReading());
* ```
*/
isReading(): boolean {
return this.requester.isReading();
}
/**
* Checks to see if this resource is being deleted
* @returns true if the resource is currently being deleted
*
* @example
* ```typescript
* resource.read().then(() => {
* // Logs "false"
* console.log(resource.isDeleting())
* });
* // Logs "true"
* console.log(resource.isDeleting());
* ```
*/
isDeleting(): boolean {
return this.requester.isDeletinng();
}
/**
* Checks to see if this resource is being read for the first time
* @returns true if the resource is currently being read for the first time
*
* @example
* ```typescript
* resource.read().then(() => {
* // Logs "false"
* console.log(resource.isDoingInitialFetch())
* });
* // Logs "true"
* console.log(resource.isDoingInitialFetch());
* ```
*/
isDoingInitialFetch(): boolean {
return this.isReading() && !this.isFetched();
}
/**
* Checks to see if this resource is being read for a subsequent time
* @returns true if the resource is currently being read for a subsequent time
*
* @example
* ```typescript
* await resource.read();
* resource.read().then(() => {
* // Logs "false"
* console.log(resource.isCreating())
* });
* // Logs "true"
* console.log(resource.isCreating());
* ```
*/
isReloading(): boolean {
return this.isReading() && this.isFetched();
}
/**
* ===========================================================================
* CHECKERS
* ===========================================================================
*/
/**
* Check to see if this resource has been fetched
* @returns true if this resource has been fetched before
*
* @example
* ```typescript
* // Logs "false"
* console.log(resource.isFetched());
* const result = await resource.read();
* if (!result.isError) {
* // Logs "true"
* console.log(resource.isFetched());
* }
* ```
*/
isFetched(): boolean {
return this.didInitialFetch;
}
/**
* Check to see if this resource is currently unfetched
* @returns true if the resource is currently unfetched
*
* @example
* ```typescript
* // Logs "true"
* console.log(resource.isUnetched());
* const result = await resource.read();
* if (!result.isError) {
* // Logs "false"
* console.log(resource.isUnfetched());
* }
* ```
*/
isUnfetched(): boolean {
return !this.didInitialFetch;
}
/**
* Is this resource currently absent (it does not exist)
* @returns true if the resource is absent, false if not, undefined if unknown
*
* @example
* ```typescript
* // Logs "undefined"
* console.log(resource.isAbsent());
* const result = await resource.read();
* if (!result.isError) {
* // False if the resource exists, true if it does not
* console.log(resource.isAbsent());
* }
* ```
*/
isAbsent(): boolean | undefined {
return this.absent;
}
/**
* Is this resource currently present on the Pod
* @returns false if the resource is absent, true if not, undefined if unknown
*
* @example
* ```typescript
* // Logs "undefined"
* console.log(resource.isPresent());
* const result = await resource.read();
* if (!result.isError) {
* // True if the resource exists, false if it does not
* console.log(resource.isPresent());
* }
* ```
*/
isPresent(): boolean | undefined {
return this.absent === undefined ? undefined : !this.absent;
}
/**
* Is this resource currently listening to notifications from this document
* @returns true if the resource is subscribed to notifications, false if not
*
* @example
* ```typescript
* await resource.subscribeToNotifications();
* // Logs "true"
* console.log(resource.isSubscribedToNotifications());
* ```
*/
isSubscribedToNotifications(): boolean {
return this.notificationSubscription.isSubscribedToNotifications();
}
/**
* ===========================================================================
* HELPER METHODS
* ===========================================================================
*/
/**
* @internal
* Emits an update event for both this resource and the parent
*/
protected emitThisAndParent() {
this.emit("update");
const parentUri = getParentUri(this.uri);
if (parentUri) {
const parentContainer = this.context.dataset.getResource(parentUri);
parentContainer.emit("update");
}
}
/**
* ===========================================================================
* READ METHODS
* ===========================================================================
*/
/**
* @internal
* A helper method updates this resource's internal state upon read success
* @param result - the result of the read success
*/
protected updateWithReadSuccess(result: ReadSuccess<Resource>) {
this.absent = result.type === "absentReadSuccess";
this.didInitialFetch = true;
}
/**
* @internal
* A helper method that handles the core functions for reading
* @returns ReadResult
*/
protected async handleRead(): Promise<ReadContainerResult | ReadLeafResult> {
const result = await this.requester.read();
this.status = result;
if (result.isError) {
this.emit("update");
return result;
}
this.updateWithReadSuccess(result);
this.emitThisAndParent();
return result;
}
/**
* @internal
* Converts the current state of this resource to a readResult
* @returns a ReadResult
*/
protected abstract toReadResult(): ReadLeafResult | ReadContainerResult;
/**
* Reads the resource
*/
abstract read(): Promise<ReadLeafResult | ReadContainerResult>;
/**
* Reads the resource if it isn't fetched yet
* @returns a ReadResult
*/
async readIfUnfetched(): Promise<ReadLeafResult | ReadContainerResult> {
if (this.didInitialFetch) {
const readResult = this.toReadResult();
this.status = readResult;
return readResult;
}
return this.read();
}
/**
* ===========================================================================
* DELETE METHODS
* ===========================================================================
*/
/**
* @internal
* A helper method updates this resource's internal state upon delete success
* @param result - the result of the delete success
*/
public updateWithDeleteSuccess(_result: DeleteSuccess<SolidResource>) {
this.absent = true;
this.didInitialFetch = true;
}
/**
* @internal
* Helper method that handles the core functions for deleting a resource
* @returns DeleteResult
*/
protected async handleDelete(): Promise<
DeleteResult<SolidLeaf | SolidContainer>
> {
const result = await this.requester.delete();
this.status = result;
if (result.isError) {
this.emit("update");
return result;
}
this.updateWithDeleteSuccess(result);
this.emitThisAndParent();
return result;
}
/**
* ===========================================================================
* CREATE METHODS
* ===========================================================================
*/
/**
* A helper method updates this resource's internal state upon create success
* @param _result - the result of the create success
*/
protected updateWithCreateSuccess(result: ResourceSuccess<Resource>) {
this.absent = false;
this.didInitialFetch = true;
if (isReadSuccess(result)) {
this.updateWithReadSuccess(result as ReadSuccess<this>);
}
}
/**
* Creates a resource at this URI and overwrites any that already exists
* @returns CreateAndOverwriteResult
*
* @example
* ```typescript
* const result = await resource.createAndOverwrite();
* if (!result.isError) {
* // Do something
* }
* ```
*/
abstract createAndOverwrite(): Promise<
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult
>;
/**
* @internal
* Helper method that handles the core functions for creating and overwriting
* a resource
* @returns DeleteResult
*/
protected async handleCreateAndOverwrite(): Promise<
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult
> {
const result = await this.requester.createDataResource(true);
this.status = result;
if (result.isError) {
this.emit("update");
return result;
}
this.updateWithCreateSuccess(result);
this.emitThisAndParent();
return result;
}
/**
* Creates a resource at this URI if the resource doesn't already exist
* @returns CreateIfAbsentResult
*
* @example
* ```typescript
* const result = await leaf.createIfAbsent();
* if (!result.isError) {
* // Do something
* }
* ```
*/
abstract createIfAbsent(): Promise<
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult
>;
/**
* @internal
* Helper method that handles the core functions for creating a resource if
* absent
* @returns DeleteResult
*/
protected async handleCreateIfAbsent(): Promise<
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult
> {
const result = await this.requester.createDataResource();
this.status = result;
if (result.isError) {
this.emit("update");
return result;
}
this.updateWithCreateSuccess(result);
this.emitThisAndParent();
return result;
}
/**
* UPDATE METHODS
*/
abstract update(
datasetChanges: DatasetChanges,
): Promise<
UpdateResult<SolidLeaf> | IgnoredInvalidUpdateSuccess<SolidContainer>
>;
/**
* ===========================================================================
* PARENT CONTAINER METHODS
* ===========================================================================
*/
/**
* Gets the root container for this resource.
* @returns The root container for this resource
*
* @example
* Suppose the root container is at `https://example.com/`
*
* ```typescript
* const resource = ldoSolidDataset
* .getResource("https://example.com/container/resource.ttl");
* const rootContainer = await resource.getRootContainer();
* if (!rootContainer.isError) {
* // logs "https://example.com/"
* console.log(rootContainer.uri);
* }
* ```
*/
abstract getRootContainer(): Promise<
| SolidContainer
| CheckRootResultError
| NoRootContainerError<SolidLeaf | SolidContainer>
>;
abstract getParentContainer(): Promise<
SolidContainer | CheckRootResultError | undefined
>;
/**
* ===========================================================================
* WEB ACCESS CONTROL METHODS
* ===========================================================================
*/
/**
* Retrieves the URI for the web access control (WAC) rules for this resource
* @param options - set the "ignoreCache" field to true to ignore any cached
* information on WAC rules.
* @returns WAC Rules results
*/
protected async getWacUri(options?: {
ignoreCache: boolean;
}): Promise<GetWacUriResult<SolidLeaf | SolidContainer>> {
const thisAsLeafOrContainer = this as unknown as SolidLeaf | SolidContainer;
// Get the wacUri if not already present
if (!options?.ignoreCache && this.wacUri) {
return new GetWacUriSuccess(thisAsLeafOrContainer, this.wacUri);
}
const wacUriResult = await getWacUri(thisAsLeafOrContainer, {
fetch: this.context.solid.fetch,
});
if (wacUriResult.isError) {
return wacUriResult;
}
this.wacUri = wacUriResult.wacUri;
return wacUriResult;
}
/**
* Retrieves web access control (WAC) rules for this resource
* @param options - set the "ignoreCache" field to true to ignore any cached
* information on WAC rules.
* @returns WAC Rules results
*
* @example
* ```typescript
* const resource = ldoSolidDataset
* .getResource("https://example.com/container/resource.ttl");
* const wacRulesResult = await resource.getWac();
* if (!wacRulesResult.isError) {
* const wacRules = wacRulesResult.wacRule;
* // True if the resource is publicly readable
* console.log(wacRules.public.read);
* // True if authenticated agents can write to the resource
* console.log(wacRules.authenticated.write);
* // True if the given WebId has append access
* console.log(
* wacRules.agent[https://example.com/person1/profile/card#me].append
* );
* // True if the given WebId has control access
* console.log(
* wacRules.agent[https://example.com/person1/profile/card#me].control
* );
* }
* ```
*/
async getWac(options?: {
ignoreCache: boolean;
}): Promise<
| GetWacUriError<SolidContainer | SolidLeaf>
| GetWacRuleError<SolidContainer | SolidLeaf>
| GetWacRuleSuccess<SolidContainer | SolidLeaf>
> {
const thisAsLeafOrContainer = this as unknown as SolidLeaf | SolidContainer;
// Return the wac rule if it's already cached
if (!options?.ignoreCache && this.wacRule) {
return new GetWacRuleSuccess(thisAsLeafOrContainer, this.wacRule);
}
// Get the wac uri
const wacUriResult = await this.getWacUri(options);
if (wacUriResult.isError) return wacUriResult;
// Get the wac rule
const wacResult = await getWacRuleWithAclUri(
wacUriResult.wacUri,
thisAsLeafOrContainer,
{
fetch: this.context.solid.fetch,
},
);
if (wacResult.isError) return wacResult;
// If the wac rules was successfully found
if (wacResult.type === "getWacRuleSuccess") {
this.wacRule = wacResult.wacRule;
return wacResult;
}
// If the WacRule is absent
const parentResource = await this.getParentContainer();
if (parentResource?.isError) return parentResource;
if (!parentResource) {
return new NoncompliantPodError(
thisAsLeafOrContainer,
`Resource "${this.uri}" has no Effective ACL resource`,
);
}
return parentResource.getWac();
}
/**
* Sets access rules for a specific resource
* @param wacRule - the access rules to set
* @returns SetWacRuleResult
*
* @example
* ```typescript
* const resource = ldoSolidDataset
* .getResource("https://example.com/container/resource.ttl");
* const wacRulesResult = await resource.setWac({
* public: {
* read: true,
* write: false,
* append: false,
* control: false
* },
* authenticated: {
* read: true,
* write: false,
* append: true,
* control: false
* },
* agent: {
* "https://example.com/person1/profile/card#me": {
* read: true,
* write: true,
* append: true,
* control: true
* }
* }
* });
* ```
*/
async setWac(
wacRule: WacRule,
): Promise<
| GetWacUriError<SolidLeaf | SolidContainer>
| SetWacRuleResult<SolidLeaf | SolidContainer>
> {
const thisAsLeafOrContainer = this as unknown as SolidLeaf | SolidContainer;
const wacUriResult = await this.getWacUri();
if (wacUriResult.isError) return wacUriResult;
const result = await setWacRuleForAclUri(
wacUriResult.wacUri,
wacRule,
thisAsLeafOrContainer,
{
fetch: this.context.solid.fetch,
},
);
if (result.isError) {
this.emit("update");
return result;
}
this.wacRule = result.wacRule;
return result;
}
/**
* ===========================================================================
* SUBSCRIPTION METHODS
* ===========================================================================
*/
/**
* Activates Websocket subscriptions on this resource. Updates, deletions,
* and creations on this resource will be tracked and all changes will be
* relected in LDO's resources and graph.
*
* @param onNotificationError - A callback function if there is an error
* with notifications.
* @returns SubscriptionId: A string to use to unsubscribe
*
* @example
* ```typescript
* const resource = solidLdoDataset
* .getResource("https://example.com/spiderman");
* // A listener for if anything about spiderman in the global dataset is
* // changed. Note that this will also listen for any local changes as well
* // as changes to remote resources to which you have notification
* // subscriptions enabled.
* solidLdoDataset.addListener(
* [namedNode("https://example.com/spiderman#spiderman"), null, null, null],
* () => {
* // Triggers when the file changes on the Pod or locally
* console.log("Something changed about SpiderMan");
* },
* );
*
* // Subscribe
* const subscriptionId = await testContainer.subscribeToNotifications({
* // These are optional callbacks. A subscription will automatically keep
* // the dataset in sync. Use these callbacks for additional functionality.
* onNotification: (message) => console.log(message),
* onNotificationError: (err) => console.log(err.message)
* });
* // ... From there you can wait for a file to be changed on the Pod.
*/
async subscribeToNotifications(
callbacks?: SubscriptionCallbacks<SolidNotificationMessage>,
): Promise<string> {
return await this.notificationSubscription.subscribeToNotifications(
callbacks,
);
}
/**
* @internal
* Function that triggers whenever a notification is recieved.
*/
protected async onNotification(
message: SolidNotificationMessage,
): Promise<void> {
const objectResource = this.context.dataset.getResource(message.object);
// Do Nothing if the resource is invalid.
if (objectResource.type === "InvalidIdentifierResource") return;
if (objectResource.type === "SolidLeaf") {
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;
}
}
}
/**
* Unsubscribes from changes made to this resource on the Pod
*
* @returns UnsubscribeResult
*
* @example
* ```typescript
* const subscriptionId = await testContainer.subscribeToNotifications();
* await testContainer.unsubscribeFromNotifications(subscriptionId);
* ```
*/
async unsubscribeFromNotifications(subscriptionId: string): Promise<void> {
return this.notificationSubscription.unsubscribeFromNotification(
subscriptionId,
);
}
/**
* Unsubscribes from all notifications on this resource
*
* @returns UnsubscribeResult[]
*
* @example
* ```typescript
* const subscriptionResult = await testContainer.subscribeToNotifications();
* await testContainer.unsubscribeFromAllNotifications();
* ```
*/
async unsubscribeFromAllNotifications(): Promise<void> {
return this.notificationSubscription.unsubscribeFromAllNotifications();
}
}