diff --git a/packages/solid/src/.ldo/solid.context.ts b/packages/solid/src/.ldo/solid.context.ts index be97612..8244ade 100644 --- a/packages/solid/src/.ldo/solid.context.ts +++ b/packages/solid/src/.ldo/solid.context.ts @@ -1,4 +1,4 @@ -import type { ContextDefinition } from "jsonld"; +import { ContextDefinition } from "jsonld"; /** * ============================================================================= @@ -15,7 +15,6 @@ export const solidContext: ContextDefinition = { modified: { "@id": "http://purl.org/dc/terms/modified", "@type": "http://www.w3.org/2001/XMLSchema#string", - "@container": "@set", }, contains: { "@id": "http://www.w3.org/ns/ldp#contains", @@ -26,11 +25,14 @@ export const solidContext: ContextDefinition = { mtime: { "@id": "http://www.w3.org/ns/posix/stat#mtime", "@type": "http://www.w3.org/2001/XMLSchema#decimal", - "@container": "@set", }, size: { "@id": "http://www.w3.org/ns/posix/stat#size", "@type": "http://www.w3.org/2001/XMLSchema#integer", + }, + storage: { + "@id": "http://www.w3.org/ns/pim/space#storage", + "@type": "@id", "@container": "@set", }, }; diff --git a/packages/solid/src/.ldo/solid.schema.ts b/packages/solid/src/.ldo/solid.schema.ts index dabb685..4d9adc0 100644 --- a/packages/solid/src/.ldo/solid.schema.ts +++ b/packages/solid/src/.ldo/solid.schema.ts @@ -1,4 +1,4 @@ -import type { Schema } from "shexj"; +import { Schema } from "shexj"; /** * ============================================================================= @@ -210,5 +210,24 @@ export const solidSchema: Schema = { extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], }, }, + { + id: "http://www.w3.org/ns/lddps#ProfileWithStorage", + type: "ShapeDecl", + shapeExpr: { + type: "Shape", + expression: { + id: "http://www.w3.org/ns/lddps#ProfileWithStorageShape", + type: "TripleConstraint", + predicate: "http://www.w3.org/ns/pim/space#storage", + valueExpr: { + type: "NodeConstraint", + nodeKind: "iri", + }, + min: 0, + max: -1, + }, + extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], + }, + }, ], }; diff --git a/packages/solid/src/.ldo/solid.shapeTypes.ts b/packages/solid/src/.ldo/solid.shapeTypes.ts index 705e60e..69ddd90 100644 --- a/packages/solid/src/.ldo/solid.shapeTypes.ts +++ b/packages/solid/src/.ldo/solid.shapeTypes.ts @@ -1,7 +1,7 @@ -import type { ShapeType } from "@ldo/ldo"; +import { ShapeType } from "@ldo/ldo"; import { solidSchema } from "./solid.schema"; import { solidContext } from "./solid.context"; -import type { Container, Resource } from "./solid.typings"; +import { Container, Resource, ProfileWithStorage } from "./solid.typings"; /** * ============================================================================= @@ -26,3 +26,12 @@ export const ResourceShapeType: ShapeType = { shape: "http://www.w3.org/ns/lddps#Resource", context: solidContext, }; + +/** + * ProfileWithStorage ShapeType + */ +export const ProfileWithStorageShapeType: ShapeType = { + schema: solidSchema, + shape: "http://www.w3.org/ns/lddps#ProfileWithStorage", + context: solidContext, +}; diff --git a/packages/solid/src/.ldo/solid.typings.ts b/packages/solid/src/.ldo/solid.typings.ts index a9593f5..b5e6276 100644 --- a/packages/solid/src/.ldo/solid.typings.ts +++ b/packages/solid/src/.ldo/solid.typings.ts @@ -1,4 +1,4 @@ -import type { ContextDefinition } from "jsonld"; +import { ContextDefinition } from "jsonld"; /** * ============================================================================= @@ -57,9 +57,6 @@ export interface Resource { | { "@id": "Resource2"; } - | { - "@id": "Container"; - } )[]; /** * Date modified @@ -74,3 +71,14 @@ export interface Resource { */ size?: number; } + +/** + * ProfileWithStorage Type + */ +export interface ProfileWithStorage { + "@id"?: string; + "@context"?: ContextDefinition; + storage?: { + "@id": string; + }[]; +} diff --git a/packages/solid/src/.shapes/solid.shex b/packages/solid/src/.shapes/solid.shex index 67dc06e..f90f1b5 100644 --- a/packages/solid/src/.shapes/solid.shex +++ b/packages/solid/src/.shapes/solid.shex @@ -6,6 +6,7 @@ PREFIX ldps: PREFIX dct: PREFIX stat: PREFIX tur: +PREFIX pim: ldps:Container EXTRA a { $ldps:ContainerShape ( @@ -34,3 +35,9 @@ ldps:Resource EXTRA a { // rdfs:comment "size of this container"; ) } + +ldps:ProfileWithStorage EXTRA a { + $ldps:ProfileWithStorageShape ( + pim:storage IRI *; + ) +} diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index e3b6a34..034235d 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -10,6 +10,11 @@ import { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; import type { SubjectNode } from "@ldo/rdf-utils"; import type { Resource } from "./resource/Resource"; +import type { CheckRootResultError } from "./requester/requests/checkRootContainer"; +import type { NoRootContainerError } from "./requester/results/error/NoRootContainerError"; +import type { ReadResultError } from "./requester/requests/readResource"; +import { ProfileWithStorageShapeType } from "./.ldo/solid.shapeTypes"; +import type { GetStorageContainerFromWebIdSuccess } from "./requester/results/success/CheckRootContainerSuccess"; /** * A SolidLdoDataset has all the functionality of an LdoDataset with the added @@ -113,4 +118,52 @@ export class SolidLdoDataset extends LdoDataset { startTransaction(linkedDataObject); return linkedDataObject; } + + /** + * 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 solidLdoDataset + * .getStorageFromWebId("https://example.com/profile/card#me"); + * if (result.isError) { + * // Do something + * } + * console.log(result.storageContainer[0].uri); + * ``` + */ + async getStorageFromWebId( + webId: LeafUri, + ): Promise< + | GetStorageContainerFromWebIdSuccess + | CheckRootResultError + | ReadResultError + | NoRootContainerError + > { + const webIdResource = this.getResource(webId); + const readResult = await webIdResource.readIfUnfetched(); + if (readResult.isError) return readResult; + const profile = this.usingType(ProfileWithStorageShapeType).fromSubject( + webId, + ); + if (profile.storage && profile.storage.length > 0) { + const containers = profile.storage.map((storageNode) => + this.getResource(storageNode["@id"] as ContainerUri), + ); + 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/solid/src/requester/results/success/CheckRootContainerSuccess.ts b/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts index e2e12cb..77a435f 100644 --- a/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts +++ b/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts @@ -1,4 +1,5 @@ -import type { ResourceSuccess } from "./SuccessResult"; +import type { Container } from "../../../resource/Container"; +import type { ResourceSuccess, SuccessResult } from "./SuccessResult"; /** * Indicates that the request to check if a resource is the root container was @@ -11,3 +12,8 @@ export interface CheckRootContainerSuccess extends ResourceSuccess { */ isRootContainer: boolean; } + +export interface GetStorageContainerFromWebIdSuccess extends SuccessResult { + type: "getStorageContainerFromWebIdSuccess"; + storageContainers: Container[]; +} diff --git a/packages/solid/test/Integration.test.ts b/packages/solid/test/Integration.test.ts index c86e7dc..4d97db8 100644 --- a/packages/solid/test/Integration.test.ts +++ b/packages/solid/test/Integration.test.ts @@ -45,6 +45,7 @@ import type { 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"; const TEST_CONTAINER_SLUG = "test_ldo/"; const TEST_CONTAINER_URI = @@ -58,6 +59,7 @@ const SAMPLE2_BINARY_URI = `${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as LeafUri; const SAMPLE_CONTAINER_URI = `${TEST_CONTAINER_URI}sample_container/` as ContainerUri; +const SAMPLE_PROFILE_URI = `${TEST_CONTAINER_URI}profile.ttl` as LeafUri; const SPIDER_MAN_TTL = `@base . @prefix rdf: . @prefix rdfs: . @@ -98,6 +100,11 @@ const TEST_CONTAINER_ACL = `<#b30e3fd1-b5a8-4763-ad9d-e95de9cf7933> a , , , ; <${WEB_ID}>; , .`; +const SAMPLE_PROFILE_TTL = ` +@prefix pim: . + +<${SAMPLE_PROFILE_URI}> pim:storage , . +`; async function testRequestLoads( request: () => Promise, @@ -191,6 +198,11 @@ describe("Integration", () => { headers: { "content-type": "text/plain", slug: "sample.txt" }, body: "some text.", }), + authFetch(TEST_CONTAINER_URI, { + method: "POST", + headers: { "content-type": "text/turtle", slug: "profile.ttl" }, + body: SAMPLE_PROFILE_TTL, + }), ]); }); @@ -208,6 +220,9 @@ describe("Integration", () => { authFetch(SAMPLE2_BINARY_URI, { method: "DELETE", }), + authFetch(SAMPLE_PROFILE_URI, { + method: "DELETE", + }), authFetch(SAMPLE_CONTAINER_URI, { method: "DELETE", }), @@ -283,7 +298,7 @@ describe("Integration", () => { isDoingInitialFetch: true, }); expect(result.type).toBe("containerReadSuccess"); - expect(resource.children().length).toBe(2); + expect(resource.children().length).toBe(3); }); it("Reads a binary leaf", async () => { @@ -497,7 +512,7 @@ describe("Integration", () => { }, ); expect(result.type).toBe("containerReadSuccess"); - expect(resource.children().length).toBe(2); + expect(resource.children().length).toBe(3); }); it("reads an unfetched leaf", async () => { @@ -528,7 +543,7 @@ describe("Integration", () => { const result = await resource.readIfUnfetched(); expect(fetchMock).not.toHaveBeenCalled(); expect(result.type).toBe("containerReadSuccess"); - expect(resource.children().length).toBe(2); + expect(resource.children().length).toBe(3); }); it("returns a cached existing data leaf", async () => { @@ -652,6 +667,46 @@ describe("Integration", () => { }); }); + /** + * Get Storage From WebId + */ + describe("getStorageFromWebId", () => { + it("Gets storage when a pim:storage field isn't present", async () => { + const result = await solidLdoDataset.getStorageFromWebId(SAMPLE_DATA_URI); + expect(result.type).toBe("getStorageContainerFromWebIdSuccess"); + const realResult = result as GetStorageContainerFromWebIdSuccess; + expect(realResult.storageContainers.length).toBe(1); + expect(realResult.storageContainers[0].uri).toBe(ROOT_CONTAINER); + }); + + it("Gets storage when a pim:storage field is present", async () => { + const result = + await solidLdoDataset.getStorageFromWebId(SAMPLE_PROFILE_URI); + expect(result.type).toBe("getStorageContainerFromWebIdSuccess"); + const realResult = result as GetStorageContainerFromWebIdSuccess; + expect(realResult.storageContainers.length).toBe(2); + expect(realResult.storageContainers[0].uri).toBe( + "https://example.com/A/", + ); + expect(realResult.storageContainers[0].uri).toBe( + "https://example.com/B/", + ); + }); + + it("Passes any errors returned from the read method", async () => { + fetchMock.mockRejectedValueOnce(new Error("Something happened.")); + const result = await solidLdoDataset.getStorageFromWebId(SAMPLE_DATA_URI); + 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); + expect(result.isError).toBe(true); + }); + }); + /** * =========================================================================== * Create