From a2d03e543171255b5a87b2c5b1af45bf47790aba Mon Sep 17 00:00:00 2001 From: Jackson Morgan Date: Sat, 22 Mar 2025 19:41:45 -0400 Subject: [PATCH] Typescript errors in tests fixed --- .../src/getStorageFromWebId.ts | 62 +++++ packages/connected-solid/src/index.ts | 1 + .../src/resources/SolidContainer.ts | 6 +- .../src/resources/SolidResource.ts | 4 +- .../connected-solid/test/ErrorResult.test.ts | 64 ------ .../connected-solid/test/Integration.test.ts | 215 +++++++++++------- .../test/solidServer.helper.ts | 4 +- packages/connected/src/ConnectedLdoDataset.ts | 28 ++- .../src/ConnectedLdoTransactionDataset.ts | 2 +- packages/connected/src/ConnectedPlugin.ts | 7 +- packages/connected/src/index.ts | 1 + packages/connected/src/methods.ts | 91 ++++++++ .../src/results/success/SuccessResult.ts | 2 +- 13 files changed, 323 insertions(+), 164 deletions(-) create mode 100644 packages/connected-solid/src/getStorageFromWebId.ts delete mode 100644 packages/connected-solid/test/ErrorResult.test.ts create mode 100644 packages/connected/src/methods.ts diff --git a/packages/connected-solid/src/getStorageFromWebId.ts b/packages/connected-solid/src/getStorageFromWebId.ts new file mode 100644 index 0000000..6cebbeb --- /dev/null +++ b/packages/connected-solid/src/getStorageFromWebId.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; +import type { SolidContainerUri, SolidLeafUri } from "./types"; +import type { GetStorageContainerFromWebIdSuccess } from "./requester/results/success/CheckRootContainerSuccess"; +import type { CheckRootResultError } from "./requester/requests/checkRootContainer"; +import type { ReadResultError } from "./requester/requests/readResource"; +import type { NoRootContainerError } from "./requester/results/error/NoRootContainerError"; +import type { SolidLeaf } from "./resources/SolidLeaf"; +import type { SolidContainer } from "./resources/SolidContainer"; +import type { SolidConnectedPlugin } from "./SolidConnectedPlugin"; +import { ProfileWithStorageShapeType } from "./.ldo/solid.shapeTypes"; + +/** + * Gets a list of root storage containers for a user given their WebId + * @param webId: The webId for the user + * @returns A list of storages if successful, an error if not + * @example + * ```typescript + * const result = await getStorageFromWebId( + * solidLdoDataset, + * "https://example.com/profile/card#me" + * ); + * if (result.isError) { + * // Do something + * } + * console.log(result.storageContainer[0].uri); + * ``` + */ +export async function getStorageFromWebId( + webId: SolidLeafUri, + dataset: ConnectedLdoDataset<(SolidConnectedPlugin | ConnectedPlugin)[]>, +): Promise< + | GetStorageContainerFromWebIdSuccess + | CheckRootResultError + | ReadResultError + | NoRootContainerError +> { + const webIdResource = dataset.getResource(webId) as SolidLeaf; + const readResult = await webIdResource.readIfUnfetched(); + if (readResult.isError) return readResult; + const profile = this.usingType(ProfileWithStorageShapeType).fromSubject( + webId, + ); + if (profile.storage && profile.storage.size > 0) { + const containers = profile.storage.map((storageNode) => + this.getResource(storageNode["@id"] as SolidContainerUri), + ); + return { + type: "getStorageContainerFromWebIdSuccess", + isError: false, + storageContainers: containers, + }; + } + const getContainerResult = await webIdResource.getRootContainer(); + if (getContainerResult.type === "container") + return { + type: "getStorageContainerFromWebIdSuccess", + isError: false, + storageContainers: [getContainerResult], + }; + return getContainerResult; +} diff --git a/packages/connected-solid/src/index.ts b/packages/connected-solid/src/index.ts index 295d63e..1946178 100644 --- a/packages/connected-solid/src/index.ts +++ b/packages/connected-solid/src/index.ts @@ -1,6 +1,7 @@ export * from "./types"; export * from "./SolidConnectedPlugin"; export * from "./createSolidLdoDataset"; +export * from "./getStorageFromWebId"; export * from "./resources/SolidResource"; export * from "./resources/SolidContainer"; diff --git a/packages/connected-solid/src/resources/SolidContainer.ts b/packages/connected-solid/src/resources/SolidContainer.ts index 5d72086..b235685 100644 --- a/packages/connected-solid/src/resources/SolidContainer.ts +++ b/packages/connected-solid/src/resources/SolidContainer.ts @@ -29,7 +29,7 @@ import type { SolidContainerUri, SolidLeafSlug, } from "../types"; -import type { AbsentReadSuccess } from "@ldo/connected"; +import type { AbsentReadSuccess, ReadSuccess } from "@ldo/connected"; import { AggregateSuccess, IgnoredInvalidUpdateSuccess } from "@ldo/connected"; import { Unfetched, @@ -135,11 +135,11 @@ export class SolidContainer extends SolidResource { * @param result - the result of the read success */ protected updateWithReadSuccess( - result: ContainerReadSuccess | AbsentReadSuccess, + result: ReadSuccess | ContainerReadSuccess, ): void { super.updateWithReadSuccess(result); if (result.type === "containerReadSuccess") { - this.rootContainer = result.isRootContainer; + this.rootContainer = (result as ContainerReadSuccess).isRootContainer; } } diff --git a/packages/connected-solid/src/resources/SolidResource.ts b/packages/connected-solid/src/resources/SolidResource.ts index b4de696..747cd2c 100644 --- a/packages/connected-solid/src/resources/SolidResource.ts +++ b/packages/connected-solid/src/resources/SolidResource.ts @@ -383,7 +383,7 @@ export abstract class SolidResource * A helper method updates this resource's internal state upon read success * @param result - the result of the read success */ - protected updateWithReadSuccess(result: ReadSuccess) { + protected updateWithReadSuccess(result: ReadSuccess) { this.absent = result.type === "absentReadSuccess"; this.didInitialFetch = true; } @@ -469,7 +469,7 @@ export abstract class SolidResource * A helper method updates this resource's internal state upon create success * @param _result - the result of the create success */ - protected updateWithCreateSuccess(result: ResourceSuccess) { + protected updateWithCreateSuccess(result: ResourceSuccess) { this.absent = false; this.didInitialFetch = true; if (isReadSuccess(result)) { diff --git a/packages/connected-solid/test/ErrorResult.test.ts b/packages/connected-solid/test/ErrorResult.test.ts deleted file mode 100644 index 50da070..0000000 --- a/packages/connected-solid/test/ErrorResult.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -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.", - ); - }); - }); -}); diff --git a/packages/connected-solid/test/Integration.test.ts b/packages/connected-solid/test/Integration.test.ts index 0b3e780..e70fb91 100644 --- a/packages/connected-solid/test/Integration.test.ts +++ b/packages/connected-solid/test/Integration.test.ts @@ -7,12 +7,6 @@ import { defaultGraph, } from "@rdfjs/data-model"; import type { CreateSuccess } from "../src/requester/results/success/CreateSuccess"; -import type { - IgnoredInvalidUpdateSuccess, - UpdateDefaultGraphSuccess, - UpdateSuccess, -} from "../src/requester/results/success/UpdateSuccess"; -import type { InvalidUriError } from "../src/requester/results/error/InvalidUriError"; import { Buffer } from "buffer"; import { PostShShapeType } from "./.ldo/post.shapeTypes"; import type { @@ -26,13 +20,26 @@ import { generateAuthFetch } from "./authFetch.helper"; import { wait } from "./utils.helper"; import fs from "fs/promises"; import path from "path"; -import type { - SolidContainer, - SolidContainerUri, - SolidLeaf, - SolidLeafUri, +import type { GetWacRuleSuccess, UpdateResultError, WacRule } from "../src"; +import { + createSolidLdoDataset, + type SolidConnectedPlugin, + type SolidContainer, + type SolidContainerUri, + type SolidLeaf, + type SolidLeafUri, } from "../src"; -import { ConnectedLdoDataset } from "@ldo/connected"; +import type { + AggregateError, + AggregateSuccess, + IgnoredInvalidUpdateSuccess, + InvalidUriError, + UnexpectedResourceError, + UpdateDefaultGraphSuccess, + UpdateSuccess, +} from "@ldo/connected"; +import { changeData, commitData, ConnectedLdoDataset } from "@ldo/connected"; +import { getStorageFromWebId } from "../src/getStorageFromWebId"; const TEST_CONTAINER_SLUG = "test_ldo/"; const TEST_CONTAINER_URI = @@ -143,7 +150,7 @@ describe("Integration", () => { Promise, [input: RequestInfo | URL, init?: RequestInit | undefined] >; - let solidLdoDataset: SolidLdoDataset; + let solidLdoDataset: ConnectedLdoDataset; let previousJestId: string | undefined; let previousNodeEnv: string | undefined; @@ -170,7 +177,8 @@ describe("Integration", () => { beforeEach(async () => { fetchMock = jest.fn(authFetch); - solidLdoDataset = createSolidLdoDataset({ fetch: fetchMock }); + solidLdoDataset = createSolidLdoDataset(); + solidLdoDataset.setContext("solid", { fetch: fetchMock }); // Create a new document called sample.ttl await authFetch(ROOT_CONTAINER, { method: "POST", @@ -267,27 +275,28 @@ describe("Integration", () => { expect(resource.isPresent()).toBe(true); }); - it("Auto reads a resource", async () => { - const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI, { - autoLoad: true, - }); - // Wait until the resource is auto-loaded - await new Promise((resolve) => { - const interval = setInterval(() => { - if (!resource.isReading()) { - clearInterval(interval); - resolve(); - } - }, 250); - }); - expect( - solidLdoDataset.match( - namedNode("http://example.org/#spiderman"), - namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), - namedNode("http://example.org/#green-goblin"), - ).size, - ).toBe(1); - }); + // TODO: Possibly re-enable if Auto-read is required, but it might not be + // it("Auto reads a resource", async () => { + // const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI, { + // autoLoad: true, + // }); + // // Wait until the resource is auto-loaded + // await new Promise((resolve) => { + // const interval = setInterval(() => { + // if (!resource.isReading()) { + // clearInterval(interval); + // resolve(); + // } + // }, 250); + // }); + // expect( + // solidLdoDataset.match( + // namedNode("http://example.org/#spiderman"), + // namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), + // namedNode("http://example.org/#green-goblin"), + // ).size, + // ).toBe(1); + // }); it("Reads a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); @@ -671,7 +680,10 @@ describe("Integration", () => { */ describe("getStorageFromWebId", () => { it("Gets storage when a pim:storage field isn't present", async () => { - const result = await solidLdoDataset.getStorageFromWebId(SAMPLE_DATA_URI); + const result = await getStorageFromWebId( + SAMPLE_DATA_URI, + solidLdoDataset, + ); expect(result.type).toBe("getStorageContainerFromWebIdSuccess"); const realResult = result as GetStorageContainerFromWebIdSuccess; expect(realResult.storageContainers.length).toBe(1); @@ -679,8 +691,10 @@ describe("Integration", () => { }); it("Gets storage when a pim:storage field is present", async () => { - const result = - await solidLdoDataset.getStorageFromWebId(SAMPLE_PROFILE_URI); + const result = await getStorageFromWebId( + SAMPLE_PROFILE_URI, + solidLdoDataset, + ); expect(result.type).toBe("getStorageContainerFromWebIdSuccess"); const realResult = result as GetStorageContainerFromWebIdSuccess; expect(realResult.storageContainers.length).toBe(2); @@ -694,14 +708,20 @@ describe("Integration", () => { it("Passes any errors returned from the read method", async () => { fetchMock.mockRejectedValueOnce(new Error("Something happened.")); - const result = await solidLdoDataset.getStorageFromWebId(SAMPLE_DATA_URI); + const result = await getStorageFromWebId( + SAMPLE_DATA_URI, + solidLdoDataset, + ); expect(result.isError).toBe(true); }); it("Passes any errors returned from the getRootContainer method", async () => { fetchMock.mockResolvedValueOnce(new Response("")); fetchMock.mockRejectedValueOnce(new Error("Something happened.")); - const result = await solidLdoDataset.getStorageFromWebId(SAMPLE_DATA_URI); + const result = await getStorageFromWebId( + SAMPLE_DATA_URI, + solidLdoDataset, + ); expect(result.isError).toBe(true); }); }); @@ -725,7 +745,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( @@ -754,7 +774,7 @@ describe("Integration", () => { }, ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(true); expect( solidLdoDataset.has( @@ -783,7 +803,7 @@ describe("Integration", () => { }, ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( @@ -896,7 +916,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( @@ -954,7 +974,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( @@ -1047,11 +1067,11 @@ describe("Integration", () => { expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< - | ServerHttpError - | UnexpectedHttpError - | UnauthenticatedHttpError - | UnexpectedResourceError - | NoncompliantPodError + | ServerHttpError + | UnexpectedHttpError + | UnauthenticatedHttpError + | UnexpectedResourceError + | NoncompliantPodError >; expect(aggregateError.errors[0].type).toBe("serverError"); }); @@ -1070,11 +1090,11 @@ describe("Integration", () => { expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< - | ServerHttpError - | UnexpectedHttpError - | UnauthenticatedHttpError - | UnexpectedResourceError - | NoncompliantPodError + | ServerHttpError + | UnexpectedHttpError + | UnauthenticatedHttpError + | UnexpectedResourceError + | NoncompliantPodError >; expect(aggregateError.errors[0].type).toBe("serverError"); }); @@ -1119,7 +1139,7 @@ describe("Integration", () => { const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); transaction.delete(goblinQuad); - return transaction.commitToPod(); + return transaction.commitToRemote(); }, solidLdoDataset.getResource(SAMPLE_DATA_URI), { @@ -1129,7 +1149,7 @@ describe("Integration", () => { ); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< - ResourceSuccess + UpdateSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type === "updateSuccess").toBe(true); @@ -1142,7 +1162,7 @@ describe("Integration", () => { () => { const transaction = solidLdoDataset.startTransaction(); transaction.delete(goblinQuad); - return transaction.commitToPod(); + return transaction.commitToRemote(); }, solidLdoDataset.getResource(SAMPLE_DATA_URI), { @@ -1152,7 +1172,7 @@ describe("Integration", () => { ); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< - ResourceSuccess + UpdateSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type === "updateSuccess").toBe(true); @@ -1165,12 +1185,13 @@ describe("Integration", () => { const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); transaction.delete(goblinQuad); - const result = await transaction.commitToPod(); + const result = await transaction.commitToRemote(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< - UpdateResultError | InvalidUriError + | UpdateResultError + | InvalidUriError >; expect(aggregateError.errors.length).toBe(1); expect(aggregateError.errors[0].type).toBe("serverError"); @@ -1183,11 +1204,12 @@ describe("Integration", () => { const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); transaction.delete(goblinQuad); - const result = await transaction.commitToPod(); + const result = await transaction.commitToRemote(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< - UpdateResultError | InvalidUriError + | UpdateResultError + | InvalidUriError >; expect(aggregateError.errors.length).toBe(1); expect(aggregateError.errors[0].type).toBe("unexpectedResourceError"); @@ -1202,11 +1224,12 @@ describe("Integration", () => { ); const transaction = solidLdoDataset.startTransaction(); transaction.add(badContainerQuad); - const result = await transaction.commitToPod(); + const result = await transaction.commitToRemote(); expect(result.isError).toBe(false); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< - UpdateSuccess | IgnoredInvalidUpdateSuccess + | UpdateSuccess + | IgnoredInvalidUpdateSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type).toBe( @@ -1223,10 +1246,10 @@ describe("Integration", () => { ); const transaction = solidLdoDataset.startTransaction(); transaction.add(defaultGraphQuad); - const result = await transaction.commitToPod(); + const result = await transaction.commitToRemote(); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< - ResourceSuccess + UpdateSuccess | UpdateDefaultGraphSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type).toBe( @@ -1254,8 +1277,8 @@ describe("Integration", () => { const [, updateResult1, updateResult2] = await Promise.all([ resource.read(), - transaction1.commitToPod(), - transaction2.commitToPod(), + transaction1.commitToRemote(), + transaction2.commitToRemote(), ]); expect(updateResult1.type).toBe("aggregateSuccess"); expect(updateResult2.type).toBe("aggregateSuccess"); @@ -1312,7 +1335,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( @@ -1348,7 +1371,7 @@ describe("Integration", () => { }, ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(true); expect( solidLdoDataset.has( @@ -1474,7 +1497,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as CreateSuccess; + const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( @@ -1634,7 +1657,7 @@ describe("Integration", () => { const result = await resource.createChildAndOverwrite(SAMPLE2_DATA_SLUG); expect(result.type).toBe("createSuccess"); - const createSuccess = result as ResourceResult; + const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_DATA_URI); expect(createSuccess.didOverwrite).toBe(false); expect( @@ -1657,7 +1680,7 @@ describe("Integration", () => { const result = await resource.createChildIfAbsent(SAMPLE2_DATA_SLUG); expect(result.type).toBe("createSuccess"); - const createSuccess = result as ResourceResult; + const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_DATA_URI); expect(createSuccess.didOverwrite).toBe(false); expect( @@ -1684,7 +1707,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as ResourceResult; + const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_BINARY_URI); expect(createSuccess.didOverwrite).toBe(false); expect( @@ -1711,7 +1734,7 @@ describe("Integration", () => { ); expect(result.type).toBe("createSuccess"); - const createSuccess = result as ResourceResult; + const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_BINARY_URI); expect(createSuccess.didOverwrite).toBe(false); expect( @@ -1740,7 +1763,9 @@ describe("Integration", () => { const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const wacResult = await container.getWac(); expect(wacResult.isError).toBe(false); - const wacSuccess = wacResult as GetWacRuleSuccess; + const wacSuccess = wacResult as GetWacRuleSuccess< + SolidLeaf | SolidContainer + >; expect(wacSuccess.wacRule.public).toEqual({ read: true, write: true, @@ -1765,7 +1790,9 @@ describe("Integration", () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(false); - const wacSuccess = wacResult as GetWacRuleSuccess; + const wacSuccess = wacResult as GetWacRuleSuccess< + SolidLeaf | SolidContainer + >; expect(wacSuccess.wacRule.public).toEqual({ read: true, write: true, @@ -1791,7 +1818,9 @@ describe("Integration", () => { await resource.getWac(); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(false); - const wacSuccess = wacResult as GetWacRuleSuccess; + const wacSuccess = wacResult as GetWacRuleSuccess< + SolidLeaf | SolidContainer + >; expect(wacSuccess.wacRule.public).toEqual({ read: true, write: true, @@ -1837,7 +1866,9 @@ describe("Integration", () => { const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); - expect((wacResult as NoncompliantPodError).message).toBe( + expect( + (wacResult as NoncompliantPodError).message, + ).toBe( `Response from ${SAMPLE_DATA_URI} is not compliant with the Solid Specification: No link header present in request.`, ); }); @@ -1853,7 +1884,9 @@ describe("Integration", () => { const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); - expect((wacResult as NoncompliantPodError).message).toBe( + expect( + (wacResult as NoncompliantPodError).message, + ).toBe( `Response from ${SAMPLE_DATA_URI} is not compliant with the Solid Specification: There must be one link with a rel="acl"`, ); }); @@ -1898,7 +1931,9 @@ describe("Integration", () => { const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); - expect((wacResult as NoncompliantPodError).message).toBe( + expect( + (wacResult as NoncompliantPodError).message, + ).toBe( `Response from card.acl is not compliant with the Solid Specification: Request returned noncompliant turtle: Unexpected "BAD" on line 1.\nBAD TURTLE`, ); }); @@ -1930,7 +1965,9 @@ describe("Integration", () => { const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); - expect((wacResult as NoncompliantPodError).message).toBe( + expect( + (wacResult as NoncompliantPodError).message, + ).toBe( `Response from ${ROOT_CONTAINER} is not compliant with the Solid Specification: Resource "${ROOT_CONTAINER}" has no Effective ACL resource`, ); }); @@ -1958,7 +1995,9 @@ describe("Integration", () => { const readResult = await resource.getWac({ ignoreCache: true }); expect(readResult.isError).toBe(false); expect(readResult.type).toBe("getWacRuleSuccess"); - const rules = (readResult as GetWacRuleSuccess).wacRule; + const rules = ( + readResult as GetWacRuleSuccess + ).wacRule; expect(rules).toEqual(newRules); }); @@ -1970,7 +2009,9 @@ describe("Integration", () => { const readResult = await resource.getWac({ ignoreCache: true }); expect(readResult.isError).toBe(false); expect(readResult.type).toBe("getWacRuleSuccess"); - const rules = (readResult as GetWacRuleSuccess).wacRule; + const rules = ( + readResult as GetWacRuleSuccess + ).wacRule; expect(rules).toEqual(newRules); }); @@ -1986,7 +2027,9 @@ describe("Integration", () => { const readResult = await resource.getWac({ ignoreCache: true }); expect(readResult.isError).toBe(false); expect(readResult.type).toBe("getWacRuleSuccess"); - const rules = (readResult as GetWacRuleSuccess).wacRule; + const rules = ( + readResult as GetWacRuleSuccess + ).wacRule; expect(rules).toEqual(moreRules); }); diff --git a/packages/connected-solid/test/solidServer.helper.ts b/packages/connected-solid/test/solidServer.helper.ts index 38069d5..848abc1 100644 --- a/packages/connected-solid/test/solidServer.helper.ts +++ b/packages/connected-solid/test/solidServer.helper.ts @@ -4,10 +4,12 @@ import * as path from "path"; import type { App } from "@solid/community-server"; import { AppRunner, resolveModulePath } from "@solid/community-server"; import "jest-rdf"; +import type { SolidContainerUri } from "../src"; export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/"; export const ROOT_ROUTE = process.env.ROOT_CONTAINER || ""; -export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`; +export const ROOT_CONTAINER = + `${SERVER_DOMAIN}${ROOT_ROUTE}` as SolidContainerUri; export const WEB_ID = process.env.WEB_ID || `${SERVER_DOMAIN}example/profile/card#me`; diff --git a/packages/connected/src/ConnectedLdoDataset.ts b/packages/connected/src/ConnectedLdoDataset.ts index dfb2a73..f6d5760 100644 --- a/packages/connected/src/ConnectedLdoDataset.ts +++ b/packages/connected/src/ConnectedLdoDataset.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { LdoDataset } from "@ldo/ldo"; +import type { LdoBase, ShapeType } from "@ldo/ldo"; +import { LdoDataset, startTransaction } from "@ldo/ldo"; import type { ConnectedPlugin } from "./ConnectedPlugin"; import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; @@ -10,6 +11,8 @@ import type { ReturnTypeFromArgs, } from "./IConnectedLdoDataset"; import { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset"; +import type { SubjectNode } from "@ldo/rdf-utils"; +import type { Resource } from "./Resource"; export class ConnectedLdoDataset< Plugins extends ConnectedPlugin[], @@ -114,6 +117,29 @@ export class ConnectedLdoDataset< return newResourceResult as any; } + /** + * Shorthand for solidLdoDataset + * .usingType(shapeType) + * .write(...resources.map((r) => r.uri)) + * .fromSubject(subject); + * @param shapeType - The shapetype to represent the data + * @param subject - A subject URI + * @param resources - The resources changes to should written to + */ + createData( + shapeType: ShapeType, + subject: string | SubjectNode, + resource: Resource, + ...additionalResources: Resource[] + ): Type { + const resources = [resource, ...additionalResources]; + const linkedDataObject = this.usingType(shapeType) + .write(...resources.map((r) => r.uri)) + .fromSubject(subject); + startTransaction(linkedDataObject); + return linkedDataObject; + } + setContext< Name extends Plugins[number]["name"], Plugin extends Extract, diff --git a/packages/connected/src/ConnectedLdoTransactionDataset.ts b/packages/connected/src/ConnectedLdoTransactionDataset.ts index ab9a3ad..7b7292d 100644 --- a/packages/connected/src/ConnectedLdoTransactionDataset.ts +++ b/packages/connected/src/ConnectedLdoTransactionDataset.ts @@ -137,7 +137,7 @@ export class ConnectedLdoTransactionDataset ); } - async commitChanges(): Promise< + async commitToRemote(): Promise< | AggregateSuccess< | Extract< Awaited>, diff --git a/packages/connected/src/ConnectedPlugin.ts b/packages/connected/src/ConnectedPlugin.ts index b1288c8..cfe7889 100644 --- a/packages/connected/src/ConnectedPlugin.ts +++ b/packages/connected/src/ConnectedPlugin.ts @@ -10,12 +10,9 @@ export interface ConnectedPlugin< ContextType = any, > { name: Name; - getResource( - uri: UriType, - context: ConnectedContext[]>, - ): ResourceType; + getResource(uri: UriType, context: ConnectedContext): ResourceType; createResource( - context: ConnectedContext[]>, + context: ConnectedContext, ): Promise; isUriValid(uri: string): uri is UriType; normalizeUri?: (uri: UriType) => UriType; diff --git a/packages/connected/src/index.ts b/packages/connected/src/index.ts index d7c66cc..b327d3b 100644 --- a/packages/connected/src/index.ts +++ b/packages/connected/src/index.ts @@ -5,6 +5,7 @@ export * from "./ConnectedPlugin"; export * from "./Resource"; export * from "./InvalidIdentifierResource"; export * from "./ConnectedContext"; +export * from "./methods"; export * from "./util/splitChangesByGraph"; diff --git a/packages/connected/src/methods.ts b/packages/connected/src/methods.ts new file mode 100644 index 0000000..f09cf3f --- /dev/null +++ b/packages/connected/src/methods.ts @@ -0,0 +1,91 @@ +import { startTransaction, type LdoBase, write, getDataset } from "@ldo/ldo"; +import type { Quad } from "@rdfjs/types"; +import { _proxyContext, getProxyFromObject } from "@ldo/jsonld-dataset-proxy"; +import type { SubscribableDataset } from "@ldo/subscribable-dataset"; +import type { Resource } from "./Resource"; +import type { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset"; +import type { + AggregateSuccess, + SuccessResult, +} from "./results/success/SuccessResult"; +import type { AggregateError, ErrorResult } from "./results/error/ErrorResult"; + +/** + * Begins tracking changes to eventually commit. + * + * @param input - A linked data object to track changes on + * @param resource - A resource that all additions will eventually be committed to + * @param additionalResources - Any additional resources that changes will eventually be committed to + * + * @returns A transactable Linked Data Object + * + * @example + * ```typescript + * import { changeData } from "@ldo/solid"; + * + * // ... + * + * const profile = solidLdoDataset + * .using(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * const resource = solidLdoDataset.getResource("https://example.com/profile"); + * + * const cProfile = changeData(profile, resource); + * cProfile.name = "My New Name"; + * const result = await commitData(cProfile); + * ``` + */ +export function changeData( + input: Type, + resource: Resource, + ...additionalResources: Resource[] +): Type { + const resources = [resource, ...additionalResources]; + // Clone the input and set a graph + const [transactionLdo] = write(...resources.map((r) => r.uri)).usingCopy( + input, + ); + // Start a transaction with the input + startTransaction(transactionLdo); + // Return + return transactionLdo; +} + +/** + * Commits the transaction to the global dataset, syncing all subscribing + * components and Solid Pods + * + * @param input - A transactable linked data object + * + * @example + * ```typescript + * import { changeData } from "@ldo/solid"; + * + * // ... + * + * const profile = solidLdoDataset + * .using(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * const resource = solidLdoDataset.getResource("https://example.com/profile"); + * + * const cProfile = changeData(profile, resource); + * cProfile.name = "My New Name"; + * const result = await commitData(cProfile); + * ``` + */ +export async function commitData( + input: LdoBase, +): Promise | AggregateError> { + const transactionDataset = getDataset( + input, + ) as ConnectedLdoTransactionDataset<[]>; + const result = await transactionDataset.commitToRemote(); + if (result.isError) return result; + // Take the LdoProxy out of commit mode. This uses hidden methods of JSONLD-DATASET-PROXY + const proxy = getProxyFromObject(input); + proxy[_proxyContext] = proxy[_proxyContext].duplicate({ + dataset: proxy[_proxyContext].state + .parentDataset as SubscribableDataset, + }); + return result; +} diff --git a/packages/connected/src/results/success/SuccessResult.ts b/packages/connected/src/results/success/SuccessResult.ts index 612e7c9..300cb71 100644 --- a/packages/connected/src/results/success/SuccessResult.ts +++ b/packages/connected/src/results/success/SuccessResult.ts @@ -22,7 +22,7 @@ export abstract class ResourceSuccess< /** * The resource that was successful */ - resource: Resource; + resource: ResourceType; constructor(resource: ResourceType) { super();