diff --git a/package-lock.json b/package-lock.json index 549b9d7..7339650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6251,6 +6251,10 @@ "resolved": "packages/solid-react", "link": true }, + "node_modules/@ldo/solid-type-index": { + "resolved": "packages/solid-type-index", + "link": true + }, "node_modules/@ldo/subscribable-dataset": { "resolved": "packages/subscribable-dataset", "link": true @@ -29728,6 +29732,84 @@ "node": ">=4.2.0" } }, + "packages/solid-type-index": { + "name": "@ldo/solid-type-index", + "version": "0.0.1-alpha.28", + "license": "MIT", + "dependencies": { + "@ldo/solid": "^0.0.1-alpha.28", + "@ldo/solid-react": "^0.0.1-alpha.28" + }, + "devDependencies": { + "@ldo/rdf-utils": "^0.0.1-alpha.24", + "@rdfjs/types": "^1.0.1", + "@testing-library/react": "^14.1.2", + "@types/jest": "^27.0.3", + "jest-environment-jsdom": "^27.0.0", + "start-server-and-test": "^2.0.3", + "ts-jest": "^27.1.2", + "ts-node": "^10.9.2" + } + }, + "packages/solid-type-index/node_modules/ts-jest": { + "version": "27.1.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz", + "integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^27.0.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@types/jest": "^27.0.0", + "babel-jest": ">=27.0.0 <28", + "jest": "^27.0.0", + "typescript": ">=3.8 <5.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "packages/solid-type-index/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "packages/solid/node_modules/ts-jest": { "version": "27.1.5", "dev": true, diff --git a/packages/solid-react/test/Integration.test.tsx b/packages/solid-react/test/Integration.test.tsx index b6e5641..fbeb8cd 100644 --- a/packages/solid-react/test/Integration.test.tsx +++ b/packages/solid-react/test/Integration.test.tsx @@ -533,7 +533,6 @@ describe("Integration Tests", () => { SAMPLE_DATA_URI, ]); useSubscribeToResource(...subscribedUris); - const resource1 = useResource(SAMPLE_DATA_URI); const resource2 = useResource(SAMPLE_BINARY_URI); const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); diff --git a/packages/solid-react/test/setUpServer.ts b/packages/solid-react/test/setUpServer.ts index 61be170..b3a54ee 100644 --- a/packages/solid-react/test/setUpServer.ts +++ b/packages/solid-react/test/setUpServer.ts @@ -4,6 +4,7 @@ import fetch from "cross-fetch"; export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/"; export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/"; export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`; +export const WEB_ID = `${SERVER_DOMAIN}${ROOT_ROUTE}profile/card#me`; export const TEST_CONTAINER_SLUG = "test_ldo/"; export const TEST_CONTAINER_URI = `${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as ContainerUri; diff --git a/packages/solid-type-index/src/.ldo/profile.context.ts b/packages/solid-type-index/src/.ldo/profile.context.ts index 534da7b..e1e31a2 100644 --- a/packages/solid-type-index/src/.ldo/profile.context.ts +++ b/packages/solid-type-index/src/.ldo/profile.context.ts @@ -1,11 +1,11 @@ -import { ContextDefinition } from "jsonld"; +import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy"; /** * ============================================================================= * profileContext: JSONLD Context for profile * ============================================================================= */ -export const profileContext: ContextDefinition = { +export const profileContext: LdoJsonldContext = { privateTypeIndex: { "@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex", "@type": "@id", diff --git a/packages/solid-type-index/src/.ldo/typeIndex.context.ts b/packages/solid-type-index/src/.ldo/typeIndex.context.ts index d0dbefd..71ee727 100644 --- a/packages/solid-type-index/src/.ldo/typeIndex.context.ts +++ b/packages/solid-type-index/src/.ldo/typeIndex.context.ts @@ -1,11 +1,11 @@ -import { ContextDefinition } from "jsonld"; +import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy"; /** * ============================================================================= * typeIndexContext: JSONLD Context for typeIndex * ============================================================================= */ -export const typeIndexContext: ContextDefinition = { +export const typeIndexContext: LdoJsonldContext = { TypeIndex: { "@id": "http://www.w3.org/ns/solid/terms#TypeIndex", "@context": { diff --git a/packages/solid-type-index/src/constants.ts b/packages/solid-type-index/src/constants.ts new file mode 100644 index 0000000..1d9e246 --- /dev/null +++ b/packages/solid-type-index/src/constants.ts @@ -0,0 +1,3 @@ +export const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; +export const TYPE_REGISTRATION = + "http://www.w3.org/ns/solid/terms#TypeRegistration"; diff --git a/packages/solid-type-index/src/getTypeIndex.ts b/packages/solid-type-index/src/getTypeIndex.ts new file mode 100644 index 0000000..0753eb3 --- /dev/null +++ b/packages/solid-type-index/src/getTypeIndex.ts @@ -0,0 +1,93 @@ +import type { ContainerUri, LeafUri, SolidLdoDataset } from "@ldo/solid"; +import { createSolidLdoDataset } from "@ldo/solid"; +import type { TypeRegistration } from "./.ldo/typeIndex.typings"; +import { guaranteeFetch } from "@ldo/solid/dist/util/guaranteeFetch"; +import type { TypeIndexProfile } from "./.ldo/profile.typings"; +import { TypeIndexProfileShapeType } from "./.ldo/profile.shapeTypes"; +import { TypeRegistrationShapeType } from "./.ldo/typeIndex.shapeTypes"; +import { RDF_TYPE, TYPE_REGISTRATION } from "./constants"; + +interface GetInstanceUrisOptions { + solidLdoDataset?: SolidLdoDataset; + fetch?: typeof fetch; +} + +export async function getTypeRegistrations( + webId: string, + options?: GetInstanceUrisOptions, +): Promise { + const fetch = guaranteeFetch(options?.fetch); + const dataset = options?.solidLdoDataset ?? createSolidLdoDataset({ fetch }); + + // Get Profile + const profileResource = dataset.getResource(webId); + const readResult = await profileResource.readIfUnfetched(); + if (readResult.isError) throw readResult; + const profile = dataset + .usingType(TypeIndexProfileShapeType) + .fromSubject(webId); + + // Get Type Indexes + const typeIndexUris = getTypeIndexesUrisFromProfile(profile); + + // Fetch the type Indexes + await Promise.all( + typeIndexUris.map(async (typeIndexUri) => { + const typeIndexResource = dataset.getResource(typeIndexUri); + const readResult = await typeIndexResource.readIfUnfetched(); + if (readResult.isError) throw readResult; + }), + ); + + // Get Type Registrations + return dataset + .usingType(TypeRegistrationShapeType) + .matchSubject(RDF_TYPE, TYPE_REGISTRATION); +} + +export function getTypeIndexesUrisFromProfile( + profile: TypeIndexProfile, +): LeafUri[] { + const uris: LeafUri[] = []; + profile.privateTypeIndex?.forEach((indexNode) => { + uris.push(indexNode["@id"] as LeafUri); + }); + profile.publicTypeIndex?.forEach((indexNode) => { + uris.push(indexNode["@id"] as LeafUri); + }); + return uris; +} + +export async function getInstanceUris( + classUri: string, + typeRegistrations: TypeRegistration[], + options?: GetInstanceUrisOptions, +): Promise { + const fetch = guaranteeFetch(options?.fetch); + const dataset = options?.solidLdoDataset ?? createSolidLdoDataset({ fetch }); + + const leafUris = new Set(); + await Promise.all( + typeRegistrations.map(async (registration) => { + if (registration.forClass["@id"] === classUri) { + // Individual registrations + registration.instance?.forEach((instance) => + leafUris.add(instance["@id"] as LeafUri), + ); + // Container registrations + await Promise.all( + registration.instanceContainer?.map(async (instanceContainer) => { + const containerResource = dataset.getResource( + instanceContainer["@id"] as ContainerUri, + ); + await containerResource.readIfUnfetched(); + containerResource.children().forEach((child) => { + if (child.type === "leaf") leafUris.add(child.uri); + }); + }) ?? [], + ); + } + }), + ); + return Array.from(leafUris); +} diff --git a/packages/solid-type-index/src/react/useInstanceUris.ts b/packages/solid-type-index/src/react/useInstanceUris.ts new file mode 100644 index 0000000..59717bc --- /dev/null +++ b/packages/solid-type-index/src/react/useInstanceUris.ts @@ -0,0 +1,45 @@ +import type { LeafUri } from "@ldo/solid"; +import { useTypeIndexProfile } from "./useTypeIndexProfile"; +import { useEffect, useMemo, useState } from "react"; +import { useSubscribeToUris } from "./util/useSubscribeToUris"; +import { useLdo, useMatchSubject } from "@ldo/solid-react"; +import { TypeRegistrationShapeType } from "../.ldo/typeIndex.shapeTypes"; +import { RDF_TYPE, TYPE_REGISTRATION } from "../constants"; +import { + getInstanceUris, + getTypeIndexesUrisFromProfile, +} from "../getTypeIndex"; + +/** + * Provides the LeafUris of everything in a type node for a specific class uri + * + * @param classUri - the class uri + * @returns - URIs of all resources registered with this node + */ +export function useInstanceUris(classUri: string): LeafUri[] { + const { dataset } = useLdo(); + const profile = useTypeIndexProfile(); + + const typeIndexUris: string[] = useMemo( + () => (profile ? getTypeIndexesUrisFromProfile(profile) : []), + [profile], + ); + + useSubscribeToUris(typeIndexUris); + + const [leafUris, setLeafUris] = useState([]); + + const typeRegistrations = useMatchSubject( + TypeRegistrationShapeType, + RDF_TYPE, + TYPE_REGISTRATION, + ); + + useEffect(() => { + getInstanceUris(classUri, typeRegistrations, { + solidLdoDataset: dataset, + }).then(setLeafUris); + }, [typeRegistrations]); + + return leafUris; +} diff --git a/packages/solid-type-index/src/react/useTypeIndex.ts b/packages/solid-type-index/src/react/useTypeIndex.ts deleted file mode 100644 index 87ce97a..0000000 --- a/packages/solid-type-index/src/react/useTypeIndex.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { LeafUri } from "@ldo/solid"; -import { useTypeIndexProfile } from "./useTypeIndexProfile"; -import { useMemo } from "react"; -import { useSubscribeToUris } from "./util/useSubscribeToUris"; - -export function useTypeIndex(classUri: string): Promise { - const profile = useTypeIndexProfile(); - - const typeIndexUris: string[] = useMemo(() => { - const uris: string[] = []; - profile?.privateTypeIndex?.forEach((indexNode) => { - uris.push(indexNode["@id"]); - }); - profile?.publicTypeIndex?.forEach((indexNode) => { - uris.push(indexNode["@id"]); - }); - return uris; - }, [profile]); - - useSubscribeToUris(typeIndexUris); - - -} diff --git a/packages/solid-type-index/src/setTypeIndex.ts b/packages/solid-type-index/src/setTypeIndex.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/solid-type-index/test/.ldo/post.context.ts b/packages/solid-type-index/test/.ldo/post.context.ts deleted file mode 100644 index 5cb3a91..0000000 --- a/packages/solid-type-index/test/.ldo/post.context.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ContextDefinition } from "jsonld"; - -/** - * ============================================================================= - * postContext: JSONLD Context for post - * ============================================================================= - */ -export const postContext: ContextDefinition = { - type: { - "@id": "@type", - }, - SocialMediaPosting: "http://schema.org/SocialMediaPosting", - CreativeWork: "http://schema.org/CreativeWork", - Thing: "http://schema.org/Thing", - articleBody: { - "@id": "http://schema.org/articleBody", - "@type": "http://www.w3.org/2001/XMLSchema#string", - }, - uploadDate: { - "@id": "http://schema.org/uploadDate", - "@type": "http://www.w3.org/2001/XMLSchema#date", - }, - image: { - "@id": "http://schema.org/image", - "@type": "@id", - }, - publisher: { - "@id": "http://schema.org/publisher", - "@type": "@id", - "@container": "@set", - }, -}; diff --git a/packages/solid-type-index/test/.ldo/post.schema.ts b/packages/solid-type-index/test/.ldo/post.schema.ts deleted file mode 100644 index 39e8b63..0000000 --- a/packages/solid-type-index/test/.ldo/post.schema.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Schema } from "shexj"; - -/** - * ============================================================================= - * postSchema: ShexJ Schema for post - * ============================================================================= - */ -export const postSchema: Schema = { - type: "Schema", - shapes: [ - { - id: "https://example.com/PostSh", - type: "ShapeDecl", - shapeExpr: { - type: "Shape", - expression: { - type: "EachOf", - expressions: [ - { - type: "TripleConstraint", - predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", - valueExpr: { - type: "NodeConstraint", - values: [ - "http://schema.org/SocialMediaPosting", - "http://schema.org/CreativeWork", - "http://schema.org/Thing", - ], - }, - }, - { - type: "TripleConstraint", - predicate: "http://schema.org/articleBody", - valueExpr: { - type: "NodeConstraint", - datatype: "http://www.w3.org/2001/XMLSchema#string", - }, - min: 0, - max: 1, - annotations: [ - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#label", - object: { - value: "articleBody", - }, - }, - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#comment", - object: { - value: "The actual body of the article. ", - }, - }, - ], - }, - { - type: "TripleConstraint", - predicate: "http://schema.org/uploadDate", - valueExpr: { - type: "NodeConstraint", - datatype: "http://www.w3.org/2001/XMLSchema#date", - }, - annotations: [ - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#label", - object: { - value: "uploadDate", - }, - }, - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#comment", - object: { - value: - "Date when this media object was uploaded to this site.", - }, - }, - ], - }, - { - type: "TripleConstraint", - predicate: "http://schema.org/image", - valueExpr: { - type: "NodeConstraint", - nodeKind: "iri", - }, - min: 0, - max: 1, - annotations: [ - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#label", - object: { - value: "image", - }, - }, - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#comment", - object: { - value: - "A media object that encodes this CreativeWork. This property is a synonym for encoding.", - }, - }, - ], - }, - { - type: "TripleConstraint", - predicate: "http://schema.org/publisher", - valueExpr: { - type: "NodeConstraint", - nodeKind: "iri", - }, - annotations: [ - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#label", - object: { - value: "publisher", - }, - }, - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#comment", - object: { - value: "The publisher of the creative work.", - }, - }, - ], - }, - ], - }, - annotations: [ - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#label", - object: { - value: "SocialMediaPost", - }, - }, - { - type: "Annotation", - predicate: "http://www.w3.org/2000/01/rdf-schema#comment", - object: { - value: - "A post to a social media platform, including blog posts, tweets, Facebook posts, etc.", - }, - }, - ], - }, - }, - ], -}; diff --git a/packages/solid-type-index/test/.ldo/post.shapeTypes.ts b/packages/solid-type-index/test/.ldo/post.shapeTypes.ts deleted file mode 100644 index 4c50683..0000000 --- a/packages/solid-type-index/test/.ldo/post.shapeTypes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ShapeType } from "@ldo/ldo"; -import { postSchema } from "./post.schema"; -import { postContext } from "./post.context"; -import { PostSh } from "./post.typings"; - -/** - * ============================================================================= - * LDO ShapeTypes post - * ============================================================================= - */ - -/** - * PostSh ShapeType - */ -export const PostShShapeType: ShapeType = { - schema: postSchema, - shape: "https://example.com/PostSh", - context: postContext, -}; diff --git a/packages/solid-type-index/test/.ldo/post.typings.ts b/packages/solid-type-index/test/.ldo/post.typings.ts deleted file mode 100644 index 1425a9a..0000000 --- a/packages/solid-type-index/test/.ldo/post.typings.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ContextDefinition } from "jsonld"; - -/** - * ============================================================================= - * Typescript Typings for post - * ============================================================================= - */ - -/** - * PostSh Type - */ -export interface PostSh { - "@id"?: string; - "@context"?: ContextDefinition; - type: - | { - "@id": "SocialMediaPosting"; - } - | { - "@id": "CreativeWork"; - } - | { - "@id": "Thing"; - }; - /** - * The actual body of the article. - */ - articleBody?: string; - /** - * Date when this media object was uploaded to this site. - */ - uploadDate: string; - /** - * A media object that encodes this CreativeWork. This property is a synonym for encoding. - */ - image?: { - "@id": string; - }; - /** - * The publisher of the creative work. - */ - publisher: { - "@id": string; - }[]; -} diff --git a/packages/solid-type-index/test/General.test.tsx b/packages/solid-type-index/test/General.test.tsx new file mode 100644 index 0000000..0b16062 --- /dev/null +++ b/packages/solid-type-index/test/General.test.tsx @@ -0,0 +1,41 @@ +import { createSolidLdoDataset } from "@ldo/solid"; +import { + MY_BOOKMARKS_1_URI, + MY_BOOKMARKS_2_URI, + setUpServer, + WEB_ID, +} from "./setUpServer"; +import { getInstanceUris, getTypeRegistrations } from "../src/getTypeIndex"; + +// Use an increased timeout, since the CSS server takes too much setup time. +jest.setTimeout(40_000); + +describe("General Tests", () => { + setUpServer(); + + it("gets the current typeindex", async () => { + const solidLdoDataset = createSolidLdoDataset(); + const typeRegistrations = await getTypeRegistrations(WEB_ID, { + solidLdoDataset, + }); + const addressBookUris = await getInstanceUris( + "http://www.w3.org/2006/vcard/ns#AddressBook", + typeRegistrations, + { solidLdoDataset }, + ); + expect(addressBookUris).toEqual( + expect.arrayContaining([ + "https://example.com/myPrivateAddressBook.ttl", + "https://example.com/myPublicAddressBook.ttl", + ]), + ); + const bookmarkUris = await getInstanceUris( + "http://www.w3.org/2002/01/bookmark#Bookmark", + typeRegistrations, + { solidLdoDataset }, + ); + expect(bookmarkUris).toEqual( + expect.arrayContaining([MY_BOOKMARKS_1_URI, MY_BOOKMARKS_2_URI]), + ); + }); +}); diff --git a/packages/solid-type-index/test/Integration.test.tsx b/packages/solid-type-index/test/Integration.test.tsx deleted file mode 100644 index 8c8ae0b..0000000 --- a/packages/solid-type-index/test/Integration.test.tsx +++ /dev/null @@ -1,431 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import type { FunctionComponent } from "react"; -import { render, screen, fireEvent, act } from "@testing-library/react"; -import { - SAMPLE_BINARY_URI, - SAMPLE_DATA_URI, - SERVER_DOMAIN, - setUpServer, -} from "./setUpServer"; -import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider"; -import { useResource } from "../src/useResource"; -import { useRootContainerFor } from "../src/useRootContainer"; -import { useLdo } from "../src/SolidLdoProvider"; -import { PostShShapeType } from "./.ldo/post.shapeTypes"; -import type { PostSh } from "./.ldo/post.typings"; -import { useSubject } from "../src/useSubject"; - -// Use an increased timeout, since the CSS server takes too much setup time. -jest.setTimeout(40_000); - -describe("Integration Tests", () => { - setUpServer(); - - /** - * =========================================================================== - * useResource - * =========================================================================== - */ - describe("useResource", () => { - it("Fetches a resource and indicates it is loading while doing so", async () => { - const UseResourceTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI); - if (resource?.isLoading()) return

Loading

; - return

{resource.status.type}

; - }; - render( - - - , - ); - await screen.findByText("Loading"); - const resourceStatus = await screen.findByRole("status"); - expect(resourceStatus.innerHTML).toBe("dataReadSuccess"); - }); - - it("returns undefined when no uri is provided, then rerenders when one is", async () => { - const UseResourceUndefinedTest: FunctionComponent = () => { - const [uri, setUri] = useState(undefined); - const resource = useResource(uri, { suppressInitialRead: true }); - if (!resource) - return ( -
-

Undefined

- -
- ); - return

{resource.status.type}

; - }; - render( - - - , - ); - await screen.findByText("Undefined"); - fireEvent.click(screen.getByText("Next")); - const resourceStatus = await screen.findByRole("status"); - expect(resourceStatus.innerHTML).toBe("unfetched"); - }); - - it("Reloads the data on mount", async () => { - const ReloadTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI, { reloadOnMount: true }); - if (resource?.isLoading()) return

Loading

; - return

{resource.status.type}

; - }; - const ReloadParent: FunctionComponent = () => { - const [showComponent, setShowComponent] = useState(true); - return ( -
- - {showComponent ? :

Hidden

} -
- ); - }; - render( - - - , - ); - await screen.findByText("Loading"); - const resourceStatus1 = await screen.findByRole("status"); - expect(resourceStatus1.innerHTML).toBe("dataReadSuccess"); - fireEvent.click(screen.getByText("Show Component")); - await screen.findByText("Hidden"); - fireEvent.click(screen.getByText("Show Component")); - await screen.findByText("Loading"); - const resourceStatus2 = await screen.findByRole("status", undefined, { - timeout: 5000, - }); - expect(resourceStatus2.innerHTML).toBe("dataReadSuccess"); - }); - - it("handles swapping to a new resource", async () => { - const SwapResourceTest: FunctionComponent = () => { - const [uri, setUri] = useState(SAMPLE_DATA_URI); - const resource = useResource(uri); - if (resource?.isLoading()) return

Loading

; - return ( -
-

{resource.status.type}

- -
- ); - }; - render( - - - , - ); - await screen.findByText("Loading"); - const resourceStatus1 = await screen.findByRole("status"); - expect(resourceStatus1.innerHTML).toBe("dataReadSuccess"); - fireEvent.click(screen.getByText("Update URI")); - await screen.findByText("Loading"); - const resourceStatus2 = await screen.findByRole("status"); - expect(resourceStatus2.innerHTML).toBe("binaryReadSuccess"); - }); - }); - - /** - * =========================================================================== - * useRootContainer - * =========================================================================== - */ - describe("useRootContainer", () => { - it("gets the root container for a sub-resource", async () => { - const RootContainerTest: FunctionComponent = () => { - const rootContainer = useRootContainerFor(SAMPLE_DATA_URI, { - suppressInitialRead: true, - }); - return rootContainer ?

{rootContainer?.uri}

: <>; - }; - render( - - - , - ); - const container = await screen.findByRole("root"); - expect(container.innerHTML).toBe(SERVER_DOMAIN); - }); - - it("returns undefined when a URI is not provided", async () => { - const RootContainerTest: FunctionComponent = () => { - const rootContainer = useRootContainerFor(undefined, { - suppressInitialRead: true, - }); - return rootContainer ? ( -

{rootContainer?.uri}

- ) : ( -

Undefined

- ); - }; - render( - - - , - ); - const container = await screen.findByRole("undefined"); - expect(container.innerHTML).toBe("Undefined"); - }); - }); - - /** - * =========================================================================== - * useLdoMethods - * =========================================================================== - */ - describe("useLdoMethods", () => { - it("uses get subject to get a linked data object", async () => { - const GetSubjectTest: FunctionComponent = () => { - const [subject, setSubject] = useState(); - const { getSubject } = useLdo(); - useEffect(() => { - const someSubject = getSubject( - PostShShapeType, - "https://example.com/subject", - ); - setSubject(someSubject); - }, []); - return subject ?

{subject["@id"]}

: <>; - }; - render( - - - , - ); - const container = await screen.findByRole("subject"); - expect(container.innerHTML).toBe("https://example.com/subject"); - }); - - it("uses createData to create a new data object", async () => { - const GetSubjectTest: FunctionComponent = () => { - const [subject, setSubject] = useState(); - const { createData, getResource } = useLdo(); - useEffect(() => { - const someSubject = createData( - PostShShapeType, - "https://example.com/subject", - getResource("https://example.com/"), - ); - someSubject.articleBody = "Cool Article"; - setSubject(someSubject); - }, []); - return subject ?

{subject.articleBody}

: <>; - }; - render( - - - , - ); - const container = await screen.findByRole("subject"); - expect(container.innerHTML).toBe("Cool Article"); - }); - }); - - /** - * =========================================================================== - * useSubject - * =========================================================================== - */ - describe("useSubject", () => { - it("renders the article body from the useSubject value", async () => { - const UseSubjectTest: FunctionComponent = () => { - useResource(SAMPLE_DATA_URI); - const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); - - return

{post.articleBody}

; - }; - render( - - - , - ); - - await screen.findByText("test"); - }); - - it("renders the array value from the useSubject value", async () => { - const UseSubjectTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI); - const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); - if (resource.isLoading() || !post) return

loading

; - - return ( -
-

{post.publisher[0]["@id"]}

-
    - {post.publisher.map((publisher) => { - return
  • {publisher["@id"]}
  • ; - })} -
-
- ); - }; - render( - - - , - ); - - const single = await screen.findByRole("single"); - expect(single.innerHTML).toBe("https://example.com/Publisher1"); - const list = await screen.findByRole("list"); - expect(list.children[0].innerHTML).toBe("https://example.com/Publisher1"); - expect(list.children[1].innerHTML).toBe("https://example.com/Publisher2"); - }); - - it("returns undefined in the subject URI is undefined", async () => { - const UseSubjectTest: FunctionComponent = () => { - useResource(SAMPLE_DATA_URI, { suppressInitialRead: true }); - const post = useSubject(PostShShapeType, undefined); - - return ( -

- {post === undefined ? "Undefined" : "Not Undefined"} -

- ); - }; - render( - - - , - ); - - const article = await screen.findByRole("article"); - expect(article.innerHTML).toBe("Undefined"); - }); - - it("returns nothing if a symbol key is provided", async () => { - const UseSubjectTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI); - const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); - if (resource.isLoading() || !post) return

loading

; - - return

{typeof post[Symbol.hasInstance]}

; - }; - render( - - - , - ); - - const article = await screen.findByRole("value"); - expect(article.innerHTML).toBe("undefined"); - }); - - it("returns an id if an id key is provided", async () => { - const UseSubjectTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI); - const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); - if (resource.isLoading() || !post) return

loading

; - - return

{post["@id"]}

; - }; - render( - - - , - ); - - const article = await screen.findByRole("value"); - expect(article.innerHTML).toBe(`${SAMPLE_DATA_URI}#Post1`); - }); - - it("does not set a value if a value is attempted to be set", async () => { - const warn = jest.spyOn(console, "warn").mockImplementation(() => {}); - const UseSubjectTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI); - const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); - if (resource.isLoading() || !post) return

loading

; - - return ( -
-

{post.articleBody}

- -
- ); - }; - render( - - - , - ); - - const article = await screen.findByRole("value"); - expect(article.innerHTML).toBe(`test`); - fireEvent.click(screen.getByText("Attempt Change")); - expect(article.innerHTML).not.toBe("bad"); - expect(warn).toHaveBeenCalledWith( - "You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.", - ); - warn.mockReset(); - }); - - it("rerenders when asked to subscribe to a resource", async () => { - const NotificationTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI, { subscribe: true }); - const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); - - const addPublisher = useCallback(async () => { - await fetch(SAMPLE_DATA_URI, { - method: "PATCH", - body: `INSERT DATA { <${SAMPLE_DATA_URI}#Post1> . }`, - headers: { - "Content-Type": "application/sparql-update", - }, - }); - }, []); - - if (resource.isLoading() || !post) return

loading

; - - return ( -
-
    - {post.publisher.map((publisher) => { - return
  • {publisher["@id"]}
  • ; - })} -
- -
- ); - }; - const { unmount } = render( - - - , - ); - - const list = await screen.findByRole("list"); - expect(list.children[0].innerHTML).toBe("https://example.com/Publisher1"); - expect(list.children[1].innerHTML).toBe("https://example.com/Publisher2"); - - // Wait for subscription to connect - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }); - - // Click button to add a publisher - await fireEvent.click(screen.getByText("Add Publisher")); - await screen.findByText("https://example.com/Publisher3"); - - // Verify the new publisher is in the list - const updatedList = await screen.findByRole("list"); - expect(updatedList.children[2].innerHTML).toBe( - "https://example.com/Publisher3", - ); - - unmount(); - - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - }); - }); - }); -}); diff --git a/packages/solid-type-index/test/React.tsx b/packages/solid-type-index/test/React.tsx new file mode 100644 index 0000000..42f6bd8 --- /dev/null +++ b/packages/solid-type-index/test/React.tsx @@ -0,0 +1,8 @@ +import { setUpServer } from "./setUpServer"; + +// Use an increased timeout, since the CSS server takes too much setup time. +jest.setTimeout(40_000); + +describe("React Tests", () => { + setUpServer(); +}); diff --git a/packages/solid-type-index/test/setUpServer.ts b/packages/solid-type-index/test/setUpServer.ts index 0551ccf..b5fb559 100644 --- a/packages/solid-type-index/test/setUpServer.ts +++ b/packages/solid-type-index/test/setUpServer.ts @@ -1,47 +1,49 @@ -import type { ContainerUri, LeafUri } from "@ldo/solid"; import fetch from "cross-fetch"; -export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/"; +export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3003/"; export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/"; export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`; -export const TEST_CONTAINER_SLUG = "test_ldo/"; -export const TEST_CONTAINER_URI = - `${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as ContainerUri; -export const SAMPLE_DATA_URI = `${TEST_CONTAINER_URI}sample.ttl` as LeafUri; -export const SAMPLE2_DATA_SLUG = "sample2.ttl"; -export const SAMPLE2_DATA_URI = - `${TEST_CONTAINER_URI}${SAMPLE2_DATA_SLUG}` as LeafUri; -export const SAMPLE_BINARY_URI = `${TEST_CONTAINER_URI}sample.txt` as LeafUri; -export const SAMPLE2_BINARY_SLUG = `sample2.txt`; -export const SAMPLE2_BINARY_URI = - `${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as LeafUri; -export const SAMPLE_CONTAINER_URI = - `${TEST_CONTAINER_URI}sample_container/` as ContainerUri; -export const EXAMPLE_POST_TTL = `@prefix schema: . +export const PROFILE_CONTAINER = `${ROOT_CONTAINER}profile/`; +export const WEB_ID = `${PROFILE_CONTAINER}card.ttl#me`; +export const PUBLIC_TYPE_INDEX_URI = `${PROFILE_CONTAINER}publicTypeIndex.ttl`; +export const PRIVATE_TYPE_INDEX_URI = `${PROFILE_CONTAINER}privateTypeIndex.ttl`; +export const MY_BOOKMARKS_CONTAINER = `${ROOT_CONTAINER}myBookmarks/`; +export const MY_BOOKMARKS_1_URI = `${ROOT_CONTAINER}myBookmarks/bookmark1.ttl`; +export const MY_BOOKMARKS_2_URI = `${ROOT_CONTAINER}myBookmarks/bookmark2.ttl`; -<#Post1> - a schema:CreativeWork, schema:Thing, schema:SocialMediaPosting ; - schema:image ; - schema:articleBody "test" ; - schema:publisher , .`; -export const TEST_CONTAINER_TTL = `@prefix dc: . -@prefix ldp: . -@prefix posix: . -@prefix xsd: . +export const PROFILE_TTL = ` +<#me> <${PROFILE_CONTAINER}publicTypeIndex.ttl> ; + <${PROFILE_CONTAINER}privateTypeIndex.ttl> .`; +export const PUBLIC_TYPE_INDEX_TTL = `@prefix solid: . +@prefix vcard: . +@prefix bk: . -<> "sample.txt"; - a ldp:Container, ldp:BasicContainer, ldp:Resource; - dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. - a ldp:Resource, ; - dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. - a ldp:Resource, ; - dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. -<> posix:mtime 1697810234; - ldp:contains , . - posix:mtime 1697810234; - posix:size 522. - posix:mtime 1697810234; - posix:size 10.`; +<> + a solid:TypeIndex ; + a solid:ListedDocument. + +<#ab09fd> a solid:TypeRegistration; + solid:forClass vcard:AddressBook; + solid:instance . + +<#bq1r5e> a solid:TypeRegistration; + solid:forClass bk:Bookmark; + solid:instanceContainer <${ROOT_CONTAINER}myBookmarks/>.`; +export const PRIVATE_TYPE_INDEX_TTL = `@prefix solid: . +@prefix vcard: . +@prefix bk: . + +<> + a solid:TypeIndex ; + a solid:UnlistedDocument. + +<#ab09fd> a solid:TypeRegistration; + solid:forClass vcard:AddressBook; + solid:instance . + +<#bq1r5e> a solid:TypeRegistration; + solid:forClass bk:Bookmark; + solid:instanceContainer <${ROOT_CONTAINER}myBookmarks/>.`; export interface SetUpServerReturn { authFetch: typeof fetch; @@ -65,44 +67,53 @@ export function setUpServer(): SetUpServerReturn { beforeEach(async () => { s.fetchMock = jest.fn(s.authFetch); // Create a new document called sample.ttl + await s.authFetch(WEB_ID, { method: "DELETE" }); await s.authFetch(ROOT_CONTAINER, { method: "POST", headers: { link: '; rel="type"', - slug: TEST_CONTAINER_SLUG, + slug: "myBookmarks/", }, }); await Promise.all([ - s.authFetch(TEST_CONTAINER_URI, { + s.authFetch(PROFILE_CONTAINER, { + method: "POST", + headers: { "content-type": "text/turtle", slug: "card.ttl" }, + body: PROFILE_TTL, + }), + s.authFetch(PROFILE_CONTAINER, { + method: "POST", + headers: { "content-type": "text/turtle", slug: "publicTypeIndex.ttl" }, + body: PUBLIC_TYPE_INDEX_TTL, + }), + s.authFetch(PROFILE_CONTAINER, { + method: "POST", + headers: { + "content-type": "text/turtle", + slug: "privateTypeIndex.ttl", + }, + body: PRIVATE_TYPE_INDEX_TTL, + }), + s.authFetch(MY_BOOKMARKS_CONTAINER, { method: "POST", - headers: { "content-type": "text/turtle", slug: "sample.ttl" }, - body: EXAMPLE_POST_TTL, + headers: { "content-type": "text/turtle", slug: "bookmark1.ttl" }, + body: "", }), - s.authFetch(TEST_CONTAINER_URI, { + s.authFetch(MY_BOOKMARKS_CONTAINER, { method: "POST", - headers: { "content-type": "text/plain", slug: "sample.txt" }, - body: "some text.", + headers: { "content-type": "text/turtle", slug: "bookmark2.ttl" }, + body: "", }), ]); }); afterEach(async () => { await Promise.all([ - s.authFetch(SAMPLE_DATA_URI, { - method: "DELETE", - }), - s.authFetch(SAMPLE2_DATA_URI, { - method: "DELETE", - }), - s.authFetch(SAMPLE_BINARY_URI, { - method: "DELETE", - }), - s.authFetch(SAMPLE2_BINARY_URI, { - method: "DELETE", - }), - s.authFetch(SAMPLE_CONTAINER_URI, { - method: "DELETE", - }), + await s.authFetch(WEB_ID, { method: "DELETE" }), + await s.authFetch(PUBLIC_TYPE_INDEX_URI, { method: "DELETE" }), + await s.authFetch(PRIVATE_TYPE_INDEX_URI, { method: "DELETE" }), + await s.authFetch(MY_BOOKMARKS_1_URI, { method: "DELETE" }), + await s.authFetch(MY_BOOKMARKS_2_URI, { method: "DELETE" }), ]); }); diff --git a/packages/solid-type-index/test/test-server/solidServer.helper.ts b/packages/solid-type-index/test/test-server/solidServer.helper.ts index 5dd45d8..9dd4703 100644 --- a/packages/solid-type-index/test/test-server/solidServer.helper.ts +++ b/packages/solid-type-index/test/test-server/solidServer.helper.ts @@ -26,7 +26,7 @@ export async function createApp(): Promise { ), variableBindings: {}, shorthand: { - port: 3_001, + port: 3_003, loggingLevel: "off", seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"), },