parent
356ecf8c67
commit
3cb56a083f
@ -1,33 +1,57 @@ |
|||||||
import type { ConnectedPlugin } from "@ldo/connected"; |
import type { ConnectedContext, ConnectedPlugin } from "@ldo/connected"; |
||||||
import type { SolidContainerUri, SolidLeafUri, SolidUri } from "./types"; |
import type { SolidContainerUri, SolidLeafUri, SolidUri } from "./types"; |
||||||
import type { SolidLeaf } from "./resources/SolidLeaf"; |
import { SolidLeaf } from "./resources/SolidLeaf"; |
||||||
import type { SolidContainer } from "./resources/SolidContainer"; |
import { SolidContainer } from "./resources/SolidContainer"; |
||||||
|
import { isSolidContainerUri, isSolidUri } from "./util/isSolidUri"; |
||||||
|
|
||||||
export interface SolidConnectedPlugin extends ConnectedPlugin { |
export interface SolidConnectedContext { |
||||||
|
fetch?: typeof fetch; |
||||||
|
} |
||||||
|
export interface SolidConnectedPlugin |
||||||
|
extends ConnectedPlugin< |
||||||
|
"solid", |
||||||
|
SolidUri, |
||||||
|
SolidLeaf | SolidContainer, |
||||||
|
SolidConnectedContext |
||||||
|
> { |
||||||
name: "solid"; |
name: "solid"; |
||||||
identifierType: SolidUri; |
|
||||||
getResource: |
getResource: |
||||||
| ((uri: SolidLeafUri) => SolidLeaf) |
| ((uri: SolidLeafUri, context: ConnectedContext<this[]>) => SolidLeaf) |
||||||
| ((uri: SolidContainerUri) => SolidContainer); |
| (( |
||||||
createResource(): Promise<SolidLeaf>; |
uri: SolidContainerUri, |
||||||
isUriValid(uri: string): uri is SolidLeafUri | SolidContainerUri; |
context: ConnectedContext<this[]>, |
||||||
normalizeUri?: (uri: string) => SolidLeafUri | SolidContainerUri; |
) => SolidContainer); |
||||||
context: { |
createResource(context: ConnectedContext<this[]>): Promise<SolidLeaf>; |
||||||
fetch?: typeof fetch; |
|
||||||
}; |
|
||||||
} |
} |
||||||
|
|
||||||
export const solidConnectedPlugin: SolidConnectedPlugin = { |
export const solidConnectedPlugin: SolidConnectedPlugin = { |
||||||
name: "solid", |
name: "solid", |
||||||
identifierType: "https://example.com", |
|
||||||
getResource(_uri: SolidUri): SolidContainer | SolidLeaf { |
getResource: function ( |
||||||
throw new Error("Not Implemented"); |
uri: SolidLeafUri | SolidContainerUri, |
||||||
|
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||||
|
): SolidLeaf | SolidContainer { |
||||||
|
if (isSolidContainerUri(uri)) { |
||||||
|
return new SolidContainer(uri, context); |
||||||
|
} else { |
||||||
|
return new SolidLeaf(uri, context); |
||||||
|
} |
||||||
}, |
}, |
||||||
|
|
||||||
createResource: function (): Promise<SolidLeaf> { |
createResource: function (): Promise<SolidLeaf> { |
||||||
throw new Error("Function not implemented."); |
throw new Error("Function not implemented."); |
||||||
}, |
}, |
||||||
isUriValid: function (uri: string): uri is SolidLeafUri | SolidContainerUri { |
|
||||||
throw new Error("Function not implemented."); |
isUriValid: function ( |
||||||
|
uri: SolidContainerUri | SolidLeafUri, |
||||||
|
): uri is SolidLeafUri | SolidContainerUri { |
||||||
|
return isSolidUri(uri); |
||||||
|
}, |
||||||
|
|
||||||
|
initialContext: { |
||||||
|
fetch: undefined, |
||||||
}, |
}, |
||||||
context: {}, |
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore "Types" only exists for the typing system
|
||||||
|
types: {}, |
||||||
}; |
}; |
||||||
|
@ -0,0 +1,10 @@ |
|||||||
|
/** |
||||||
|
* A message sent from the Pod as a notification |
||||||
|
*/ |
||||||
|
export interface SolidNotificationMessage { |
||||||
|
"@context": string | string[]; |
||||||
|
id: string; |
||||||
|
type: "Update" | "Delete" | "Remove" | "Add"; |
||||||
|
object: string; |
||||||
|
published: string; |
||||||
|
} |
@ -0,0 +1,146 @@ |
|||||||
|
import type { ConnectedContext } from "@ldo/connected"; |
||||||
|
import type { NotificationCallbackError } from "./results/NotificationErrors"; |
||||||
|
import { v4 } from "uuid"; |
||||||
|
import type { SolidContainer } from "../resources/SolidContainer"; |
||||||
|
import type { SolidLeaf } from "../resources/SolidLeaf"; |
||||||
|
import type { SolidNotificationMessage } from "./SolidNotificationMessage"; |
||||||
|
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||||
|
|
||||||
|
export interface SubscriptionCallbacks { |
||||||
|
onNotification?: (message: SolidNotificationMessage) => void; |
||||||
|
// TODO: make notification errors more specific
|
||||||
|
onNotificationError?: (error: Error) => void; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Abstract class for notification subscription methods. |
||||||
|
*/ |
||||||
|
export abstract class SolidNotificationSubscription { |
||||||
|
protected resource: SolidContainer | SolidLeaf; |
||||||
|
protected parentSubscription: (message: SolidNotificationMessage) => void; |
||||||
|
protected context: ConnectedContext<SolidConnectedPlugin[]>; |
||||||
|
protected subscriptions: Record<string, SubscriptionCallbacks> = {}; |
||||||
|
private isOpen: boolean = false; |
||||||
|
|
||||||
|
constructor( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
parentSubscription: (message: SolidNotificationMessage) => void, |
||||||
|
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||||
|
) { |
||||||
|
this.resource = resource; |
||||||
|
this.parentSubscription = parentSubscription; |
||||||
|
this.context = context; |
||||||
|
} |
||||||
|
|
||||||
|
public isSubscribedToNotifications(): boolean { |
||||||
|
return this.isOpen; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* PUBLIC |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* subscribeToNotifications |
||||||
|
*/ |
||||||
|
async subscribeToNotifications( |
||||||
|
subscriptionCallbacks?: SubscriptionCallbacks, |
||||||
|
): Promise<string> { |
||||||
|
const subscriptionId = v4(); |
||||||
|
this.subscriptions[subscriptionId] = subscriptionCallbacks ?? {}; |
||||||
|
if (!this.isOpen) { |
||||||
|
await this.open(); |
||||||
|
this.setIsOpen(true); |
||||||
|
} |
||||||
|
return subscriptionId; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* unsubscribeFromNotification |
||||||
|
*/ |
||||||
|
async unsubscribeFromNotification(subscriptionId: string): Promise<void> { |
||||||
|
if ( |
||||||
|
!!this.subscriptions[subscriptionId] && |
||||||
|
Object.keys(this.subscriptions).length === 1 |
||||||
|
) { |
||||||
|
await this.close(); |
||||||
|
this.setIsOpen(false); |
||||||
|
} |
||||||
|
delete this.subscriptions[subscriptionId]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* unsubscribeFromAllNotifications |
||||||
|
*/ |
||||||
|
async unsubscribeFromAllNotifications(): Promise<void> { |
||||||
|
await Promise.all( |
||||||
|
Object.keys(this.subscriptions).map((id) => |
||||||
|
this.unsubscribeFromNotification(id), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* HELPERS |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Opens the subscription |
||||||
|
*/ |
||||||
|
protected abstract open(): Promise<void>; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Closes the subscription |
||||||
|
*/ |
||||||
|
protected abstract close(): Promise<void>; |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* CALLBACKS |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* onNotification |
||||||
|
*/ |
||||||
|
protected onNotification(message: SolidNotificationMessage): void { |
||||||
|
this.parentSubscription(message); |
||||||
|
Object.values(this.subscriptions).forEach(({ onNotification }) => { |
||||||
|
onNotification?.(message); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* onNotificationError |
||||||
|
*/ |
||||||
|
protected onNotificationError(message: NotificationCallbackError): void { |
||||||
|
Object.values(this.subscriptions).forEach(({ onNotificationError }) => { |
||||||
|
onNotificationError?.(message); |
||||||
|
}); |
||||||
|
if (message.type === "disconnectedNotAttemptingReconnectError") { |
||||||
|
this.setIsOpen(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* setIsOpen |
||||||
|
*/ |
||||||
|
protected setIsOpen(status: boolean) { |
||||||
|
const shouldUpdate = status !== this.isOpen; |
||||||
|
this.isOpen = status; |
||||||
|
if (shouldUpdate) this.resource.emit("update"); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,134 @@ |
|||||||
|
import { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||||
|
import { SubscriptionClient } from "@solid-notifications/subscription"; |
||||||
|
import { WebSocket } from "ws"; |
||||||
|
import { |
||||||
|
DisconnectedAttemptingReconnectError, |
||||||
|
DisconnectedNotAttemptingReconnectError, |
||||||
|
UnsupportedNotificationError, |
||||||
|
} from "./results/NotificationErrors"; |
||||||
|
import type { NotificationMessage } from "./NotificationMessage"; |
||||||
|
import type { Resource } from "../Resource"; |
||||||
|
import type { SolidLdoDatasetContext } from "../../SolidLdoDatasetContext"; |
||||||
|
import type { |
||||||
|
ChannelType, |
||||||
|
NotificationChannel, |
||||||
|
} from "@solid-notifications/types"; |
||||||
|
import { SolidNotificationSubscription } from "./SolidNotificationSubscription"; |
||||||
|
|
||||||
|
const CHANNEL_TYPE = |
||||||
|
"http://www.w3.org/ns/solid/notifications#WebSocketChannel2023"; |
||||||
|
|
||||||
|
export class Websocket2023NotificationSubscription extends SolidNotificationSubscription { |
||||||
|
private socket: WebSocket | undefined; |
||||||
|
private createWebsocket: (address: string) => WebSocket; |
||||||
|
|
||||||
|
// Reconnection data
|
||||||
|
// How often we should attempt a reconnection
|
||||||
|
private reconnectInterval = 5000; |
||||||
|
// How many attempts have already been tried for a reconnection
|
||||||
|
private reconnectAttempts = 0; |
||||||
|
// Whether or not the socket was manually closes
|
||||||
|
private isManualClose = false; |
||||||
|
// Maximum number of attempts to reconnect
|
||||||
|
private maxReconnectAttempts = 6; |
||||||
|
|
||||||
|
constructor( |
||||||
|
resource: Resource, |
||||||
|
parentSubscription: (message: NotificationMessage) => void, |
||||||
|
context: SolidLdoDatasetContext, |
||||||
|
createWebsocket?: (address: string) => WebSocket, |
||||||
|
) { |
||||||
|
super(resource, parentSubscription, context); |
||||||
|
this.createWebsocket = createWebsocket ?? createWebsocketDefault; |
||||||
|
} |
||||||
|
|
||||||
|
async open(): Promise<void> { |
||||||
|
try { |
||||||
|
const notificationChannel = await this.discoverNotificationChannel(); |
||||||
|
await this.subscribeToWebsocket(notificationChannel); |
||||||
|
} catch (err) { |
||||||
|
if ( |
||||||
|
err instanceof Error && |
||||||
|
err.message.startsWith("Discovery did not succeed") |
||||||
|
) { |
||||||
|
this.onNotificationError( |
||||||
|
new UnsupportedNotificationError(this.resource.uri, err.message), |
||||||
|
); |
||||||
|
} else { |
||||||
|
this.onNotificationError( |
||||||
|
UnexpectedResourceError.fromThrown(this.resource.uri, err), |
||||||
|
); |
||||||
|
} |
||||||
|
this.onClose(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async discoverNotificationChannel(): Promise<NotificationChannel> { |
||||||
|
const client = new SubscriptionClient(this.context.fetch); |
||||||
|
return await client.subscribe( |
||||||
|
this.resource.uri, |
||||||
|
CHANNEL_TYPE as ChannelType, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public async subscribeToWebsocket( |
||||||
|
notificationChannel: NotificationChannel, |
||||||
|
): Promise<void> { |
||||||
|
this.socket = this.createWebsocket( |
||||||
|
notificationChannel.receiveFrom as string, |
||||||
|
); |
||||||
|
|
||||||
|
this.socket.onopen = () => { |
||||||
|
this.reconnectAttempts = 0; // Reset attempts on successful connection
|
||||||
|
this.isManualClose = false; // Reset manual close flag
|
||||||
|
}; |
||||||
|
|
||||||
|
this.socket.onmessage = (message) => { |
||||||
|
const messageData = message.data.toString(); |
||||||
|
// TODO uncompliant Pod error on misformatted message
|
||||||
|
this.onNotification(JSON.parse(messageData) as NotificationMessage); |
||||||
|
}; |
||||||
|
|
||||||
|
this.socket.onclose = this.onClose.bind(this); |
||||||
|
|
||||||
|
this.socket.onerror = (err) => { |
||||||
|
this.onNotificationError( |
||||||
|
new UnexpectedResourceError(this.resource.uri, err.error), |
||||||
|
); |
||||||
|
}; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
private onClose() { |
||||||
|
if (!this.isManualClose) { |
||||||
|
// Attempt to reconnect only if the disconnection was unintentional
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) { |
||||||
|
this.reconnectAttempts++; |
||||||
|
setTimeout(() => { |
||||||
|
this.open(); |
||||||
|
}, this.reconnectInterval); |
||||||
|
this.onNotificationError( |
||||||
|
new DisconnectedAttemptingReconnectError( |
||||||
|
this.resource.uri, |
||||||
|
`Attempting to reconnect to Websocket for ${this.resource.uri}.`, |
||||||
|
), |
||||||
|
); |
||||||
|
} else { |
||||||
|
this.onNotificationError( |
||||||
|
new DisconnectedNotAttemptingReconnectError( |
||||||
|
this.resource.uri, |
||||||
|
`Lost connection to websocket for ${this.resource.uri}.`, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected async close(): Promise<void> { |
||||||
|
this.socket?.terminate(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function createWebsocketDefault(address: string) { |
||||||
|
return new WebSocket(address); |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import type { UnexpectedResourceError } from "../../../requester/results/error/ErrorResult"; |
||||||
|
import { ResourceError } from "../../../requester/results/error/ErrorResult"; |
||||||
|
|
||||||
|
export type NotificationCallbackError = |
||||||
|
| DisconnectedAttemptingReconnectError |
||||||
|
| DisconnectedNotAttemptingReconnectError |
||||||
|
| UnsupportedNotificationError |
||||||
|
| UnexpectedResourceError; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the requested method for receiving notifications is not |
||||||
|
* supported by this Pod. |
||||||
|
*/ |
||||||
|
export class UnsupportedNotificationError extends ResourceError { |
||||||
|
readonly type = "unsupportedNotificationError" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the socket has disconnected and is attempting to reconnect. |
||||||
|
*/ |
||||||
|
export class DisconnectedAttemptingReconnectError extends ResourceError { |
||||||
|
readonly type = "disconnectedAttemptingReconnectError" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the socket has disconnected and is attempting to reconnect. |
||||||
|
*/ |
||||||
|
export class DisconnectedNotAttemptingReconnectError extends ResourceError { |
||||||
|
readonly type = "disconnectedNotAttemptingReconnectError" as const; |
||||||
|
} |
@ -0,0 +1,203 @@ |
|||||||
|
import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher"; |
||||||
|
import type { ConnectedContext } from "@ldo/connected"; |
||||||
|
import type { |
||||||
|
ContainerCreateAndOverwriteResult, |
||||||
|
ContainerCreateIfAbsentResult, |
||||||
|
LeafCreateAndOverwriteResult, |
||||||
|
LeafCreateIfAbsentResult, |
||||||
|
} from "./requests/createDataResource"; |
||||||
|
import { createDataResource } from "./requests/createDataResource"; |
||||||
|
import type { |
||||||
|
ReadContainerResult, |
||||||
|
ReadLeafResult, |
||||||
|
} from "./requests/readResource"; |
||||||
|
import { readResource } from "./requests/readResource"; |
||||||
|
import type { DeleteResult } from "./requests/deleteResource"; |
||||||
|
import { deleteResource } from "./requests/deleteResource"; |
||||||
|
import { modifyQueueByMergingEventsWithTheSameKeys } from "./util/modifyQueueFuntions"; |
||||||
|
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||||
|
import type { SolidContainer } from "../resources/SolidContainer"; |
||||||
|
import type { SolidLeaf } from "../resources/SolidLeaf"; |
||||||
|
|
||||||
|
const READ_KEY = "read"; |
||||||
|
const CREATE_KEY = "createDataResource"; |
||||||
|
const DELETE_KEY = "delete"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* |
||||||
|
* A singleton for handling batched requests |
||||||
|
*/ |
||||||
|
export abstract class BatchedRequester< |
||||||
|
ResourceType extends SolidContainer | SolidLeaf, |
||||||
|
> { |
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* A request batcher to maintain state for ongoing requests |
||||||
|
*/ |
||||||
|
protected readonly requestBatcher = new RequestBatcher(); |
||||||
|
|
||||||
|
/** |
||||||
|
* The uri of the resource |
||||||
|
*/ |
||||||
|
abstract readonly resource: ResourceType; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* ConnectedContext for the parent Dataset |
||||||
|
*/ |
||||||
|
protected context: ConnectedContext<SolidConnectedPlugin[]>; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param context - SolidLdoDatasetContext for the parent SolidLdoDataset |
||||||
|
*/ |
||||||
|
constructor(context: ConnectedContext<SolidConnectedPlugin[]>) { |
||||||
|
this.context = context; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the resource is currently making any request |
||||||
|
* @returns true if the resource is making any requests |
||||||
|
*/ |
||||||
|
isLoading(): boolean { |
||||||
|
return this.requestBatcher.isLoading(ANY_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the resource is currently executing a create request |
||||||
|
* @returns true if the resource is currently executing a create request |
||||||
|
*/ |
||||||
|
isCreating(): boolean { |
||||||
|
return this.requestBatcher.isLoading(CREATE_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the resource is currently executing a read request |
||||||
|
* @returns true if the resource is currently executing a read request |
||||||
|
*/ |
||||||
|
isReading(): boolean { |
||||||
|
return this.requestBatcher.isLoading(READ_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the resource is currently executing a delete request |
||||||
|
* @returns true if the resource is currently executing a delete request |
||||||
|
*/ |
||||||
|
isDeletinng(): boolean { |
||||||
|
return this.requestBatcher.isLoading(DELETE_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Read this resource. |
||||||
|
* @returns A ReadLeafResult or a ReadContainerResult depending on the uri of |
||||||
|
* this resource |
||||||
|
*/ |
||||||
|
async read(): Promise<ReadLeafResult | ReadContainerResult> { |
||||||
|
const transaction = this.context.dataset.startTransaction(); |
||||||
|
const result = await this.requestBatcher.queueProcess({ |
||||||
|
name: READ_KEY, |
||||||
|
args: [ |
||||||
|
this.resource, |
||||||
|
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||||
|
], |
||||||
|
perform: readResource, |
||||||
|
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(READ_KEY), |
||||||
|
after: (result) => { |
||||||
|
if (!result.isError) { |
||||||
|
transaction.commit(); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Delete this resource |
||||||
|
* @returns A DeleteResult |
||||||
|
*/ |
||||||
|
async delete(): Promise<DeleteResult<ResourceType>> { |
||||||
|
const transaction = this.context.dataset.startTransaction(); |
||||||
|
const result = await this.requestBatcher.queueProcess({ |
||||||
|
name: DELETE_KEY, |
||||||
|
args: [ |
||||||
|
this.resource, |
||||||
|
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||||
|
], |
||||||
|
perform: deleteResource, |
||||||
|
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(DELETE_KEY), |
||||||
|
after: (result) => { |
||||||
|
if (!result.isError) { |
||||||
|
transaction.commit(); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
return result as DeleteResult<ResourceType>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a Resource |
||||||
|
* @param overwrite - If true, this will orverwrite the resource if it already |
||||||
|
* exists |
||||||
|
* @returns A ContainerCreateAndOverwriteResult or a |
||||||
|
* LeafCreateAndOverwriteResult depending on this resource's URI |
||||||
|
*/ |
||||||
|
createDataResource( |
||||||
|
overwrite: true, |
||||||
|
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>; |
||||||
|
createDataResource( |
||||||
|
overwrite?: false, |
||||||
|
): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult>; |
||||||
|
createDataResource( |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise< |
||||||
|
| ContainerCreateAndOverwriteResult |
||||||
|
| LeafCreateAndOverwriteResult |
||||||
|
| ContainerCreateIfAbsentResult |
||||||
|
| LeafCreateIfAbsentResult |
||||||
|
>; |
||||||
|
async createDataResource( |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise< |
||||||
|
| ContainerCreateAndOverwriteResult |
||||||
|
| LeafCreateAndOverwriteResult |
||||||
|
| ContainerCreateIfAbsentResult |
||||||
|
| LeafCreateIfAbsentResult |
||||||
|
> { |
||||||
|
const transaction = this.context.dataset.startTransaction(); |
||||||
|
const result = await this.requestBatcher.queueProcess({ |
||||||
|
name: CREATE_KEY, |
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore Apparently this is a nasty type and I can't be bothered to fix it
|
||||||
|
args: [ |
||||||
|
this.resource, |
||||||
|
overwrite ?? false, |
||||||
|
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||||
|
], |
||||||
|
perform: createDataResource, |
||||||
|
modifyQueue: (queue, currentlyLoading, args) => { |
||||||
|
const lastElementInQueue = queue[queue.length - 1]; |
||||||
|
if ( |
||||||
|
lastElementInQueue && |
||||||
|
lastElementInQueue.name === CREATE_KEY && |
||||||
|
!!lastElementInQueue.args[1] === !!args[1] |
||||||
|
) { |
||||||
|
return lastElementInQueue; |
||||||
|
} |
||||||
|
if ( |
||||||
|
currentlyLoading && |
||||||
|
currentlyLoading.name === CREATE_KEY && |
||||||
|
!!currentlyLoading.args[1] === !!args[1] |
||||||
|
) { |
||||||
|
return currentlyLoading; |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
}, |
||||||
|
after: (result) => { |
||||||
|
if (!result.isError) { |
||||||
|
transaction.commit(); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
import type { ConnectedContext } from "@ldo/connected"; |
||||||
|
import type { SolidContainer } from "../resources/SolidContainer"; |
||||||
|
import { BatchedRequester } from "./BatchedRequester"; |
||||||
|
import type { CheckRootResult } from "./requests/checkRootContainer"; |
||||||
|
import { checkRootContainer } from "./requests/checkRootContainer"; |
||||||
|
import type { |
||||||
|
ContainerCreateAndOverwriteResult, |
||||||
|
ContainerCreateIfAbsentResult, |
||||||
|
} from "./requests/createDataResource"; |
||||||
|
import type { ReadContainerResult } from "./requests/readResource"; |
||||||
|
import { modifyQueueByMergingEventsWithTheSameKeys } from "./util/modifyQueueFuntions"; |
||||||
|
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||||
|
|
||||||
|
export const IS_ROOT_CONTAINER_KEY = "isRootContainer"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* |
||||||
|
* A singleton to handle batched requests for containers |
||||||
|
*/ |
||||||
|
export class ContainerBatchedRequester extends BatchedRequester<SolidContainer> { |
||||||
|
/** |
||||||
|
* The URI of the container |
||||||
|
*/ |
||||||
|
readonly resource: SolidContainer; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param uri - The URI of the container |
||||||
|
* @param context - ConnectedContext of the parent dataset |
||||||
|
*/ |
||||||
|
constructor( |
||||||
|
resource: SolidContainer, |
||||||
|
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||||
|
) { |
||||||
|
super(context); |
||||||
|
this.resource = resource; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads the container |
||||||
|
* @returns A ReadContainerResult |
||||||
|
*/ |
||||||
|
read(): Promise<ReadContainerResult> { |
||||||
|
return super.read() as Promise<ReadContainerResult>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates the container |
||||||
|
* @param overwrite - If true, this will orverwrite the resource if it already |
||||||
|
* exists |
||||||
|
*/ |
||||||
|
createDataResource( |
||||||
|
overwrite: true, |
||||||
|
): Promise<ContainerCreateAndOverwriteResult>; |
||||||
|
createDataResource(overwrite?: false): Promise<ContainerCreateIfAbsentResult>; |
||||||
|
createDataResource( |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>; |
||||||
|
createDataResource( |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise< |
||||||
|
ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult |
||||||
|
> { |
||||||
|
return super.createDataResource(overwrite) as Promise< |
||||||
|
ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult |
||||||
|
>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks to see if this container is a root container |
||||||
|
* @returns A CheckRootResult |
||||||
|
*/ |
||||||
|
async isRootContainer(): Promise<CheckRootResult> { |
||||||
|
return this.requestBatcher.queueProcess({ |
||||||
|
name: IS_ROOT_CONTAINER_KEY, |
||||||
|
args: [this.resource, { fetch: this.context.solid.fetch }], |
||||||
|
perform: checkRootContainer, |
||||||
|
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys( |
||||||
|
IS_ROOT_CONTAINER_KEY, |
||||||
|
), |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,179 @@ |
|||||||
|
import type { DatasetChanges } from "@ldo/rdf-utils"; |
||||||
|
import { mergeDatasetChanges } from "@ldo/subscribable-dataset"; |
||||||
|
import type { Quad } from "@rdfjs/types"; |
||||||
|
import { BatchedRequester } from "./BatchedRequester"; |
||||||
|
import type { |
||||||
|
LeafCreateAndOverwriteResult, |
||||||
|
LeafCreateIfAbsentResult, |
||||||
|
} from "./requests/createDataResource"; |
||||||
|
import type { ReadLeafResult } from "./requests/readResource"; |
||||||
|
import type { UpdateResult } from "./requests/updateDataResource"; |
||||||
|
import { updateDataResource } from "./requests/updateDataResource"; |
||||||
|
import { uploadResource } from "./requests/uploadResource"; |
||||||
|
import type { SolidLeaf } from "../resources/SolidLeaf"; |
||||||
|
import type { ConnectedContext } from "@ldo/connected"; |
||||||
|
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||||
|
|
||||||
|
export const UPDATE_KEY = "update"; |
||||||
|
export const UPLOAD_KEY = "upload"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* |
||||||
|
* A singleton to handle batched requests for leafs |
||||||
|
*/ |
||||||
|
export class LeafBatchedRequester extends BatchedRequester<SolidLeaf> { |
||||||
|
/** |
||||||
|
* The URI of the leaf |
||||||
|
*/ |
||||||
|
readonly resource: SolidLeaf; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param uri - the URI of the leaf |
||||||
|
* @param context - SolidLdoDatasetContext of the parent dataset |
||||||
|
*/ |
||||||
|
constructor( |
||||||
|
resource: SolidLeaf, |
||||||
|
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||||
|
) { |
||||||
|
super(context); |
||||||
|
this.resource = resource; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the resource is currently executing an update request |
||||||
|
* @returns true if the resource is currently executing an update request |
||||||
|
*/ |
||||||
|
isUpdating(): boolean { |
||||||
|
return this.requestBatcher.isLoading(UPDATE_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the resource is currently executing an upload request |
||||||
|
* @returns true if the resource is currently executing an upload request |
||||||
|
*/ |
||||||
|
isUploading(): boolean { |
||||||
|
return this.requestBatcher.isLoading(UPLOAD_KEY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads the leaf |
||||||
|
* @returns A ReadLeafResult |
||||||
|
*/ |
||||||
|
async read(): Promise<ReadLeafResult> { |
||||||
|
return super.read() as Promise<ReadLeafResult>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates the leaf as a data resource |
||||||
|
* @param overwrite - If true, this will orverwrite the resource if it already |
||||||
|
* exists |
||||||
|
*/ |
||||||
|
createDataResource(overwrite: true): Promise<LeafCreateAndOverwriteResult>; |
||||||
|
createDataResource(overwrite?: false): Promise<LeafCreateIfAbsentResult>; |
||||||
|
createDataResource( |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>; |
||||||
|
createDataResource( |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult> { |
||||||
|
return super.createDataResource(overwrite) as Promise< |
||||||
|
LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult |
||||||
|
>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Update the data on this resource |
||||||
|
* @param changes - DatasetChanges that should be applied to the Pod |
||||||
|
*/ |
||||||
|
async updateDataResource( |
||||||
|
changes: DatasetChanges<Quad>, |
||||||
|
): Promise<UpdateResult<SolidLeaf>> { |
||||||
|
const result = await this.requestBatcher.queueProcess({ |
||||||
|
name: UPDATE_KEY, |
||||||
|
args: [ |
||||||
|
this.resource, |
||||||
|
changes, |
||||||
|
{ |
||||||
|
fetch: this.context.solid.fetch, |
||||||
|
dataset: this.context.dataset, |
||||||
|
}, |
||||||
|
], |
||||||
|
perform: updateDataResource, |
||||||
|
modifyQueue: (queue, currentlyProcessing, [, changes]) => { |
||||||
|
if (queue[queue.length - 1]?.name === UPDATE_KEY) { |
||||||
|
// Merge Changes
|
||||||
|
const originalChanges = queue[queue.length - 1].args[1]; |
||||||
|
mergeDatasetChanges(originalChanges, changes); |
||||||
|
return queue[queue.length - 1]; |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
}, |
||||||
|
}); |
||||||
|
return result as UpdateResult<SolidLeaf>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Upload a binary at this resource's URI |
||||||
|
* @param blob - A binary blob |
||||||
|
* @param mimeType - the mime type of the blob |
||||||
|
* @param overwrite: If true, will overwrite an existing file |
||||||
|
*/ |
||||||
|
upload( |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite: true, |
||||||
|
): Promise<LeafCreateAndOverwriteResult>; |
||||||
|
upload( |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite?: false, |
||||||
|
): Promise<LeafCreateIfAbsentResult>; |
||||||
|
upload( |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise<LeafCreateAndOverwriteResult | LeafCreateIfAbsentResult>; |
||||||
|
async upload( |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite?: boolean, |
||||||
|
): Promise<LeafCreateAndOverwriteResult | LeafCreateIfAbsentResult> { |
||||||
|
const transaction = this.context.dataset.startTransaction(); |
||||||
|
const result = await this.requestBatcher.queueProcess({ |
||||||
|
name: UPLOAD_KEY, |
||||||
|
args: [ |
||||||
|
this.resource, |
||||||
|
blob, |
||||||
|
mimeType, |
||||||
|
overwrite, |
||||||
|
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||||
|
], |
||||||
|
perform: uploadResource, |
||||||
|
modifyQueue: (queue, currentlyLoading, args) => { |
||||||
|
const lastElementInQueue = queue[queue.length - 1]; |
||||||
|
if ( |
||||||
|
lastElementInQueue && |
||||||
|
lastElementInQueue.name === UPLOAD_KEY && |
||||||
|
!!lastElementInQueue.args[3] === !!args[3] |
||||||
|
) { |
||||||
|
return lastElementInQueue; |
||||||
|
} |
||||||
|
if ( |
||||||
|
currentlyLoading && |
||||||
|
currentlyLoading.name === UPLOAD_KEY && |
||||||
|
!!currentlyLoading.args[3] === !!args[3] |
||||||
|
) { |
||||||
|
return currentlyLoading; |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
}, |
||||||
|
after: (result) => { |
||||||
|
if (!result.isError) { |
||||||
|
transaction.commit(); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
import type { BasicRequestOptions } from "./requestOptions"; |
||||||
|
import { parse as parseLinkHeader } from "http-link-header"; |
||||||
|
import { CheckRootContainerSuccess } from "../results/success/CheckRootContainerSuccess"; |
||||||
|
import type { |
||||||
|
HttpErrorResultType, |
||||||
|
UnexpectedHttpError, |
||||||
|
} from "../results/error/HttpErrorResult"; |
||||||
|
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||||
|
import { UnexpectedResourceError } from "@ldo/connected"; |
||||||
|
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
|
||||||
|
/** |
||||||
|
* checkRootContainer result |
||||||
|
*/ |
||||||
|
export type CheckRootResult = CheckRootContainerSuccess | CheckRootResultError; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible errors checkRootResult can return |
||||||
|
*/ |
||||||
|
export type CheckRootResultError = |
||||||
|
| HttpErrorResultType<SolidContainer> |
||||||
|
| UnexpectedHttpError<SolidContainer> |
||||||
|
| UnexpectedResourceError<SolidContainer>; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Checks provided headers to see if a given URI is a root container as defined |
||||||
|
* in the [solid specification section 4.1](https://solidproject.org/TR/protocol#storage-resource)
|
||||||
|
* |
||||||
|
* @param uri - the URI of the container resource |
||||||
|
* @param headers - headers returned when making a GET request to the resource |
||||||
|
* @returns CheckRootContainerSuccess if there is not error |
||||||
|
*/ |
||||||
|
export function checkHeadersForRootContainer( |
||||||
|
resource: SolidContainer, |
||||||
|
headers: Headers, |
||||||
|
): CheckRootContainerSuccess { |
||||||
|
const linkHeader = headers.get("link"); |
||||||
|
if (!linkHeader) { |
||||||
|
return new CheckRootContainerSuccess(resource, false); |
||||||
|
} |
||||||
|
const parsedLinkHeader = parseLinkHeader(linkHeader); |
||||||
|
const types = parsedLinkHeader.get("rel", "type"); |
||||||
|
const isRootContainer = types.some( |
||||||
|
(type) => type.uri === "http://www.w3.org/ns/pim/space#Storage", |
||||||
|
); |
||||||
|
return new CheckRootContainerSuccess(resource, isRootContainer); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Performs a request to the Pod to check if the given URI is a root container |
||||||
|
* as defined in the [solid specification section 4.1](https://solidproject.org/TR/protocol#storage-resource)
|
||||||
|
* |
||||||
|
* @param uri - the URI of the container resource |
||||||
|
* @param options - options variable to pass a fetch function |
||||||
|
* @returns CheckResourceSuccess if there is no error |
||||||
|
* |
||||||
|
* @example |
||||||
|
* ```typescript
|
||||||
|
* import { checkRootContainer } from "@ldo/solid"; |
||||||
|
* import { fetch } from "@inrupt/solid-client-authn-browser"; |
||||||
|
* |
||||||
|
* const result = await checkRootContainer("https://example.com/", { fetch }); |
||||||
|
* if (!result.isError) { |
||||||
|
* // true if the container is a root container
|
||||||
|
* console.log(result.isRootContainer); |
||||||
|
* } |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export async function checkRootContainer( |
||||||
|
resource: SolidContainer, |
||||||
|
options?: BasicRequestOptions, |
||||||
|
): Promise<CheckRootResult> { |
||||||
|
try { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
// Fetch options to determine the document type
|
||||||
|
// Note cache: "no-store": we don't want to depend on cached results because
|
||||||
|
// web browsers do not cache link headers
|
||||||
|
// https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1959
|
||||||
|
const response = await fetch(resource.uri, { |
||||||
|
method: "HEAD", |
||||||
|
cache: "no-store", |
||||||
|
}); |
||||||
|
const httpErrorResult = HttpErrorResult.checkResponse(resource, response); |
||||||
|
if (httpErrorResult) return httpErrorResult; |
||||||
|
|
||||||
|
return checkHeadersForRootContainer(resource, response.headers); |
||||||
|
} catch (err) { |
||||||
|
return UnexpectedResourceError.fromThrown(resource, err); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,243 @@ |
|||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import { |
||||||
|
addResourceRdfToContainer, |
||||||
|
getParentUri, |
||||||
|
getSlug, |
||||||
|
} from "../../util/rdfUtils"; |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { UnexpectedResourceError } from "@ldo/connected"; |
||||||
|
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
||||||
|
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||||
|
import { CreateSuccess } from "../results/success/CreateSuccess"; |
||||||
|
import type { AbsentReadSuccess } from "../results/success/ReadSuccess"; |
||||||
|
import type { DeleteResultError } from "./deleteResource"; |
||||||
|
import { deleteResource } from "./deleteResource"; |
||||||
|
import type { |
||||||
|
ReadContainerResult, |
||||||
|
ReadLeafResult, |
||||||
|
ReadResultError, |
||||||
|
} from "./readResource"; |
||||||
|
import { readResource } from "./readResource"; |
||||||
|
import type { DatasetRequestOptions } from "./requestOptions"; |
||||||
|
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||||
|
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values when creating and overwriting a container |
||||||
|
*/ |
||||||
|
export type ContainerCreateAndOverwriteResult = |
||||||
|
| CreateSuccess<SolidContainer> |
||||||
|
| CreateAndOverwriteResultErrors<SolidContainer>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values when creating and overwriting a leaf |
||||||
|
*/ |
||||||
|
export type LeafCreateAndOverwriteResult = |
||||||
|
| CreateSuccess<SolidLeaf> |
||||||
|
| CreateAndOverwriteResultErrors<SolidLeaf>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values when creating a container if absent |
||||||
|
*/ |
||||||
|
export type ContainerCreateIfAbsentResult = |
||||||
|
| CreateSuccess<SolidContainer> |
||||||
|
| Exclude<ReadContainerResult, AbsentReadSuccess<Resource>> |
||||||
|
| CreateIfAbsentResultErrors<SolidLeaf>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values when creating a leaf if absent |
||||||
|
*/ |
||||||
|
export type LeafCreateIfAbsentResult = |
||||||
|
| CreateSuccess<SolidLeaf> |
||||||
|
| Exclude<ReadLeafResult, AbsentReadSuccess<Resource>> |
||||||
|
| CreateIfAbsentResultErrors<SolidLeaf>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible errors returned by creating and overwriting a resource |
||||||
|
*/ |
||||||
|
export type CreateAndOverwriteResultErrors<ResourceType extends Resource> = |
||||||
|
| DeleteResultError<ResourceType> |
||||||
|
| CreateErrors<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible errors returned by creating a resource if absent |
||||||
|
*/ |
||||||
|
export type CreateIfAbsentResultErrors<ResourceType extends Resource> = |
||||||
|
| ReadResultError<ResourceType> |
||||||
|
| CreateErrors<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible errors returned by creating a resource |
||||||
|
*/ |
||||||
|
export type CreateErrors<ResourceType extends Resource> = |
||||||
|
| HttpErrorResultType<ResourceType> |
||||||
|
| UnexpectedResourceError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a data resource (RDF resource) at the provided URI. This resource |
||||||
|
* could also be a container. |
||||||
|
* |
||||||
|
* @param uri - The URI of the resource |
||||||
|
* @param overwrite - If true, the request will overwrite any previous resource |
||||||
|
* at this URI. |
||||||
|
* @param options - Options to provide a fetch function and a local dataset to |
||||||
|
* update. |
||||||
|
* @returns One of many create results depending on the input |
||||||
|
* |
||||||
|
* @example |
||||||
|
* `createDataResource` can be used to create containers. |
||||||
|
* |
||||||
|
* ```typescript
|
||||||
|
* import { createDataResource } from "@ldo/solid"; |
||||||
|
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||||
|
* |
||||||
|
* const result = await createDataResource( |
||||||
|
* "https://example.com/container/", |
||||||
|
* true, |
||||||
|
* { fetch }, |
||||||
|
* ); |
||||||
|
* if (!result.isError) { |
||||||
|
* // Do something
|
||||||
|
* } |
||||||
|
* ``` |
||||||
|
* |
||||||
|
* @example |
||||||
|
* `createDataResource` can also create a blank data resource at the provided |
||||||
|
* URI. |
||||||
|
* |
||||||
|
* ```typescript
|
||||||
|
* import { createDataResource } from "@ldo/solid"; |
||||||
|
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||||
|
* |
||||||
|
* const result = await createDataResource( |
||||||
|
* "https://example.com/container/someResource.ttl", |
||||||
|
* true, |
||||||
|
* { fetch }, |
||||||
|
* ); |
||||||
|
* if (!result.isError) { |
||||||
|
* // Do something
|
||||||
|
* } |
||||||
|
* ``` |
||||||
|
* |
||||||
|
* @example |
||||||
|
* Any local RDFJS dataset passed to the `options` field will be updated with |
||||||
|
* any new RDF data from the create process. |
||||||
|
* |
||||||
|
* ```typescript
|
||||||
|
* import { createDataResource } from "@ldo/solid"; |
||||||
|
* import { createDataset } from "@ldo/dataset" |
||||||
|
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||||
|
* |
||||||
|
* const localDataset = createDataset(); |
||||||
|
* const result = await createDataResource( |
||||||
|
* "https://example.com/container/someResource.ttl", |
||||||
|
* true, |
||||||
|
* { fetch, dataset: localDataset }, |
||||||
|
* ); |
||||||
|
* if (!result.isError) { |
||||||
|
* // Do something
|
||||||
|
* } |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export function createDataResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
overwrite: true, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ContainerCreateAndOverwriteResult>; |
||||||
|
export function createDataResource( |
||||||
|
resouce: SolidLeaf, |
||||||
|
overwrite: true, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateAndOverwriteResult>; |
||||||
|
export function createDataResource( |
||||||
|
resouce: SolidContainer, |
||||||
|
overwrite?: false, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ContainerCreateIfAbsentResult>; |
||||||
|
export function createDataResource( |
||||||
|
resouce: SolidLeaf, |
||||||
|
overwrite?: false, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateIfAbsentResult>; |
||||||
|
export function createDataResource( |
||||||
|
resouce: SolidContainer, |
||||||
|
overwrite?: boolean, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>; |
||||||
|
export function createDataResource( |
||||||
|
resouce: SolidLeaf, |
||||||
|
overwrite?: boolean, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>; |
||||||
|
export function createDataResource( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
overwrite: true, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>; |
||||||
|
export function createDataResource( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
overwrite?: false, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateIfAbsentResult | LeafCreateIfAbsentResult>; |
||||||
|
export function createDataResource( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
overwrite?: boolean, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise< |
||||||
|
| ContainerCreateAndOverwriteResult |
||||||
|
| LeafCreateAndOverwriteResult |
||||||
|
| ContainerCreateIfAbsentResult |
||||||
|
| LeafCreateIfAbsentResult |
||||||
|
>; |
||||||
|
export async function createDataResource( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
overwrite?: boolean, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise< |
||||||
|
| ContainerCreateAndOverwriteResult |
||||||
|
| LeafCreateAndOverwriteResult |
||||||
|
| ContainerCreateIfAbsentResult |
||||||
|
| LeafCreateIfAbsentResult |
||||||
|
> { |
||||||
|
try { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
let didOverwrite = false; |
||||||
|
if (overwrite) { |
||||||
|
const deleteResult = await deleteResource(resource, options); |
||||||
|
// Return if it wasn't deleted
|
||||||
|
if (deleteResult.isError) return deleteResult; |
||||||
|
didOverwrite = deleteResult.resourceExisted; |
||||||
|
} else { |
||||||
|
// Perform a read to check if it exists
|
||||||
|
const readResult = await readResource(resource, options); |
||||||
|
|
||||||
|
// If it does exist stop and return.
|
||||||
|
if (readResult.type !== "absentReadSuccess") { |
||||||
|
return readResult; |
||||||
|
} |
||||||
|
} |
||||||
|
// Create the document
|
||||||
|
const parentUri = getParentUri(resource.uri)!; |
||||||
|
const headers: HeadersInit = { |
||||||
|
"content-type": "text/turtle", |
||||||
|
slug: getSlug(resource.uri), |
||||||
|
}; |
||||||
|
if (resource.type === "container") { |
||||||
|
headers.link = '<http://www.w3.org/ns/ldp#Container>; rel="type"'; |
||||||
|
} |
||||||
|
const response = await fetch(parentUri, { |
||||||
|
method: "post", |
||||||
|
headers, |
||||||
|
}); |
||||||
|
|
||||||
|
const httpError = HttpErrorResult.checkResponse(resource, response); |
||||||
|
if (httpError) return httpError; |
||||||
|
|
||||||
|
if (options?.dataset) { |
||||||
|
addResourceRdfToContainer(resource.uri, options.dataset); |
||||||
|
} |
||||||
|
return new CreateSuccess(resource, didOverwrite); |
||||||
|
} catch (err) { |
||||||
|
return UnexpectedResourceError.fromThrown(resource, err); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,109 @@ |
|||||||
|
import { namedNode } from "@rdfjs/data-model"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import { deleteResourceRdfFromContainer } from "../../util/rdfUtils"; |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { UnexpectedResourceError } from "@ldo/connected"; |
||||||
|
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
||||||
|
import { UnexpectedHttpError } from "../results/error/HttpErrorResult"; |
||||||
|
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||||
|
import { DeleteSuccess } from "../results/success/DeleteSuccess"; |
||||||
|
import type { DatasetRequestOptions } from "./requestOptions"; |
||||||
|
import type { IBulkEditableDataset } from "@ldo/subscribable-dataset"; |
||||||
|
import type { Quad } from "@rdfjs/types"; |
||||||
|
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||||
|
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values for deleteResource |
||||||
|
*/ |
||||||
|
export type DeleteResult<ResourceType extends Resource> = |
||||||
|
| DeleteSuccess<ResourceType> |
||||||
|
| DeleteResultError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible errors that can be returned by deleteResource |
||||||
|
*/ |
||||||
|
export type DeleteResultError<ResourceType extends Resource> = |
||||||
|
| HttpErrorResultType<ResourceType> |
||||||
|
| UnexpectedResourceError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* Deletes a resource on a Pod at a given URL. |
||||||
|
* |
||||||
|
* @param uri - The URI for the resource that should be deleted |
||||||
|
* @param options - Options to provide a fetch function and a local dataset to |
||||||
|
* update. |
||||||
|
* @returns a DeleteResult |
||||||
|
* |
||||||
|
* @example |
||||||
|
* `deleteResource` will send a request to a Solid Pod using the provided fetch |
||||||
|
* function. A local dataset can also be provided. It will be updated with any |
||||||
|
* new information from the delete. |
||||||
|
* |
||||||
|
* ```typescript
|
||||||
|
* import { deleteResource } from "@ldo/solid"; |
||||||
|
* import { createDataset } from "@ldo/dataset" |
||||||
|
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||||
|
* |
||||||
|
* const localDataset = createDataset(); |
||||||
|
* const result = await deleteResource( |
||||||
|
* "https://example.com/container/someResource.ttl", |
||||||
|
* { fetch, dataset: localDataset }, |
||||||
|
* ); |
||||||
|
* if (!result.isError) { |
||||||
|
* // Do something
|
||||||
|
* } |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export async function deleteResource( |
||||||
|
resource: SolidContainer, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<DeleteResult<SolidContainer>>; |
||||||
|
export async function deleteResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<DeleteResult<SolidLeaf>>; |
||||||
|
export async function deleteResource( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<DeleteResult<SolidContainer | SolidLeaf>>; |
||||||
|
export async function deleteResource( |
||||||
|
resource: SolidContainer | SolidLeaf, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<DeleteResult<SolidContainer | SolidLeaf>> { |
||||||
|
try { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
const response = await fetch(resource.uri, { |
||||||
|
method: "delete", |
||||||
|
}); |
||||||
|
const errorResult = HttpErrorResult.checkResponse(resource, response); |
||||||
|
if (errorResult) return errorResult; |
||||||
|
|
||||||
|
// Specifically check for a 205. Annoyingly, the server will return 200 even
|
||||||
|
// if it hasn't been deleted when you're unauthenticated. 404 happens when
|
||||||
|
// the document never existed
|
||||||
|
if (response.status === 205 || response.status === 404) { |
||||||
|
if (options?.dataset) |
||||||
|
updateDatasetOnSuccessfulDelete(resource.uri, options.dataset); |
||||||
|
return new DeleteSuccess(resource, response.status === 205); |
||||||
|
} |
||||||
|
return new UnexpectedHttpError(resource, response); |
||||||
|
} catch (err) { |
||||||
|
return UnexpectedResourceError.fromThrown(resource, err); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Assuming a successful delete has just been performed, this function updates |
||||||
|
* datastores to reflect that. |
||||||
|
* |
||||||
|
* @param uri - The uri of the resouce that was removed |
||||||
|
* @param dataset - The dataset that should be updated |
||||||
|
*/ |
||||||
|
export function updateDatasetOnSuccessfulDelete( |
||||||
|
uri: string, |
||||||
|
dataset: IBulkEditableDataset<Quad>, |
||||||
|
): void { |
||||||
|
dataset.deleteMatches(undefined, undefined, undefined, namedNode(uri)); |
||||||
|
deleteResourceRdfFromContainer(uri, dataset); |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
import type { UnexpectedHttpError } from "../results/error/HttpErrorResult"; |
||||||
|
import { |
||||||
|
HttpErrorResult, |
||||||
|
type HttpErrorResultType, |
||||||
|
} from "../results/error/HttpErrorResult"; |
||||||
|
import { |
||||||
|
addRawTurtleToDataset, |
||||||
|
addResourceRdfToContainer, |
||||||
|
} from "../../util/rdfUtils"; |
||||||
|
import type { DatasetRequestOptions } from "./requestOptions"; |
||||||
|
import { |
||||||
|
BinaryReadSuccess, |
||||||
|
DataReadSuccess, |
||||||
|
} from "../results/success/ReadSuccess"; |
||||||
|
import { ContainerReadSuccess } from "../results/success/ReadSuccess"; |
||||||
|
import { AbsentReadSuccess } from "../results/success/ReadSuccess"; |
||||||
|
import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { UnexpectedResourceError } from "@ldo/connected"; |
||||||
|
import { checkHeadersForRootContainer } from "./checkRootContainer"; |
||||||
|
import { namedNode } from "@rdfjs/data-model"; |
||||||
|
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||||
|
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values for reading a leaf |
||||||
|
*/ |
||||||
|
export type ReadLeafResult = |
||||||
|
| BinaryReadSuccess |
||||||
|
| DataReadSuccess |
||||||
|
| AbsentReadSuccess<SolidLeaf> |
||||||
|
| ReadResultError<SolidContainer>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible return values for reading a container |
||||||
|
*/ |
||||||
|
export type ReadContainerResult = |
||||||
|
| ContainerReadSuccess |
||||||
|
| AbsentReadSuccess<SolidContainer> |
||||||
|
| ReadResultError<SolidContainer>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All possible errors the readResource function can return |
||||||
|
*/ |
||||||
|
export type ReadResultError<ResourceType extends Resource> = |
||||||
|
| HttpErrorResultType<ResourceType> |
||||||
|
| NoncompliantPodError<ResourceType> |
||||||
|
| UnexpectedHttpError<ResourceType> |
||||||
|
| UnexpectedResourceError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads resource at a provided URI and returns the result |
||||||
|
* |
||||||
|
* @param uri - The URI of the resource |
||||||
|
* @param options - Options to provide a fetch function and a local dataset to |
||||||
|
* update. |
||||||
|
* @returns ReadResult |
||||||
|
* |
||||||
|
* @example |
||||||
|
* ```typescript
|
||||||
|
* import { deleteResource } from "@ldo/solid"; |
||||||
|
* import { createDataset } from "@ldo/dataset" |
||||||
|
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||||
|
* |
||||||
|
* const dataset = createDataset(); |
||||||
|
* const result = await readResource( |
||||||
|
* "https://example.com/container/someResource.ttl", |
||||||
|
* { fetch, dataset }, |
||||||
|
* ); |
||||||
|
* if (!result.isError) { |
||||||
|
* if (result.type === "absentReadSuccess") { |
||||||
|
* // There was no problem reading the resource, but it doesn't exist
|
||||||
|
* } else if (result.type === "dataReadSuccess") { |
||||||
|
* // The resource was read and it is an RDF resource. The dataset provided
|
||||||
|
* // dataset will also be loaded with the data from the resource
|
||||||
|
* } else if (result.type === "binaryReadSuccess") { |
||||||
|
* // The resource is a binary
|
||||||
|
* console.log(result.blob); |
||||||
|
* console.log(result.mimeType); |
||||||
|
* } |
||||||
|
* } |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export async function readResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ReadLeafResult>; |
||||||
|
export async function readResource( |
||||||
|
resource: SolidContainer, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ReadContainerResult>; |
||||||
|
export async function readResource( |
||||||
|
resource: SolidLeaf | SolidContainer, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ReadLeafResult | ReadContainerResult>; |
||||||
|
export async function readResource( |
||||||
|
resource: SolidLeaf | SolidContainer, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<ReadLeafResult | ReadContainerResult> { |
||||||
|
try { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
// Fetch options to determine the document type
|
||||||
|
const response = await fetch(resource.uri, { |
||||||
|
headers: { accept: "text/turtle, */*" }, |
||||||
|
}); |
||||||
|
if (response.status === 404) { |
||||||
|
// Clear existing data if present
|
||||||
|
if (options?.dataset) { |
||||||
|
options.dataset.deleteMatches( |
||||||
|
undefined, |
||||||
|
undefined, |
||||||
|
undefined, |
||||||
|
namedNode(resource.uri), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return new AbsentReadSuccess(resource, false); |
||||||
|
} |
||||||
|
const httpErrorResult = HttpErrorResult.checkResponse(resource, response); |
||||||
|
if (httpErrorResult) return httpErrorResult; |
||||||
|
|
||||||
|
// Add this resource to the container
|
||||||
|
if (options?.dataset) { |
||||||
|
addResourceRdfToContainer(resource.uri, options.dataset); |
||||||
|
} |
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type"); |
||||||
|
if (!contentType) { |
||||||
|
return new NoncompliantPodError( |
||||||
|
resource, |
||||||
|
"Resource requests must return a content-type header.", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (contentType.startsWith("text/turtle")) { |
||||||
|
// Parse Turtle
|
||||||
|
const rawTurtle = await response.text(); |
||||||
|
if (options?.dataset) { |
||||||
|
const result = await addRawTurtleToDataset( |
||||||
|
rawTurtle, |
||||||
|
options.dataset, |
||||||
|
resource.uri, |
||||||
|
); |
||||||
|
if (result) return result; |
||||||
|
} |
||||||
|
if (resource.type === "container") { |
||||||
|
const result = checkHeadersForRootContainer(resource, response.headers); |
||||||
|
return new ContainerReadSuccess( |
||||||
|
resource, |
||||||
|
false, |
||||||
|
result.isRootContainer, |
||||||
|
); |
||||||
|
} |
||||||
|
return new DataReadSuccess(resource as SolidLeaf, false); |
||||||
|
} else { |
||||||
|
// Load Blob
|
||||||
|
const blob = await response.blob(); |
||||||
|
return new BinaryReadSuccess( |
||||||
|
resource as SolidLeaf, |
||||||
|
false, |
||||||
|
blob, |
||||||
|
contentType, |
||||||
|
); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
return UnexpectedResourceError.fromThrown(resource, err); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
import type { IBulkEditableDataset } from "@ldo/subscribable-dataset"; |
||||||
|
import type { Quad } from "@rdfjs/types"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Request Options to be passed to request functions |
||||||
|
*/ |
||||||
|
export interface BasicRequestOptions { |
||||||
|
/** |
||||||
|
* A fetch function usually imported from @inrupt/solid-client-authn-js |
||||||
|
*/ |
||||||
|
fetch?: typeof fetch; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Request options with a dataset component |
||||||
|
*/ |
||||||
|
export interface DatasetRequestOptions extends BasicRequestOptions { |
||||||
|
/** |
||||||
|
* A dataset to be modified with any new information obtained from a request |
||||||
|
*/ |
||||||
|
dataset?: IBulkEditableDataset<Quad>; |
||||||
|
} |
@ -0,0 +1,118 @@ |
|||||||
|
import type { DatasetChanges } from "@ldo/rdf-utils"; |
||||||
|
import { changesToSparqlUpdate } from "@ldo/rdf-utils"; |
||||||
|
import type { Quad } from "@rdfjs/types"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { UnexpectedResourceError } from "@ldo/connected"; |
||||||
|
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
||||||
|
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||||
|
import { UpdateSuccess } from "../results/success/UpdateSuccess"; |
||||||
|
import type { DatasetRequestOptions } from "./requestOptions"; |
||||||
|
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||||
|
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||||
|
|
||||||
|
/** |
||||||
|
* All return values for updateDataResource |
||||||
|
*/ |
||||||
|
export type UpdateResult<ResourceType extends Resource> = |
||||||
|
| UpdateSuccess<ResourceType> |
||||||
|
| UpdateResultError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* All errors updateDataResource can return |
||||||
|
*/ |
||||||
|
export type UpdateResultError<ResourceType extends Resource> = |
||||||
|
| HttpErrorResultType<ResourceType> |
||||||
|
| UnexpectedResourceError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* Updates a specific data resource with the provided dataset changes |
||||||
|
* |
||||||
|
* @param uri - the URI of the data resource |
||||||
|
* @param datasetChanges - A set of triples added and removed from this dataset |
||||||
|
* @param options - Options to provide a fetch function and a local dataset to |
||||||
|
* update. |
||||||
|
* @returns An UpdateResult |
||||||
|
* |
||||||
|
* @example |
||||||
|
* ```typescript
|
||||||
|
* import { |
||||||
|
* updateDataResource, |
||||||
|
* transactionChanges, |
||||||
|
* changeData, |
||||||
|
* createSolidLdoDataset, |
||||||
|
* } from "@ldo/solid"; |
||||||
|
* import { fetch } from "@inrupt/solid-client-authn-browser"; |
||||||
|
* |
||||||
|
* // Initialize an LDO dataset
|
||||||
|
* const solidLdoDataset = createSolidLdoDataset(); |
||||||
|
* // Get a Linked Data Object
|
||||||
|
* const profile = solidLdoDataset |
||||||
|
* .usingType(ProfileShapeType) |
||||||
|
* .fromSubject("https://example.com/profile#me"); |
||||||
|
* // Create a transaction to change data
|
||||||
|
* const cProfile = changeData( |
||||||
|
* profile, |
||||||
|
* solidLdoDataset.getResource("https://example.com/profile"), |
||||||
|
* ); |
||||||
|
* cProfile.name = "John Doe"; |
||||||
|
* // Get data in "DatasetChanges" form
|
||||||
|
* const datasetChanges = transactionChanges(someLinkedDataObject); |
||||||
|
* // Use "updateDataResource" to apply the changes
|
||||||
|
* const result = await updateDataResource( |
||||||
|
* "https://example.com/profile", |
||||||
|
* datasetChanges, |
||||||
|
* { fetch, dataset: solidLdoDataset }, |
||||||
|
* ); |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export async function updateDataResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
datasetChanges: DatasetChanges<Quad>, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<UpdateResult<SolidLeaf>>; |
||||||
|
export async function updateDataResource( |
||||||
|
resource: SolidContainer, |
||||||
|
datasetChanges: DatasetChanges<Quad>, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<UpdateResult<SolidContainer>>; |
||||||
|
export async function updateDataResource( |
||||||
|
resource: SolidLeaf | SolidContainer, |
||||||
|
datasetChanges: DatasetChanges<Quad>, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<UpdateResult<SolidLeaf | SolidContainer>>; |
||||||
|
export async function updateDataResource( |
||||||
|
resource: SolidLeaf | SolidContainer, |
||||||
|
datasetChanges: DatasetChanges<Quad>, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<UpdateResult<SolidLeaf | SolidContainer>> { |
||||||
|
try { |
||||||
|
// Optimistically add data
|
||||||
|
options?.dataset?.bulk(datasetChanges); |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
|
||||||
|
// Make request
|
||||||
|
const sparqlUpdate = await changesToSparqlUpdate(datasetChanges); |
||||||
|
const response = await fetch(resource.uri, { |
||||||
|
method: "PATCH", |
||||||
|
body: sparqlUpdate, |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/sparql-update", |
||||||
|
}, |
||||||
|
}); |
||||||
|
const httpError = HttpErrorResult.checkResponse(resource, response); |
||||||
|
if (httpError) { |
||||||
|
// Handle error rollback
|
||||||
|
if (options?.dataset) { |
||||||
|
options.dataset.bulk({ |
||||||
|
added: datasetChanges.removed, |
||||||
|
removed: datasetChanges.added, |
||||||
|
}); |
||||||
|
} |
||||||
|
return httpError; |
||||||
|
} |
||||||
|
return new UpdateSuccess(resource); |
||||||
|
} catch (err) { |
||||||
|
return UnexpectedResourceError.fromThrown(resource, err); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,110 @@ |
|||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import { |
||||||
|
addResourceRdfToContainer, |
||||||
|
getParentUri, |
||||||
|
getSlug, |
||||||
|
} from "../../util/rdfUtils"; |
||||||
|
import { UnexpectedResourceError } from "@ldo/connected"; |
||||||
|
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||||
|
import type { |
||||||
|
LeafCreateAndOverwriteResult, |
||||||
|
LeafCreateIfAbsentResult, |
||||||
|
} from "./createDataResource"; |
||||||
|
import { deleteResource } from "./deleteResource"; |
||||||
|
import { readResource } from "./readResource"; |
||||||
|
import type { DatasetRequestOptions } from "./requestOptions"; |
||||||
|
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||||
|
import { CreateSuccess } from "../results/success/CreateSuccess"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Uploads a binary resource at the provided URI |
||||||
|
* |
||||||
|
* @param uri - The URI of the resource |
||||||
|
* @param overwrite - If true, the request will overwrite any previous resource |
||||||
|
* at this URI. |
||||||
|
* @param options - Options to provide a fetch function and a local dataset to |
||||||
|
* update. |
||||||
|
* @returns One of many create results depending on the input |
||||||
|
* |
||||||
|
* @example |
||||||
|
* Any local RDFJS dataset passed to the `options` field will be updated with |
||||||
|
* any new RDF data from the create process. |
||||||
|
* |
||||||
|
* ```typescript
|
||||||
|
* import { createDataResource } from "@ldo/solid"; |
||||||
|
* import { createDataset } from "@ldo/dataset" |
||||||
|
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||||
|
* |
||||||
|
* const localDataset = createDataset(); |
||||||
|
* const result = await uploadResource( |
||||||
|
* "https://example.com/container/someResource.txt", |
||||||
|
* new Blob("some text."), |
||||||
|
* "text/txt", |
||||||
|
* true, |
||||||
|
* { fetch, dataset: localDataset }, |
||||||
|
* ); |
||||||
|
* if (!result.isError) { |
||||||
|
* // Do something
|
||||||
|
* } |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export function uploadResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite: true, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateAndOverwriteResult>; |
||||||
|
export function uploadResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite?: false, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateIfAbsentResult>; |
||||||
|
export async function uploadResource( |
||||||
|
resource: SolidLeaf, |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
overwrite?: boolean, |
||||||
|
options?: DatasetRequestOptions, |
||||||
|
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult> { |
||||||
|
try { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
let didOverwrite = false; |
||||||
|
if (overwrite) { |
||||||
|
const deleteResult = await deleteResource(resource, options); |
||||||
|
// Return if it wasn't deleted
|
||||||
|
if (deleteResult.isError) return deleteResult; |
||||||
|
didOverwrite = deleteResult.resourceExisted; |
||||||
|
} else { |
||||||
|
// Perform a read to check if it exists
|
||||||
|
const readResult = await readResource(resource, options); |
||||||
|
// If it does exist stop and return.
|
||||||
|
if (readResult.type !== "absentReadSuccess") { |
||||||
|
return readResult; |
||||||
|
} |
||||||
|
} |
||||||
|
// Create the document
|
||||||
|
const parentUri = getParentUri(resource.uri)!; |
||||||
|
const response = await fetch(parentUri, { |
||||||
|
method: "post", |
||||||
|
headers: { |
||||||
|
"content-type": mimeType, |
||||||
|
slug: getSlug(resource.uri), |
||||||
|
}, |
||||||
|
body: blob, |
||||||
|
}); |
||||||
|
|
||||||
|
const httpError = HttpErrorResult.checkResponse(resource, response); |
||||||
|
if (httpError) return httpError; |
||||||
|
|
||||||
|
if (options?.dataset) { |
||||||
|
addResourceRdfToContainer(resource.uri, options.dataset); |
||||||
|
} |
||||||
|
return new CreateSuccess(resource, didOverwrite); |
||||||
|
} catch (err) { |
||||||
|
const thing = UnexpectedResourceError.fromThrown(resource, err); |
||||||
|
return thing; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
/* istanbul ignore file */ |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { ResourceError } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* An error: Could not fetch access rules |
||||||
|
*/ |
||||||
|
export class AccessRuleFetchError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceError<ResourceType> { |
||||||
|
readonly type = "accessRuleFetchError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param resource - The resource for which access rules couldn't be |
||||||
|
* fetched |
||||||
|
* @param message - A custom message for the error |
||||||
|
*/ |
||||||
|
constructor(resource: ResourceType, message?: string) { |
||||||
|
super( |
||||||
|
resource, |
||||||
|
message ?? `${resource.uri} had trouble fetching access rules.`, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,176 @@ |
|||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { ResourceError } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A set of standard errors that can be returned as a result of an HTTP request |
||||||
|
*/ |
||||||
|
export type HttpErrorResultType<ResourceType extends Resource> = |
||||||
|
| ServerHttpError<ResourceType> |
||||||
|
| UnexpectedHttpError<ResourceType> |
||||||
|
| UnauthenticatedHttpError<ResourceType> |
||||||
|
| UnauthorizedHttpError<ResourceType>; |
||||||
|
|
||||||
|
/** |
||||||
|
* An error caused by an HTTP request |
||||||
|
*/ |
||||||
|
export abstract class HttpErrorResult< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceError<ResourceType> { |
||||||
|
/** |
||||||
|
* The status of the HTTP request |
||||||
|
*/ |
||||||
|
public readonly status: number; |
||||||
|
|
||||||
|
/** |
||||||
|
* Headers returned by the HTTP request |
||||||
|
*/ |
||||||
|
public readonly headers: Headers; |
||||||
|
|
||||||
|
/** |
||||||
|
* Response returned by the HTTP request |
||||||
|
*/ |
||||||
|
public readonly response: Response; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param resource - the resource |
||||||
|
* @param response - The response returned by the HTTP requests |
||||||
|
* @param message - A custom message for the error |
||||||
|
*/ |
||||||
|
constructor(resource: ResourceType, response: Response, message?: string) { |
||||||
|
super( |
||||||
|
resource, |
||||||
|
message || |
||||||
|
`Request for ${resource.uri} returned ${response.status} (${response.statusText}).`, |
||||||
|
); |
||||||
|
this.status = response.status; |
||||||
|
this.headers = response.headers; |
||||||
|
this.response = response; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks to see if a given response does not constitute an HTTP Error |
||||||
|
* @param response - The response of the request |
||||||
|
* @returns true if the response does not constitute an HTTP Error |
||||||
|
*/ |
||||||
|
static isnt(response: Response) { |
||||||
|
return ( |
||||||
|
!(response.status >= 200 && response.status < 300) && |
||||||
|
response.status !== 404 && |
||||||
|
response.status !== 304 |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks a given response to see if it is a ServerHttpError, an |
||||||
|
* UnauthenticatedHttpError or a some unexpected error. |
||||||
|
* @param uri - The uri of the request |
||||||
|
* @param response - The response of the request |
||||||
|
* @returns An error if the response calls for it. Undefined if not. |
||||||
|
*/ |
||||||
|
static checkResponse<ResourceType extends Resource>( |
||||||
|
resource: ResourceType, |
||||||
|
response: Response, |
||||||
|
) { |
||||||
|
if (ServerHttpError.is(response)) { |
||||||
|
return new ServerHttpError(resource, response); |
||||||
|
} |
||||||
|
if (UnauthenticatedHttpError.is(response)) { |
||||||
|
return new UnauthenticatedHttpError(resource, response); |
||||||
|
} |
||||||
|
if (UnauthorizedHttpError.is(response)) { |
||||||
|
return new UnauthorizedHttpError(resource, response); |
||||||
|
} |
||||||
|
if (HttpErrorResult.isnt(response)) { |
||||||
|
return new UnexpectedHttpError(resource, response); |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An unexpected error as a result of an HTTP request. This is usually returned |
||||||
|
* when the HTTP request returns a status code LDO does not recognize. |
||||||
|
*/ |
||||||
|
export class UnexpectedHttpError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends HttpErrorResult<ResourceType> { |
||||||
|
readonly type = "unexpectedHttpError" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An UnauthenticatedHttpError triggers when a Solid server returns a 401 status |
||||||
|
* indicating that the request is not authenticated. |
||||||
|
*/ |
||||||
|
export class UnauthenticatedHttpError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends HttpErrorResult<ResourceType> { |
||||||
|
readonly type = "unauthenticatedError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates if a specific response constitutes an UnauthenticatedHttpError |
||||||
|
* @param response - The request response |
||||||
|
* @returns true if this response constitutes an UnauthenticatedHttpError |
||||||
|
*/ |
||||||
|
static is(response: Response) { |
||||||
|
return response.status === 401; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An UnauthenticatedHttpError triggers when a Solid server returns a 403 status |
||||||
|
* indicating that the request is not authorized. |
||||||
|
*/ |
||||||
|
export class UnauthorizedHttpError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends HttpErrorResult<ResourceType> { |
||||||
|
readonly type = "unauthorizedError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates if a specific response constitutes an UnauthenticatedHttpError |
||||||
|
* @param response - The request response |
||||||
|
* @returns true if this response constitutes an UnauthenticatedHttpError |
||||||
|
*/ |
||||||
|
static is(response: Response) { |
||||||
|
return response.status === 403; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An NotFoundHttpError triggers when a Solid server returns a 404 status. This |
||||||
|
* error is not returned in most cases as a "absent" resource is not considered |
||||||
|
* an error, but it is thrown while trying for find a WAC rule for a resource |
||||||
|
* that does not exist. |
||||||
|
*/ |
||||||
|
export class NotFoundHttpError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends HttpErrorResult<ResourceType> { |
||||||
|
readonly type = "notFoundError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates if a specific response constitutes an NotFoundHttpError |
||||||
|
* @param response - The request response |
||||||
|
* @returns true if this response constitutes an NotFoundHttpError |
||||||
|
*/ |
||||||
|
static is(response: Response) { |
||||||
|
return response.status === 404; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A ServerHttpError triggers when a Solid server returns a 5XX status, |
||||||
|
* indicating that an error happened on the server. |
||||||
|
*/ |
||||||
|
export class ServerHttpError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends HttpErrorResult<ResourceType> { |
||||||
|
readonly type = "serverError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates if a specific response constitutes a ServerHttpError |
||||||
|
* @param response - The request response |
||||||
|
* @returns true if this response constitutes a ServerHttpError |
||||||
|
*/ |
||||||
|
static is(response: Response) { |
||||||
|
return response.status >= 500 && response.status < 600; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { ResourceError } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* An InvalidUriError is returned when a URI was provided that is not a valid |
||||||
|
* URI. |
||||||
|
*/ |
||||||
|
export class InvalidUriError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceError<ResourceType> { |
||||||
|
readonly type = "invalidUriError" as const; |
||||||
|
|
||||||
|
constructor(resource: ResourceType, message?: string) { |
||||||
|
super(resource, message || `${resource.uri} is an invalid uri.`); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { ResourceError } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A NoncompliantPodError is returned when the server responded in a way that is |
||||||
|
* not compliant with the Solid specification. |
||||||
|
*/ |
||||||
|
export class NoRootContainerError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceError<ResourceType> { |
||||||
|
readonly type = "noRootContainerError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param resource - the requested resource |
||||||
|
* @param message - a custom message for the error |
||||||
|
*/ |
||||||
|
constructor(resource: ResourceType) { |
||||||
|
super(resource, `${resource.uri} has not root container.`); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { ResourceError } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* A NoncompliantPodError is returned when the server responded in a way that is |
||||||
|
* not compliant with the Solid specification. |
||||||
|
*/ |
||||||
|
export class NoncompliantPodError< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceError<ResourceType> { |
||||||
|
readonly type = "noncompliantPodError" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param resource - the requested resource |
||||||
|
* @param message - a custom message for the error |
||||||
|
*/ |
||||||
|
constructor(resource: ResourceType, message?: string) { |
||||||
|
super( |
||||||
|
resource, |
||||||
|
`Response from ${resource.uri} is not compliant with the Solid Specification: ${message}`, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import type { SolidContainer } from "../../../resources/SolidContainer"; |
||||||
|
import { ResourceSuccess, SuccessResult } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the request to check if a resource is the root container was |
||||||
|
* a success. |
||||||
|
*/ |
||||||
|
export class CheckRootContainerSuccess extends ResourceSuccess<SolidContainer> { |
||||||
|
type = "checkRootContainerSuccess" as const; |
||||||
|
/** |
||||||
|
* True if this resoure is the root container |
||||||
|
*/ |
||||||
|
isRootContainer: boolean; |
||||||
|
|
||||||
|
constructor(resource: SolidContainer, isRootContainer: boolean) { |
||||||
|
super(resource); |
||||||
|
this.isRootContainer = isRootContainer; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class GetStorageContainerFromWebIdSuccess extends SuccessResult { |
||||||
|
type = "getStorageContainerFromWebIdSuccess" as const; |
||||||
|
storageContainers: SolidContainer[]; |
||||||
|
|
||||||
|
constructor(storageContainers: SolidContainer[]) { |
||||||
|
super(); |
||||||
|
this.storageContainers = storageContainers; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import { ResourceSuccess } from "@ldo/connected"; |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the request to create the resource was a success. |
||||||
|
*/ |
||||||
|
export class CreateSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceSuccess<ResourceType> { |
||||||
|
type = "createSuccess" as const; |
||||||
|
/** |
||||||
|
* True if there was a resource that existed before at the given URI that was |
||||||
|
* overwritten |
||||||
|
*/ |
||||||
|
didOverwrite: boolean; |
||||||
|
|
||||||
|
constructor(resource: ResourceType, didOverwrite: boolean) { |
||||||
|
super(resource); |
||||||
|
this.didOverwrite = didOverwrite; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
import { ResourceSuccess } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the request to delete a resource was a success. |
||||||
|
*/ |
||||||
|
export class DeleteSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceSuccess<ResourceType> { |
||||||
|
type = "deleteSuccess" as const; |
||||||
|
|
||||||
|
/** |
||||||
|
* True if there was a resource at the provided URI that was deleted. False if |
||||||
|
* a resource didn't exist. |
||||||
|
*/ |
||||||
|
resourceExisted: boolean; |
||||||
|
|
||||||
|
constructor(resource: ResourceType, resourceExisted: boolean) { |
||||||
|
super(resource); |
||||||
|
this.resourceExisted = resourceExisted; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,105 @@ |
|||||||
|
import { ResourceSuccess } from "@ldo/connected"; |
||||||
|
import type { Resource, ResourceResult } from "@ldo/connected"; |
||||||
|
import type { SolidLeaf } from "../../../resources/SolidLeaf"; |
||||||
|
import type { SolidContainer } from "../../../resources/SolidContainer"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the request to read a resource was a success |
||||||
|
*/ |
||||||
|
export abstract class ReadSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceSuccess<ResourceType> { |
||||||
|
/** |
||||||
|
* True if the resource was recalled from local memory rather than a recent |
||||||
|
* request |
||||||
|
*/ |
||||||
|
recalledFromMemory: boolean; |
||||||
|
|
||||||
|
constructor(resource: ResourceType, recalledFromMemory: boolean) { |
||||||
|
super(resource); |
||||||
|
this.recalledFromMemory = recalledFromMemory; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the read request was successful and that the resource |
||||||
|
* retrieved was a binary resource. |
||||||
|
*/ |
||||||
|
export class BinaryReadSuccess extends ReadSuccess<SolidLeaf> { |
||||||
|
type = "binaryReadSuccess" as const; |
||||||
|
/** |
||||||
|
* The raw data for the binary resource |
||||||
|
*/ |
||||||
|
blob: Blob; |
||||||
|
/** |
||||||
|
* The mime type of the binary resource |
||||||
|
*/ |
||||||
|
mimeType: string; |
||||||
|
|
||||||
|
constructor( |
||||||
|
resource: SolidLeaf, |
||||||
|
recalledFromMemory: boolean, |
||||||
|
blob: Blob, |
||||||
|
mimeType: string, |
||||||
|
) { |
||||||
|
super(resource, recalledFromMemory); |
||||||
|
this.blob = blob; |
||||||
|
this.mimeType = mimeType; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the read request was successful and that the resource |
||||||
|
* retrieved was a data (RDF) resource. |
||||||
|
*/ |
||||||
|
export class DataReadSuccess extends ReadSuccess<SolidLeaf> { |
||||||
|
type = "dataReadSuccess" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the read request was successful and that the resource |
||||||
|
* retrieved was a container resource. |
||||||
|
*/ |
||||||
|
export class ContainerReadSuccess extends ReadSuccess<SolidContainer> { |
||||||
|
type: "containerReadSuccess"; |
||||||
|
/** |
||||||
|
* True if this container is a root container |
||||||
|
*/ |
||||||
|
isRootContainer: boolean; |
||||||
|
|
||||||
|
constructor( |
||||||
|
resource: SolidContainer, |
||||||
|
recalledFromMemory: boolean, |
||||||
|
isRootContainer: boolean, |
||||||
|
) { |
||||||
|
super(resource, recalledFromMemory); |
||||||
|
this.isRootContainer = isRootContainer; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that the read request was successful, but no resource exists at |
||||||
|
* the provided URI. |
||||||
|
*/ |
||||||
|
export class AbsentReadSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ReadSuccess<ResourceType> { |
||||||
|
type = "absentReadSuccess" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A helper function that checks to see if a result is a ReadSuccess result |
||||||
|
* |
||||||
|
* @param result - the result to check |
||||||
|
* @returns true if the result is a ReadSuccessResult result |
||||||
|
*/ |
||||||
|
export function isReadSuccess<ResourceType extends Resource>( |
||||||
|
result: ResourceResult<Resource>, |
||||||
|
): result is ReadSuccess<ResourceType> { |
||||||
|
return ( |
||||||
|
result.type === "binaryReadSuccess" || |
||||||
|
result.type === "dataReadSuccess" || |
||||||
|
result.type === "absentReadSuccess" || |
||||||
|
result.type === "containerReadSuccess" |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
import { ResourceSuccess } from "@ldo/connected"; |
||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that an update request to a resource was successful |
||||||
|
*/ |
||||||
|
export class UpdateSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceSuccess<ResourceType> { |
||||||
|
type = "updateSuccess" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that an update request to the default graph was successful. This |
||||||
|
* data was not written to a Pod. It was only written locally. |
||||||
|
*/ |
||||||
|
export class UpdateDefaultGraphSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceSuccess<ResourceType> { |
||||||
|
type = "updateDefaultGraphSuccess" as const; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Indicates that LDO ignored an invalid update (usually because a container |
||||||
|
* attempted an update) |
||||||
|
*/ |
||||||
|
export class IgnoredInvalidUpdateSuccess< |
||||||
|
ResourceType extends Resource, |
||||||
|
> extends ResourceSuccess<ResourceType> { |
||||||
|
type = "ignoredInvalidUpdateSuccess" as const; |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||||
|
import type { WaitingProcess } from "../../util/RequestBatcher"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* |
||||||
|
* A helper function for a common way to modify the batch queue. This merges |
||||||
|
* the incoming request with the currently executing request or the last request |
||||||
|
* in the queue if its keys are the same. |
||||||
|
* |
||||||
|
* @param key - the key of the incoming request |
||||||
|
* @returns a modifyQueue function |
||||||
|
*/ |
||||||
|
export function modifyQueueByMergingEventsWithTheSameKeys(key: string) { |
||||||
|
return ( |
||||||
|
queue: WaitingProcess<any[], any>[], |
||||||
|
currentlyLoading: WaitingProcess<any[], any> | undefined, |
||||||
|
) => { |
||||||
|
if (queue.length === 0 && currentlyLoading?.name === key) { |
||||||
|
return currentlyLoading; |
||||||
|
} else if (queue[queue.length - 1]?.name === key) { |
||||||
|
return queue[queue.length - 1]; |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
}; |
||||||
|
} |
@ -1,3 +1,14 @@ |
|||||||
|
import type { Resource } from "@ldo/connected"; |
||||||
import { SolidResource } from "./SolidResource"; |
import { SolidResource } from "./SolidResource"; |
||||||
|
import type { SolidContainerUri, SolidLeafUri } from "../types"; |
||||||
|
|
||||||
export class SolidLeaf extends SolidResource {} |
export class SolidLeaf |
||||||
|
extends SolidResource |
||||||
|
implements Resource<SolidLeafUri> { |
||||||
|
public uri: SolidLeafUri; |
||||||
|
|
||||||
|
constructor() { |
||||||
|
super() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
@ -1,126 +1,821 @@ |
|||||||
import type { |
import type { |
||||||
|
ConnectedContext, |
||||||
ConnectedResult, |
ConnectedResult, |
||||||
Resource, |
Resource, |
||||||
|
ResourceEventEmitter, |
||||||
ResourceResult, |
ResourceResult, |
||||||
SubscriptionCallbacks, |
ResourceSuccess, |
||||||
} from "@ldo/connected"; |
} from "@ldo/connected"; |
||||||
import type { SolidContainerUri, SolidLeafUri } from "../types"; |
import type { SolidContainerUri, SolidLeafUri } from "../types"; |
||||||
|
import EventEmitter from "events"; |
||||||
|
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||||
|
import type { BatchedRequester } from "../requester/BatchedRequester"; |
||||||
|
import type { WacRule } from "../wac/WacRule"; |
||||||
|
import type { SolidNotificationSubscription } from "../notifications/SolidNotificationSubscription"; |
||||||
|
import { Websocket2023NotificationSubscription } from "../notifications/Websocket2023NotificationSubscription"; |
||||||
|
import { getParentUri } from "../util/rdfUtils"; |
||||||
|
import { |
||||||
|
isReadSuccess, |
||||||
|
type ReadSuccess, |
||||||
|
} from "../requester/results/success/ReadSuccess"; |
||||||
|
import type { |
||||||
|
ReadContainerResult, |
||||||
|
ReadLeafResult, |
||||||
|
} from "../requester/requests/readResource"; |
||||||
|
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; |
||||||
|
import type { DeleteResult } from "../requester/requests/deleteResource"; |
||||||
|
import type { |
||||||
|
ContainerCreateAndOverwriteResult, |
||||||
|
ContainerCreateIfAbsentResult, |
||||||
|
LeafCreateAndOverwriteResult, |
||||||
|
LeafCreateIfAbsentResult, |
||||||
|
} from "../requester/requests/createDataResource"; |
||||||
|
import type { SolidContainer } from "./SolidContainer"; |
||||||
|
import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; |
||||||
|
import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; |
||||||
|
import type { SolidLeaf } from "./SolidLeaf"; |
||||||
|
import type { GetWacUriError } from "../wac/getWacUri"; |
||||||
|
import { getWacUri, type GetWacUriResult } from "../wac/getWacUri"; |
||||||
|
import { getWacRuleWithAclUri, type GetWacRuleResult } from "../wac/getWacRule"; |
||||||
|
import { setWacRuleForAclUri } from "../wac/setWacRule"; |
||||||
|
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; |
||||||
|
|
||||||
export class SolidResource |
export abstract class SolidResource |
||||||
|
extends (EventEmitter as new () => ResourceEventEmitter) |
||||||
implements Resource<SolidLeafUri | SolidContainerUri> |
implements Resource<SolidLeafUri | SolidContainerUri> |
||||||
{ |
{ |
||||||
uri: SolidLeafUri | SolidContainerUri; |
/** |
||||||
type: string; |
* @internal |
||||||
status: ConnectedResult; |
* The ConnectedContext from the Parent Dataset |
||||||
isLoading(): boolean { |
*/ |
||||||
throw new Error("Method not implemented."); |
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: "leaf" | "container"; |
||||||
|
|
||||||
|
/** |
||||||
|
* The status of the last request made for this resource |
||||||
|
*/ |
||||||
|
abstract status: ConnectedResult; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Batched Requester for the Resource |
||||||
|
*/ |
||||||
|
protected abstract readonly requester: BatchedRequester<this>; |
||||||
|
|
||||||
|
/** |
||||||
|
* @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: SolidNotificationSubscription; |
||||||
|
|
||||||
|
/** |
||||||
|
* 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, |
||||||
|
this.onNotification.bind(this), |
||||||
|
this.context, |
||||||
|
); |
||||||
} |
} |
||||||
isFetched(): boolean { |
|
||||||
|
readIfAbsent(): Promise<ResourceResult<this>> { |
||||||
throw new Error("Method not implemented."); |
throw new Error("Method not implemented."); |
||||||
} |
} |
||||||
isUnfetched(): boolean { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* =========================================================================== |
||||||
|
* 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(); |
||||||
} |
} |
||||||
isDoingInitialFetch(): boolean { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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(); |
||||||
} |
} |
||||||
isPresent(): boolean { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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(); |
||||||
} |
} |
||||||
isAbsent(): boolean { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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(); |
||||||
} |
} |
||||||
isSubscribedToNotifications(): boolean { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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(); |
||||||
} |
} |
||||||
read(): Promise<ResourceResult<this>> { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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(); |
||||||
} |
} |
||||||
readIfAbsent(): Promise<ResourceResult<this>> { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* =========================================================================== |
||||||
|
* 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; |
||||||
} |
} |
||||||
subscribeToNotifications(callbacks?: SubscriptionCallbacks): Promise<string> { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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; |
||||||
} |
} |
||||||
unsubscribeFromNotifications(subscriptionId: string): Promise<void> { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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; |
||||||
} |
} |
||||||
unsubscribeFromAllNotifications(): Promise<void> { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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; |
||||||
} |
} |
||||||
addListener<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* Is this resource currently listening to notifications from this document |
||||||
): this { |
* @returns true if the resource is subscribed to notifications, false if not |
||||||
throw new Error("Method not implemented."); |
* |
||||||
|
* @example |
||||||
|
* ```typescript
|
||||||
|
* await resource.subscribeToNotifications(); |
||||||
|
* // Logs "true"
|
||||||
|
* console.log(resource.isSubscribedToNotifications()); |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
isSubscribedToNotifications(): boolean { |
||||||
|
return this.notificationSubscription.isSubscribedToNotifications(); |
||||||
} |
} |
||||||
on<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* =========================================================================== |
||||||
): this { |
* HELPER METHODS |
||||||
throw new Error("Method not implemented."); |
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @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"); |
||||||
|
} |
||||||
} |
} |
||||||
once<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* =========================================================================== |
||||||
): this { |
* READ METHODS |
||||||
throw new Error("Method not implemented."); |
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @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<this>) { |
||||||
|
this.absent = result.type === "absentReadSuccess"; |
||||||
|
this.didInitialFetch = true; |
||||||
} |
} |
||||||
prependListener<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* @internal |
||||||
): this { |
* A helper method that handles the core functions for reading |
||||||
throw new Error("Method not implemented."); |
* @returns ReadResult |
||||||
|
*/ |
||||||
|
protected async handleRead(): Promise<ReadContainerResult | ReadLeafResult> { |
||||||
|
const result = await this.requester.read(); |
||||||
|
this.status = result; |
||||||
|
if (result.isError) return result; |
||||||
|
this.updateWithReadSuccess(result); |
||||||
|
this.emitThisAndParent(); |
||||||
|
return result; |
||||||
} |
} |
||||||
prependOnceListener<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* @internal |
||||||
): this { |
* Converts the current state of this resource to a readResult |
||||||
throw new Error("Method not implemented."); |
* @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(); |
||||||
} |
} |
||||||
off<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* =========================================================================== |
||||||
): this { |
* DELETE METHODS |
||||||
throw new Error("Method not implemented."); |
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @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<this>) { |
||||||
|
this.absent = true; |
||||||
|
this.didInitialFetch = true; |
||||||
} |
} |
||||||
removeAllListeners<E extends "update" | "notification">( |
|
||||||
event?: E | undefined, |
/** |
||||||
): this { |
* @internal |
||||||
throw new Error("Method not implemented."); |
* Helper method that handles the core functions for deleting a resource |
||||||
|
* @returns DeleteResult |
||||||
|
*/ |
||||||
|
protected async handleDelete(): Promise<DeleteResult<this>> { |
||||||
|
const result = await this.requester.delete(); |
||||||
|
this.status = result; |
||||||
|
if (result.isError) return result; |
||||||
|
this.updateWithDeleteSuccess(result); |
||||||
|
this.emitThisAndParent(); |
||||||
|
return result; |
||||||
} |
} |
||||||
removeListener<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
listener: { update: () => void; notification: () => void }[E], |
* =========================================================================== |
||||||
): this { |
* CREATE METHODS |
||||||
throw new Error("Method not implemented."); |
* =========================================================================== |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* A helper method updates this resource's internal state upon create success |
||||||
|
* @param _result - the result of the create success |
||||||
|
*/ |
||||||
|
protected updateWithCreateSuccess(result: ResourceSuccess<this>) { |
||||||
|
this.absent = false; |
||||||
|
this.didInitialFetch = true; |
||||||
|
if (isReadSuccess(result)) { |
||||||
|
this.updateWithReadSuccess(result); |
||||||
|
} |
||||||
} |
} |
||||||
emit<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
...args: Parameters<{ update: () => void; notification: () => void }[E]> |
* Creates a resource at this URI and overwrites any that already exists |
||||||
): boolean { |
* @returns CreateAndOverwriteResult |
||||||
throw new Error("Method not implemented."); |
* |
||||||
|
* @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) return result; |
||||||
|
this.updateWithCreateSuccess(result); |
||||||
|
this.emitThisAndParent(); |
||||||
|
return result; |
||||||
} |
} |
||||||
eventNames(): (string | symbol)[] { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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) return result; |
||||||
|
this.updateWithCreateSuccess(result); |
||||||
|
this.emitThisAndParent(); |
||||||
|
return result; |
||||||
} |
} |
||||||
rawListeners<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
): { update: () => void; notification: () => void }[E][] { |
* =========================================================================== |
||||||
throw new Error("Method not implemented."); |
* 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> { |
||||||
|
// Get the wacUri if not already present
|
||||||
|
if (!options?.ignoreCache && this.wacUri) { |
||||||
|
return { |
||||||
|
type: "getWacUriSuccess", |
||||||
|
wacUri: this.wacUri, |
||||||
|
isError: false, |
||||||
|
uri: this.uri, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const wacUriResult = await getWacUri(this.uri, { |
||||||
|
fetch: this.context.solid.fetch, |
||||||
|
}); |
||||||
|
if (wacUriResult.isError) { |
||||||
|
return wacUriResult; |
||||||
|
} |
||||||
|
this.wacUri = wacUriResult.wacUri; |
||||||
|
return wacUriResult; |
||||||
} |
} |
||||||
listeners<E extends "update" | "notification">( |
|
||||||
event: E, |
/** |
||||||
): { update: () => void; notification: () => void }[E][] { |
* Retrieves web access control (WAC) rules for this resource |
||||||
throw new Error("Method not implemented."); |
* @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 | GetWacRuleResult> { |
||||||
|
// Return the wac rule if it's already cached
|
||||||
|
if (!options?.ignoreCache && this.wacRule) { |
||||||
|
return { |
||||||
|
type: "getWacRuleSuccess", |
||||||
|
uri: this.uri, |
||||||
|
isError: false, |
||||||
|
wacRule: 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, { |
||||||
|
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( |
||||||
|
this, |
||||||
|
`Resource "${this.uri}" has no Effective ACL resource`, |
||||||
|
); |
||||||
|
} |
||||||
|
return parentResource.getWac(); |
||||||
} |
} |
||||||
listenerCount<E extends "update" | "notification">(event: E): number { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* 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 | SetWacRuleResult> { |
||||||
|
const wacUriResult = await this.getWacUri(); |
||||||
|
if (wacUriResult.isError) return wacUriResult; |
||||||
|
|
||||||
|
const result = await setWacRuleForAclUri( |
||||||
|
wacUriResult.wacUri, |
||||||
|
wacRule, |
||||||
|
this.uri, |
||||||
|
{ |
||||||
|
fetch: this.context.solid.fetch, |
||||||
|
}, |
||||||
|
); |
||||||
|
if (result.isError) return result; |
||||||
|
this.wacRule = result.wacRule; |
||||||
|
return result; |
||||||
} |
} |
||||||
getMaxListeners(): number { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* =========================================================================== |
||||||
|
* 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, |
||||||
|
): Promise<string> { |
||||||
|
return await this.notificationSubscription.subscribeToNotifications( |
||||||
|
callbacks, |
||||||
|
); |
||||||
} |
} |
||||||
setMaxListeners(maxListeners: number): this { |
|
||||||
throw new Error("Method not implemented."); |
/** |
||||||
|
* @internal |
||||||
|
* Function that triggers whenever a notification is recieved. |
||||||
|
*/ |
||||||
|
protected async onNotification(message: NotificationMessage): Promise<void> { |
||||||
|
const objectResource = this.context.solidLdoDataset.getResource( |
||||||
|
message.object, |
||||||
|
); |
||||||
|
switch (message.type) { |
||||||
|
case "Update": |
||||||
|
case "Add": |
||||||
|
await objectResource.read(); |
||||||
|
return; |
||||||
|
case "Delete": |
||||||
|
case "Remove": |
||||||
|
// Delete the resource without have to make an additional read request
|
||||||
|
updateDatasetOnSuccessfulDelete( |
||||||
|
message.object, |
||||||
|
this.context.solidLdoDataset, |
||||||
|
); |
||||||
|
objectResource.updateWithDeleteSuccess({ |
||||||
|
type: "deleteSuccess", |
||||||
|
isError: false, |
||||||
|
uri: message.object, |
||||||
|
resourceExisted: true, |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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(); |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,187 @@ |
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||||
|
export interface WaitingProcess<Args extends any[], Return> { |
||||||
|
name: string; |
||||||
|
args: Args; |
||||||
|
perform: (...args: Args) => Promise<Return>; |
||||||
|
awaitingResolutions: ((returnValue: Return) => void)[]; |
||||||
|
awaitingRejections: ((err: any) => void)[]; |
||||||
|
after?: (result: Return) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const ANY_KEY = "any"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Options for processes that are waiting to execute |
||||||
|
*/ |
||||||
|
export interface WaitingProcessOptions<Args extends any[], Return> { |
||||||
|
/** |
||||||
|
* The name of the process like "read" or "delete" |
||||||
|
*/ |
||||||
|
name: string; |
||||||
|
/** |
||||||
|
* The arguements supplied to the process |
||||||
|
*/ |
||||||
|
args: Args; |
||||||
|
/** |
||||||
|
* A function that will be triggered when it's time to execute this process |
||||||
|
* @param args - arguments supplied to the process |
||||||
|
* @returns a return type |
||||||
|
*/ |
||||||
|
perform: (...args: Args) => Promise<Return>; |
||||||
|
/** |
||||||
|
* A custom function to modify the queue based on the current state of the |
||||||
|
* queue |
||||||
|
* @param processQueue - The current process queue |
||||||
|
* @param currentlyProcessing - The Process that is currently executing |
||||||
|
* @param args - provided args |
||||||
|
* @returns A WaitingProcess that this request should listen to, or undefined |
||||||
|
* if it should create its own |
||||||
|
*/ |
||||||
|
modifyQueue: ( |
||||||
|
processQueue: WaitingProcess<any[], any>[], |
||||||
|
currentlyProcessing: WaitingProcess<any[], any> | undefined, |
||||||
|
args: Args, |
||||||
|
) => WaitingProcess<any[], any> | undefined; |
||||||
|
after?: (result: Return) => void; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* A utility for batching a request |
||||||
|
*/ |
||||||
|
export class RequestBatcher { |
||||||
|
/** |
||||||
|
* A mapping between a process key and the last time in UTC a process of that |
||||||
|
* key was executed. |
||||||
|
*/ |
||||||
|
private lastRequestTimestampMap: Record<string, number> = {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* A pointer to the current process the batcher is working on |
||||||
|
*/ |
||||||
|
private currentlyProcessing: WaitingProcess<any[], any> | undefined = |
||||||
|
undefined; |
||||||
|
|
||||||
|
/** |
||||||
|
* A queue of upcoming processes |
||||||
|
*/ |
||||||
|
private processQueue: WaitingProcess<any[], any>[] = []; |
||||||
|
|
||||||
|
/** |
||||||
|
* The amount of time (in milliseconds) between requests of the same key |
||||||
|
*/ |
||||||
|
public batchMillis: number; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param options - options, including the value for batchMillis |
||||||
|
*/ |
||||||
|
constructor( |
||||||
|
options?: Partial<{ |
||||||
|
batchMillis: number; |
||||||
|
}>, |
||||||
|
) { |
||||||
|
this.batchMillis = options?.batchMillis || 1000; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if the request batcher is currently working on a process |
||||||
|
* @param key - the key of the process to check |
||||||
|
* @returns true if the batcher is currently working on the provided process |
||||||
|
*/ |
||||||
|
public isLoading(key: string): boolean { |
||||||
|
if (key === ANY_KEY) return !!this.currentlyProcessing; |
||||||
|
return this.currentlyProcessing?.name === key; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Triggers the next process in the queue or triggers a timeout to wait to |
||||||
|
* execute the next process in the queue if not enough time has passed since |
||||||
|
* the last process was triggered. |
||||||
|
*/ |
||||||
|
private triggerOrWaitProcess() { |
||||||
|
if (!this.processQueue[0] || this.currentlyProcessing) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.currentlyProcessing = this.processQueue.shift(); |
||||||
|
const processName = this.currentlyProcessing!.name; |
||||||
|
|
||||||
|
// Set last request timestamp if not available
|
||||||
|
if (!this.lastRequestTimestampMap[processName]) { |
||||||
|
this.lastRequestTimestampMap[processName] = Date.UTC(0, 0, 0, 0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
const lastRequestTimestamp = this.lastRequestTimestampMap[processName]; |
||||||
|
const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; |
||||||
|
|
||||||
|
const triggerProcess = async () => { |
||||||
|
this.lastRequestTimestampMap[processName] = Date.now(); |
||||||
|
this.lastRequestTimestampMap[ANY_KEY] = Date.now(); |
||||||
|
// Remove the process from the queue
|
||||||
|
const processToTrigger = this.currentlyProcessing; |
||||||
|
if (processToTrigger) { |
||||||
|
this.currentlyProcessing = processToTrigger; |
||||||
|
try { |
||||||
|
const returnValue = await processToTrigger.perform( |
||||||
|
...processToTrigger.args, |
||||||
|
); |
||||||
|
if (processToTrigger.after) { |
||||||
|
processToTrigger.after(returnValue); |
||||||
|
} |
||||||
|
processToTrigger.awaitingResolutions.forEach((callback) => { |
||||||
|
callback(returnValue); |
||||||
|
}); |
||||||
|
} catch (err) { |
||||||
|
processToTrigger.awaitingRejections.forEach((callback) => { |
||||||
|
callback(err); |
||||||
|
}); |
||||||
|
} |
||||||
|
this.currentlyProcessing = undefined; |
||||||
|
|
||||||
|
this.triggerOrWaitProcess(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if (timeSinceLastTrigger < this.batchMillis) { |
||||||
|
setTimeout(triggerProcess, this.batchMillis - timeSinceLastTrigger); |
||||||
|
} else { |
||||||
|
triggerProcess(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds a process to the queue and waits for the process to be complete |
||||||
|
* @param options - WaitingProcessOptions |
||||||
|
* @returns A promise that resolves when the process resolves |
||||||
|
*/ |
||||||
|
public async queueProcess<Args extends any[], ReturnType>( |
||||||
|
options: WaitingProcessOptions<Args, ReturnType>, |
||||||
|
): Promise<ReturnType> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const shouldAwait = options.modifyQueue( |
||||||
|
this.processQueue, |
||||||
|
this.currentlyProcessing, |
||||||
|
options.args, |
||||||
|
); |
||||||
|
|
||||||
|
if (shouldAwait) { |
||||||
|
shouldAwait.awaitingResolutions.push(resolve); |
||||||
|
shouldAwait.awaitingRejections.push(reject); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const waitingProcess: WaitingProcess<Args, ReturnType> = { |
||||||
|
name: options.name, |
||||||
|
args: options.args, |
||||||
|
perform: options.perform, |
||||||
|
awaitingResolutions: [resolve], |
||||||
|
awaitingRejections: [reject], |
||||||
|
after: options.after, |
||||||
|
}; |
||||||
|
// HACK: Ugly cast
|
||||||
|
this.processQueue.push( |
||||||
|
waitingProcess as unknown as WaitingProcess<any[], any>, |
||||||
|
); |
||||||
|
this.triggerOrWaitProcess(); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
import crossFetch from "cross-fetch"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Guantees that some kind of fetch is available |
||||||
|
* |
||||||
|
* @param fetchInput - A potential fetch object |
||||||
|
* @returns a proper fetch object. Cross-fetch is default |
||||||
|
*/ |
||||||
|
export function guaranteeFetch(fetchInput?: typeof fetch): typeof fetch { |
||||||
|
return fetchInput || crossFetch; |
||||||
|
} |
@ -0,0 +1,149 @@ |
|||||||
|
import type { LdoDataset } from "@ldo/ldo"; |
||||||
|
import { parseRdf } from "@ldo/ldo"; |
||||||
|
import { namedNode, quad as createQuad } from "@rdfjs/data-model"; |
||||||
|
import type { Dataset } from "@rdfjs/types"; |
||||||
|
import type { ContainerUri } from "./uriTypes"; |
||||||
|
import { isContainerUri } from "./uriTypes"; |
||||||
|
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; |
||||||
|
import { UnexpectedResourceError } from "../requester/results/error/ErrorResult"; |
||||||
|
|
||||||
|
export const ldpContains = namedNode("http://www.w3.org/ns/ldp#contains"); |
||||||
|
export const rdfType = namedNode( |
||||||
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||||
|
); |
||||||
|
export const ldpResource = namedNode("http://www.w3.org/ns/ldp#Resource"); |
||||||
|
export const ldpContainer = namedNode("http://www.w3.org/ns/ldp#Container"); |
||||||
|
export const ldpBasicContainer = namedNode( |
||||||
|
"http://www.w3.org/ns/ldp#BasicContainer", |
||||||
|
); |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Gets the URI of a parent according the the Solid Spec |
||||||
|
* |
||||||
|
* @param uri - the child URI |
||||||
|
* @returns A parent URI or undefined if not possible |
||||||
|
*/ |
||||||
|
export function getParentUri(uri: string): ContainerUri | undefined { |
||||||
|
const urlObject = new URL(uri); |
||||||
|
const pathItems = urlObject.pathname.split("/"); |
||||||
|
if ( |
||||||
|
pathItems.length < 2 || |
||||||
|
(pathItems.length === 2 && pathItems[1].length === 0) |
||||||
|
) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
if (pathItems[pathItems.length - 1] === "") { |
||||||
|
pathItems.pop(); |
||||||
|
} |
||||||
|
pathItems.pop(); |
||||||
|
urlObject.pathname = `${pathItems.join("/")}/`; |
||||||
|
return urlObject.toString() as ContainerUri; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Gets the slug (last part of the path) for a given URI |
||||||
|
* |
||||||
|
* @param uri - the full URI |
||||||
|
* @returns the slug of the URI |
||||||
|
*/ |
||||||
|
export function getSlug(uri: string): string { |
||||||
|
const urlObject = new URL(uri); |
||||||
|
const pathItems = urlObject.pathname.split("/"); |
||||||
|
return pathItems[pathItems.length - 1] || pathItems[pathItems.length - 2]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Deletes mention of a resource from the provided dataset |
||||||
|
* |
||||||
|
* @param resourceUri - the resource to delete |
||||||
|
* @param dataset - dataset to modify |
||||||
|
*/ |
||||||
|
export function deleteResourceRdfFromContainer( |
||||||
|
resourceUri: string, |
||||||
|
dataset: Dataset, |
||||||
|
) { |
||||||
|
const parentUri = getParentUri(resourceUri); |
||||||
|
if (parentUri) { |
||||||
|
const parentNode = namedNode(parentUri); |
||||||
|
const resourceNode = namedNode(resourceUri); |
||||||
|
dataset.delete( |
||||||
|
createQuad(parentNode, ldpContains, resourceNode, parentNode), |
||||||
|
); |
||||||
|
dataset.deleteMatches(resourceNode, undefined, undefined, parentNode); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Adds a resource to a container in an RDF dataset |
||||||
|
* |
||||||
|
* @param resourceUri - the resource to add |
||||||
|
* @param dataset - the dataset to modify |
||||||
|
*/ |
||||||
|
export function addResourceRdfToContainer( |
||||||
|
resourceUri: string, |
||||||
|
dataset: Dataset, |
||||||
|
) { |
||||||
|
const parentUri = getParentUri(resourceUri); |
||||||
|
if (parentUri) { |
||||||
|
const parentNode = namedNode(parentUri); |
||||||
|
const resourceNode = namedNode(resourceUri); |
||||||
|
dataset.add(createQuad(parentNode, ldpContains, resourceNode, parentNode)); |
||||||
|
dataset.add(createQuad(resourceNode, rdfType, ldpResource, parentNode)); |
||||||
|
if (isContainerUri(resourceUri)) { |
||||||
|
dataset.add( |
||||||
|
createQuad(resourceNode, rdfType, ldpBasicContainer, parentNode), |
||||||
|
); |
||||||
|
dataset.add(createQuad(resourceNode, rdfType, ldpContainer, parentNode)); |
||||||
|
} |
||||||
|
addResourceRdfToContainer(parentUri, dataset); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Adds raw turtle to the provided dataset |
||||||
|
* @param rawTurtle - String of raw turtle |
||||||
|
* @param dataset - the dataset to modify |
||||||
|
* @param baseUri - base URI to parsing turtle |
||||||
|
* @returns Undefined if successful, noncompliantPodError if not |
||||||
|
*/ |
||||||
|
export async function addRawTurtleToDataset( |
||||||
|
rawTurtle: string, |
||||||
|
dataset: Dataset, |
||||||
|
baseUri: string, |
||||||
|
): Promise<undefined | NoncompliantPodError> { |
||||||
|
const rawTurtleResult = await rawTurtleToDataset(rawTurtle, baseUri); |
||||||
|
if (rawTurtleResult.isError) return rawTurtleResult; |
||||||
|
const loadedDataset = rawTurtleResult.dataset; |
||||||
|
const graphNode = namedNode(baseUri); |
||||||
|
// Destroy all triples that were once a part of this resouce
|
||||||
|
dataset.deleteMatches(undefined, undefined, undefined, graphNode); |
||||||
|
// Add the triples from the fetched item
|
||||||
|
dataset.addAll( |
||||||
|
loadedDataset.map((quad) => |
||||||
|
createQuad(quad.subject, quad.predicate, quad.object, graphNode), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export async function rawTurtleToDataset( |
||||||
|
rawTurtle: string, |
||||||
|
baseUri: string, |
||||||
|
): Promise<{ isError: false; dataset: LdoDataset } | NoncompliantPodError> { |
||||||
|
try { |
||||||
|
const loadedDataset = await parseRdf(rawTurtle, { |
||||||
|
baseIRI: baseUri, |
||||||
|
}); |
||||||
|
return { isError: false, dataset: loadedDataset }; |
||||||
|
} catch (err) { |
||||||
|
const error = UnexpectedResourceError.fromThrown(baseUri, err); |
||||||
|
return new NoncompliantPodError( |
||||||
|
baseUri, |
||||||
|
`Request returned noncompliant turtle: ${error.message}\n${rawTurtle}`, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
/** |
||||||
|
* A list of modes that a certain agent has access to |
||||||
|
*/ |
||||||
|
export interface AccessModeList { |
||||||
|
read: boolean; |
||||||
|
append: boolean; |
||||||
|
write: boolean; |
||||||
|
control: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A list of modes for each kind of agent |
||||||
|
*/ |
||||||
|
export interface WacRule { |
||||||
|
public: AccessModeList; |
||||||
|
authenticated: AccessModeList; |
||||||
|
agent: Record<string, AccessModeList>; |
||||||
|
} |
@ -0,0 +1,118 @@ |
|||||||
|
import type { GetWacRuleSuccess } from "./results/GetWacRuleSuccess"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; |
||||||
|
import type { HttpErrorResultType } from "../../requester/results/error/HttpErrorResult"; |
||||||
|
import { HttpErrorResult } from "../../requester/results/error/HttpErrorResult"; |
||||||
|
import type { NoncompliantPodError } from "../../requester/results/error/NoncompliantPodError"; |
||||||
|
import type { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||||
|
import { rawTurtleToDataset } from "../../util/rdfUtils"; |
||||||
|
import { AuthorizationShapeType } from "../../.ldo/wac.shapeTypes"; |
||||||
|
import type { AccessModeList, WacRule } from "./WacRule"; |
||||||
|
import type { Authorization } from "../../.ldo/wac.typings"; |
||||||
|
import type { WacRuleAbsent } from "./results/WacRuleAbsent"; |
||||||
|
|
||||||
|
export type GetWacRuleError = |
||||||
|
| HttpErrorResultType |
||||||
|
| NoncompliantPodError |
||||||
|
| UnexpectedResourceError; |
||||||
|
export type GetWacRuleResult = |
||||||
|
| GetWacRuleSuccess |
||||||
|
| GetWacRuleError |
||||||
|
| WacRuleAbsent; |
||||||
|
|
||||||
|
/** |
||||||
|
* Given the URI of an ACL document, return the Web Access Control (WAC) rules |
||||||
|
* @param aclUri: The URI for the ACL document |
||||||
|
* @param options: Options object to include an authenticated fetch function |
||||||
|
* @returns GetWacRuleResult |
||||||
|
*/ |
||||||
|
export async function getWacRuleWithAclUri( |
||||||
|
aclUri: string, |
||||||
|
options?: BasicRequestOptions, |
||||||
|
): Promise<GetWacRuleResult> { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
const response = await fetch(aclUri); |
||||||
|
const errorResult = HttpErrorResult.checkResponse(aclUri, response); |
||||||
|
if (errorResult) return errorResult; |
||||||
|
|
||||||
|
if (response.status === 404) { |
||||||
|
return { |
||||||
|
type: "wacRuleAbsent", |
||||||
|
uri: aclUri, |
||||||
|
isError: false, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
// Parse Turtle
|
||||||
|
const rawTurtle = await response.text(); |
||||||
|
const rawTurtleResult = await rawTurtleToDataset(rawTurtle, aclUri); |
||||||
|
if (rawTurtleResult.isError) return rawTurtleResult; |
||||||
|
const dataset = rawTurtleResult.dataset; |
||||||
|
const authorizations = dataset |
||||||
|
.usingType(AuthorizationShapeType) |
||||||
|
.matchSubject( |
||||||
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||||
|
"http://www.w3.org/ns/auth/acl#Authorization", |
||||||
|
); |
||||||
|
|
||||||
|
const wacRule: WacRule = { |
||||||
|
public: { |
||||||
|
read: false, |
||||||
|
write: false, |
||||||
|
append: false, |
||||||
|
control: false, |
||||||
|
}, |
||||||
|
authenticated: { |
||||||
|
read: false, |
||||||
|
write: false, |
||||||
|
append: false, |
||||||
|
control: false, |
||||||
|
}, |
||||||
|
agent: {}, |
||||||
|
}; |
||||||
|
|
||||||
|
function applyAccessModesToList( |
||||||
|
accessModeList: AccessModeList, |
||||||
|
authorization: Authorization, |
||||||
|
): void { |
||||||
|
authorization.mode?.forEach((mode) => { |
||||||
|
accessModeList[mode["@id"].toLowerCase()] = true; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
authorizations.forEach((authorization) => { |
||||||
|
if ( |
||||||
|
authorization.agentClass?.some( |
||||||
|
(agentClass) => agentClass["@id"] === "Agent", |
||||||
|
) |
||||||
|
) { |
||||||
|
applyAccessModesToList(wacRule.public, authorization); |
||||||
|
applyAccessModesToList(wacRule.authenticated, authorization); |
||||||
|
} |
||||||
|
if ( |
||||||
|
authorization.agentClass?.some( |
||||||
|
(agentClass) => agentClass["@id"] === "AuthenticatedAgent", |
||||||
|
) |
||||||
|
) { |
||||||
|
applyAccessModesToList(wacRule.authenticated, authorization); |
||||||
|
} |
||||||
|
authorization.agent?.forEach((agent) => { |
||||||
|
if (!wacRule.agent[agent["@id"]]) { |
||||||
|
wacRule.agent[agent["@id"]] = { |
||||||
|
read: false, |
||||||
|
write: false, |
||||||
|
append: false, |
||||||
|
control: false, |
||||||
|
}; |
||||||
|
} |
||||||
|
applyAccessModesToList(wacRule.agent[agent["@id"]], authorization); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
type: "getWacRuleSuccess", |
||||||
|
uri: aclUri, |
||||||
|
isError: false, |
||||||
|
wacRule, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
import type { GetWacUriSuccess } from "./results/GetWacUriSuccess"; |
||||||
|
import type { HttpErrorResultType } from "../../requester/results/error/HttpErrorResult"; |
||||||
|
import { |
||||||
|
HttpErrorResult, |
||||||
|
NotFoundHttpError, |
||||||
|
} from "../../requester/results/error/HttpErrorResult"; |
||||||
|
import { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; |
||||||
|
import { NoncompliantPodError } from "../../requester/results/error/NoncompliantPodError"; |
||||||
|
import { parse as parseLinkHeader } from "http-link-header"; |
||||||
|
import type { LeafUri } from "../../util/uriTypes"; |
||||||
|
|
||||||
|
export type GetWacUriError = |
||||||
|
| HttpErrorResultType |
||||||
|
| NotFoundHttpError |
||||||
|
| NoncompliantPodError |
||||||
|
| UnexpectedResourceError; |
||||||
|
export type GetWacUriResult = GetWacUriSuccess | GetWacUriError; |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the URI for the WAC rules of a specific resource |
||||||
|
* @param resourceUri: the URI of the resource |
||||||
|
* @param options: Options object to include an authenticated fetch function |
||||||
|
* @returns GetWacUriResult |
||||||
|
*/ |
||||||
|
export async function getWacUri( |
||||||
|
resourceUri: string, |
||||||
|
options?: BasicRequestOptions, |
||||||
|
): Promise<GetWacUriResult> { |
||||||
|
try { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
const response = await fetch(resourceUri, { |
||||||
|
method: "head", |
||||||
|
}); |
||||||
|
const errorResult = HttpErrorResult.checkResponse(resourceUri, response); |
||||||
|
if (errorResult) return errorResult; |
||||||
|
if (NotFoundHttpError.is(response)) { |
||||||
|
return new NotFoundHttpError( |
||||||
|
resourceUri, |
||||||
|
response, |
||||||
|
"Could not get access control rules because the resource does not exist.", |
||||||
|
); |
||||||
|
} |
||||||
|
// Get the URI from the link header
|
||||||
|
const linkHeader = response.headers.get("link"); |
||||||
|
if (!linkHeader) { |
||||||
|
return new NoncompliantPodError( |
||||||
|
resourceUri, |
||||||
|
"No link header present in request.", |
||||||
|
); |
||||||
|
} |
||||||
|
const parsedLinkHeader = parseLinkHeader(linkHeader); |
||||||
|
const aclUris = parsedLinkHeader.get("rel", "acl"); |
||||||
|
if (aclUris.length !== 1) { |
||||||
|
return new NoncompliantPodError( |
||||||
|
resourceUri, |
||||||
|
`There must be one link with a rel="acl"`, |
||||||
|
); |
||||||
|
} |
||||||
|
return { |
||||||
|
type: "getWacUriSuccess", |
||||||
|
isError: false, |
||||||
|
uri: resourceUri, |
||||||
|
wacUri: aclUris[0].uri as LeafUri, |
||||||
|
}; |
||||||
|
} catch (err: unknown) { |
||||||
|
return UnexpectedResourceError.fromThrown(resourceUri, err); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||||
|
import type { WacRule } from "../WacRule"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returned when a WAC rule is successfully retrieved |
||||||
|
*/ |
||||||
|
export interface GetWacRuleSuccess extends ResourceSuccess { |
||||||
|
type: "getWacRuleSuccess"; |
||||||
|
/** |
||||||
|
* The rule that was retrieved |
||||||
|
*/ |
||||||
|
wacRule: WacRule; |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||||
|
import type { LeafUri } from "../../../util/uriTypes"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returned when the URI for a resources ACL document was successfully retried |
||||||
|
*/ |
||||||
|
export interface GetWacUriSuccess extends ResourceSuccess { |
||||||
|
type: "getWacUriSuccess"; |
||||||
|
/** |
||||||
|
* The URI of the ACL document |
||||||
|
*/ |
||||||
|
wacUri: LeafUri; |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||||
|
import type { WacRule } from "../WacRule"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returned when rules were successfully written |
||||||
|
*/ |
||||||
|
export interface SetWacRuleSuccess extends ResourceSuccess { |
||||||
|
type: "setWacRuleSuccess"; |
||||||
|
/** |
||||||
|
* The written rule |
||||||
|
*/ |
||||||
|
wacRule: WacRule; |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returned if no WAC rule was returned from the server |
||||||
|
*/ |
||||||
|
export interface WacRuleAbsent extends ResourceSuccess { |
||||||
|
type: "wacRuleAbsent"; |
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
import { createLdoDataset } from "@ldo/ldo"; |
||||||
|
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; |
||||||
|
import type { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||||
|
import { |
||||||
|
HttpErrorResult, |
||||||
|
type HttpErrorResultType, |
||||||
|
} from "../../requester/results/error/HttpErrorResult"; |
||||||
|
import { isContainerUri, type LeafUri } from "../../util/uriTypes"; |
||||||
|
import type { AccessModeList, WacRule } from "./WacRule"; |
||||||
|
import type { SetWacRuleSuccess } from "./results/SetWacRuleSuccess"; |
||||||
|
import type { Authorization } from "../../.ldo/wac.typings"; |
||||||
|
import { AuthorizationShapeType } from "../../.ldo/wac.shapeTypes"; |
||||||
|
import { v4 } from "uuid"; |
||||||
|
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||||
|
|
||||||
|
export type SetWacRuleError = HttpErrorResultType | UnexpectedResourceError; |
||||||
|
export type SetWacRuleResult = SetWacRuleSuccess | SetWacRuleError; |
||||||
|
|
||||||
|
/** |
||||||
|
* Given the URI of an ACL document and some WAC rules, set the WAC rules of |
||||||
|
* that document |
||||||
|
* @param aclUri: The URI for the ACL document |
||||||
|
* @param newRule: A new WAC rule to set. This will overwrite old rules |
||||||
|
* @param accessTo: The document this rule refers to |
||||||
|
* @param options: Options object to include an authenticated fetch function |
||||||
|
* @returns SetWacRuleResult |
||||||
|
*/ |
||||||
|
export async function setWacRuleForAclUri( |
||||||
|
aclUri: LeafUri, |
||||||
|
newRule: WacRule, |
||||||
|
accessTo: string, |
||||||
|
options?: BasicRequestOptions, |
||||||
|
): Promise<SetWacRuleResult> { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
// The rule map keeps track of all the rules that are currently being used
|
||||||
|
// so that similar rules can be grouped together
|
||||||
|
const ruleMap: Record<string, Authorization> = {}; |
||||||
|
// The dataset that will eventually be sent to the Pod
|
||||||
|
const dataset = createLdoDataset(); |
||||||
|
|
||||||
|
// Helper function to add rules to the dataset by grouping them in the ruleMap
|
||||||
|
function addRuleToDataset( |
||||||
|
type: "public" | "authenticated" | "agent", |
||||||
|
accessModeList: AccessModeList, |
||||||
|
agentId?: string, |
||||||
|
) { |
||||||
|
const accessModeListHash = hashAccessModeList(accessModeList); |
||||||
|
// No need to add if all access is false
|
||||||
|
if (accessModeListHash === "") return; |
||||||
|
if (!ruleMap[accessModeListHash]) { |
||||||
|
const authorization = dataset |
||||||
|
.usingType(AuthorizationShapeType) |
||||||
|
.fromSubject(`${aclUri}#${v4()}`); |
||||||
|
authorization.type = { "@id": "Authorization" }; |
||||||
|
if (accessModeList.read) authorization.mode?.add({ "@id": "Read" }); |
||||||
|
if (accessModeList.write) authorization.mode?.add({ "@id": "Write" }); |
||||||
|
if (accessModeList.append) authorization.mode?.add({ "@id": "Append" }); |
||||||
|
if (accessModeList.control) authorization.mode?.add({ "@id": "Control" }); |
||||||
|
authorization.accessTo = { "@id": accessTo }; |
||||||
|
if (isContainerUri(accessTo)) { |
||||||
|
authorization.default = { "@id": accessTo }; |
||||||
|
} |
||||||
|
ruleMap[accessModeListHash] = authorization; |
||||||
|
} |
||||||
|
const authorization = ruleMap[accessModeListHash]; |
||||||
|
// Add agents to the rule
|
||||||
|
if (type === "public") { |
||||||
|
authorization.agentClass?.add({ "@id": "Agent" }); |
||||||
|
} else if (type === "authenticated") { |
||||||
|
authorization.agentClass?.add({ "@id": "AuthenticatedAgent" }); |
||||||
|
} else if (type === "agent" && agentId) { |
||||||
|
authorization.agent?.add({ "@id": agentId }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Add each rule to the dataset
|
||||||
|
addRuleToDataset("public", newRule.public); |
||||||
|
addRuleToDataset("authenticated", newRule.authenticated); |
||||||
|
Object.entries(newRule.agent).forEach(([agentUri, accessModeList]) => { |
||||||
|
addRuleToDataset("agent", accessModeList, agentUri); |
||||||
|
}); |
||||||
|
|
||||||
|
// Save to Pod
|
||||||
|
const response = await fetch(aclUri, { |
||||||
|
method: "PUT", |
||||||
|
headers: { |
||||||
|
"content-type": "text/turtle", |
||||||
|
}, |
||||||
|
body: dataset.toString(), |
||||||
|
}); |
||||||
|
const errorResult = HttpErrorResult.checkResponse(aclUri, response); |
||||||
|
if (errorResult) return errorResult; |
||||||
|
|
||||||
|
return { |
||||||
|
type: "setWacRuleSuccess", |
||||||
|
uri: aclUri, |
||||||
|
isError: false, |
||||||
|
wacRule: newRule, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
// Hashes the access mode list for use in the rule map
|
||||||
|
function hashAccessModeList(list: AccessModeList): string { |
||||||
|
return Object.entries(list).reduce( |
||||||
|
(agg, [key, isPresent]) => (isPresent ? agg + key : agg), |
||||||
|
"", |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue