Begin refactor for context

main
Jackson Morgan 6 months ago
parent 9ae7dea357
commit 11bf103980
  1. 42
      packages/connected-solid/src/index.ts
  2. 2
      packages/connected-solid/src/requester/requests/createDataResource.ts
  3. 6
      packages/connected-solid/src/requester/requests/readResource.ts
  4. 16
      packages/connected-solid/src/requester/results/error/InvalidUriError.ts
  5. 29
      packages/connected-solid/src/requester/results/success/SolidReadSuccess.ts
  6. 4
      packages/connected-solid/src/resources/SolidContainer.ts
  7. 4
      packages/connected-solid/src/resources/SolidLeaf.ts
  8. 2
      packages/connected-solid/src/resources/SolidResource.ts
  9. 64
      packages/connected-solid/test/ErrorResult.test.ts
  10. 51
      packages/connected-solid/test/Integration.test.ts
  11. 98
      packages/connected-solid/test/Websocket2023NotificationSubscription.test.ts
  12. 4
      packages/connected-solid/test/uriTypes.test.ts
  13. 11
      packages/connected/src/ConnectedContext.ts
  14. 30
      packages/connected/src/ConnectedLdoDataset.ts
  15. 199
      packages/connected/src/ConnectedLdoTransactionDataset.ts
  16. 9
      packages/connected/src/ConnectedPlugin.ts
  17. 57
      packages/connected/src/IConnectedLdoDataset.ts
  18. 5
      packages/connected/src/InvalidIdentifierResource.ts
  19. 12
      packages/connected/src/Resource.ts
  20. 4
      packages/connected/src/index.ts
  21. 30
      packages/connected/src/results/success/ReadSuccess.ts
  22. 6
      packages/connected/src/results/success/UpdateSuccess.ts
  23. 70
      packages/connected/src/util/splitChangesByGraph.ts

@ -1,8 +1,50 @@
export * from "./types"; export * from "./types";
export * from "./SolidConnectedPlugin"; export * from "./SolidConnectedPlugin";
export * from "./createSolidLdoDataset";
export * from "./resources/SolidResource"; export * from "./resources/SolidResource";
export * from "./resources/SolidContainer"; export * from "./resources/SolidContainer";
export * from "./resources/SolidLeaf"; export * from "./resources/SolidLeaf";
export * from "./requester/BatchedRequester";
export * from "./requester/ContainerBatchedRequester";
export * from "./requester/LeafBatchedRequester";
export * from "./requester/requests/checkRootContainer";
export * from "./requester/requests/createDataResource";
export * from "./requester/requests/deleteResource";
export * from "./requester/requests/readResource";
export * from "./requester/requests/requestOptions";
export * from "./requester/requests/updateDataResource";
export * from "./requester/requests/uploadResource";
export * from "./requester/results/success/CheckRootContainerSuccess";
export * from "./requester/results/success/CreateSuccess";
export * from "./requester/results/success/DeleteSuccess";
export * from "./requester/results/success/SolidReadSuccess";
export * from "./requester/results/error/AccessControlError";
export * from "./requester/results/error/HttpErrorResult";
export * from "./requester/results/error/NoRootContainerError";
export * from "./requester/results/error/NoncompliantPodError";
export * from "./requester/util/modifyQueueFuntions";
export * from "./util/isSolidUri"; export * from "./util/isSolidUri";
export * from "./util/guaranteeFetch";
export * from "./util/rdfUtils";
export * from "./util/RequestBatcher";
export * from "./wac/getWacRule";
export * from "./wac/getWacUri";
export * from "./wac/setWacRule";
export * from "./wac/WacRule";
export * from "./wac/results/GetWacRuleSuccess";
export * from "./wac/results/GetWacUriSuccess";
export * from "./wac/results/SetWacRuleSuccess";
export * from "./wac/results/WacRuleAbsent";
export * from "./notifications/SolidNotificationMessage";
export * from "./notifications/SolidNotificationSubscription";
export * from "./notifications/Websocket2023NotificationSubscription";
export * from "./notifications/results/NotificationErrors";

@ -10,7 +10,7 @@ import { UnexpectedResourceError } from "@ldo/connected";
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; import type { HttpErrorResultType } from "../results/error/HttpErrorResult";
import { HttpErrorResult } from "../results/error/HttpErrorResult"; import { HttpErrorResult } from "../results/error/HttpErrorResult";
import { CreateSuccess } from "../results/success/CreateSuccess"; import { CreateSuccess } from "../results/success/CreateSuccess";
import type { AbsentReadSuccess } from "../results/success/ReadSuccess"; import type { AbsentReadSuccess } from "../results/success/SolidReadSuccess";
import type { DeleteResultError } from "./deleteResource"; import type { DeleteResultError } from "./deleteResource";
import { deleteResource } from "./deleteResource"; import { deleteResource } from "./deleteResource";
import type { import type {

@ -11,9 +11,9 @@ import type { DatasetRequestOptions } from "./requestOptions";
import { import {
BinaryReadSuccess, BinaryReadSuccess,
DataReadSuccess, DataReadSuccess,
} from "../results/success/ReadSuccess"; } from "../results/success/SolidReadSuccess";
import { ContainerReadSuccess } from "../results/success/ReadSuccess"; import { ContainerReadSuccess } from "../results/success/SolidReadSuccess";
import { AbsentReadSuccess } from "../results/success/ReadSuccess"; import { AbsentReadSuccess } from "../results/success/SolidReadSuccess";
import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; import { NoncompliantPodError } from "../results/error/NoncompliantPodError";
import { guaranteeFetch } from "../../util/guaranteeFetch"; import { guaranteeFetch } from "../../util/guaranteeFetch";
import type { Resource } from "@ldo/connected"; import type { Resource } from "@ldo/connected";

@ -1,16 +0,0 @@
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.`);
}
}

@ -1,26 +1,7 @@
import { ResourceSuccess } from "@ldo/connected";
import type { Resource, ResourceResult } from "@ldo/connected"; import type { Resource, ResourceResult } from "@ldo/connected";
import type { SolidLeaf } from "../../../resources/SolidLeaf"; import type { SolidLeaf } from "../../../resources/SolidLeaf";
import type { SolidContainer } from "../../../resources/SolidContainer"; 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 * Indicates that the read request was successful and that the resource
* retrieved was a binary resource. * retrieved was a binary resource.
@ -77,16 +58,6 @@ export class ContainerReadSuccess extends ReadSuccess<SolidContainer> {
} }
} }
/**
* 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 * A helper function that checks to see if a result is a ReadSuccess result
* *

@ -19,8 +19,8 @@ import type {
ReadResultError, ReadResultError,
} from "../requester/requests/readResource"; } from "../requester/requests/readResource";
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess";
import type { AbsentReadSuccess } from "../requester/results/success/ReadSuccess"; import type { AbsentReadSuccess } from "../requester/results/success/SolidReadSuccess";
import type { ContainerReadSuccess } from "../requester/results/success/ReadSuccess"; import type { ContainerReadSuccess } from "../requester/results/success/SolidReadSuccess";
import { getParentUri, ldpContains } from "../util/rdfUtils"; import { getParentUri, ldpContains } from "../util/rdfUtils";
import { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; import { NoRootContainerError } from "../requester/results/error/NoRootContainerError";
import type { SharedStatuses } from "./SolidResource"; import type { SharedStatuses } from "./SolidResource";

@ -10,11 +10,11 @@ import type { DeleteResult } from "../requester/requests/deleteResource";
import type { ReadLeafResult } from "../requester/requests/readResource"; import type { ReadLeafResult } from "../requester/requests/readResource";
import type { UpdateResult } from "../requester/requests/updateDataResource"; import type { UpdateResult } from "../requester/requests/updateDataResource";
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess";
import type { AbsentReadSuccess } from "../requester/results/success/ReadSuccess"; import type { AbsentReadSuccess } from "../requester/results/success/SolidReadSuccess";
import type { import type {
BinaryReadSuccess, BinaryReadSuccess,
DataReadSuccess, DataReadSuccess,
} from "../requester/results/success/ReadSuccess"; } from "../requester/results/success/SolidReadSuccess";
import { getParentUri } from "../util/rdfUtils"; import { getParentUri } from "../util/rdfUtils";
import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError";
import type { SharedStatuses } from "./SolidResource"; import type { SharedStatuses } from "./SolidResource";

@ -21,7 +21,7 @@ import { getParentUri } from "../util/rdfUtils";
import { import {
isReadSuccess, isReadSuccess,
type ReadSuccess, type ReadSuccess,
} from "../requester/results/success/ReadSuccess"; } from "../requester/results/success/SolidReadSuccess";
import type { import type {
ReadContainerResult, ReadContainerResult,
ReadLeafResult, ReadLeafResult,

@ -0,0 +1,64 @@
import {
AggregateError,
ErrorResult,
ResourceError,
UnexpectedResourceError,
} from "../src/requester/results/error/ErrorResult";
import { InvalidUriError } from "../src/requester/results/error/InvalidUriError";
describe("ErrorResult", () => {
describe("fromThrown", () => {
it("returns an UnexpecteResourceError if a string is provided", () => {
expect(
UnexpectedResourceError.fromThrown("https://example.com/", "hello")
.message,
).toBe("hello");
});
it("returns an UnexpecteResourceError if an odd valud is provided", () => {
expect(
UnexpectedResourceError.fromThrown("https://example.com/", 5).message,
).toBe("Error of type number thrown: 5");
});
});
describe("AggregateError", () => {
it("flattens aggregate errors provided to the constructor", () => {
const err1 = UnexpectedResourceError.fromThrown("https://abc.com", "1");
const err2 = UnexpectedResourceError.fromThrown("https://abc.com", "2");
const err3 = UnexpectedResourceError.fromThrown("https://abc.com", "3");
const err4 = UnexpectedResourceError.fromThrown("https://abc.com", "4");
const aggErr1 = new AggregateError([err1, err2]);
const aggErr2 = new AggregateError([err3, err4]);
const finalAgg = new AggregateError([aggErr1, aggErr2]);
expect(finalAgg.errors.length).toBe(4);
});
});
describe("default messages", () => {
class ConcreteResourceError extends ResourceError {
readonly type = "concreteResourceError" as const;
}
class ConcreteErrorResult extends ErrorResult {
readonly type = "concreteErrorResult" as const;
}
it("ResourceError fallsback to a default message if none is provided", () => {
expect(new ConcreteResourceError("https://example.com/").message).toBe(
"An unkown error for https://example.com/",
);
});
it("ErrorResult fallsback to a default message if none is provided", () => {
expect(new ConcreteErrorResult().message).toBe(
"An unkown error was encountered.",
);
});
it("InvalidUriError fallsback to a default message if none is provided", () => {
expect(new InvalidUriError("https://example.com/").message).toBe(
"https://example.com/ is an invalid uri.",
);
});
});
});

@ -1,18 +1,4 @@
import type { App } from "@solid/community-server"; import type { App } from "@solid/community-server";
import type {
Container,
ContainerUri,
Leaf,
LeafUri,
SolidLdoDataset,
UpdateResultError,
} from "../src";
import {
changeData,
commitData,
createSolidLdoDataset,
SolidLdoTransactionDataset,
} from "../src";
import { ROOT_CONTAINER, WEB_ID, createApp } from "./solidServer.helper"; import { ROOT_CONTAINER, WEB_ID, createApp } from "./solidServer.helper";
import { import {
namedNode, namedNode,
@ -21,20 +7,11 @@ import {
defaultGraph, defaultGraph,
} from "@rdfjs/data-model"; } from "@rdfjs/data-model";
import type { CreateSuccess } from "../src/requester/results/success/CreateSuccess"; import type { CreateSuccess } from "../src/requester/results/success/CreateSuccess";
import type { AggregateSuccess } from "../src/requester/results/success/SuccessResult";
import type { import type {
IgnoredInvalidUpdateSuccess, IgnoredInvalidUpdateSuccess,
UpdateDefaultGraphSuccess, UpdateDefaultGraphSuccess,
UpdateSuccess, UpdateSuccess,
} from "../src/requester/results/success/UpdateSuccess"; } from "../src/requester/results/success/UpdateSuccess";
import type {
ResourceResult,
ResourceSuccess,
} from "../src/resource/resourceResult/ResourceResult";
import type {
AggregateError,
UnexpectedResourceError,
} from "../src/requester/results/error/ErrorResult";
import type { InvalidUriError } from "../src/requester/results/error/InvalidUriError"; import type { InvalidUriError } from "../src/requester/results/error/InvalidUriError";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { PostShShapeType } from "./.ldo/post.shapeTypes"; import { PostShShapeType } from "./.ldo/post.shapeTypes";
@ -44,27 +21,33 @@ import type {
UnexpectedHttpError, UnexpectedHttpError,
} from "../src/requester/results/error/HttpErrorResult"; } from "../src/requester/results/error/HttpErrorResult";
import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError"; import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError";
import type { GetWacRuleSuccess } from "../src/resource/wac/results/GetWacRuleSuccess";
import type { WacRule } from "../src/resource/wac/WacRule";
import type { GetStorageContainerFromWebIdSuccess } from "../src/requester/results/success/CheckRootContainerSuccess"; import type { GetStorageContainerFromWebIdSuccess } from "../src/requester/results/success/CheckRootContainerSuccess";
import { generateAuthFetch } from "./authFetch.helper"; import { generateAuthFetch } from "./authFetch.helper";
import { wait } from "./utils.helper"; import { wait } from "./utils.helper";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
import type {
SolidContainer,
SolidContainerUri,
SolidLeaf,
SolidLeafUri,
} from "../src";
import { ConnectedLdoDataset } from "@ldo/connected";
const TEST_CONTAINER_SLUG = "test_ldo/"; const TEST_CONTAINER_SLUG = "test_ldo/";
const TEST_CONTAINER_URI = const TEST_CONTAINER_URI =
`${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as ContainerUri; `${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as SolidContainerUri;
const SAMPLE_DATA_URI = `${TEST_CONTAINER_URI}sample.ttl` as LeafUri; const SAMPLE_DATA_URI = `${TEST_CONTAINER_URI}sample.ttl` as SolidLeafUri;
const SAMPLE2_DATA_SLUG = "sample2.ttl"; const SAMPLE2_DATA_SLUG = "sample2.ttl";
const SAMPLE2_DATA_URI = `${TEST_CONTAINER_URI}${SAMPLE2_DATA_SLUG}` as LeafUri; const SAMPLE2_DATA_URI =
const SAMPLE_BINARY_URI = `${TEST_CONTAINER_URI}sample.txt` as LeafUri; `${TEST_CONTAINER_URI}${SAMPLE2_DATA_SLUG}` as SolidLeafUri;
const SAMPLE_BINARY_URI = `${TEST_CONTAINER_URI}sample.txt` as SolidLeafUri;
const SAMPLE2_BINARY_SLUG = `sample2.txt`; const SAMPLE2_BINARY_SLUG = `sample2.txt`;
const SAMPLE2_BINARY_URI = const SAMPLE2_BINARY_URI =
`${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as LeafUri; `${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as SolidLeafUri;
const SAMPLE_CONTAINER_URI = const SAMPLE_CONTAINER_URI =
`${TEST_CONTAINER_URI}sample_container/` as ContainerUri; `${TEST_CONTAINER_URI}sample_container/` as SolidContainerUri;
const SAMPLE_PROFILE_URI = `${TEST_CONTAINER_URI}profile.ttl` as LeafUri; const SAMPLE_PROFILE_URI = `${TEST_CONTAINER_URI}profile.ttl` as SolidLeafUri;
const SPIDER_MAN_TTL = `@base <http://example.org/> . const SPIDER_MAN_TTL = `@base <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@ -113,7 +96,7 @@ const SAMPLE_PROFILE_TTL = `
async function testRequestLoads<ReturnVal>( async function testRequestLoads<ReturnVal>(
request: () => Promise<ReturnVal>, request: () => Promise<ReturnVal>,
loadingResource: Leaf | Container, loadingResource: SolidLeaf | SolidContainer,
loadingValues: Partial<{ loadingValues: Partial<{
isLoading: boolean; isLoading: boolean;
isCreating: boolean; isCreating: boolean;
@ -1303,7 +1286,7 @@ describe("Integration", () => {
it("allows a transaction on a transaction", () => { it("allows a transaction on a transaction", () => {
const transaction = solidLdoDataset.startTransaction(); const transaction = solidLdoDataset.startTransaction();
const transaction2 = transaction.startTransaction(); const transaction2 = transaction.startTransaction();
expect(transaction2).toBeInstanceOf(SolidLdoTransactionDataset); expect(transaction2).toBeInstanceOf(ConnectedLdoDataset);
}); });
/** /**

@ -1,48 +1,54 @@
import type { WebSocket, Event, ErrorEvent } from "ws"; describe("Websocket Trivial", () => {
import { Websocket2023NotificationSubscription } from "../src/resource/notifications/Websocket2023NotificationSubscription"; it("is trivial", () => {
import type { SolidLdoDatasetContext } from "../src"; expect(true).toBe(true);
import { Leaf } from "../src";
import type { NotificationChannel } from "@solid-notifications/types";
describe("Websocket2023NotificationSubscription", () => {
it("returns an error when websockets have an error", async () => {
const WebSocketMock: WebSocket = {} as WebSocket;
const subscription = new Websocket2023NotificationSubscription(
new Leaf("https://example.com", {
fetch,
} as unknown as SolidLdoDatasetContext),
() => {},
{} as unknown as SolidLdoDatasetContext,
() => WebSocketMock,
);
const subPromise = subscription.subscribeToWebsocket({
receiveFrom: "http://example.com",
} as unknown as NotificationChannel);
WebSocketMock.onopen?.({} as Event);
await subPromise;
WebSocketMock.onerror?.({ error: new Error("Test Error") } as ErrorEvent);
});
it("returns an error when websockets have an error at the beginning", async () => {
const WebSocketMock: WebSocket = {} as WebSocket;
const subscription = new Websocket2023NotificationSubscription(
new Leaf("https://example.com", {
fetch,
} as unknown as SolidLdoDatasetContext),
() => {},
{} as unknown as SolidLdoDatasetContext,
() => WebSocketMock,
);
const subPromise = subscription.subscribeToWebsocket({
receiveFrom: "http://example.com",
} as unknown as NotificationChannel);
WebSocketMock.onerror?.({ error: new Error("Test Error") } as ErrorEvent);
await subPromise;
}); });
}); });
// import type { WebSocket, Event, ErrorEvent } from "ws";
// import { Websocket2023NotificationSubscription } from "../src/notifications/Websocket2023NotificationSubscription";
// import type { SolidLdoDatasetContext } from "../src";
// import { Leaf } from "../src";
// import type { NotificationChannel } from "@solid-notifications/types";
// describe("Websocket2023NotificationSubscription", () => {
// it("returns an error when websockets have an error", async () => {
// const WebSocketMock: WebSocket = {} as WebSocket;
// const subscription = new Websocket2023NotificationSubscription(
// new Leaf("https://example.com", {
// fetch,
// } as unknown as SolidLdoDatasetContext),
// () => {},
// {} as unknown as SolidLdoDatasetContext,
// () => WebSocketMock,
// );
// const subPromise = subscription.subscribeToWebsocket({
// receiveFrom: "http://example.com",
// } as unknown as NotificationChannel);
// WebSocketMock.onopen?.({} as Event);
// await subPromise;
// WebSocketMock.onerror?.({ error: new Error("Test Error") } as ErrorEvent);
// });
// it("returns an error when websockets have an error at the beginning", async () => {
// const WebSocketMock: WebSocket = {} as WebSocket;
// const subscription = new Websocket2023NotificationSubscription(
// new Leaf("https://example.com", {
// fetch,
// } as unknown as SolidLdoDatasetContext),
// () => {},
// {} as unknown as SolidLdoDatasetContext,
// () => WebSocketMock,
// );
// const subPromise = subscription.subscribeToWebsocket({
// receiveFrom: "http://example.com",
// } as unknown as NotificationChannel);
// WebSocketMock.onerror?.({ error: new Error("Test Error") } as ErrorEvent);
// await subPromise;
// });
// });

@ -1,7 +1,7 @@
import { isLeafUri } from "../src"; import { isSolidLeafUri } from "../src";
describe("isLeafUri", () => { describe("isLeafUri", () => {
it("returns true if the given value is a leaf URI", () => { it("returns true if the given value is a leaf URI", () => {
expect(isLeafUri("https://example.com/index.ttl")).toBe(true); expect(isSolidLeafUri("https://example.com/index.ttl")).toBe(true);
}); });
}); });

@ -1,11 +0,0 @@
import type { ConnectedLdoDataset } from "./ConnectedLdoDataset";
import type { ConnectedPlugin } from "./ConnectedPlugin";
export type ConnectedContext<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Plugins extends ConnectedPlugin<any, any, any, any>[],
> = {
dataset: ConnectedLdoDataset<Plugins>;
} & {
[P in Plugins[number] as P["name"]]: P["types"]["context"];
};

@ -5,17 +5,16 @@ import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset";
import { InvalidIdentifierResource } from "./InvalidIdentifierResource"; import { InvalidIdentifierResource } from "./InvalidIdentifierResource";
import type { ConnectedContext } from "./ConnectedContext"; import type { ConnectedContext } from "./ConnectedContext";
import type {
IConnectedLdoDataset,
ReturnTypeFromArgs,
} from "./IConnectedLdoDataset";
import { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset";
type ReturnTypeFromArgs<Func, Arg> = Func extends ( export class ConnectedLdoDataset<Plugins extends ConnectedPlugin[]>
arg: Arg, extends LdoDataset
context: any, implements IConnectedLdoDataset<Plugins>
) => infer R {
? R
: never;
export class ConnectedLdoDataset<
Plugins extends ConnectedPlugin<any, any, any, any>[],
> extends LdoDataset {
private plugins: Plugins; private plugins: Plugins;
/** /**
* @internal * @internal
@ -100,8 +99,6 @@ export class ConnectedLdoDataset<
Plugin extends Extract<Plugins[number], { name: Name }>, Plugin extends Extract<Plugins[number], { name: Name }>,
>(name: Name): Promise<ReturnType<Plugin["createResource"]>> { >(name: Name): Promise<ReturnType<Plugin["createResource"]>> {
const validPlugin = this.plugins.find((plugin) => name === plugin.name)!; const validPlugin = this.plugins.find((plugin) => name === plugin.name)!;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore I'm not sure why this doesn't work
const newResourceResult = await validPlugin.createResource(this.context); const newResourceResult = await validPlugin.createResource(this.context);
if (newResourceResult.isError) return newResourceResult; if (newResourceResult.isError) return newResourceResult;
this.resourceMap.set(newResourceResult.uri, newResourceResult); this.resourceMap.set(newResourceResult.uri, newResourceResult);
@ -116,4 +113,13 @@ export class ConnectedLdoDataset<
// @ts-ignore // @ts-ignore
this.context[name] = context; this.context[name] = context;
} }
public startTransaction(): ConnectedLdoTransactionDataset<Plugins> {
return new ConnectedLdoTransactionDataset(
this,
this.context,
this.datasetFactory,
this.transactionDatasetFactory,
);
}
} }

@ -0,0 +1,199 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { LdoTransactionDataset } from "@ldo/ldo";
import type { DatasetFactory, Quad } from "@rdfjs/types";
import {
updateDatasetInBulk,
type ITransactionDatasetFactory,
} from "@ldo/subscribable-dataset";
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils";
import type { ConnectedLdoDataset } from "./ConnectedLdoDataset";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { ConnectedContext } from "./ConnectedContext";
import type { InvalidIdentifierResource } from "./InvalidIdentifierResource";
import type { IConnectedLdoDataset } from "./IConnectedLdoDataset";
import { splitChangesByGraph } from "./util/splitChangesByGraph";
import type {
IgnoredInvalidUpdateSuccess,
UpdateSuccess,
} from "./results/success/UpdateSuccess";
import { UpdateDefaultGraphSuccess } from "./results/success/UpdateSuccess";
import { AggregateError } from "./results/error/ErrorResult";
import type { AggregateSuccess } from "./results/success/SuccessResult";
/**
* A SolidLdoTransactionDataset has all the functionality of a SolidLdoDataset
* and represents a transaction to the parent SolidLdoDataset.
*
* It is recommended to use the `startTransaction` method on a SolidLdoDataset
* to initialize this class
*
* @example
* ```typescript
* import { createSolidLdoDataset } from "@ldo/solid";
* import { ProfileShapeType } from "./.ldo/profile.shapeTypes.ts"
*
* // ...
*
* const solidLdoDataset = createSolidLdoDataset();
*
* const profileDocument = solidLdoDataset
* .getResource("https://example.com/profile");
* await profileDocument.read();
*
* const transaction = solidLdoDataset.startTransaction();
*
* const profile = transaction
* .using(ProfileShapeType)
* .fromSubject("https://example.com/profile#me");
* profile.name = "Some Name";
* await transaction.commitToPod();
* ```
*/
export class ConnectedLdoTransactionDataset<Plugins extends ConnectedPlugin[]>
extends LdoTransactionDataset
implements IConnectedLdoDataset<Plugins>
{
/**
* @internal
*/
public context: ConnectedContext<Plugins>;
/**
* @internal
* Serves no purpose
*/
protected resourceMap: Map<string, Plugins[number]["types"]["resource"]> =
new Map();
/**
* @param context - SolidLdoDatasetContext
* @param datasetFactory - An optional dataset factory
* @param transactionDatasetFactory - A factory for creating transaction datasets
* @param initialDataset - A set of triples to initialize this dataset
*/
constructor(
parentDataset: IConnectedLdoDataset<Plugins>,
context: ConnectedContext<Plugins>,
datasetFactory: DatasetFactory,
transactionDatasetFactory: ITransactionDatasetFactory<Quad>,
) {
super(parentDataset, datasetFactory, transactionDatasetFactory);
this.context = context;
}
getResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string,
>(
uri: UriType,
pluginName?: Name | undefined,
): UriType extends Plugin["types"]["uri"]
? Plugin["getResource"] extends (arg: UriType, context: any) => infer R
? R
: never
: InvalidIdentifierResource | ReturnType<Plugin["getResource"]> {
return this.context.dataset.getResource(uri, pluginName);
}
createResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
>(name: Name): Promise<ReturnType<Plugin["createResource"]>> {
return this.context.dataset.createResource(name);
}
setContext<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
>(name: Name, context: Plugin["types"]["context"]): void {
this.context.dataset.setContext(name, context);
}
/**
* Retireves a representation (either a LeafResource or a ContainerResource)
* of a Solid Resource at the given URI. This resource represents the
* current state of the resource: whether it is currently fetched or in the
* process of fetching as well as some information about it.
*
* @param uri - the URI of the resource
* @param options - Special options for getting the resource
*
* @returns a Leaf or Container Resource
*
* @example
* ```typescript
* const profileDocument = solidLdoDataset
* .getResource("https://example.com/profile");
* ```
*/
public startTransaction(): ConnectedLdoTransactionDataset<Plugins> {
return new ConnectedLdoTransactionDataset(
this,
this.context,
this.datasetFactory,
this.transactionDatasetFactory,
);
}
async commitChanges(): Promise<
| AggregateSuccess<
| UpdateSuccess<Plugins[number]["types"]["resource"]>
| UpdateDefaultGraphSuccess
>
| AggregateError<
Extract<
ReturnType<Plugins[number]["types"]["resource"]["update"]>,
{ isError: true }
>
>
> {
const changes = this.getChanges();
const changesByGraph = splitChangesByGraph(changes);
// Iterate through all changes by graph in
const results: [
GraphNode,
DatasetChanges<Quad>,
(
| ReturnType<Plugins[number]["types"]["resource"]["update"]>
| IgnoredInvalidUpdateSuccess<any>
| UpdateDefaultGraphSuccess
),
][] = await Promise.all(
Array.from(changesByGraph.entries()).map(
async ([graph, datasetChanges]) => {
if (graph.termType === "DefaultGraph") {
// Undefined means that this is the default graph
updateDatasetInBulk(this.parentDataset, datasetChanges);
return [graph, datasetChanges, new UpdateDefaultGraphSuccess()];
}
const resource = this.getResource(graph.value);
const updateResult = await resource.update(datasetChanges);
return [graph, datasetChanges, updateResult];
},
),
);
// If one has errored, return error
const errors = results.filter((result) => result[2].isError);
if (errors.length > 0) {
return new AggregateError(
errors.map((result) => result[2] as UpdateResultError),
);
}
return {
isError: false,
type: "aggregateSuccess",
results: results
.map((result) => result[2])
.filter(
(result): result is ResourceResult<UpdateSuccess, Leaf> =>
result.type === "updateSuccess" ||
result.type === "updateDefaultGraphSuccess" ||
result.type === "ignoredInvalidUpdateSuccess",
),
};
}
}

@ -1,12 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ConnectedContext } from "./ConnectedContext"; import type { ConnectedContext } from "./ConnectedContext";
import type { Resource } from "./Resource"; import type { Resource } from "./Resource";
import type { ErrorResult } from "./results/error/ErrorResult"; import type { ErrorResult } from "./results/error/ErrorResult";
export interface ConnectedPlugin< export interface ConnectedPlugin<
Name extends string, Name extends string = string,
UriType extends string, UriType extends string = string,
ResourceType extends Resource<UriType>, ResourceType extends Resource<UriType> = Resource<UriType>,
ContextType, ContextType = any,
> { > {
name: Name; name: Name;
getResource(uri: UriType, context: ConnectedContext<this[]>): ResourceType; getResource(uri: UriType, context: ConnectedContext<this[]>): ResourceType;

@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LdoDataset } from "@ldo/ldo";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { InvalidIdentifierResource } from "./InvalidIdentifierResource";
export type ReturnTypeFromArgs<Func, Arg> = Func extends (
arg: Arg,
context: any,
) => infer R
? R
: never;
export interface IConnectedLdoDataset<Plugins extends ConnectedPlugin[]>
extends LdoDataset {
/**
* Retireves a representation of a Resource at the given URI. This resource
* represents the current state of the resource: whether it is currently
* fetched or in the process of fetching as well as some information about it.
*
* @param uri - the URI of the resource
* @param pluginName - optionally, force this function to choose a specific
* plugin to use rather than perform content negotiation.
*
* @returns a Resource
*
* @example
* ```typescript
* const profileDocument = solidLdoDataset
* .getResource("https://example.com/profile");
* ```
*/
getResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string,
>(
uri: UriType,
pluginName?: Name,
): UriType extends Plugin["types"]["uri"]
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ReturnType<Plugin["getResource"]> | InvalidIdentifierResource;
createResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
>(
name: Name,
): Promise<ReturnType<Plugin["createResource"]>>;
setContext<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
>(
name: Name,
context: Plugin["types"]["context"],
);
}

@ -44,10 +44,7 @@ export class InvalidIdentifierResource
async readIfAbsent(): Promise<InvalidUriError<this>> { async readIfAbsent(): Promise<InvalidUriError<this>> {
return this.status; return this.status;
} }
async createAndOverwrite(): Promise<InvalidUriError<this>> { async update(): Promise<InvalidUriError<this>> {
return this.status;
}
async createIfAbsent(): Promise<InvalidUriError<this>> {
return this.status; return this.status;
} }
async subscribeToNotifications(_callbacks): Promise<string> { async subscribeToNotifications(_callbacks): Promise<string> {

@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
import type { ConnectedResult } from "./results/ConnectedResult"; import type { ConnectedResult } from "./results/ConnectedResult";
import type { ResourceResult } from "./results/ResourceResult"; import type { DatasetChanges } from "@ldo/rdf-utils";
import type { UpdateSuccess } from "./results/success/UpdateSuccess";
import type { ResourceError } from "./results/error/ErrorResult";
import type { ReadSuccess } from "./results/success/ReadSuccess";
export type ResourceEventEmitter = TypedEmitter<{ export type ResourceEventEmitter = TypedEmitter<{
update: () => void; update: () => void;
@ -21,8 +24,11 @@ export interface Resource<UriType extends string = string>
isPresent(): boolean | undefined; isPresent(): boolean | undefined;
isAbsent(): boolean | undefined; isAbsent(): boolean | undefined;
isSubscribedToNotifications(): boolean; isSubscribedToNotifications(): boolean;
read(): Promise<ResourceResult<any>>; read(): Promise<ReadSuccess<any> | ResourceError<any>>;
readIfAbsent(): Promise<ResourceResult<any>>; readIfAbsent(): Promise<ReadSuccess<any> | ResourceError<any>>;
update(
datasetChanges: DatasetChanges,
): Promise<UpdateSuccess<any> | ResourceError<any>>;
subscribeToNotifications(callbacks?: { subscribeToNotifications(callbacks?: {
onNotification: (message: any) => void; onNotification: (message: any) => void;
onNotificationError: (err: Error) => void; onNotificationError: (err: Error) => void;

@ -1,9 +1,13 @@
export * from "./IConnectedLdoDataset";
export * from "./ConnectedLdoDataset"; export * from "./ConnectedLdoDataset";
export * from "./ConnectedLdoTransactionDataset";
export * from "./ConnectedPlugin"; export * from "./ConnectedPlugin";
export * from "./Resource"; export * from "./Resource";
export * from "./InvalidIdentifierResource"; export * from "./InvalidIdentifierResource";
export * from "./ConnectedContext"; export * from "./ConnectedContext";
export * from "./util/splitChangesByGraph";
export * from "./results/ConnectedResult"; export * from "./results/ConnectedResult";
export * from "./results/ResourceResult"; export * from "./results/ResourceResult";
export * from "./results/error/ErrorResult"; export * from "./results/error/ErrorResult";

@ -0,0 +1,30 @@
import { ResourceSuccess } from "@ldo/connected";
import type { Resource } from "@ldo/connected";
/**
* 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, but no resource exists at
* the provided URI.
*/
export class AbsentReadSuccess<
ResourceType extends Resource,
> extends ReadSuccess<ResourceType> {
type = "absentReadSuccess" as const;
}

@ -1,4 +1,4 @@
import { ResourceSuccess } from "@ldo/connected"; import { ResourceSuccess, SuccessResult } from "@ldo/connected";
import type { Resource } from "@ldo/connected"; import type { Resource } from "@ldo/connected";
/** /**
@ -14,9 +14,7 @@ export class UpdateSuccess<
* Indicates that an update request to the default graph was successful. This * Indicates that an update request to the default graph was successful. This
* data was not written to a Pod. It was only written locally. * data was not written to a Pod. It was only written locally.
*/ */
export class UpdateDefaultGraphSuccess< export class UpdateDefaultGraphSuccess extends SuccessResult {
ResourceType extends Resource,
> extends ResourceSuccess<ResourceType> {
type = "updateDefaultGraphSuccess" as const; type = "updateDefaultGraphSuccess" as const;
} }

@ -0,0 +1,70 @@
import { createDataset } from "@ldo/dataset";
import type { GraphNode, DatasetChanges } from "@ldo/rdf-utils";
import type { Quad } from "@rdfjs/types";
import { defaultGraph, namedNode, quad as createQuad } from "@rdfjs/data-model";
/**
* @internal
* Converts an RDFJS Graph Node to a string hash
* @param graphNode - the node to convert
* @returns a unique string corresponding to the node
*/
export function graphNodeToString(graphNode: GraphNode): string {
return graphNode.termType === "DefaultGraph"
? "defaultGraph()"
: graphNode.value;
}
/**
* @internal
* Converts a unique string to a GraphNode
* @param input - the unique string
* @returns A graph node
*/
export function stringToGraphNode(input: string): GraphNode {
return input === "defaultGraph()" ? defaultGraph() : namedNode(input);
}
/**
* Splits all changes in a DatasetChanges into individual DatasetChanges grouped
* by the quad graph.
* @param changes - Changes to split
* @returns A map between the quad graph and the changes associated with that
* graph
*/
export function splitChangesByGraph(
changes: DatasetChanges<Quad>,
): Map<GraphNode, DatasetChanges<Quad>> {
const changesMap: Record<string, DatasetChanges<Quad>> = {};
changes.added?.forEach((quad) => {
const graphHash = graphNodeToString(quad.graph as GraphNode);
if (!changesMap[graphHash]) {
changesMap[graphHash] = {};
}
if (!changesMap[graphHash].added) {
changesMap[graphHash].added = createDataset();
}
changesMap[graphHash].added?.add(
createQuad(quad.subject, quad.predicate, quad.object, quad.graph),
);
});
changes.removed?.forEach((quad) => {
const graphHash = graphNodeToString(quad.graph as GraphNode);
if (!changesMap[graphHash]) {
changesMap[graphHash] = {};
}
if (!changesMap[graphHash].removed) {
changesMap[graphHash].removed = createDataset();
}
changesMap[graphHash].removed?.add(
createQuad(quad.subject, quad.predicate, quad.object, quad.graph),
);
});
const finalMap = new Map<GraphNode, DatasetChanges<Quad>>();
Object.entries(changesMap).forEach(([key, value]) => {
finalMap.set(stringToGraphNode(key), value);
});
return finalMap;
}
Loading…
Cancel
Save