parent
3b4efda881
commit
c69fef3070
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"extends": ["../../.eslintrc"] |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
test/test-server/data |
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2023 Jackson Morgan |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,138 @@ |
|||||||
|
# @ldo/solid-react |
||||||
|
|
||||||
|
`@ldo/solid-react` provides tool and hooks for easily building Solid applications using react. |
||||||
|
|
||||||
|
## Guide |
||||||
|
|
||||||
|
A full walkthrough for using the `@ldo/solid` library can be found in the [For Solid + React Guide](https://ldo.js.org/latest/guides/solid_react/) |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
Navigate into your project's root folder and run the following command: |
||||||
|
``` |
||||||
|
cd my_project/ |
||||||
|
npx run @ldo/cli init |
||||||
|
``` |
||||||
|
|
||||||
|
Now install the @ldo/solid library |
||||||
|
|
||||||
|
``` |
||||||
|
npm i @ldo/solid @ldo/solid-react |
||||||
|
``` |
||||||
|
|
||||||
|
<details> |
||||||
|
<summary> |
||||||
|
Manual Installation |
||||||
|
</summary> |
||||||
|
|
||||||
|
If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently. |
||||||
|
|
||||||
|
``` |
||||||
|
npm i @ldo/ldo @ldo/solid @ldo/solid-react |
||||||
|
``` |
||||||
|
</details> |
||||||
|
|
||||||
|
## Simple Example |
||||||
|
|
||||||
|
Below is a simple example of @ldo/solid-react in a real use-case. Assume that a ShapeType was previously generated and placed at `./.ldo/solidProfile.shapeTypess`. |
||||||
|
|
||||||
|
|
||||||
|
```typescript |
||||||
|
import type { FunctionComponent } from "react"; |
||||||
|
import React, { useCallback } from "react"; |
||||||
|
import { |
||||||
|
BrowserSolidLdoProvider, |
||||||
|
useResource, |
||||||
|
useSolidAuth, |
||||||
|
useSubject, |
||||||
|
} from "@ldo/solid-react"; |
||||||
|
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes"; |
||||||
|
import { changeData, commitData } from "@ldo/solid"; |
||||||
|
|
||||||
|
// The base component for the app |
||||||
|
const App: FunctionComponent = () => { |
||||||
|
return ( |
||||||
|
/* The application should be surrounded with the BrowserSolidLdoProvider |
||||||
|
this will set up all the underlying infrastructure for the application */ |
||||||
|
<BrowserSolidLdoProvider> |
||||||
|
<Login /> |
||||||
|
</BrowserSolidLdoProvider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
// A component that handles login |
||||||
|
const Login: FunctionComponent = () => { |
||||||
|
// Get login information using the "useSolidAuth" hook |
||||||
|
const { login, logout, session } = useSolidAuth(); |
||||||
|
|
||||||
|
const onLogin = useCallback(() => { |
||||||
|
const issuer = prompt("What is your Solid IDP?"); |
||||||
|
// Call the "login" function to initiate login |
||||||
|
if (issuer) login(issuer); |
||||||
|
}, []); |
||||||
|
|
||||||
|
// You can use session.isLoggedIn to check if the user is logged in |
||||||
|
if (session.isLoggedIn) { |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
{/* Get the user's webId from session.webId */} |
||||||
|
<p>Logged in as {session.webId}</p> |
||||||
|
{/* Use the logout function to log out */} |
||||||
|
<button onClick={logout}>Log Out</button> |
||||||
|
<Profile /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
return <button onClick={onLogin}>Log In</button>; |
||||||
|
}; |
||||||
|
|
||||||
|
// Renders the name on the profile |
||||||
|
const Profile: FunctionComponent = () => { |
||||||
|
const { session } = useSolidAuth(); |
||||||
|
// With useResource, you can automatically fetch a resource |
||||||
|
const resource = useResource(session.webId); |
||||||
|
// With useSubject, you can extract data from that resource |
||||||
|
const profile = useSubject(SolidProfileShapeShapeType, session.webId); |
||||||
|
|
||||||
|
const onNameChange = useCallback(async (e) => { |
||||||
|
// Ensure that the |
||||||
|
if (!profile || !resource) return; |
||||||
|
// Change data lets you create a new object to make changes to |
||||||
|
const cProfile = changeData(profile, resource); |
||||||
|
// Change the name |
||||||
|
cProfile.name = e.target.value; |
||||||
|
// Commit the data back to the Pod |
||||||
|
await commitData(cProfile); |
||||||
|
}, []); |
||||||
|
|
||||||
|
return <input type="text" value={profile?.name} onChange={onNameChange} />; |
||||||
|
}; |
||||||
|
|
||||||
|
export default App; |
||||||
|
``` |
||||||
|
|
||||||
|
## API Details |
||||||
|
|
||||||
|
Providers |
||||||
|
|
||||||
|
- [BrowserSolidLdoProvider](https://ldo.js.org/latest/api/react/BrowserSolidLdoProvider/) |
||||||
|
- [SolidLdoProvider](https://ldo.js.org/latest/api/react/SolidLdoProvider/) |
||||||
|
|
||||||
|
Hooks |
||||||
|
- [useLdo](https://ldo.js.org/latest/api/react/useLdo/) |
||||||
|
- [useResource](https://ldo.js.org/latest/api/react/useResource/) |
||||||
|
- [useRootContainer](https://ldo.js.org/latest/api/react/useRootContainer/) |
||||||
|
- [useSolidAuth](https://ldo.js.org/latest/api/react/useSolidAuth/) |
||||||
|
- [useSubject](https://ldo.js.org/latest/api/react/useSubject/) |
||||||
|
- [useMatchSubject](https://ldo.js.org/latest/api/react/useMatchSubject/) |
||||||
|
- [useMatchObject](https://ldo.js.org/latest/api/react/useMatchSubject/) |
||||||
|
- [useSubscribeToResource](https://ldo.js.org/latest/api/react/useMatchSubject/) |
||||||
|
|
||||||
|
## Sponsorship |
||||||
|
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/). |
||||||
|
|
||||||
|
[<img src="https://nlnet.nl/logo/banner.png" alt="nlnet foundation logo" width="300" />](https://nlnet.nl/) |
||||||
|
[<img src="https://nlnet.nl/image/logos/NGI0Entrust_tag.svg" alt="NGI Zero Entrust Logo" width="300" />](https://nlnet.nl/) |
||||||
|
|
||||||
|
## Liscense |
||||||
|
MIT |
@ -0,0 +1,6 @@ |
|||||||
|
const sharedConfig = require("../../jest.config.js"); |
||||||
|
module.exports = { |
||||||
|
...sharedConfig, |
||||||
|
rootDir: "./", |
||||||
|
testEnvironment: "jsdom", |
||||||
|
}; |
@ -0,0 +1,2 @@ |
|||||||
|
import "@inrupt/jest-jsdom-polyfills"; |
||||||
|
globalThis.fetch = async () => new Response(); |
@ -0,0 +1,56 @@ |
|||||||
|
{ |
||||||
|
"name": "@ldo/solid-react", |
||||||
|
"version": "1.0.0-alpha.1", |
||||||
|
"description": "A React library for LDO and Solid", |
||||||
|
"main": "dist/index.js", |
||||||
|
"scripts": { |
||||||
|
"build": "tsc --project tsconfig.build.json", |
||||||
|
"watch": "tsc --watch", |
||||||
|
"test": "npm run test:integration", |
||||||
|
"test:watch": "jest --watch", |
||||||
|
"prepublishOnly": "npm run test && npm run build", |
||||||
|
"build:ldo": "ldo build --input src/shapes --output src/ldo", |
||||||
|
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern", |
||||||
|
"test:integration": "start-server-and-test start-test-server http://localhost:3002 start-integration-test", |
||||||
|
"start-test-server": "ts-node ./test/test-server/runServer.ts", |
||||||
|
"start-integration-test": "jest --coverage" |
||||||
|
}, |
||||||
|
"repository": { |
||||||
|
"type": "git", |
||||||
|
"url": "git+https://github.com/o-development/ldobjects.git" |
||||||
|
}, |
||||||
|
"author": "Jackson Morgan", |
||||||
|
"license": "MIT", |
||||||
|
"bugs": { |
||||||
|
"url": "https://github.com/o-development/ldobjects/issues" |
||||||
|
}, |
||||||
|
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme", |
||||||
|
"devDependencies": { |
||||||
|
"@ldo/rdf-utils": "^1.0.0-alpha.1", |
||||||
|
"@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" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@inrupt/solid-client-authn-browser": "^2.0.0", |
||||||
|
"@ldo/dataset": "^1.0.0-alpha.1", |
||||||
|
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1", |
||||||
|
"@ldo/ldo": "^1.0.0-alpha.1", |
||||||
|
"@ldo/connected": "^1.0.0-alpha.1", |
||||||
|
"@ldo/subscribable-dataset": "^1.0.0-alpha.1", |
||||||
|
"@rdfjs/data-model": "^1.2.0", |
||||||
|
"cross-fetch": "^3.1.6" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"dist", |
||||||
|
"src" |
||||||
|
], |
||||||
|
"publishConfig": { |
||||||
|
"access": "public" |
||||||
|
}, |
||||||
|
"gitHead": "0287cd6371f06630763568dec5e41653f7b8583e" |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react"; |
||||||
|
import type { FunctionComponent, PropsWithChildren } from "react"; |
||||||
|
import type { LoginOptions, SessionInfo } from "./SolidAuthContext"; |
||||||
|
import { SolidAuthContext } from "./SolidAuthContext"; |
||||||
|
import { |
||||||
|
getDefaultSession, |
||||||
|
handleIncomingRedirect, |
||||||
|
login as libraryLogin, |
||||||
|
logout as libraryLogout, |
||||||
|
fetch as libraryFetch, |
||||||
|
} from "@inrupt/solid-client-authn-browser"; |
||||||
|
import { SolidLdoProvider } from "./SolidLdoProvider"; |
||||||
|
|
||||||
|
const PRE_REDIRECT_URI = "PRE_REDIRECT_URI"; |
||||||
|
|
||||||
|
export const BrowserSolidLdoProvider: FunctionComponent<PropsWithChildren> = ({ |
||||||
|
children, |
||||||
|
}) => { |
||||||
|
const [session, setSession] = useState<SessionInfo>(getDefaultSession().info); |
||||||
|
const [ranInitialAuthCheck, setRanInitialAuthCheck] = useState(false); |
||||||
|
|
||||||
|
const runInitialAuthCheck = useCallback(async () => { |
||||||
|
if (!window.localStorage.getItem(PRE_REDIRECT_URI)) { |
||||||
|
window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href); |
||||||
|
} |
||||||
|
|
||||||
|
await handleIncomingRedirect({ |
||||||
|
restorePreviousSession: true, |
||||||
|
}); |
||||||
|
// Set timout to ensure this happens after the redirect
|
||||||
|
setTimeout(() => { |
||||||
|
setSession({ ...getDefaultSession().info }); |
||||||
|
window.history.replaceState( |
||||||
|
{}, |
||||||
|
"", |
||||||
|
window.localStorage.getItem(PRE_REDIRECT_URI), |
||||||
|
); |
||||||
|
window.localStorage.removeItem(PRE_REDIRECT_URI); |
||||||
|
|
||||||
|
setRanInitialAuthCheck(true); |
||||||
|
}, 0); |
||||||
|
}, []); |
||||||
|
|
||||||
|
const login = useCallback(async (issuer: string, options?: LoginOptions) => { |
||||||
|
const cleanUrl = new URL(window.location.href); |
||||||
|
cleanUrl.hash = ""; |
||||||
|
cleanUrl.search = ""; |
||||||
|
const fullOptions = { |
||||||
|
redirectUrl: cleanUrl.toString(), |
||||||
|
clientName: "Solid App", |
||||||
|
oidcIssuer: issuer, |
||||||
|
...options, |
||||||
|
}; |
||||||
|
window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href); |
||||||
|
await libraryLogin(fullOptions); |
||||||
|
setSession({ ...getDefaultSession().info }); |
||||||
|
}, []); |
||||||
|
|
||||||
|
const logout = useCallback(async () => { |
||||||
|
await libraryLogout(); |
||||||
|
setSession({ ...getDefaultSession().info }); |
||||||
|
}, []); |
||||||
|
|
||||||
|
const signUp = useCallback( |
||||||
|
async (issuer: string, options?: LoginOptions) => { |
||||||
|
// The typings on @inrupt/solid-client-authn-core have not yet been updated
|
||||||
|
// TODO: remove this ts-ignore when they are updated.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
return login(issuer, { ...options, prompt: "create" }); |
||||||
|
}, |
||||||
|
[login], |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
runInitialAuthCheck(); |
||||||
|
}, []); |
||||||
|
|
||||||
|
const solidAuthFunctions = useMemo( |
||||||
|
() => ({ |
||||||
|
runInitialAuthCheck, |
||||||
|
login, |
||||||
|
logout, |
||||||
|
signUp, |
||||||
|
session, |
||||||
|
ranInitialAuthCheck, |
||||||
|
fetch: libraryFetch, |
||||||
|
}), |
||||||
|
[login, logout, ranInitialAuthCheck, runInitialAuthCheck, session, signUp], |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<SolidAuthContext.Provider value={solidAuthFunctions}> |
||||||
|
<SolidLdoProvider>{children}</SolidLdoProvider> |
||||||
|
</SolidAuthContext.Provider> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,26 @@ |
|||||||
|
import type { |
||||||
|
ISessionInfo, |
||||||
|
ILoginInputOptions, |
||||||
|
} from "@inrupt/solid-client-authn-core"; |
||||||
|
import { createContext, useContext } from "react"; |
||||||
|
|
||||||
|
export type SessionInfo = ISessionInfo; |
||||||
|
export type LoginOptions = ILoginInputOptions; |
||||||
|
|
||||||
|
export interface SolidAuthFunctions { |
||||||
|
login: (issuer: string, loginOptions?: LoginOptions) => Promise<void>; |
||||||
|
logout: () => Promise<void>; |
||||||
|
signUp: (issuer: string, loginOptions?: LoginOptions) => Promise<void>; |
||||||
|
fetch: typeof fetch; |
||||||
|
session: SessionInfo; |
||||||
|
ranInitialAuthCheck: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
// There is no initial value for this context. It will be given in the provider
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
export const SolidAuthContext = createContext<SolidAuthFunctions>(undefined); |
||||||
|
|
||||||
|
export function useSolidAuth(): SolidAuthFunctions { |
||||||
|
return useContext(SolidAuthContext); |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import React, { createContext, useContext } from "react"; |
||||||
|
import { |
||||||
|
useMemo, |
||||||
|
type FunctionComponent, |
||||||
|
type PropsWithChildren, |
||||||
|
useEffect, |
||||||
|
} from "react"; |
||||||
|
import { useSolidAuth } from "./SolidAuthContext"; |
||||||
|
import type { SolidLdoDataset } from "@ldo/solid"; |
||||||
|
import { createSolidLdoDataset } from "@ldo/solid"; |
||||||
|
import type { UseLdoMethods } from "./useLdoMethods"; |
||||||
|
import { createUseLdoMethods } from "./useLdoMethods"; |
||||||
|
|
||||||
|
export const SolidLdoReactContext = |
||||||
|
// This will be set in the provider
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
createContext<UseLdoMethods>(undefined); |
||||||
|
|
||||||
|
export function useLdo(): UseLdoMethods { |
||||||
|
return useContext(SolidLdoReactContext); |
||||||
|
} |
||||||
|
|
||||||
|
export interface SolidLdoProviderProps extends PropsWithChildren {} |
||||||
|
|
||||||
|
export const SolidLdoProvider: FunctionComponent<SolidLdoProviderProps> = ({ |
||||||
|
children, |
||||||
|
}) => { |
||||||
|
const { fetch } = useSolidAuth(); |
||||||
|
|
||||||
|
// Initialize storeDependencies before render
|
||||||
|
const solidLdoDataset: SolidLdoDataset = useMemo(() => { |
||||||
|
const ldoDataset = createSolidLdoDataset({ |
||||||
|
fetch, |
||||||
|
}); |
||||||
|
ldoDataset.setMaxListeners(1000); |
||||||
|
return ldoDataset; |
||||||
|
}, []); |
||||||
|
|
||||||
|
// Keep context in sync with props
|
||||||
|
useEffect(() => { |
||||||
|
solidLdoDataset.context.fetch = fetch; |
||||||
|
}, [fetch]); |
||||||
|
|
||||||
|
const value: UseLdoMethods = useMemo( |
||||||
|
() => createUseLdoMethods(solidLdoDataset), |
||||||
|
[solidLdoDataset], |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<SolidLdoReactContext.Provider value={value}> |
||||||
|
{children} |
||||||
|
</SolidLdoReactContext.Provider> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,62 @@ |
|||||||
|
/* istanbul ignore file */ |
||||||
|
import React, { useCallback, useMemo } from "react"; |
||||||
|
import type { FunctionComponent, PropsWithChildren } from "react"; |
||||||
|
import type { LoginOptions, SessionInfo } from "./SolidAuthContext"; |
||||||
|
import { SolidAuthContext } from "./SolidAuthContext"; |
||||||
|
import libraryFetch from "cross-fetch"; |
||||||
|
import { SolidLdoProvider } from "./SolidLdoProvider"; |
||||||
|
|
||||||
|
const DUMMY_SESSION: SessionInfo = { |
||||||
|
isLoggedIn: false, |
||||||
|
webId: undefined, |
||||||
|
clientAppId: undefined, |
||||||
|
sessionId: "no_session", |
||||||
|
expirationDate: undefined, |
||||||
|
}; |
||||||
|
|
||||||
|
export const UnauthenticatedSolidLdoProvider: FunctionComponent< |
||||||
|
PropsWithChildren |
||||||
|
> = ({ children }) => { |
||||||
|
const login = useCallback( |
||||||
|
async (_issuer: string, _options?: LoginOptions) => { |
||||||
|
throw new Error( |
||||||
|
"login is not available for a UnauthenticatedSolidLdoProvider", |
||||||
|
); |
||||||
|
}, |
||||||
|
[], |
||||||
|
); |
||||||
|
|
||||||
|
const logout = useCallback(async () => { |
||||||
|
throw new Error( |
||||||
|
"logout is not available for a UnauthenticatedSolidLdoProvider", |
||||||
|
); |
||||||
|
}, []); |
||||||
|
|
||||||
|
const signUp = useCallback( |
||||||
|
async (_issuer: string, _options?: LoginOptions) => { |
||||||
|
throw new Error( |
||||||
|
"signUp is not available for a UnauthenticatedSolidLdoProvider", |
||||||
|
); |
||||||
|
}, |
||||||
|
[], |
||||||
|
); |
||||||
|
|
||||||
|
const solidAuthFunctions = useMemo( |
||||||
|
() => ({ |
||||||
|
runInitialAuthCheck: () => {}, |
||||||
|
login, |
||||||
|
logout, |
||||||
|
signUp, |
||||||
|
session: DUMMY_SESSION, |
||||||
|
ranInitialAuthCheck: true, |
||||||
|
fetch: libraryFetch, |
||||||
|
}), |
||||||
|
[login, logout, signUp], |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<SolidAuthContext.Provider value={solidAuthFunctions}> |
||||||
|
<SolidLdoProvider>{children}</SolidLdoProvider> |
||||||
|
</SolidAuthContext.Provider> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,12 @@ |
|||||||
|
export * from "./BrowserSolidLdoProvider"; |
||||||
|
export * from "./UnauthenticatedSolidLdoProvider"; |
||||||
|
export * from "./SolidAuthContext"; |
||||||
|
|
||||||
|
export { useLdo } from "./SolidLdoProvider"; |
||||||
|
|
||||||
|
// hooks
|
||||||
|
export * from "./useResource"; |
||||||
|
export * from "./useSubject"; |
||||||
|
export * from "./useMatchSubject"; |
||||||
|
export * from "./useMatchObject"; |
||||||
|
export * from "./useRootContainer"; |
@ -0,0 +1,84 @@ |
|||||||
|
import type { LdoBase, ShapeType } from "@ldo/ldo"; |
||||||
|
import type { SubjectNode } from "@ldo/rdf-utils"; |
||||||
|
import type { |
||||||
|
Resource, |
||||||
|
SolidLdoDataset, |
||||||
|
SolidLdoTransactionDataset, |
||||||
|
} from "@ldo/solid"; |
||||||
|
import { changeData, commitData } from "@ldo/solid"; |
||||||
|
|
||||||
|
export interface UseLdoMethods { |
||||||
|
dataset: SolidLdoDataset; |
||||||
|
getResource: SolidLdoDataset["getResource"]; |
||||||
|
getSubject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject: string | SubjectNode, |
||||||
|
): Type; |
||||||
|
createData<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject: string | SubjectNode, |
||||||
|
resource: Resource, |
||||||
|
...additionalResources: Resource[] |
||||||
|
): Type; |
||||||
|
changeData<Type extends LdoBase>( |
||||||
|
input: Type, |
||||||
|
resource: Resource, |
||||||
|
...additionalResources: Resource[] |
||||||
|
): Type; |
||||||
|
commitData( |
||||||
|
input: LdoBase, |
||||||
|
): ReturnType<SolidLdoTransactionDataset["commitToPod"]>; |
||||||
|
} |
||||||
|
|
||||||
|
export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { |
||||||
|
return { |
||||||
|
dataset: dataset, |
||||||
|
/** |
||||||
|
* Gets a resource |
||||||
|
*/ |
||||||
|
getResource: dataset.getResource.bind(dataset), |
||||||
|
/** |
||||||
|
* Returns a Linked Data Object for a subject |
||||||
|
* @param shapeType The shape type for the data |
||||||
|
* @param subject Subject Node |
||||||
|
* @returns A Linked Data Object |
||||||
|
*/ |
||||||
|
getSubject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject: string | SubjectNode, |
||||||
|
): Type { |
||||||
|
return dataset.usingType(shapeType).fromSubject(subject); |
||||||
|
}, |
||||||
|
/** |
||||||
|
* Begins tracking changes to eventually commit for a new subject |
||||||
|
* @param shapeType The shape type that defines the created data |
||||||
|
* @param subject The RDF subject for a Linked Data Object |
||||||
|
* @param resources Any number of resources to which this data should be written |
||||||
|
* @returns A Linked Data Object to modify and commit |
||||||
|
*/ |
||||||
|
createData<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject: string | SubjectNode, |
||||||
|
resource: Resource, |
||||||
|
...additionalResources: Resource[] |
||||||
|
): Type { |
||||||
|
return dataset.createData( |
||||||
|
shapeType, |
||||||
|
subject, |
||||||
|
resource, |
||||||
|
...additionalResources, |
||||||
|
); |
||||||
|
}, |
||||||
|
/** |
||||||
|
* Begins tracking changes to eventually commit |
||||||
|
* @param input A linked data object to track changes on |
||||||
|
* @param resources |
||||||
|
*/ |
||||||
|
changeData: changeData, |
||||||
|
/** |
||||||
|
* Commits the transaction to the global dataset, syncing all subscribing |
||||||
|
* components and Solid Pods |
||||||
|
*/ |
||||||
|
commitData: commitData, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo"; |
||||||
|
import type { QuadMatch } from "@ldo/rdf-utils"; |
||||||
|
import type { LdoBuilder } from "@ldo/ldo"; |
||||||
|
import { useCallback } from "react"; |
||||||
|
import { useTrackingProxy } from "./util/useTrackingProxy"; |
||||||
|
|
||||||
|
export function useMatchObject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject?: QuadMatch[0] | string, |
||||||
|
predicate?: QuadMatch[1] | string, |
||||||
|
graph?: QuadMatch[3] | string, |
||||||
|
): LdSet<Type> { |
||||||
|
const matchObject = useCallback( |
||||||
|
(builder: LdoBuilder<Type>) => { |
||||||
|
return builder.matchObject(subject, predicate, graph); |
||||||
|
}, |
||||||
|
[subject, predicate, graph], |
||||||
|
); |
||||||
|
|
||||||
|
return useTrackingProxy(shapeType, matchObject); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo"; |
||||||
|
import type { QuadMatch } from "@ldo/rdf-utils"; |
||||||
|
import type { LdoBuilder } from "@ldo/ldo"; |
||||||
|
import { useCallback } from "react"; |
||||||
|
import { useTrackingProxy } from "./util/useTrackingProxy"; |
||||||
|
|
||||||
|
export function useMatchSubject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
predicate?: QuadMatch[1] | string, |
||||||
|
object?: QuadMatch[2] | string, |
||||||
|
graph?: QuadMatch[3] | string, |
||||||
|
): LdSet<Type> { |
||||||
|
const matchSubject = useCallback( |
||||||
|
(builder: LdoBuilder<Type>) => { |
||||||
|
return builder.matchSubject(predicate, object, graph); |
||||||
|
}, |
||||||
|
[predicate, object, graph], |
||||||
|
); |
||||||
|
|
||||||
|
return useTrackingProxy(shapeType, matchSubject); |
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
import { useMemo, useEffect, useRef, useState, useCallback } from "react"; |
||||||
|
import type { |
||||||
|
Container, |
||||||
|
ContainerUri, |
||||||
|
LeafUri, |
||||||
|
Resource, |
||||||
|
Leaf, |
||||||
|
} from "@ldo/solid"; |
||||||
|
import { useLdo } from "./SolidLdoProvider"; |
||||||
|
|
||||||
|
export interface UseResourceOptions { |
||||||
|
suppressInitialRead?: boolean; |
||||||
|
reloadOnMount?: boolean; |
||||||
|
subscribe?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export function useResource( |
||||||
|
uri: ContainerUri, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Container; |
||||||
|
export function useResource(uri: LeafUri, options?: UseResourceOptions): Leaf; |
||||||
|
export function useResource( |
||||||
|
uri: string, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Leaf | Container; |
||||||
|
export function useResource( |
||||||
|
uri?: ContainerUri, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Container | undefined; |
||||||
|
export function useResource( |
||||||
|
uri?: LeafUri, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Leaf | undefined; |
||||||
|
export function useResource( |
||||||
|
uri?: string, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Leaf | Container | undefined; |
||||||
|
export function useResource( |
||||||
|
uri?: string, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Leaf | Container | undefined { |
||||||
|
const { getResource } = useLdo(); |
||||||
|
const subscriptionIdRef = useRef<string | undefined>(); |
||||||
|
|
||||||
|
// Get the resource
|
||||||
|
const resource = useMemo(() => { |
||||||
|
if (uri) { |
||||||
|
const resource = getResource(uri); |
||||||
|
// Run read operations if necissary
|
||||||
|
if (!options?.suppressInitialRead) { |
||||||
|
if (options?.reloadOnMount) { |
||||||
|
resource.read(); |
||||||
|
} else { |
||||||
|
resource.readIfUnfetched(); |
||||||
|
} |
||||||
|
} |
||||||
|
return resource; |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
}, [getResource, uri]); |
||||||
|
const [resourceRepresentation, setResourceRepresentation] = |
||||||
|
useState(resource); |
||||||
|
const pastResource = useRef< |
||||||
|
{ resource?: Resource; callback: () => void } | undefined |
||||||
|
>(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (options?.subscribe) { |
||||||
|
resource |
||||||
|
?.subscribeToNotifications() |
||||||
|
.then((subscriptionId) => (subscriptionIdRef.current = subscriptionId)); |
||||||
|
} else if (subscriptionIdRef.current) { |
||||||
|
resource?.unsubscribeFromNotifications(subscriptionIdRef.current); |
||||||
|
} |
||||||
|
return () => { |
||||||
|
if (subscriptionIdRef.current) |
||||||
|
resource?.unsubscribeFromNotifications(subscriptionIdRef.current); |
||||||
|
}; |
||||||
|
}, [resource, options?.subscribe]); |
||||||
|
|
||||||
|
// Callback function to force the react dom to reload.
|
||||||
|
const forceReload = useCallback( |
||||||
|
// Wrap the resource in a proxy so it's techically a different object
|
||||||
|
() => { |
||||||
|
if (resource) setResourceRepresentation(new Proxy(resource, {})); |
||||||
|
}, |
||||||
|
[resource], |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
// Remove listeners for the previous resource
|
||||||
|
if (pastResource.current?.resource) { |
||||||
|
pastResource.current.resource.off( |
||||||
|
"update", |
||||||
|
pastResource.current.callback, |
||||||
|
); |
||||||
|
} |
||||||
|
// Set a new past resource to the current resource
|
||||||
|
pastResource.current = { resource, callback: forceReload }; |
||||||
|
if (resource) { |
||||||
|
// Add listener
|
||||||
|
resource.on("update", forceReload); |
||||||
|
setResourceRepresentation(new Proxy(resource, {})); |
||||||
|
|
||||||
|
// Unsubscribe on unmount
|
||||||
|
return () => { |
||||||
|
resource.off("update", forceReload); |
||||||
|
}; |
||||||
|
} else { |
||||||
|
setResourceRepresentation(undefined); |
||||||
|
} |
||||||
|
}, [resource]); |
||||||
|
return resourceRepresentation; |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
import type { Container, ContainerUri } from "@ldo/solid"; |
||||||
|
import { useEffect, useState } from "react"; |
||||||
|
import type { UseResourceOptions } from "./useResource"; |
||||||
|
import { useResource } from "./useResource"; |
||||||
|
import { useLdo } from "./SolidLdoProvider"; |
||||||
|
|
||||||
|
export function useRootContainerFor( |
||||||
|
uri?: string, |
||||||
|
options?: UseResourceOptions, |
||||||
|
): Container | undefined { |
||||||
|
const { getResource } = useLdo(); |
||||||
|
|
||||||
|
const [rootContainerUri, setRootContainerUri] = useState< |
||||||
|
ContainerUri | undefined |
||||||
|
>(undefined); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (uri) { |
||||||
|
const givenResource = getResource(uri); |
||||||
|
givenResource.getRootContainer().then((result) => { |
||||||
|
if (!result.isError) { |
||||||
|
setRootContainerUri(result.uri); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
setRootContainerUri(undefined); |
||||||
|
} |
||||||
|
}, [uri]); |
||||||
|
|
||||||
|
return useResource(rootContainerUri, options); |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import type { SubjectNode } from "@ldo/rdf-utils"; |
||||||
|
import type { ShapeType } from "@ldo/ldo"; |
||||||
|
import type { LdoBuilder } from "@ldo/ldo"; |
||||||
|
import type { LdoBase } from "@ldo/ldo"; |
||||||
|
import { useCallback } from "react"; |
||||||
|
|
||||||
|
import { useTrackingProxy } from "./util/useTrackingProxy"; |
||||||
|
|
||||||
|
export function useSubject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject: string | SubjectNode, |
||||||
|
): Type; |
||||||
|
export function useSubject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject?: string | SubjectNode, |
||||||
|
): Type | undefined; |
||||||
|
export function useSubject<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject?: string | SubjectNode, |
||||||
|
): Type | undefined { |
||||||
|
const fromSubject = useCallback( |
||||||
|
(builder: LdoBuilder<Type>) => { |
||||||
|
if (!subject) return; |
||||||
|
return builder.fromSubject(subject); |
||||||
|
}, |
||||||
|
[subject], |
||||||
|
); |
||||||
|
|
||||||
|
return useTrackingProxy(shapeType, fromSubject); |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
import { useLdo } from "./SolidLdoProvider"; |
||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
|
||||||
|
export function useSubscribeToResource(...uris: string[]): void { |
||||||
|
const { dataset } = useLdo(); |
||||||
|
const currentlySubscribed = useRef<Record<string, string>>({}); |
||||||
|
useEffect(() => { |
||||||
|
const resources = uris.map((uri) => dataset.getResource(uri)); |
||||||
|
const previousSubscriptions = { ...currentlySubscribed.current }; |
||||||
|
Promise.all<void>( |
||||||
|
resources.map(async (resource) => { |
||||||
|
if (!previousSubscriptions[resource.uri]) { |
||||||
|
// Prevent multiple triggers from created subscriptions while waiting
|
||||||
|
// for connection
|
||||||
|
currentlySubscribed.current[resource.uri] = "AWAITING"; |
||||||
|
// Read and subscribe
|
||||||
|
await resource.readIfUnfetched(); |
||||||
|
currentlySubscribed.current[resource.uri] = |
||||||
|
await resource.subscribeToNotifications(); |
||||||
|
} else { |
||||||
|
delete previousSubscriptions[resource.uri]; |
||||||
|
} |
||||||
|
}), |
||||||
|
).then(async () => { |
||||||
|
// Unsubscribe from all remaining previous subscriptions
|
||||||
|
await Promise.all( |
||||||
|
Object.entries(previousSubscriptions).map( |
||||||
|
async ([resourceUri, subscriptionId]) => { |
||||||
|
// Unsubscribe
|
||||||
|
delete currentlySubscribed.current[resourceUri]; |
||||||
|
const resource = dataset.getResource(resourceUri); |
||||||
|
await resource.unsubscribeFromNotifications(subscriptionId); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
}); |
||||||
|
}, [uris]); |
||||||
|
|
||||||
|
// Cleanup Subscriptions
|
||||||
|
useEffect(() => { |
||||||
|
return () => { |
||||||
|
Promise.all( |
||||||
|
Object.entries(currentlySubscribed.current).map( |
||||||
|
async ([resourceUri, subscriptionId]) => { |
||||||
|
const resource = dataset.getResource(resourceUri); |
||||||
|
await resource.unsubscribeFromNotifications(subscriptionId); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
import type { |
||||||
|
ProxyContextOptions, |
||||||
|
SubjectProxy, |
||||||
|
SetProxy, |
||||||
|
} from "@ldo/jsonld-dataset-proxy"; |
||||||
|
import { ProxyContext } from "@ldo/jsonld-dataset-proxy"; |
||||||
|
import type { QuadMatch } from "@ldo/rdf-utils"; |
||||||
|
import type { SubscribableDataset } from "@ldo/subscribable-dataset"; |
||||||
|
import type { BlankNode, NamedNode, Quad } from "@rdfjs/types"; |
||||||
|
import { createTrackingSubjectProxy } from "./TrackingSubjectProxy"; |
||||||
|
import { createTrackingSetProxy } from "./TrackingSetProxy"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* Options to be passed to the tracking proxy |
||||||
|
*/ |
||||||
|
export interface TrackingProxyContextOptions extends ProxyContextOptions { |
||||||
|
dataset: SubscribableDataset<Quad>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* This proxy exists to ensure react components rerender at the right time. It |
||||||
|
* keeps track of every key accessed in a Linked Data Object and only when the |
||||||
|
* dataset is updated with that key does it rerender the react component. |
||||||
|
*/ |
||||||
|
export class TrackingProxyContext extends ProxyContext { |
||||||
|
private listener: () => void; |
||||||
|
private subscribableDataset: SubscribableDataset<Quad>; |
||||||
|
|
||||||
|
constructor(options: TrackingProxyContextOptions, listener: () => void) { |
||||||
|
super(options); |
||||||
|
this.subscribableDataset = options.dataset; |
||||||
|
this.listener = listener; |
||||||
|
} |
||||||
|
|
||||||
|
// Adds the listener to the subscribable dataset while ensuring deduping of the listener
|
||||||
|
public addListener(eventName: QuadMatch) { |
||||||
|
const listeners = this.subscribableDataset.listeners(eventName); |
||||||
|
if (!listeners.includes(this.listener)) { |
||||||
|
this.subscribableDataset.on(eventName, this.listener); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected createNewSubjectProxy(node: NamedNode | BlankNode): SubjectProxy { |
||||||
|
return createTrackingSubjectProxy(this, node); |
||||||
|
} |
||||||
|
|
||||||
|
protected createNewSetProxy( |
||||||
|
quadMatch: QuadMatch, |
||||||
|
isSubjectOriented?: boolean, |
||||||
|
isLangStringSet?: boolean, |
||||||
|
): SetProxy { |
||||||
|
return createTrackingSetProxy( |
||||||
|
this, |
||||||
|
quadMatch, |
||||||
|
isSubjectOriented, |
||||||
|
isLangStringSet, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
import { createNewSetProxy, type SetProxy } from "@ldo/jsonld-dataset-proxy"; |
||||||
|
import type { TrackingProxyContext } from "./TrackingProxyContext"; |
||||||
|
import type { QuadMatch } from "@ldo/rdf-utils"; |
||||||
|
|
||||||
|
export function createTrackingSetProxy( |
||||||
|
proxyContext: TrackingProxyContext, |
||||||
|
quadMatch: QuadMatch, |
||||||
|
isSubjectOriented?: boolean, |
||||||
|
isLangStringSet?: boolean, |
||||||
|
): SetProxy { |
||||||
|
const baseSetProxy = createNewSetProxy( |
||||||
|
quadMatch, |
||||||
|
isSubjectOriented ?? false, |
||||||
|
proxyContext, |
||||||
|
isLangStringSet, |
||||||
|
); |
||||||
|
|
||||||
|
return new Proxy(baseSetProxy, { |
||||||
|
get: (target: SetProxy, key: string | symbol, receiver) => { |
||||||
|
if (trackingMethods.has(key)) { |
||||||
|
proxyContext.addListener(quadMatch); |
||||||
|
} else if (disallowedMethods.has(key)) { |
||||||
|
console.warn( |
||||||
|
"You've attempted to modify 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.", |
||||||
|
); |
||||||
|
} |
||||||
|
return Reflect.get(target, key, receiver); |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
const trackingMethods = new Set([ |
||||||
|
"has", |
||||||
|
"size", |
||||||
|
"entries", |
||||||
|
"keys", |
||||||
|
"values", |
||||||
|
Symbol.iterator, |
||||||
|
"every", |
||||||
|
"every", |
||||||
|
"some", |
||||||
|
"forEach", |
||||||
|
"map", |
||||||
|
"reduce", |
||||||
|
"toArray", |
||||||
|
"toJSON", |
||||||
|
"difference", |
||||||
|
"intersection", |
||||||
|
"isDisjointFrom", |
||||||
|
"isSubsetOf", |
||||||
|
"isSupersetOf", |
||||||
|
"symmetricDifference", |
||||||
|
"union", |
||||||
|
]); |
||||||
|
|
||||||
|
const disallowedMethods = new Set<string | symbol>(["add", "clear", "delete"]); |
@ -0,0 +1,43 @@ |
|||||||
|
import type { SubjectProxyTarget } from "@ldo/jsonld-dataset-proxy"; |
||||||
|
import { |
||||||
|
createSubjectHandler, |
||||||
|
type SubjectProxy, |
||||||
|
} from "@ldo/jsonld-dataset-proxy"; |
||||||
|
import type { BlankNode, NamedNode } from "@rdfjs/types"; |
||||||
|
import type { TrackingProxyContext } from "./TrackingProxyContext"; |
||||||
|
import { namedNode } from "@rdfjs/data-model"; |
||||||
|
|
||||||
|
export function createTrackingSubjectProxy( |
||||||
|
proxyContext: TrackingProxyContext, |
||||||
|
node: NamedNode | BlankNode, |
||||||
|
): SubjectProxy { |
||||||
|
const baseHandler = createSubjectHandler(proxyContext); |
||||||
|
const oldGetFunction = baseHandler.get; |
||||||
|
const newGetFunction: ProxyHandler<SubjectProxyTarget>["get"] = ( |
||||||
|
target: SubjectProxyTarget, |
||||||
|
key: string | symbol, |
||||||
|
receiver, |
||||||
|
) => { |
||||||
|
const subject = target["@id"]; |
||||||
|
const rdfTypes = proxyContext.getRdfType(subject); |
||||||
|
if (typeof key === "symbol") { |
||||||
|
// Do Nothing
|
||||||
|
} else if (key === "@id") { |
||||||
|
proxyContext.addListener([subject, null, null, null]); |
||||||
|
} else if (!proxyContext.contextUtil.isSet(key, rdfTypes)) { |
||||||
|
const predicate = namedNode( |
||||||
|
proxyContext.contextUtil.keyToIri(key, rdfTypes), |
||||||
|
); |
||||||
|
proxyContext.addListener([subject, predicate, null, null]); |
||||||
|
} |
||||||
|
return oldGetFunction && oldGetFunction(target, key, receiver); |
||||||
|
}; |
||||||
|
baseHandler.get = newGetFunction; |
||||||
|
baseHandler.set = () => { |
||||||
|
console.warn( |
||||||
|
"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.", |
||||||
|
); |
||||||
|
return true; |
||||||
|
}; |
||||||
|
return new Proxy({ "@id": node }, baseHandler) as unknown as SubjectProxy; |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import { |
||||||
|
ContextUtil, |
||||||
|
JsonldDatasetProxyBuilder, |
||||||
|
} from "@ldo/jsonld-dataset-proxy"; |
||||||
|
import { LdoBuilder } from "@ldo/ldo"; |
||||||
|
import type { LdoBase, ShapeType } from "@ldo/ldo"; |
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react"; |
||||||
|
import { TrackingProxyContext } from "./TrackingProxyContext"; |
||||||
|
import { defaultGraph } from "@rdfjs/data-model"; |
||||||
|
import { useLdo } from "../SolidLdoProvider"; |
||||||
|
|
||||||
|
export function useTrackingProxy<Type extends LdoBase, ReturnType>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
createLdo: (builder: LdoBuilder<Type>) => ReturnType, |
||||||
|
): ReturnType { |
||||||
|
const { dataset } = useLdo(); |
||||||
|
|
||||||
|
const [forceUpdateCounter, setForceUpdateCounter] = useState(0); |
||||||
|
const forceUpdate = useCallback( |
||||||
|
() => setForceUpdateCounter((val) => val + 1), |
||||||
|
[], |
||||||
|
); |
||||||
|
|
||||||
|
// The main linked data object
|
||||||
|
const linkedDataObject = useMemo(() => { |
||||||
|
// Remove all current subscriptions
|
||||||
|
dataset.removeListenerFromAllEvents(forceUpdate); |
||||||
|
|
||||||
|
// Rebuild the LdoBuilder from scratch to inject TrackingProxyContext
|
||||||
|
const contextUtil = new ContextUtil(shapeType.context); |
||||||
|
const proxyContext = new TrackingProxyContext( |
||||||
|
{ |
||||||
|
dataset, |
||||||
|
contextUtil, |
||||||
|
writeGraphs: [defaultGraph()], |
||||||
|
languageOrdering: ["none", "en", "other"], |
||||||
|
}, |
||||||
|
forceUpdate, |
||||||
|
); |
||||||
|
const builder = new LdoBuilder( |
||||||
|
new JsonldDatasetProxyBuilder(proxyContext), |
||||||
|
shapeType, |
||||||
|
); |
||||||
|
return createLdo(builder); |
||||||
|
}, [shapeType, dataset, forceUpdateCounter, forceUpdate, createLdo]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
// Unregister force update listener upon unmount
|
||||||
|
return () => { |
||||||
|
dataset.removeListenerFromAllEvents(forceUpdate); |
||||||
|
}; |
||||||
|
}, [shapeType]); |
||||||
|
|
||||||
|
return linkedDataObject; |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
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", |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,155 @@ |
|||||||
|
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.", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
@ -0,0 +1,19 @@ |
|||||||
|
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<PostSh> = { |
||||||
|
schema: postSchema, |
||||||
|
shape: "https://example.com/PostSh", |
||||||
|
context: postContext, |
||||||
|
}; |
@ -0,0 +1,45 @@ |
|||||||
|
import { LdSet, LdoJsonldContext } from "@ldo/ldo"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* Typescript Typings for post |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* PostSh Type |
||||||
|
*/ |
||||||
|
export interface PostSh { |
||||||
|
"@id"?: string; |
||||||
|
"@context"?: LdoJsonldContext; |
||||||
|
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: LdSet<{ |
||||||
|
"@id": string; |
||||||
|
}>; |
||||||
|
} |
@ -0,0 +1,645 @@ |
|||||||
|
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"; |
||||||
|
import { useMatchSubject } from "../src/useMatchSubject"; |
||||||
|
import { useMatchObject } from "../src/useMatchObject"; |
||||||
|
import { useSubscribeToResource } from "../src/useSubscribeToResource"; |
||||||
|
|
||||||
|
// 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 <p>Loading</p>; |
||||||
|
return <p role="status">{resource.status.type}</p>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseResourceTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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<string | undefined>(undefined); |
||||||
|
const resource = useResource(uri, { suppressInitialRead: true }); |
||||||
|
if (!resource) |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<p>Undefined</p> |
||||||
|
<button onClick={() => setUri(SAMPLE_DATA_URI)}>Next</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
return <p role="status">{resource.status.type}</p>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseResourceUndefinedTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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 <p>Loading</p>; |
||||||
|
return <p role="status">{resource.status.type}</p>; |
||||||
|
}; |
||||||
|
const ReloadParent: FunctionComponent = () => { |
||||||
|
const [showComponent, setShowComponent] = useState(true); |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<button onClick={() => setShowComponent(!showComponent)}> |
||||||
|
Show Component |
||||||
|
</button> |
||||||
|
{showComponent ? <ReloadTest /> : <p>Hidden</p>} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<ReloadParent /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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 <p>Loading</p>; |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<p role="status">{resource.status.type}</p> |
||||||
|
<button onClick={() => setUri(SAMPLE_BINARY_URI)}> |
||||||
|
Update URI |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<SwapResourceTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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 ? <p role="root">{rootContainer?.uri}</p> : <></>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<RootContainerTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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 ? ( |
||||||
|
<p role="root">{rootContainer?.uri}</p> |
||||||
|
) : ( |
||||||
|
<p role="undefined">Undefined</p> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<RootContainerTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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<PostSh | undefined>(); |
||||||
|
const { getSubject } = useLdo(); |
||||||
|
useEffect(() => { |
||||||
|
const someSubject = getSubject( |
||||||
|
PostShShapeType, |
||||||
|
"https://example.com/subject", |
||||||
|
); |
||||||
|
setSubject(someSubject); |
||||||
|
}, []); |
||||||
|
return subject ? <p role="subject">{subject["@id"]}</p> : <></>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<GetSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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<PostSh | undefined>(); |
||||||
|
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 ? <p role="subject">{subject.articleBody}</p> : <></>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<GetSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
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 <p role="article">{post.articleBody}</p>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
await screen.findByText("test"); |
||||||
|
}); |
||||||
|
|
||||||
|
it("renders the set 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 <p>loading</p>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<p role="single">{post.publisher.toArray()[0]["@id"]}</p> |
||||||
|
<ul role="list"> |
||||||
|
{post.publisher.map((publisher) => { |
||||||
|
return <li key={publisher["@id"]}>{publisher["@id"]}</li>; |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
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 ( |
||||||
|
<p role="article"> |
||||||
|
{post === undefined ? "Undefined" : "Not Undefined"} |
||||||
|
</p> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
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 <p>loading</p>; |
||||||
|
|
||||||
|
return <p role="value">{typeof post[Symbol.hasInstance]}</p>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
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 <p>loading</p>; |
||||||
|
|
||||||
|
return <p role="value">{post["@id"]}</p>; |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
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 <p>loading</p>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<p role="value">{post.articleBody}</p> |
||||||
|
<button |
||||||
|
onClick={() => { |
||||||
|
post.articleBody = "bad"; |
||||||
|
post.publisher.add({ "@id": "example" }); |
||||||
|
}} |
||||||
|
> |
||||||
|
Attempt Change |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
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.", |
||||||
|
); |
||||||
|
expect(warn).toHaveBeenCalledWith( |
||||||
|
"You've attempted to modify 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 [isSubscribed, setIsSubscribed] = useState(true); |
||||||
|
const resource = useResource(SAMPLE_DATA_URI, { |
||||||
|
subscribe: isSubscribed, |
||||||
|
}); |
||||||
|
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> <http://schema.org/publisher> <https://example.com/Publisher3> . }`, |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/sparql-update", |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, []); |
||||||
|
|
||||||
|
if (resource.isLoading() || !post) return <p>loading</p>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<p role="resource"> |
||||||
|
{resource.isSubscribedToNotifications().toString()} |
||||||
|
</p> |
||||||
|
<ul role="list"> |
||||||
|
{post.publisher.map((publisher) => { |
||||||
|
return <li key={publisher["@id"]}>{publisher["@id"]}</li>; |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
<button onClick={addPublisher}>Add Publisher</button> |
||||||
|
<button onClick={() => setIsSubscribed(false)}>Unsubscribe</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
const { unmount } = render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<NotificationTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
// Wait for subscription to connect
|
||||||
|
await act(async () => { |
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000)); |
||||||
|
}); |
||||||
|
|
||||||
|
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"); |
||||||
|
const resourceP = await screen.findByRole("resource"); |
||||||
|
expect(resourceP.innerHTML).toBe("true"); |
||||||
|
|
||||||
|
// 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", |
||||||
|
); |
||||||
|
|
||||||
|
await fireEvent.click(screen.getByText("Unsubscribe")); |
||||||
|
const resourcePUpdated = await screen.findByRole("resource"); |
||||||
|
expect(resourcePUpdated.innerHTML).toBe("false"); |
||||||
|
|
||||||
|
unmount(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* useMatchSubject |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
describe("useMatchSubject", () => { |
||||||
|
it("returns an array of matched subjects", async () => { |
||||||
|
const UseMatchSubjectTest: FunctionComponent = () => { |
||||||
|
const resource = useResource(SAMPLE_DATA_URI); |
||||||
|
const posts = useMatchSubject( |
||||||
|
PostShShapeType, |
||||||
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||||
|
"http://schema.org/CreativeWork", |
||||||
|
); |
||||||
|
if (resource.isLoading()) return <p>loading</p>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<ul role="list"> |
||||||
|
{posts.map((post) => { |
||||||
|
return <li key={post["@id"]}>{post["@id"]}</li>; |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseMatchSubjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
const list = await screen.findByRole("list"); |
||||||
|
expect(list.children[0].innerHTML).toBe( |
||||||
|
"http://localhost:3002/example/test_ldo/sample.ttl#Post1", |
||||||
|
); |
||||||
|
expect(list.children[1].innerHTML).toBe( |
||||||
|
"http://localhost:3002/example/test_ldo/sample.ttl#Post2", |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* useMatchObject |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
describe("useMatchObject", () => { |
||||||
|
it("returns an array of matched objects", async () => { |
||||||
|
const UseMatchObjectTest: FunctionComponent = () => { |
||||||
|
const resource = useResource(SAMPLE_DATA_URI); |
||||||
|
const publishers = useMatchObject( |
||||||
|
PostShShapeType, |
||||||
|
"http://localhost:3002/example/test_ldo/sample.ttl#Post1", |
||||||
|
"http://schema.org/publisher", |
||||||
|
); |
||||||
|
if (resource.isLoading()) return <p>loading</p>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<ul role="list"> |
||||||
|
{publishers.map((publisher) => { |
||||||
|
return <li key={publisher["@id"]}>{publisher["@id"]}</li>; |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<UseMatchObjectTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
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"); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
/** |
||||||
|
* =========================================================================== |
||||||
|
* useSubscribeToResource |
||||||
|
* =========================================================================== |
||||||
|
*/ |
||||||
|
describe("useSubscribeToResource", () => { |
||||||
|
it("handles useSubscribeToResource", async () => { |
||||||
|
const NotificationTest: FunctionComponent = () => { |
||||||
|
const [subscribedUris, setSubScribedUris] = useState<string[]>([ |
||||||
|
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`); |
||||||
|
|
||||||
|
const addPublisher = useCallback(async () => { |
||||||
|
await fetch(SAMPLE_DATA_URI, { |
||||||
|
method: "PATCH", |
||||||
|
body: `INSERT DATA { <${SAMPLE_DATA_URI}#Post1> <http://schema.org/publisher> <https://example.com/Publisher3> . }`, |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/sparql-update", |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, []); |
||||||
|
|
||||||
|
if (resource1.isLoading() || resource2.isLoading()) |
||||||
|
return <p>Loading</p>; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<p role="resource1"> |
||||||
|
{resource1.isSubscribedToNotifications().toString()} |
||||||
|
</p> |
||||||
|
<p role="resource2"> |
||||||
|
{resource2.isSubscribedToNotifications().toString()} |
||||||
|
</p> |
||||||
|
<ul role="list"> |
||||||
|
{post.publisher.map((publisher) => { |
||||||
|
return <li key={publisher["@id"]}>{publisher["@id"]}</li>; |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
<button onClick={addPublisher}>Add Publisher</button> |
||||||
|
<button |
||||||
|
onClick={() => |
||||||
|
setSubScribedUris([SAMPLE_DATA_URI, SAMPLE_BINARY_URI]) |
||||||
|
} |
||||||
|
> |
||||||
|
Subscribe More |
||||||
|
</button> |
||||||
|
<button onClick={() => setSubScribedUris([SAMPLE_BINARY_URI])}> |
||||||
|
Subscribe Less |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
const { unmount } = render( |
||||||
|
<UnauthenticatedSolidLdoProvider> |
||||||
|
<NotificationTest /> |
||||||
|
</UnauthenticatedSolidLdoProvider>, |
||||||
|
); |
||||||
|
|
||||||
|
const preResource1P = await screen.findByRole("resource1"); |
||||||
|
expect(preResource1P.innerHTML).toBe("false"); |
||||||
|
|
||||||
|
// Wait for subscription to connect
|
||||||
|
await act(async () => { |
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000)); |
||||||
|
}); |
||||||
|
|
||||||
|
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"); |
||||||
|
const resource1P = await screen.findByRole("resource1"); |
||||||
|
expect(resource1P.innerHTML).toBe("true"); |
||||||
|
const resource2P = await screen.findByRole("resource2"); |
||||||
|
expect(resource2P.innerHTML).toBe("false"); |
||||||
|
|
||||||
|
// 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", |
||||||
|
); |
||||||
|
|
||||||
|
await fireEvent.click(screen.getByText("Subscribe More")); |
||||||
|
// Wait for subscription to connect
|
||||||
|
await act(async () => { |
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000)); |
||||||
|
}); |
||||||
|
|
||||||
|
const resource1PUpdated = await screen.findByRole("resource1"); |
||||||
|
expect(resource1PUpdated.innerHTML).toBe("true"); |
||||||
|
const resource2PUpdated = await screen.findByRole("resource2"); |
||||||
|
expect(resource2PUpdated.innerHTML).toBe("true"); |
||||||
|
|
||||||
|
await fireEvent.click(screen.getByText("Subscribe Less")); |
||||||
|
// Wait for subscription to connect
|
||||||
|
await act(async () => { |
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000)); |
||||||
|
}); |
||||||
|
|
||||||
|
const resource1PUpdatedAgain = await screen.findByRole("resource1"); |
||||||
|
expect(resource1PUpdatedAgain.innerHTML).toBe("false"); |
||||||
|
const resource2PUpdatedAgain = await screen.findByRole("resource2"); |
||||||
|
expect(resource2PUpdatedAgain.innerHTML).toBe("true"); |
||||||
|
|
||||||
|
unmount(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,113 @@ |
|||||||
|
import type { ContainerUri, LeafUri } from "@ldo/solid"; |
||||||
|
import fetch from "cross-fetch"; |
||||||
|
|
||||||
|
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3002/"; |
||||||
|
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; |
||||||
|
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: <http://schema.org/> .
|
||||||
|
|
||||||
|
<#Post1> |
||||||
|
a schema:CreativeWork, schema:Thing, schema:SocialMediaPosting ; |
||||||
|
schema:image <https://example.com/postImage.jpg> ; |
||||||
|
schema:articleBody "test" ; |
||||||
|
schema:publisher <https://example.com/Publisher1>, <https://example.com/Publisher2> . |
||||||
|
<#Post2> |
||||||
|
a schema:CreativeWork, schema:Thing, schema:SocialMediaPosting .`;
|
||||||
|
export const TEST_CONTAINER_TTL = `@prefix dc: <http://purl.org/dc/terms/>.
|
||||||
|
@prefix ldp: <http://www.w3.org/ns/ldp#>. |
||||||
|
@prefix posix: <http://www.w3.org/ns/posix/stat#>. |
||||||
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>. |
||||||
|
|
||||||
|
<> <urn:npm:solid:community-server:http:slug> "sample.txt"; |
||||||
|
a ldp:Container, ldp:BasicContainer, ldp:Resource; |
||||||
|
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. |
||||||
|
<sample.ttl> a ldp:Resource, <http://www.w3.org/ns/iana/media-types/text/turtle#Resource>; |
||||||
|
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. |
||||||
|
<sample.txt> a ldp:Resource, <http://www.w3.org/ns/iana/media-types/text/plain#Resource>; |
||||||
|
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. |
||||||
|
<> posix:mtime 1697810234; |
||||||
|
ldp:contains <sample.ttl>, <sample.txt>. |
||||||
|
<sample.ttl> posix:mtime 1697810234; |
||||||
|
posix:size 522. |
||||||
|
<sample.txt> posix:mtime 1697810234; |
||||||
|
posix:size 10.`;
|
||||||
|
|
||||||
|
export interface SetUpServerReturn { |
||||||
|
authFetch: typeof fetch; |
||||||
|
fetchMock: jest.Mock< |
||||||
|
Promise<Response>, |
||||||
|
[input: RequestInfo | URL, init?: RequestInit | undefined] |
||||||
|
>; |
||||||
|
} |
||||||
|
|
||||||
|
export function setUpServer(): SetUpServerReturn { |
||||||
|
// Ignore to build s
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const s: SetUpServerReturn = {}; |
||||||
|
|
||||||
|
beforeAll(async () => { |
||||||
|
// s.authFetch = await getAuthenticatedFetch();
|
||||||
|
s.authFetch = fetch; |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
s.fetchMock = jest.fn(s.authFetch); |
||||||
|
// Create a new document called sample.ttl
|
||||||
|
await s.authFetch(ROOT_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { |
||||||
|
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"', |
||||||
|
slug: TEST_CONTAINER_SLUG, |
||||||
|
}, |
||||||
|
}); |
||||||
|
await Promise.all([ |
||||||
|
s.authFetch(TEST_CONTAINER_URI, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "sample.ttl" }, |
||||||
|
body: EXAMPLE_POST_TTL, |
||||||
|
}), |
||||||
|
s.authFetch(TEST_CONTAINER_URI, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/plain", slug: "sample.txt" }, |
||||||
|
body: "some text.", |
||||||
|
}), |
||||||
|
]); |
||||||
|
}); |
||||||
|
|
||||||
|
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", |
||||||
|
}), |
||||||
|
]); |
||||||
|
}); |
||||||
|
|
||||||
|
return s; |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
{ |
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", |
||||||
|
"import": [ |
||||||
|
"css:config/app/init/initialize-intro.json", |
||||||
|
"css:config/app/main/default.json", |
||||||
|
"css:config/app/variables/default.json", |
||||||
|
"css:config/http/handler/default.json", |
||||||
|
"css:config/http/middleware/default.json", |
||||||
|
"css:config/http/notifications/all.json", |
||||||
|
"css:config/http/server-factory/http.json", |
||||||
|
"css:config/http/static/default.json", |
||||||
|
"css:config/identity/access/public.json", |
||||||
|
"css:config/identity/email/default.json", |
||||||
|
"css:config/identity/handler/default.json", |
||||||
|
"css:config/identity/oidc/default.json", |
||||||
|
"css:config/identity/ownership/token.json", |
||||||
|
"css:config/identity/pod/static.json", |
||||||
|
"css:config/ldp/authentication/dpop-bearer.json", |
||||||
|
"css:config/ldp/authorization/webacl.json", |
||||||
|
"css:config/ldp/handler/default.json", |
||||||
|
"css:config/ldp/metadata-parser/default.json", |
||||||
|
"css:config/ldp/metadata-writer/default.json", |
||||||
|
"css:config/ldp/modes/default.json", |
||||||
|
"css:config/storage/backend/memory.json", |
||||||
|
"css:config/storage/key-value/resource-store.json", |
||||||
|
"css:config/storage/location/root.json", |
||||||
|
"css:config/storage/middleware/default.json", |
||||||
|
"css:config/util/auxiliary/acl.json", |
||||||
|
"css:config/util/identifiers/suffix.json", |
||||||
|
"css:config/util/index/default.json", |
||||||
|
"css:config/util/logging/winston.json", |
||||||
|
"css:config/util/representation-conversion/default.json", |
||||||
|
"css:config/util/resource-locker/memory.json", |
||||||
|
"css:config/util/variables/default.json" |
||||||
|
], |
||||||
|
"@graph": [ |
||||||
|
{ |
||||||
|
"comment": "A Solid server that stores its resources in memory and uses WAC for authorization." |
||||||
|
}, |
||||||
|
{ |
||||||
|
"comment": "The location of the new pod templates folder.", |
||||||
|
"@type": "Override", |
||||||
|
"overrideInstance": { |
||||||
|
"@id": "urn:solid-server:default:PodResourcesGenerator" |
||||||
|
}, |
||||||
|
"overrideParameters": { |
||||||
|
"@type": "StaticFolderGenerator", |
||||||
|
"templateFolder": "./test/test-server/configs/template" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
[ |
||||||
|
{ |
||||||
|
"email": "hello@example.com", |
||||||
|
"password": "abc123", |
||||||
|
"pods": [ |
||||||
|
{ "name": "example" } |
||||||
|
] |
||||||
|
} |
||||||
|
] |
@ -0,0 +1,13 @@ |
|||||||
|
@prefix : <#>. |
||||||
|
@prefix acl: <http://www.w3.org/ns/auth/acl#>. |
||||||
|
@prefix foaf: <http://xmlns.com/foaf/0.1/>. |
||||||
|
@prefix eve: <./>. |
||||||
|
@prefix c: <./profile/card#>. |
||||||
|
|
||||||
|
:ControlReadWrite |
||||||
|
a acl:Authorization; |
||||||
|
acl:accessTo eve:; |
||||||
|
acl:agent c:me, <mailto:info@o.team>; |
||||||
|
acl:agentClass foaf:Agent; |
||||||
|
acl:default eve:; |
||||||
|
acl:mode acl:Control, acl:Read, acl:Write. |
@ -0,0 +1,19 @@ |
|||||||
|
# ACL resource for the WebID profile document |
||||||
|
@prefix acl: <http://www.w3.org/ns/auth/acl#>. |
||||||
|
@prefix foaf: <http://xmlns.com/foaf/0.1/>. |
||||||
|
|
||||||
|
# The WebID profile is readable by the public. |
||||||
|
# This is required for discovery and verification, |
||||||
|
# e.g. when checking identity providers. |
||||||
|
<#public> |
||||||
|
a acl:Authorization; |
||||||
|
acl:agentClass foaf:Agent; |
||||||
|
acl:accessTo <./card>; |
||||||
|
acl:mode acl:Read. |
||||||
|
|
||||||
|
# The owner has full access to the profile |
||||||
|
<#owner> |
||||||
|
a acl:Authorization; |
||||||
|
acl:agent <{{webId}}>; |
||||||
|
acl:accessTo <./card>; |
||||||
|
acl:mode acl:Read, acl:Write, acl:Control. |
@ -0,0 +1,7 @@ |
|||||||
|
import { createApp } from "./solidServer.helper"; |
||||||
|
|
||||||
|
async function run() { |
||||||
|
const app = await createApp(); |
||||||
|
await app.start(); |
||||||
|
} |
||||||
|
run(); |
@ -0,0 +1,39 @@ |
|||||||
|
// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9
|
||||||
|
|
||||||
|
import * as path from "path"; |
||||||
|
import type { App } from "@solid/community-server"; |
||||||
|
import { AppRunner, resolveModulePath } from "@solid/community-server"; |
||||||
|
|
||||||
|
export async function createApp(): Promise<App> { |
||||||
|
if (process.env.SERVER) { |
||||||
|
return { |
||||||
|
start: () => {}, |
||||||
|
stop: () => {}, |
||||||
|
} as App; |
||||||
|
} |
||||||
|
const appRunner = new AppRunner(); |
||||||
|
|
||||||
|
return appRunner.create({ |
||||||
|
loaderProperties: { |
||||||
|
mainModulePath: resolveModulePath(""), |
||||||
|
typeChecking: false, |
||||||
|
}, |
||||||
|
config: path.join( |
||||||
|
__dirname, |
||||||
|
"configs", |
||||||
|
"components-config", |
||||||
|
"unauthenticatedServer.json", |
||||||
|
), |
||||||
|
variableBindings: {}, |
||||||
|
shorthand: { |
||||||
|
port: 3_002, |
||||||
|
loggingLevel: "off", |
||||||
|
seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"), |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export interface ISecretData { |
||||||
|
id: string; |
||||||
|
secret: string; |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.base.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "./dist", |
||||||
|
"lib": ["dom"] |
||||||
|
}, |
||||||
|
"include": ["./src"] |
||||||
|
} |
Loading…
Reference in new issue