Added solid-react and fix hot reload

main
jaxoncreed 2 years ago
parent b7397f8f6d
commit 1db769103f
  1. 2
      .gitignore
  2. 5
      jest.config.js
  3. 52895
      package-lock.json
  4. 2
      package.json
  5. 28
      packages/demo-react/craco.config.js
  6. 25
      packages/demo-react/package.json
  7. 31
      packages/demo-react/src/App.tsx
  8. 34
      packages/demo-react/src/BlurTextInput.tsx
  9. 40
      packages/demo-react/src/Layout.tsx
  10. 39
      packages/demo-react/src/Profile.tsx
  11. 156
      packages/demo-react/src/ldo/solidProfile.context.ts
  12. 749
      packages/demo-react/src/ldo/solidProfile.schema.ts
  13. 71
      packages/demo-react/src/ldo/solidProfile.shapeTypes.ts
  14. 293
      packages/demo-react/src/ldo/solidProfile.typings.ts
  15. 121
      packages/demo-react/src/shapes/solidProfile.shex
  16. 16
      packages/solid-react/.babelrc
  17. 3
      packages/solid-react/.eslintrc
  18. 13
      packages/solid-react/README.md
  19. 5
      packages/solid-react/jest.config.js
  20. 50
      packages/solid-react/package.json
  21. 19
      packages/solid-react/src/LdoContext.ts
  22. 66
      packages/solid-react/src/LdoProvider.tsx
  23. 98
      packages/solid-react/src/SolidAuthProvider.ts
  24. 36
      packages/solid-react/src/document/DocumentStore.ts
  25. 107
      packages/solid-react/src/document/FetchableDocument.ts
  26. 80
      packages/solid-react/src/document/accessRules/AccessRules.ts
  27. 17
      packages/solid-react/src/document/accessRules/AccessRulesStore.ts
  28. 10
      packages/solid-react/src/document/errors/DocumentError.ts
  29. 11
      packages/solid-react/src/document/errors/DocumentFetchError.ts
  30. 91
      packages/solid-react/src/document/resource/Resource.ts
  31. 10
      packages/solid-react/src/document/resource/binaryResource/BinaryResource.ts
  32. 20
      packages/solid-react/src/document/resource/binaryResource/BinaryResourceStore.ts
  33. 127
      packages/solid-react/src/document/resource/dataResource/DataResource.ts
  34. 20
      packages/solid-react/src/document/resource/dataResource/DataResourceStore.ts
  35. 102
      packages/solid-react/src/document/resource/dataResource/containerResource/ContainerResource.ts
  36. 52
      packages/solid-react/src/document/resource/dataResource/containerResource/ContainerResourceStore.ts
  37. 17
      packages/solid-react/src/documentHooks/useAccessRules.ts
  38. 7
      packages/solid-react/src/documentHooks/useBinaryResource.ts
  39. 10
      packages/solid-react/src/documentHooks/useContainerResource.ts
  40. 7
      packages/solid-react/src/documentHooks/useDataResource.ts
  41. 45
      packages/solid-react/src/documentHooks/useDocument.ts
  42. 41
      packages/solid-react/src/index.ts
  43. 36
      packages/solid-react/src/ldo/solid.context.ts
  44. 214
      packages/solid-react/src/ldo/solid.schema.ts
  45. 28
      packages/solid-react/src/ldo/solid.shapeTypes.ts
  46. 73
      packages/solid-react/src/ldo/solid.typings.ts
  47. 98
      packages/solid-react/src/ldoHooks/helpers/TrackingProxyContext.ts
  48. 83
      packages/solid-react/src/ldoHooks/helpers/UpdateManager.ts
  49. 0
      packages/solid-react/src/ldoHooks/useMatchingObjects.ts
  50. 0
      packages/solid-react/src/ldoHooks/useMatchingSubjects.ts
  51. 59
      packages/solid-react/src/ldoHooks/useSubject.ts
  52. 36
      packages/solid-react/src/shapes/solid.shex
  53. 117
      packages/solid-react/src/useLdo.ts
  54. 30
      packages/solid-react/src/util/changesToSparqlUpdate.ts
  55. 33
      packages/solid-react/src/util/createGlobalHook.tsx
  56. 51
      packages/solid-react/src/util/splitChangesByGraph.ts
  57. 6
      packages/solid-react/src/util/useForceReload.ts
  58. 5
      packages/solid-react/test/trivial.test.ts
  59. 8
      packages/solid-react/tsconfig.build.json
  60. 21
      tsconfig.base.json
  61. 24
      tsconfig.json

2
.gitignore vendored

@ -13,3 +13,5 @@ packages/*/dist
npm-debug.log*
yarn-debug.log*
yarn-error.log*
coverage/

@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
coveragePathIgnorePatterns: ["/test/"],
};

52895
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -5,6 +5,8 @@
"packages/*"
],
"scripts": {
"test": "lerna run test",
"build": "lerna run build",
"demo-react": "lerna run start --scope @ldo/demo-react",
"lint": "lerna run lint"
},

@ -0,0 +1,28 @@
// this file overrides the default CRA configurations (webpack, eslint, babel, etc)
// Ingnore because config scripts can't use the import variable
// eslint-disable-next-line @typescript-eslint/no-var-requires
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
module.exports = {
webpack: {
configure: (config) => {
// Remove ModuleScopePlugin which throws when we try to import something
// outside of src/.
config.resolve.plugins.pop();
// Resolve the path aliases.
config.resolve.plugins.push(new TsconfigPathsPlugin());
// Let Babel compile outside of src/.
const oneOfRule = config.module.rules.find((rule) => rule.oneOf);
const tsRule = oneOfRule.oneOf.find((rule) =>
rule.test.toString().includes("ts|tsx"),
);
tsRule.include = undefined;
tsRule.exclude = /node_modules/;
return config;
},
},
};

@ -3,20 +3,20 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@ldo/solid-react": "^0.0.0",
"ldo": "^1.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"solid-authn-react-native": "^2.0.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"lint": "eslint src/** --fix"
"lint": "eslint src/** --fix",
"build:ldo": "ldo build --input src/shapes --output src/ldo"
},
"eslintConfig": {
"extends": [
@ -31,9 +31,16 @@
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 chrome version",
"last 1 safari version"
]
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"@types/jsonld": "^1.5.9",
"@types/shexj": "^2.1.4",
"ldo-cli": "^3.0.1",
"tsconfig-paths-webpack-plugin": "^4.1.0"
}
}

@ -1,23 +1,18 @@
import React, { FunctionComponent } from "react";
import Profile from "./Profile";
import { SolidAuthProvider, LdoProvider } from "@ldo/solid-react";
import { fetch } from "solid-authn-react-native";
import Layout from "./Layout";
const App: FunctionComponent = () => {
const ProfileApp: FunctionComponent = () => {
return (
<div className="App">
<header className="App-header">
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<SolidAuthProvider>
<LdoProvider fetch={fetch}>
<Layout>
<Profile />
</Layout>
</LdoProvider>
</SolidAuthProvider>
);
};
export default App;
export default ProfileApp;

@ -0,0 +1,34 @@
import React, { FunctionComponent, useState } from "react";
interface BlurTextInputProps {
value: string;
onBlurText: (text: string) => void;
}
const BlurTextInput: FunctionComponent<BlurTextInputProps> = ({
value,
onBlurText,
}) => {
const [isFocused, setIsFocused] = useState(false);
const [text, setText] = useState(value);
return (
<input
type="text"
value={isFocused ? text : value}
onChange={(e) => setText(e.target.value)}
onFocus={() => {
setIsFocused(true);
setText(value);
}}
onBlur={(e) => {
setIsFocused(false);
if (e.target.value !== value) {
onBlurText(e.target.value);
}
}}
/>
);
};
export default BlurTextInput;

@ -0,0 +1,40 @@
import React, {
FunctionComponent,
PropsWithChildren,
useCallback,
} from "react";
import { useSolidAuth } from "@ldo/solid-react";
const Layout: FunctionComponent<PropsWithChildren> = ({ children }) => {
const { login, session, logout } = useSolidAuth();
const loginCb = useCallback(async () => {
const issuer = prompt(
"Enter your Pod Provider",
"https://solidcommunity.net",
);
if (issuer) {
await login(issuer);
}
}, []);
return (
<div>
<h1>LDO Solid React Test</h1>
{session.isLoggedIn ? (
<div>
<p>
Logged in as {session.webId}{" "}
<button onClick={logout}>Log Out</button>
</p>
<hr />
{children}
</div>
) : (
<button onClick={loginCb}>Log In</button>
)}
</div>
);
};
export default Layout;

@ -0,0 +1,39 @@
import React, { FunctionComponent } from "react";
import { SolidProfileShapeShapeType } from "./ldo/solidProfile.shapeTypes";
import BlurTextInput from "./BlurTextInput";
import {
useSolidAuth,
useLdo,
useDataResource,
useSubject,
} from "@ldo/solid-react";
const Profile: FunctionComponent = () => {
const { changeData, commitData } = useLdo();
const { session } = useSolidAuth();
const webId = session.webId!;
const webIdResource = useDataResource(webId);
const [profile, profileError] = useSubject(SolidProfileShapeShapeType, webId);
if (webIdResource.isLoading) {
return <p>Loading</p>;
} else if (profileError) {
return <p>profileError.message</p>;
} else {
return (
<div>
<label>Name:</label>
<BlurTextInput
value={profile.name || ""}
onBlurText={async (text) => {
const cProfile = changeData(profile, webIdResource);
cProfile.name = text;
await commitData(cProfile);
}}
/>
</div>
);
}
};
export default Profile;

@ -0,0 +1,156 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* solidProfileContext: JSONLD Context for solidProfile
* =============================================================================
*/
export const solidProfileContext: ContextDefinition = {
type: {
"@id": "@type",
"@container": "@set",
},
Person: "http://schema.org/Person",
Person2: "http://xmlns.com/foaf/0.1/Person",
fn: {
"@id": "http://www.w3.org/2006/vcard/ns#fn",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
name: {
"@id": "http://xmlns.com/foaf/0.1/name",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
hasAddress: {
"@id": "http://www.w3.org/2006/vcard/ns#hasAddress",
"@type": "@id",
"@container": "@set",
},
countryName: {
"@id": "http://www.w3.org/2006/vcard/ns#country-name",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
locality: {
"@id": "http://www.w3.org/2006/vcard/ns#locality",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
postalCode: {
"@id": "http://www.w3.org/2006/vcard/ns#postal-code",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
region: {
"@id": "http://www.w3.org/2006/vcard/ns#region",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
streetAddress: {
"@id": "http://www.w3.org/2006/vcard/ns#street-address",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
hasEmail: {
"@id": "http://www.w3.org/2006/vcard/ns#hasEmail",
"@type": "@id",
"@container": "@set",
},
Dom: "http://www.w3.org/2006/vcard/ns#Dom",
Home: "http://www.w3.org/2006/vcard/ns#Home",
ISDN: "http://www.w3.org/2006/vcard/ns#ISDN",
Internet: "http://www.w3.org/2006/vcard/ns#Internet",
Intl: "http://www.w3.org/2006/vcard/ns#Intl",
Label: "http://www.w3.org/2006/vcard/ns#Label",
Parcel: "http://www.w3.org/2006/vcard/ns#Parcel",
Postal: "http://www.w3.org/2006/vcard/ns#Postal",
Pref: "http://www.w3.org/2006/vcard/ns#Pref",
Work: "http://www.w3.org/2006/vcard/ns#Work",
X400: "http://www.w3.org/2006/vcard/ns#X400",
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
"@container": "@set",
},
hasPhoto: {
"@id": "http://www.w3.org/2006/vcard/ns#hasPhoto",
"@type": "@id",
},
img: {
"@id": "http://xmlns.com/foaf/0.1/img",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
hasTelephone: {
"@id": "http://www.w3.org/2006/vcard/ns#hasTelephone",
"@type": "@id",
"@container": "@set",
},
phone: {
"@id": "http://www.w3.org/2006/vcard/ns#phone",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
organizationName: {
"@id": "http://www.w3.org/2006/vcard/ns#organization-name",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
role: {
"@id": "http://www.w3.org/2006/vcard/ns#role",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
trustedApp: {
"@id": "http://www.w3.org/ns/auth/acl#trustedApp",
"@type": "@id",
"@container": "@set",
},
mode: {
"@id": "http://www.w3.org/ns/auth/acl#mode",
"@container": "@set",
},
Append: "http://www.w3.org/ns/auth/acl#Append",
Control: "http://www.w3.org/ns/auth/acl#Control",
Read: "http://www.w3.org/ns/auth/acl#Read",
Write: "http://www.w3.org/ns/auth/acl#Write",
origin: {
"@id": "http://www.w3.org/ns/auth/acl#origin",
"@type": "@id",
},
key: {
"@id": "http://www.w3.org/ns/auth/cert#key",
"@type": "@id",
"@container": "@set",
},
modulus: {
"@id": "http://www.w3.org/ns/auth/cert#modulus",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
exponent: {
"@id": "http://www.w3.org/ns/auth/cert#exponent",
"@type": "http://www.w3.org/2001/XMLSchema#integer",
},
inbox: {
"@id": "http://www.w3.org/ns/ldp#inbox",
"@type": "@id",
},
preferencesFile: {
"@id": "http://www.w3.org/ns/pim/space#preferencesFile",
"@type": "@id",
},
storage: {
"@id": "http://www.w3.org/ns/pim/space#storage",
"@type": "@id",
"@container": "@set",
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@container": "@set",
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@container": "@set",
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@container": "@set",
},
};

@ -0,0 +1,749 @@
import { Schema } from "shexj";
/**
* =============================================================================
* solidProfileSchema: ShexJ Schema for solidProfile
* =============================================================================
*/
export const solidProfileSchema: Schema = {
type: "Schema",
shapes: [
{
id: "https://shaperepo.com/schemas/solidProfile#SolidProfileShape",
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/Person"],
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "Defines the node as a Person (from Schema.org)",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
valueExpr: {
type: "NodeConstraint",
values: ["http://xmlns.com/foaf/0.1/Person"],
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "Defines the node as a Person (from foaf)",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#fn",
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#comment",
object: {
value:
"The formatted name of a person. Example: John Smith",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/name",
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#comment",
object: {
value: "An alternate way to define a person's name.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#hasAddress",
valueExpr:
"https://shaperepo.com/schemas/solidProfile#AddressShape",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The person's street address.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#hasEmail",
valueExpr:
"https://shaperepo.com/schemas/solidProfile#EmailShape",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The person's email.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#hasPhoto",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "A link to the person's photo",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/img",
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#comment",
object: {
value: "Photo link but in string form",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#hasTelephone",
valueExpr:
"https://shaperepo.com/schemas/solidProfile#PhoneNumberShape",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "Person's telephone number",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#phone",
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#comment",
object: {
value:
"An alternative way to define a person's telephone number using a string",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#organization-name",
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#comment",
object: {
value:
"The name of the organization with which the person is affiliated",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#role",
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#comment",
object: {
value:
"The name of the person's role in their organization",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/auth/acl#trustedApp",
valueExpr:
"https://shaperepo.com/schemas/solidProfile#TrustedAppShape",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A list of app origins that are trusted by this user",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/auth/cert#key",
valueExpr:
"https://shaperepo.com/schemas/solidProfile#RSAPublicKeyShape",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A list of RSA public keys that are associated with private keys the user holds.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/ldp#inbox",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"The user's LDP inbox to which apps can post notifications",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/pim/space#preferencesFile",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The user's preferences",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/pim/space#storage",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"The location of a Solid storage server related to this WebId",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/solid/terms#account",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The user's account",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/solid/terms#privateTypeIndex",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A registry of all types used on the user's Pod (for private access only)",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/solid/terms#publicTypeIndex",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A registry of all types used on the user's Pod (for public access)",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/knows",
valueExpr:
"https://shaperepo.com/schemas/solidProfile#SolidProfileShape",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A list of WebIds for all the people this user knows.",
},
},
],
},
],
},
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
},
},
{
id: "https://shaperepo.com/schemas/solidProfile#AddressShape",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#country-name",
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#comment",
object: {
value: "The name of the user's country of residence",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#locality",
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#comment",
object: {
value:
"The name of the user's locality (City, Town etc.) of residence",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#postal-code",
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#comment",
object: {
value: "The user's postal code",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#region",
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#comment",
object: {
value:
"The name of the user's region (State, Province etc.) of residence",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#street-address",
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#comment",
object: {
value: "The user's street address",
},
},
],
},
],
},
},
},
{
id: "https://shaperepo.com/schemas/solidProfile#EmailShape",
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://www.w3.org/2006/vcard/ns#Dom",
"http://www.w3.org/2006/vcard/ns#Home",
"http://www.w3.org/2006/vcard/ns#ISDN",
"http://www.w3.org/2006/vcard/ns#Internet",
"http://www.w3.org/2006/vcard/ns#Intl",
"http://www.w3.org/2006/vcard/ns#Label",
"http://www.w3.org/2006/vcard/ns#Parcel",
"http://www.w3.org/2006/vcard/ns#Postal",
"http://www.w3.org/2006/vcard/ns#Pref",
"http://www.w3.org/2006/vcard/ns#Work",
"http://www.w3.org/2006/vcard/ns#X400",
],
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The type of email.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#value",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"The value of an email as a mailto link (Example <mailto:jane@example.com>)",
},
},
],
},
],
},
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
},
},
{
id: "https://shaperepo.com/schemas/solidProfile#PhoneNumberShape",
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://www.w3.org/2006/vcard/ns#Dom",
"http://www.w3.org/2006/vcard/ns#Home",
"http://www.w3.org/2006/vcard/ns#ISDN",
"http://www.w3.org/2006/vcard/ns#Internet",
"http://www.w3.org/2006/vcard/ns#Intl",
"http://www.w3.org/2006/vcard/ns#Label",
"http://www.w3.org/2006/vcard/ns#Parcel",
"http://www.w3.org/2006/vcard/ns#Postal",
"http://www.w3.org/2006/vcard/ns#Pref",
"http://www.w3.org/2006/vcard/ns#Work",
"http://www.w3.org/2006/vcard/ns#X400",
],
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "They type of Phone Number",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/2006/vcard/ns#value",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"The value of a phone number as a tel link (Example <tel:555-555-5555>)",
},
},
],
},
],
},
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
},
},
{
id: "https://shaperepo.com/schemas/solidProfile#TrustedAppShape",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/auth/acl#mode",
valueExpr: {
type: "NodeConstraint",
values: [
"http://www.w3.org/ns/auth/acl#Append",
"http://www.w3.org/ns/auth/acl#Control",
"http://www.w3.org/ns/auth/acl#Read",
"http://www.w3.org/ns/auth/acl#Write",
],
},
min: 1,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The level of access provided to this origin",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/auth/acl#origin",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The app origin the user trusts",
},
},
],
},
],
},
},
},
{
id: "https://shaperepo.com/schemas/solidProfile#RSAPublicKeyShape",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/auth/cert#modulus",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#string",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "RSA Modulus",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/auth/cert#exponent",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#integer",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "RSA Exponent",
},
},
],
},
],
},
},
},
],
};

@ -0,0 +1,71 @@
import { ShapeType } from "ldo";
import { solidProfileSchema } from "./solidProfile.schema";
import { solidProfileContext } from "./solidProfile.context";
import {
SolidProfileShape,
AddressShape,
EmailShape,
PhoneNumberShape,
TrustedAppShape,
RSAPublicKeyShape,
} from "./solidProfile.typings";
/**
* =============================================================================
* LDO ShapeTypes solidProfile
* =============================================================================
*/
/**
* SolidProfileShape ShapeType
*/
export const SolidProfileShapeShapeType: ShapeType<SolidProfileShape> = {
schema: solidProfileSchema,
shape: "https://shaperepo.com/schemas/solidProfile#SolidProfileShape",
context: solidProfileContext,
};
/**
* AddressShape ShapeType
*/
export const AddressShapeShapeType: ShapeType<AddressShape> = {
schema: solidProfileSchema,
shape: "https://shaperepo.com/schemas/solidProfile#AddressShape",
context: solidProfileContext,
};
/**
* EmailShape ShapeType
*/
export const EmailShapeShapeType: ShapeType<EmailShape> = {
schema: solidProfileSchema,
shape: "https://shaperepo.com/schemas/solidProfile#EmailShape",
context: solidProfileContext,
};
/**
* PhoneNumberShape ShapeType
*/
export const PhoneNumberShapeShapeType: ShapeType<PhoneNumberShape> = {
schema: solidProfileSchema,
shape: "https://shaperepo.com/schemas/solidProfile#PhoneNumberShape",
context: solidProfileContext,
};
/**
* TrustedAppShape ShapeType
*/
export const TrustedAppShapeShapeType: ShapeType<TrustedAppShape> = {
schema: solidProfileSchema,
shape: "https://shaperepo.com/schemas/solidProfile#TrustedAppShape",
context: solidProfileContext,
};
/**
* RSAPublicKeyShape ShapeType
*/
export const RSAPublicKeyShapeShapeType: ShapeType<RSAPublicKeyShape> = {
schema: solidProfileSchema,
shape: "https://shaperepo.com/schemas/solidProfile#RSAPublicKeyShape",
context: solidProfileContext,
};

@ -0,0 +1,293 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* Typescript Typings for solidProfile
* =============================================================================
*/
/**
* SolidProfileShape Type
*/
export interface SolidProfileShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* Defines the node as a Person (from Schema.org) | Defines the node as a Person (from foaf)
*/
type: (
| {
"@id": "Person";
}
| {
"@id": "Person2";
}
)[];
/**
* The formatted name of a person. Example: John Smith
*/
fn?: string;
/**
* An alternate way to define a person's name.
*/
name?: string;
/**
* The person's street address.
*/
hasAddress?: AddressShape[];
/**
* The person's email.
*/
hasEmail?: EmailShape[];
/**
* A link to the person's photo
*/
hasPhoto?: {
"@id": string;
};
/**
* Photo link but in string form
*/
img?: string;
/**
* Person's telephone number
*/
hasTelephone?: PhoneNumberShape[];
/**
* An alternative way to define a person's telephone number using a string
*/
phone?: string;
/**
* The name of the organization with which the person is affiliated
*/
organizationName?: string;
/**
* The name of the person's role in their organization
*/
role?: string;
/**
* A list of app origins that are trusted by this user
*/
trustedApp?: TrustedAppShape[];
/**
* A list of RSA public keys that are associated with private keys the user holds.
*/
key?: RSAPublicKeyShape[];
/**
* The user's LDP inbox to which apps can post notifications
*/
inbox: {
"@id": string;
};
/**
* The user's preferences
*/
preferencesFile?: {
"@id": string;
};
/**
* The location of a Solid storage server related to this WebId
*/
storage?: {
"@id": string;
}[];
/**
* The user's account
*/
account?: {
"@id": string;
};
/**
* A registry of all types used on the user's Pod (for private access only)
*/
privateTypeIndex?: {
"@id": string;
}[];
/**
* A registry of all types used on the user's Pod (for public access)
*/
publicTypeIndex?: {
"@id": string;
}[];
/**
* A list of WebIds for all the people this user knows.
*/
knows?: SolidProfileShape[];
}
/**
* AddressShape Type
*/
export interface AddressShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* The name of the user's country of residence
*/
countryName?: string;
/**
* The name of the user's locality (City, Town etc.) of residence
*/
locality?: string;
/**
* The user's postal code
*/
postalCode?: string;
/**
* The name of the user's region (State, Province etc.) of residence
*/
region?: string;
/**
* The user's street address
*/
streetAddress?: string;
}
/**
* EmailShape Type
*/
export interface EmailShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* The type of email.
*/
type?:
| {
"@id": "Dom";
}
| {
"@id": "Home";
}
| {
"@id": "ISDN";
}
| {
"@id": "Internet";
}
| {
"@id": "Intl";
}
| {
"@id": "Label";
}
| {
"@id": "Parcel";
}
| {
"@id": "Postal";
}
| {
"@id": "Pref";
}
| {
"@id": "Work";
}
| {
"@id": "X400";
};
/**
* The value of an email as a mailto link (Example <mailto:jane@example.com>)
*/
value: {
"@id": string;
};
}
/**
* PhoneNumberShape Type
*/
export interface PhoneNumberShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* They type of Phone Number
*/
type?:
| {
"@id": "Dom";
}
| {
"@id": "Home";
}
| {
"@id": "ISDN";
}
| {
"@id": "Internet";
}
| {
"@id": "Intl";
}
| {
"@id": "Label";
}
| {
"@id": "Parcel";
}
| {
"@id": "Postal";
}
| {
"@id": "Pref";
}
| {
"@id": "Work";
}
| {
"@id": "X400";
};
/**
* The value of a phone number as a tel link (Example <tel:555-555-5555>)
*/
value: {
"@id": string;
};
}
/**
* TrustedAppShape Type
*/
export interface TrustedAppShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* The level of access provided to this origin
*/
mode: (
| {
"@id": "Append";
}
| {
"@id": "Control";
}
| {
"@id": "Read";
}
| {
"@id": "Write";
}
)[];
/**
* The app origin the user trusts
*/
origin: {
"@id": string;
};
}
/**
* RSAPublicKeyShape Type
*/
export interface RSAPublicKeyShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* RSA Modulus
*/
modulus: string;
/**
* RSA Exponent
*/
exponent: number;
}

@ -0,0 +1,121 @@
PREFIX srs: <https://shaperepo.com/schemas/solidProfile#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schem: <http://schema.org/>
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX acl: <http://www.w3.org/ns/auth/acl#>
PREFIX cert: <http://www.w3.org/ns/auth/cert#>
PREFIX ldp: <http://www.w3.org/ns/ldp#>
PREFIX sp: <http://www.w3.org/ns/pim/space#>
PREFIX solid: <http://www.w3.org/ns/solid/terms#>
srs:SolidProfileShape EXTRA a {
a [ schem:Person ]
// rdfs:comment "Defines the node as a Person (from Schema.org)" ;
a [ foaf:Person ]
// rdfs:comment "Defines the node as a Person (from foaf)" ;
vcard:fn xsd:string ?
// rdfs:comment "The formatted name of a person. Example: John Smith" ;
foaf:name xsd:string ?
// rdfs:comment "An alternate way to define a person's name." ;
vcard:hasAddress @srs:AddressShape *
// rdfs:comment "The person's street address." ;
vcard:hasEmail @srs:EmailShape *
// rdfs:comment "The person's email." ;
vcard:hasPhoto IRI ?
// rdfs:comment "A link to the person's photo" ;
foaf:img xsd:string ?
// rdfs:comment "Photo link but in string form" ;
vcard:hasTelephone @srs:PhoneNumberShape *
// rdfs:comment "Person's telephone number" ;
vcard:phone xsd:string ?
// rdfs:comment "An alternative way to define a person's telephone number using a string" ;
vcard:organization-name xsd:string ?
// rdfs:comment "The name of the organization with which the person is affiliated" ;
vcard:role xsd:string ?
// rdfs:comment "The name of the person's role in their organization" ;
acl:trustedApp @srs:TrustedAppShape *
// rdfs:comment "A list of app origins that are trusted by this user" ;
cert:key @srs:RSAPublicKeyShape *
// rdfs:comment "A list of RSA public keys that are associated with private keys the user holds." ;
ldp:inbox IRI
// rdfs:comment "The user's LDP inbox to which apps can post notifications" ;
sp:preferencesFile IRI ?
// rdfs:comment "The user's preferences" ;
sp:storage IRI *
// rdfs:comment "The location of a Solid storage server related to this WebId" ;
solid:account IRI ?
// rdfs:comment "The user's account" ;
solid:privateTypeIndex IRI *
// rdfs:comment "A registry of all types used on the user's Pod (for private access only)" ;
solid:publicTypeIndex IRI *
// rdfs:comment "A registry of all types used on the user's Pod (for public access)" ;
foaf:knows @srs:SolidProfileShape *
// rdfs:comment "A list of WebIds for all the people this user knows." ;
}
srs:AddressShape {
vcard:country-name xsd:string ?
// rdfs:comment "The name of the user's country of residence" ;
vcard:locality xsd:string ?
// rdfs:comment "The name of the user's locality (City, Town etc.) of residence" ;
vcard:postal-code xsd:string ?
// rdfs:comment "The user's postal code" ;
vcard:region xsd:string ?
// rdfs:comment "The name of the user's region (State, Province etc.) of residence" ;
vcard:street-address xsd:string ?
// rdfs:comment "The user's street address" ;
}
srs:EmailShape EXTRA a {
a [
vcard:Dom
vcard:Home
vcard:ISDN
vcard:Internet
vcard:Intl
vcard:Label
vcard:Parcel
vcard:Postal
vcard:Pref
vcard:Work
vcard:X400
] ?
// rdfs:comment "The type of email." ;
vcard:value IRI
// rdfs:comment "The value of an email as a mailto link (Example <mailto:jane@example.com>)" ;
}
srs:PhoneNumberShape EXTRA a {
a [
vcard:Dom
vcard:Home
vcard:ISDN
vcard:Internet
vcard:Intl
vcard:Label
vcard:Parcel
vcard:Postal
vcard:Pref
vcard:Work
vcard:X400
] ?
// rdfs:comment "They type of Phone Number" ;
vcard:value IRI
// rdfs:comment "The value of a phone number as a tel link (Example <tel:555-555-5555>)" ;
}
srs:TrustedAppShape {
acl:mode [acl:Append acl:Control acl:Read acl:Write] +
// rdfs:comment "The level of access provided to this origin" ;
acl:origin IRI
// rdfs:comment "The app origin the user trusts"
}
srs:RSAPublicKeyShape {
cert:modulus xsd:string
// rdfs:comment "RSA Modulus" ;
cert:exponent xsd:integer
// rdfs:comment "RSA Exponent" ;
}

@ -0,0 +1,16 @@
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": 100
}
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": []
}

@ -0,0 +1,3 @@
{
"extends": ["../../eslintrc"]
}

@ -0,0 +1,13 @@
# Solid-Dataset
Pre-alpha
## Notes:
- Any quads in the default graph will not be synced to Solid Pods, but will be added to the dataset without any syncing
## TODO:
- Create an event for each thing that happens. Like "Loaded"
- You should be able to initialize classes with loading = true so that there isn't a flash on initial load
- Access rule stuff just doesn't work. I might need to program a custom implementation for that
- There should probably be separate libraries for React native and Web React

@ -0,0 +1,5 @@
const sharedConfig = require('../../jest.config.js');
module.exports = {
...sharedConfig,
'rootDir': './',
}

@ -0,0 +1,50 @@
{
"name": "@ldo/solid-react",
"version": "0.0.0",
"description": "A React library for LDO and Solid",
"main": "dist/index.js",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"watch": "tsc --watch",
"test": "jest --coverage",
"test:watch": "jest --watch",
"docs": "typedoc",
"prepublishOnly": "npm run test && npm run build && npm run docs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"storybook:ldo": "ldo build --input stories/shapes --output stories/ldo",
"build:ldo": "ldo build --input lib/shapes --output lib/ldo"
},
"repository": {
"type": "git",
"url": "git+https://github.com/o-development/devtool-boilerplate.git"
},
"author": "Jackson Morgan",
"license": "MIT",
"bugs": {
"url": "https://github.com/o-development/devtool-boilerplate/issues"
},
"homepage": "https://github.com/o-development/devtool-boilerplate#readme",
"devDependencies": {
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.11",
"@rdfjs/types": "^1.1.0",
"@types/jest": "^29.0.3",
"@types/jsonld": "^1.5.8",
"@types/n3": "^1.10.4",
"@types/shexj": "^2.1.4",
"ldo-cli": "^3.0.1",
"prop-types": "^15.8.1",
"ts-jest": "^29.0.2"
},
"dependencies": {
"@inrupt/solid-client": "^1.29.0",
"cross-fetch": "^3.1.6",
"jsonld-dataset-proxy": "^1.2.3",
"ldo": "^1.0.3",
"o-dataset-pack": "^0.2.14",
"solid-authn-react-native": "^2.0.3",
"stream": "^0.0.2"
}
}

@ -0,0 +1,19 @@
import { createContext, useContext } from "react";
import { BinaryResourceStoreDependencies } from "./document/resource/binaryResource/BinaryResourceStore";
import { DataResourceStoreDependencies } from "./document/resource/dataResource/DataResourceStore";
import { AccessRulesStoreDependencies } from "./document/accessRules/AccessRulesStore";
import { ContainerResourceStoreDependencies } from "./document/resource/dataResource/containerResource/ContainerResourceStore";
export interface LdoContextData
extends BinaryResourceStoreDependencies,
DataResourceStoreDependencies,
AccessRulesStoreDependencies,
ContainerResourceStoreDependencies {}
// No default parameter is required as it will be set in the provider
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const LdoContext = createContext<LdoContextData>({});
export const LdoContextProvider = LdoContext.Provider;
export const useLdoContext = () => useContext(LdoContext);

@ -0,0 +1,66 @@
import React, {
FunctionComponent,
PropsWithChildren,
useEffect,
useMemo,
} from "react";
import crossFetch from "cross-fetch";
import { createLdoDataset } from "ldo";
import { LdoContextData, LdoContextProvider } from "./LdoContext";
import { UpdateManager } from "./ldoHooks/helpers/UpdateManager";
import { BinaryResourceStore } from "./document/resource/binaryResource/BinaryResourceStore";
import { DataResourceStore } from "./document/resource/dataResource/DataResourceStore";
import { ContainerResourceStore } from "./document/resource/dataResource/containerResource/ContainerResourceStore";
import { AccessRulesStore } from "./document/accessRules/AccessRulesStore";
import { Dataset } from "@rdfjs/types";
export interface LdoProviderProps extends PropsWithChildren {
fetch?: typeof fetch;
dataset?: Dataset;
onDocumentError?: LdoContextData["onDocumentError"];
}
/**
* Main Ldo Provider
*/
export const LdoProvider: FunctionComponent<
PropsWithChildren<LdoProviderProps>
> = ({ dataset, fetch, onDocumentError, children }) => {
const finalFetch = useMemo(() => fetch || crossFetch, [fetch]);
const ldoDataset = useMemo(() => createLdoDataset(dataset), [dataset]);
// Initialize storeDependencies before render
const storeDependencies = useMemo(() => {
// Ingnoring this because we're setting up circular dependencies
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const dependencies: LdoContextData = {
onDocumentError,
fetch: finalFetch,
dataset: ldoDataset,
updateManager: new UpdateManager(),
};
const binaryResourceStore = new BinaryResourceStore(dependencies);
const dataResourceStore = new DataResourceStore(dependencies);
const containerResourceStore = new ContainerResourceStore(dependencies);
const accessRulesStore = new AccessRulesStore(dependencies);
dependencies.binaryResourceStore = binaryResourceStore;
dependencies.dataResourceStore = dataResourceStore;
dependencies.containerResourceStore = containerResourceStore;
dependencies.accessRulesStore = accessRulesStore;
return dependencies;
}, []);
// Update the resource manager in case fetch or ldo dataset changes
useEffect(() => {
storeDependencies.fetch = finalFetch;
storeDependencies.dataset = ldoDataset;
storeDependencies.onDocumentError = onDocumentError;
}, [finalFetch, ldoDataset, onDocumentError]);
return (
<LdoContextProvider value={storeDependencies}>
{children}
</LdoContextProvider>
);
};

@ -0,0 +1,98 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ISessionInfo,
handleIncomingRedirect,
login as libraryLogin,
getDefaultSession,
logout as libraryLogout,
fetch as libraryFetch,
} from "solid-authn-react-native";
import { createGlobalHook } from "./util/createGlobalHook";
const PRE_REDIRECT_URI = "PRE_REDIRECT_URI";
interface AuthGlobalHookReturn {
runInitialAuthCheck: () => Promise<void>;
login: (issuer: string) => Promise<void>;
logout: () => Promise<void>;
signUp: (issuer: string) => Promise<void>;
fetch: typeof fetch;
session: ISessionInfo;
ranInitialAuthCheck: boolean;
}
function useAuthGlobalHookFunc(): AuthGlobalHookReturn {
const [session, setSession] = useState<ISessionInfo>(
getDefaultSession().info
);
const [ranInitialAuthCheck, setRanInitialAuthCheck] = useState(false);
const runInitialAuthCheck = useCallback(async () => {
// TODO: Change this to dependency injection so it works in React Native
if (!window.localStorage.getItem(PRE_REDIRECT_URI)) {
window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href);
}
await handleIncomingRedirect({
restorePreviousSession: true,
});
setSession({ ...getDefaultSession().info });
window.history.replaceState(
{},
"",
window.localStorage.getItem(PRE_REDIRECT_URI)
);
window.localStorage.removeItem(PRE_REDIRECT_URI);
setRanInitialAuthCheck(true);
}, []);
const login = useCallback(
async (issuer: string, clientName = "Solid App") => {
window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href);
await libraryLogin({
oidcIssuer: issuer,
// TODO: this ties this to in-browser use
redirectUrl: window.location.href,
clientName,
});
setSession({ ...getDefaultSession().info });
},
[]
);
const logout = useCallback(async () => {
await libraryLogout();
setSession({ ...getDefaultSession().info });
}, []);
const signUp = useCallback(async (issuer: string) => {
/* Do nothing for now */
console.log(`Signup Pressed with issuer ${issuer}`);
}, []);
useEffect(() => {
runInitialAuthCheck();
}, []);
return useMemo(
() => ({
runInitialAuthCheck,
login,
logout,
signUp,
session,
ranInitialAuthCheck,
fetch: libraryFetch,
}),
[login, logout, ranInitialAuthCheck, runInitialAuthCheck, session, signUp]
);
}
const authGlobalHook = createGlobalHook(useAuthGlobalHookFunc);
export const SolidAuthContext = authGlobalHook.Context;
export const SolidAuthProvider = authGlobalHook.Provider;
export const useSolidAuth = authGlobalHook.useGlobal;

@ -0,0 +1,36 @@
import { FetchableDocument } from "./FetchableDocument";
// This may eventually have fields
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DocumentStoreDependencies {}
export abstract class DocumentStore<
DocumentType extends FetchableDocument,
Initializer,
Dependencies extends DocumentStoreDependencies
> {
protected documentMap: Map<Initializer, DocumentType>;
protected dependencies: Dependencies;
constructor(dependencies: Dependencies) {
this.documentMap = new Map();
this.dependencies = dependencies;
}
get(initializerInput: Initializer): DocumentType {
const initializer = this.normalizeInitializer(initializerInput);
const document = this.documentMap.get(initializer);
if (document) {
return document;
}
const newDocument = this.create(initializer);
this.documentMap.set(initializer, newDocument);
return newDocument;
}
protected abstract create(initializer: Initializer): DocumentType;
protected normalizeInitializer(initializer: Initializer): Initializer {
return initializer;
}
}

@ -0,0 +1,107 @@
import EventEmitter from "events";
import { DocumentError } from "./errors/DocumentError";
export interface FetchableDocumentDependencies {
onDocumentError?: (error: DocumentError) => void;
}
const STATE_UPDATE = "stateUpdate";
export abstract class FetchableDocument extends EventEmitter {
protected _isLoading: boolean;
protected _isWriting: boolean;
protected _didInitialFetch: boolean;
protected _error?: DocumentError;
private dependencies;
constructor(dependencies: FetchableDocumentDependencies) {
super();
this._isLoading = false;
this._isWriting = false;
this._didInitialFetch = false;
this.dependencies = dependencies;
}
/**
* ===========================================================================
* Getters
* ===========================================================================
*/
get isLoading() {
return this._isLoading;
}
get didInitialFetch() {
return this._didInitialFetch;
}
get error() {
return this._error;
}
get isWriting() {
return this._isWriting;
}
protected get onDocumentError() {
return this.dependencies.onDocumentError;
}
/**
* ===========================================================================
* Methods
* ===========================================================================
*/
async read() {
this._isLoading = true;
this.emitStateUpdate();
const documentError = await this.fetchDocument();
this._isLoading = false;
this._didInitialFetch = true;
if (documentError) {
this.setError(documentError);
}
this.emitStateUpdate();
}
async reload() {
return this.read();
}
protected abstract fetchDocument(): Promise<DocumentError | undefined>;
protected beginWrite() {
this._isWriting = true;
this.emitStateUpdate();
}
protected endWrite(error?: DocumentError) {
if (error) {
this.setError(error);
}
this._isWriting = false;
this.emitStateUpdate();
}
setError(error: DocumentError) {
this._error = error;
this.emitStateUpdate();
if (this.onDocumentError) {
this.onDocumentError(error);
}
}
/**
* Emitter Information
*/
protected emitStateUpdate() {
this.emit(STATE_UPDATE);
}
onStateUpdate(callback: () => void) {
this.on(STATE_UPDATE, callback);
}
offStateUpdate(callback: () => void) {
this.off(STATE_UPDATE, callback);
}
}

@ -0,0 +1,80 @@
import {
universalAccess,
AccessModes as IAccessModes,
} from "@inrupt/solid-client";
import {
FetchableDocument,
FetchableDocumentDependencies,
} from "../FetchableDocument";
import { Resource } from "../resource/Resource";
import { DocumentError } from "../errors/DocumentError";
export type AccessModes = IAccessModes;
export interface AccessRulesDependencies extends FetchableDocumentDependencies {
fetch: typeof fetch;
}
export class AccessRules extends FetchableDocument {
readonly resource: Resource;
private _publicAccess: IAccessModes | null;
private _agentAccess: Record<string, IAccessModes> | null;
private dependencies0;
constructor(resource: Resource, dependencies: AccessRulesDependencies) {
super(dependencies);
this._publicAccess = null;
this._agentAccess = null;
this.dependencies0 = dependencies;
this.resource = resource;
}
/**
* ===========================================================================
* Getters
* ===========================================================================
*/
get publicAccess() {
return this._publicAccess;
}
get agentAccess() {
return this._agentAccess;
}
protected get fetch() {
return this.dependencies0.fetch;
}
/**
* ===========================================================================
* Methods
* ===========================================================================
*/
protected async fetchDocument() {
try {
const [publicAccess, agentAccess] = await Promise.all([
universalAccess.getPublicAccess(this.resource.uri, {
fetch: this.fetch,
}),
universalAccess.getAgentAccessAll(this.resource.uri, {
fetch: this.fetch,
}),
]);
this._publicAccess = publicAccess || {
read: false,
write: false,
append: false,
controlRead: false,
controlWrite: false,
};
this._agentAccess = agentAccess || {};
return undefined;
} catch (err: unknown) {
if (typeof err === "object" && (err as Error).message) {
this.setError(new DocumentError(this, (err as Error).message));
}
this.setError(new DocumentError(this, "Error Fetching Access Rules"));
}
}
}

@ -0,0 +1,17 @@
import { DocumentStore, DocumentStoreDependencies } from "../DocumentStore";
import { Resource } from "../resource/Resource";
import { AccessRules, AccessRulesDependencies } from "./AccessRules";
export interface AccessRulesStoreDependencies
extends DocumentStoreDependencies,
AccessRulesDependencies {}
export class AccessRulesStore extends DocumentStore<
AccessRules,
Resource,
AccessRulesStoreDependencies
> {
create(initializer: Resource) {
return new AccessRules(initializer, this.dependencies);
}
}

@ -0,0 +1,10 @@
import { FetchableDocument } from "../FetchableDocument";
export class DocumentError extends Error {
public readonly document: FetchableDocument;
constructor(document: FetchableDocument, message: string) {
super(message);
this.document = document;
}
}

@ -0,0 +1,11 @@
import { FetchableDocument } from "../FetchableDocument";
import { DocumentError } from "./DocumentError";
export class DocumentFetchError extends DocumentError {
public readonly status: number;
constructor(document: FetchableDocument, status: number, message: string) {
super(document, message);
this.status = status;
}
}

@ -0,0 +1,91 @@
import {
FetchableDocument,
FetchableDocumentDependencies,
} from "../FetchableDocument";
import { AccessRulesStore } from "../accessRules/AccessRulesStore";
import { DocumentFetchError } from "../errors/DocumentFetchError";
import { ContainerResource } from "./dataResource/containerResource/ContainerResource";
import { ContainerResourceStore } from "./dataResource/containerResource/ContainerResourceStore";
export interface ResourceDependencies extends FetchableDocumentDependencies {
fetch: typeof fetch;
accessRulesStore: AccessRulesStore;
containerResourceStore: ContainerResourceStore;
}
export abstract class Resource extends FetchableDocument {
public readonly uri: string;
private dependencies1;
constructor(uri: string, dependencies: ResourceDependencies) {
super(dependencies);
this.uri = uri;
this.dependencies1 = dependencies;
}
/**
* ===========================================================================
* Getters
* ===========================================================================
*/
get accessRules() {
return this.accessRulesStore.get(this);
}
get parentContainer(): ContainerResource | undefined {
return this.containerResourceStore.getContainerForResouce(this);
}
get ["@id"]() {
return this.uri;
}
protected get fetch() {
return this.dependencies1.fetch;
}
protected get accessRulesStore() {
return this.dependencies1.accessRulesStore;
}
protected get containerResourceStore() {
return this.dependencies1.containerResourceStore;
}
/**
* ===========================================================================
* Methods
* ===========================================================================
*/
async delete() {
this.beginWrite();
const response = await this.fetch(this.uri, {
method: "DELETE",
});
if (response.status >= 200 && response.status < 300) {
this.endWrite();
this.parentContainer?.removeContainedResources(this);
return;
}
this.endWrite(
new DocumentFetchError(
this,
response.status,
`Could not delete ${this.uri}`
)
);
}
/**
* ===========================================================================
* Static Methods
* ===========================================================================
*/
/**
* Takes in a URL and will normalize it to the document it's fetching
*/
static normalizeUri(uri: string): string {
const [strippedHashUri] = uri.split("#");
return strippedHashUri;
}
}

@ -0,0 +1,10 @@
import { DocumentError } from "../../errors/DocumentError";
import { Resource, ResourceDependencies } from "../Resource";
export declare type BinaryResourceDependencies = ResourceDependencies;
export class BinaryResource extends Resource {
fetchDocument(): Promise<DocumentError | undefined> {
throw new Error("Method not implemented.");
}
}

@ -0,0 +1,20 @@
import { DocumentStore, DocumentStoreDependencies } from "../../DocumentStore";
import { BinaryResource, BinaryResourceDependencies } from "./BinaryResource";
export interface BinaryResourceStoreDependencies
extends DocumentStoreDependencies,
BinaryResourceDependencies {}
export class BinaryResourceStore extends DocumentStore<
BinaryResource,
string,
BinaryResourceStoreDependencies
> {
create(initializer: string) {
return new BinaryResource(initializer, this.dependencies);
}
protected normalizeInitializer(initializer: string): string {
return BinaryResource.normalizeUri(initializer);
}
}

@ -0,0 +1,127 @@
import { LdoDataset, parseRdf } from "ldo";
import { Resource, ResourceDependencies } from "../Resource";
import { DocumentFetchError } from "../../errors/DocumentFetchError";
import { DocumentError } from "../../errors/DocumentError";
import { namedNode, quad as createQuad } from "@rdfjs/data-model";
import { DatasetChanges } from "o-dataset-pack";
import { Quad } from "@rdfjs/types";
import { changesToSparqlUpdate } from "../../../util/changesToSparqlUpdate";
import { UpdateManager } from "../../../ldoHooks/helpers/UpdateManager";
export interface DataResourceDependencies extends ResourceDependencies {
dataset: LdoDataset;
updateManager: UpdateManager;
}
export class DataResource extends Resource {
private dependencies2;
constructor(uri: string, dependencies: DataResourceDependencies) {
super(uri, dependencies);
this.dependencies2 = dependencies;
}
/**
* ===========================================================================
* Getters
* ===========================================================================
*/
protected get dataset() {
return this.dependencies2.dataset;
}
protected get updateManager() {
return this.dependencies2.updateManager;
}
/**
* ===========================================================================
* Methods
* ===========================================================================
*/
async create() {
// TODO
}
protected async fetchDocument(): Promise<DocumentError | undefined> {
// Fetch the document using auth fetch
const response = await this.fetch(this.uri, {
headers: {
accept: "text/turtle",
},
});
// Handle Error
if (response.status !== 200) {
// TODO: Handle edge cases
return new DocumentFetchError(
this,
response.status,
`Error fetching resource ${this.uri}`
);
}
// Parse the incoming turtle into a dataset
const rawTurtle = await response.text();
let loadedDataset;
try {
loadedDataset = await parseRdf(rawTurtle, {
baseIRI: this.uri,
});
} catch (err) {
if (typeof err === "object" && (err as Error).message) {
return new DocumentError(this, (err as Error).message);
}
return new DocumentError(this, "Server returned poorly formatted Turtle");
}
// Start transaction
const transactionalDataset = this.dataset.startTransaction();
const graphNode = namedNode(this.uri);
// Destroy all triples that were once a part of this resouce
loadedDataset.deleteMatches(undefined, undefined, undefined, graphNode);
// Add the triples from the fetched item
loadedDataset.forEach((quad) => {
transactionalDataset.add(
createQuad(quad.subject, quad.predicate, quad.object, graphNode)
);
});
const changes = transactionalDataset.getChanges();
this.updateManager.notifyListenersOfChanges(changes);
transactionalDataset.commit();
return undefined;
}
async update(
changes: DatasetChanges<Quad>
): Promise<DocumentError | undefined> {
this.beginWrite();
// Convert changes to transactional Dataset
const transactionalDataset = this.dataset.startTransaction();
changes.added?.forEach((quad) => transactionalDataset.add(quad));
changes.removed?.forEach((quad) => transactionalDataset.delete(quad));
// Commit data optimistically
transactionalDataset.commit();
this.updateManager.notifyListenersOfChanges(changes);
// Make request
const sparqlUpdate = await changesToSparqlUpdate(changes);
const response = await this.fetch(this.uri, {
method: "PATCH",
body: sparqlUpdate,
headers: {
"Content-Type": "application/sparql-update",
},
});
if (response.status < 200 || response.status > 299) {
// Handle Error by rollback
transactionalDataset.rollback();
this.updateManager.notifyListenersOfChanges(changes);
this.endWrite(
new DocumentFetchError(
this,
response.status,
`Problem writing to ${this.uri}`
)
);
return;
}
this.endWrite();
}
}

@ -0,0 +1,20 @@
import { DocumentStore, DocumentStoreDependencies } from "../../DocumentStore";
import { DataResource, DataResourceDependencies } from "./DataResource";
export interface DataResourceStoreDependencies
extends DocumentStoreDependencies,
DataResourceDependencies {}
export class DataResourceStore extends DocumentStore<
DataResource,
string,
DataResourceStoreDependencies
> {
protected create(initializer: string) {
return new DataResource(initializer, this.dependencies);
}
protected normalizeInitializer(initializer: string): string {
return DataResource.normalizeUri(initializer);
}
}

@ -0,0 +1,102 @@
import { ContainerShapeType } from "../../../../ldo/solid.shapeTypes";
import { Resource } from "../../Resource";
import { BinaryResourceStore } from "../../binaryResource/BinaryResourceStore";
import { DataResource, DataResourceDependencies } from "../DataResource";
import { DataResourceStore } from "../DataResourceStore";
export interface ContainerResourceDependencies
extends DataResourceDependencies {
dataResourceStore: DataResourceStore;
binaryResourceStore: BinaryResourceStore;
}
export class ContainerResource extends DataResource {
private _contains: Set<Resource>;
private dependencies3: ContainerResourceDependencies;
constructor(uri: string, dependencies: ContainerResourceDependencies) {
super(uri, dependencies);
this._contains = new Set();
this.dependencies3 = dependencies;
}
/**
* ===========================================================================
* Getters
* ===========================================================================
*/
get contains() {
return Array.from(this._contains);
}
protected get binaryResourceStore() {
return this.dependencies3.binaryResourceStore;
}
protected get dataResourceStore() {
return this.dependencies3.dataResourceStore;
}
/**
* ===========================================================================
* Methods
* ===========================================================================
*/
protected async fetchDocument() {
const error = await super.fetchDocument();
if (error) {
return error;
}
// Update the contains
const container = this.dataset
.usingType(ContainerShapeType)
.fromSubject(this.uri);
const resourcesToAdd: Resource[] = [];
container.contains?.forEach((resourceData) => {
if (resourceData["@id"]) {
if (resourceData.type?.some((type) => type["@id"] === "Container")) {
resourcesToAdd.push(
this.containerResourceStore.get(resourceData["@id"])
);
} else {
if (resourceData["@id"].endsWith(".ttl")) {
resourcesToAdd.push(
this.dataResourceStore.get(resourceData["@id"])
);
} else {
resourcesToAdd.push(
this.binaryResourceStore.get(resourceData["@id"])
);
}
}
}
});
this.addContainedResources(...resourcesToAdd);
}
public addContainedResources(...resources: Resource[]) {
let someResourceUpdated = false;
resources.forEach((resource) => {
if (!this._contains.has(resource)) {
someResourceUpdated = true;
this._contains.add(resource);
this.parentContainer?.addContainedResources(this);
}
});
if (someResourceUpdated) {
this.emitStateUpdate();
}
}
public removeContainedResources(...resources: Resource[]) {
let someResourceUpdated = false;
resources.forEach((resource) => {
if (this._contains.has(resource)) {
someResourceUpdated = true;
this._contains.delete(resource);
}
});
if (someResourceUpdated) {
this.emitStateUpdate();
}
}
}

@ -0,0 +1,52 @@
import {
DocumentStore,
DocumentStoreDependencies,
} from "../../../DocumentStore";
import { Resource } from "../../Resource";
import {
ContainerResource,
ContainerResourceDependencies,
} from "./ContainerResource";
export interface ContainerResourceStoreDependencies
extends ContainerResourceDependencies,
DocumentStoreDependencies {}
export class ContainerResourceStore extends DocumentStore<
ContainerResource,
string,
ContainerResourceStoreDependencies
> {
protected create(initializer: string) {
return new ContainerResource(initializer, this.dependencies);
}
protected normalizeInitializer(initializer: string) {
return ContainerResource.normalizeUri(initializer);
}
getContainerForResouce(resource: Resource) {
const parentUri = ContainerResourceStore.getParentUri(resource.uri);
return parentUri ? this.get(parentUri) : undefined;
}
/**
* Returns the parent container URI
*/
static getParentUri(uri: string) {
const urlObject = new URL(uri);
const pathItems = urlObject.pathname.split("/");
if (
pathItems.length < 2 ||
(pathItems.length === 2 && pathItems[1].length === 0)
) {
return undefined;
}
if (pathItems[pathItems.length - 1] === "") {
pathItems.pop();
}
pathItems.pop();
urlObject.pathname = `${pathItems.join("/")}/`;
return urlObject.toString();
}
}

@ -0,0 +1,17 @@
import { useMemo } from "react";
import { useLdoContext } from "../LdoContext";
import { UseDocumentOptions, useDocument } from "./useDocument";
import { Resource } from "../document/resource/Resource";
export function useAccessRules(
resource: string | Resource,
options?: UseDocumentOptions
) {
const { dataResourceStore, accessRulesStore } = useLdoContext();
const realResource = useMemo(() => {
return typeof resource === "string"
? dataResourceStore.get(resource)
: resource;
}, [resource]);
return useDocument(realResource, accessRulesStore, options);
}

@ -0,0 +1,7 @@
import { useLdoContext } from "../LdoContext";
import { UseDocumentOptions, useDocument } from "./useDocument";
export function useBinaryResource(uri: string, options?: UseDocumentOptions) {
const { binaryResourceStore } = useLdoContext();
return useDocument(uri, binaryResourceStore, options);
}

@ -0,0 +1,10 @@
import { useLdoContext } from "../LdoContext";
import { UseDocumentOptions, useDocument } from "./useDocument";
export function useContainerResource(
uri: string,
options?: UseDocumentOptions
) {
const { containerResourceStore } = useLdoContext();
return useDocument(uri, containerResourceStore, options);
}

@ -0,0 +1,7 @@
import { useLdoContext } from "../LdoContext";
import { UseDocumentOptions, useDocument } from "./useDocument";
export function useDataResource(uri: string, options?: UseDocumentOptions) {
const { dataResourceStore } = useLdoContext();
return useDocument(uri, dataResourceStore, options);
}

@ -0,0 +1,45 @@
import { useEffect, useMemo } from "react";
import {
DocumentStore,
DocumentStoreDependencies,
} from "../document/DocumentStore";
import { FetchableDocument } from "../document/FetchableDocument";
import { useForceUpdate } from "../util/useForceReload";
export interface UseDocumentOptions {
suppressLoadOnMount: boolean;
}
export function useDocument<
DocumentType extends FetchableDocument,
Initializer
>(
initializer: Initializer,
documentStore: DocumentStore<
DocumentType,
Initializer,
DocumentStoreDependencies
>,
options?: UseDocumentOptions
) {
const document = useMemo(() => {
return documentStore.get(initializer);
}, [initializer, documentStore]);
const forceUpdate = useForceUpdate();
useEffect(() => {
// Set up the listener for state update
function onStateUpdateCallback() {
forceUpdate();
}
document.onStateUpdate(onStateUpdateCallback);
// Load the resource if load on mount is true
if (!options?.suppressLoadOnMount) {
document.read();
}
return () => document.offStateUpdate(onStateUpdateCallback);
}, []);
return document;
}

@ -0,0 +1,41 @@
// document
export * from "./document/FetchableDocument";
export * from "./document/DocumentStore";
// document/errors
export * from "./document/errors/DocumentError";
export * from "./document/errors/DocumentFetchError";
// document/accessRules
export * from "./document/accessRules/AccessRules";
export * from "./document/accessRules/AccessRulesStore";
// document/resource
export * from "./document/resource/Resource";
// document/resource/binaryResource
export * from "./document/resource/binaryResource/BinaryResource";
export * from "./document/resource/binaryResource/BinaryResourceStore";
// document/resource/dataResource
export * from "./document/resource/dataResource/DataResource";
export * from "./document/resource/dataResource/DataResourceStore";
// document/resource/containerResource
export * from "./document/resource/dataResource/containerResource/ContainerResource";
export * from "./document/resource/dataResource/containerResource/ContainerResourceStore";
// documentHooks
export * from "./documentHooks/useAccessRules";
export * from "./documentHooks/useBinaryResource";
export * from "./documentHooks/useContainerResource";
export * from "./documentHooks/useDataResource";
export * from "./documentHooks/useDocument";
// ldoHooks
export * from "./ldoHooks/useSubject";
// export
export * from "./useLdo";
export * from "./LdoProvider";
export * from "./SolidAuthProvider";

@ -0,0 +1,36 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* solidContext: JSONLD Context for solid
* =============================================================================
*/
export const solidContext: ContextDefinition = {
type: {
"@id": "@type",
"@container": "@set",
},
Container: "http://www.w3.org/ns/ldp#Container",
Resource: "http://www.w3.org/ns/ldp#Resource",
modified: {
"@id": "http://purl.org/dc/terms/modified",
"@type": "http://www.w3.org/2001/XMLSchema#string",
"@container": "@set",
},
contains: {
"@id": "http://www.w3.org/ns/ldp#contains",
"@type": "@id",
"@container": "@set",
},
Resource2: "http://www.w3.org/ns/iana/media-types/text/turtle#Resource",
mtime: {
"@id": "http://www.w3.org/ns/posix/stat#mtime",
"@type": "http://www.w3.org/2001/XMLSchema#decimal",
"@container": "@set",
},
size: {
"@id": "http://www.w3.org/ns/posix/stat#size",
"@type": "http://www.w3.org/2001/XMLSchema#integer",
"@container": "@set",
},
};

@ -0,0 +1,214 @@
import { Schema } from "shexj";
/**
* =============================================================================
* solidSchema: ShexJ Schema for solid
* =============================================================================
*/
export const solidSchema: Schema = {
type: "Schema",
shapes: [
{
id: "http://www.w3.org/ns/lddps#Container",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
id: "http://www.w3.org/ns/lddps#ContainerShape",
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
valueExpr: {
type: "NodeConstraint",
values: [
"http://www.w3.org/ns/ldp#Container",
"http://www.w3.org/ns/ldp#Resource",
],
},
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "A container on a Solid server",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://purl.org/dc/terms/modified",
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#comment",
object: {
value: "Date modified",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/ldp#contains",
valueExpr: "http://www.w3.org/ns/lddps#Resource",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "Defines a Solid Resource",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/posix/stat#mtime",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#decimal",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "?",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/posix/stat#size",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#integer",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "size of this container",
},
},
],
},
],
},
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
},
},
{
id: "http://www.w3.org/ns/lddps#Resource",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
id: "http://www.w3.org/ns/lddps#ResourceShape",
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
valueExpr: {
type: "NodeConstraint",
values: [
"http://www.w3.org/ns/ldp#Resource",
"http://www.w3.org/ns/iana/media-types/text/turtle#Resource",
],
},
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "Any resource on a Solid server",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://purl.org/dc/terms/modified",
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#comment",
object: {
value: "Date modified",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/posix/stat#mtime",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#decimal",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "?",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://www.w3.org/ns/posix/stat#size",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#integer",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "size of this container",
},
},
],
},
],
},
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
},
},
],
};

@ -0,0 +1,28 @@
import { ShapeType } from "ldo";
import { solidSchema } from "./solid.schema";
import { solidContext } from "./solid.context";
import { Container, Resource } from "./solid.typings";
/**
* =============================================================================
* LDO ShapeTypes solid
* =============================================================================
*/
/**
* Container ShapeType
*/
export const ContainerShapeType: ShapeType<Container> = {
schema: solidSchema,
shape: "http://www.w3.org/ns/lddps#Container",
context: solidContext,
};
/**
* Resource ShapeType
*/
export const ResourceShapeType: ShapeType<Resource> = {
schema: solidSchema,
shape: "http://www.w3.org/ns/lddps#Resource",
context: solidContext,
};

@ -0,0 +1,73 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* Typescript Typings for solid
* =============================================================================
*/
/**
* Container Type
*/
export interface Container {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* A container on a Solid server
*/
type?: (
| {
"@id": "Container";
}
| {
"@id": "Resource";
}
)[];
/**
* Date modified
*/
modified?: string;
/**
* Defines a Solid Resource
*/
contains?: (Resource | Container)[];
/**
* ?
*/
mtime?: number;
/**
* size of this container
*/
size?: number;
}
/**
* Resource Type
*/
export interface Resource {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* Any resource on a Solid server
*/
type?: (
| {
"@id": "Resource";
}
| {
"@id": "Resource2";
}
)[];
/**
* Date modified
*/
modified?: string;
/**
* ?
*/
mtime?: number;
/**
* size of this container
*/
size?: number;
}

@ -0,0 +1,98 @@
import {
ArrayProxyTarget,
ProxyContext,
SubjectProxyTarget,
ProxyContextOptions,
} from "jsonld-dataset-proxy";
import { UpdateManager } from "./UpdateManager";
import { namedNode } from "@rdfjs/data-model";
export class TrackingProxyContext extends ProxyContext {
private updateManager: UpdateManager;
private listener: () => void;
constructor(
options: ProxyContextOptions,
updateManager: UpdateManager,
listener: () => void
) {
super(options);
this.updateManager = updateManager;
this.listener = listener;
}
protected createSubjectHandler(): ProxyHandler<SubjectProxyTarget> {
const baseHandler = super.createSubjectHandler();
const oldGetFunction = baseHandler.get;
const newGetFunction: ProxyHandler<SubjectProxyTarget>["get"] = (
target: SubjectProxyTarget,
key: string | symbol,
receiver
) => {
const subject = target["@id"];
if (typeof key === "symbol") {
// Do Nothing
} else if (key === "@id") {
this.updateManager.registerListener(
[subject, null, null],
this.listener
);
} else if (!this.contextUtil.isArray(key)) {
const predicate = namedNode(this.contextUtil.keyToIri(key));
this.updateManager.registerListener(
[subject, predicate, null],
this.listener
);
}
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 baseHandler;
}
protected createArrayHandler(): ProxyHandler<ArrayProxyTarget> {
const baseHandler = super.createArrayHandler();
const oldGetFunction = baseHandler.get;
const newGetFunction: ProxyHandler<ArrayProxyTarget>["get"] = (
target: ArrayProxyTarget,
key: string | symbol,
receiver
) => {
if (qualifiedArrayMethods.has(key)) {
this.updateManager.registerListener(
[target[0][0], target[0][1], target[0][2]],
this.listener
);
}
return oldGetFunction && oldGetFunction(target, key, receiver);
};
baseHandler.get = newGetFunction;
return baseHandler;
}
}
const qualifiedArrayMethods = new Set([
"forEach",
"map",
"reduce",
Symbol.iterator,
"entries",
"every",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"includes, indexOf",
"keys",
"lastIndexOf",
"reduceRight",
"some",
"values",
]);

@ -0,0 +1,83 @@
import { DatasetChanges, createDataset } from "o-dataset-pack";
import {
QuadMatch,
SubjectType,
PredicateType,
ObjectType,
nodeToString,
} from "jsonld-dataset-proxy";
import { Quad } from "@rdfjs/types";
export type TripleMatch = [QuadMatch[0], QuadMatch[1], QuadMatch[2]];
export class UpdateManager {
private tripleMatchListenerMap: Record<string, Set<() => void>> = {};
private listenerHashMap: Map<() => void, Set<string>> = new Map();
private tripleMatchToHash(tripleMatch: TripleMatch): string {
return `${nodeToString(tripleMatch[0])}${nodeToString(
tripleMatch[1]
)}${nodeToString(tripleMatch[2])}`;
}
registerListener(tripleMatch: TripleMatch, callback: () => void): void {
const hash = this.tripleMatchToHash(tripleMatch);
if (!this.tripleMatchListenerMap[hash]) {
this.tripleMatchListenerMap[hash] = new Set();
}
if (!this.listenerHashMap.has(callback)) {
this.listenerHashMap.set(callback, new Set());
}
this.tripleMatchListenerMap[hash].add(callback);
this.listenerHashMap.get(callback)?.add(hash);
}
removeListener(callback: () => void) {
const hashSet = this.listenerHashMap.get(callback);
if (hashSet) {
hashSet.forEach((hash) => {
this.tripleMatchListenerMap[hash]?.delete(callback);
});
}
}
notifyListenersOfChanges(changes: DatasetChanges<Quad>): void {
const listenersToNotify = new Set<() => void>();
const allQuads = createDataset();
allQuads.addAll(changes.added || []);
allQuads.addAll(changes.removed || []);
// Iterate through all quads looking for any dataset match they effect
allQuads.forEach((tempQuad) => {
// Cast the input because RDFJS types assume RDF 1.2 where a Subject can
// be a Quad
const quad = tempQuad as {
subject: SubjectType;
predicate: PredicateType;
object: ObjectType;
};
const tripleMatches: TripleMatch[] = [
[null, null, null],
[quad.subject, null, null],
[quad.subject, quad.predicate, null],
[quad.subject, null, quad.object],
[null, quad.predicate, null],
[null, quad.predicate, quad.object],
[null, null, quad.object],
[quad.subject, quad.predicate, quad.object],
];
tripleMatches.forEach((tripleMatch) => {
const hash = this.tripleMatchToHash(tripleMatch);
this.tripleMatchListenerMap[hash]?.forEach((callback) => {
listenersToNotify.add(callback);
});
delete this.tripleMatchListenerMap[hash];
});
});
listenersToNotify.forEach((listener) => {
listener();
});
}
}

@ -0,0 +1,59 @@
import {
ContextUtil,
JsonldDatasetProxyBuilder,
SubjectType,
} from "jsonld-dataset-proxy";
import { LdoBuilder, ShapeType } from "ldo";
import { LdoBase } from "ldo/dist/util";
import { useLdoContext } from "../LdoContext";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TrackingProxyContext } from "./helpers/TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
export function useSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectType
): [Type, undefined] | [undefined, Error] {
const { dataset, updateManager } = useLdoContext();
const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
const forceUpdate = useCallback(
() => setForceUpdateCounter((val) => val + 1),
[setForceUpdateCounter]
);
// The main linked data object
const linkedDataObject = useMemo(() => {
// 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"],
},
updateManager,
forceUpdate
);
const builder = new LdoBuilder(
new JsonldDatasetProxyBuilder(proxyContext),
shapeType
);
return builder.fromSubject(subject);
}, [
shapeType,
subject,
dataset,
updateManager,
forceUpdateCounter,
forceUpdate,
]);
useEffect(() => {
// Unregister force update listener upon unmount
return () => updateManager.removeListener(forceUpdate);
}, [shapeType, subject]);
return [linkedDataObject, undefined];
}

@ -0,0 +1,36 @@
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ldp: <http://www.w3.org/ns/ldp#>
PREFIX ldps: <http://www.w3.org/ns/lddps#>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX stat: <http://www.w3.org/ns/posix/stat#>
PREFIX tur: <http://www.w3.org/ns/iana/media-types/text/turtle#>
ldps:Container EXTRA a {
$ldps:ContainerShape (
a [ ldp:Container ldp:Resource ]*
// rdfs:comment "A container on a Solid server";
dct:modified xsd:string?
// rdfs:comment "Date modified";
ldp:contains @ldps:Resource*
// rdfs:comment "Defines a Solid Resource";
stat:mtime xsd:decimal?
// rdfs:comment "?";
stat:size xsd:integer?
// rdfs:comment "size of this container";
)
}
ldps:Resource EXTRA a {
$ldps:ResourceShape (
a [ ldp:Resource tur:Resource ]*
// rdfs:comment "Any resource on a Solid server";
dct:modified xsd:string?
// rdfs:comment "Date modified";
stat:mtime xsd:decimal?
// rdfs:comment "?";
stat:size xsd:integer?
// rdfs:comment "size of this container";
)
}

@ -0,0 +1,117 @@
import { useCallback, useMemo } from "react";
import { useLdoContext } from "./LdoContext";
import {
LdoDataset,
ShapeType,
startTransaction,
transactionChanges,
write,
} from "ldo";
import { splitChangesByGraph } from "./util/splitChangesByGraph";
import { LdoBase } from "ldo/dist/util";
import { Resource } from "./document/resource/Resource";
import { DataResource } from "./document/resource/dataResource/DataResource";
import { BinaryResource } from "./document/resource/binaryResource/BinaryResource";
import { ContainerResource } from "./document/resource/dataResource/containerResource/ContainerResource";
import { AccessRules } from "./document/accessRules/AccessRules";
import { SubjectType } from "jsonld-dataset-proxy";
import { DatasetChanges } from "o-dataset-pack";
import { Quad } from "@rdfjs/types";
export interface UseLdoReturn {
changeData<Type extends LdoBase>(input: Type, ...resources: Resource[]): Type;
commitData(input: LdoBase): Promise<void>;
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectType,
...resources: Resource[]
): Type;
dataset: LdoDataset;
getDataResource: (uri: string) => DataResource;
getBinaryResource: (uri: string) => BinaryResource;
getContainerResource: (uri: string) => ContainerResource;
getAccessRules: (resource: Resource) => AccessRules;
}
export function useLdo(): UseLdoReturn {
const {
dataResourceStore,
containerResourceStore,
binaryResourceStore,
accessRulesStore,
dataset,
} = useLdoContext();
/**
* Begins tracking changes to eventually commit
*/
const changeData = useCallback(
<Type extends LdoBase>(input: Type, ...resources: Resource[]) => {
// Clone the input and set a graph
const [transactionLdo] = write(...resources.map((r) => r.uri)).usingCopy(
input
);
// Start a transaction with the input
startTransaction(transactionLdo);
// Return
return transactionLdo;
},
[dataset]
);
/**
* Begins tracking changes to eventually commit for a new subject
*/
const createData = useCallback(
<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectType,
...resources: Resource[]
) => {
const linkedDataObject = dataset
.usingType(shapeType)
.write(...resources.map((r) => r.uri))
.fromSubject(subject);
startTransaction(linkedDataObject);
return linkedDataObject;
},
[]
);
/**
* Commits the transaction to the global dataset, syncing all subscribing
* components and Solid Pods
*/
const commitData = useCallback(
async (input: LdoBase) => {
const changes = transactionChanges(input);
const changesByGraph = splitChangesByGraph(
changes as DatasetChanges<Quad>
);
// Make queries
await Promise.all(
Array.from(changesByGraph.entries()).map(
async ([graph, datasetChanges]) => {
if (graph.termType === "DefaultGraph") {
return;
}
const resource = dataResourceStore.get(graph.value);
await resource.update(datasetChanges);
}
)
);
},
[dataset, fetch]
);
// Returns the values
return useMemo(
() => ({
dataset,
changeData,
createData,
commitData,
getDataResource: (uri) => dataResourceStore.get(uri),
getBinaryResource: (uri) => binaryResourceStore.get(uri),
getContainerResource: (uri) => containerResourceStore.get(uri),
getAccessRules: (resource) => accessRulesStore.get(resource),
}),
[dataset, changeData, commitData]
);
}

@ -0,0 +1,30 @@
import { DatasetChanges } from "o-dataset-pack";
import { datasetToString } from "ldo/dist/datasetConverters";
import { Quad } from "@rdfjs/types";
import { quad as createQuad } from "@rdfjs/data-model";
// TODO: This file is a clone from the one in the base ldo library. This resused
// code should be put into a helper library once everything becomes a monorepo.
export async function changesToSparqlUpdate(changes: DatasetChanges<Quad>) {
let output = "";
if (changes.removed) {
const removedTriples = changes.removed.map((quad) =>
createQuad(quad.subject, quad.predicate, quad.object)
);
output += `DELETE DATA { ${await datasetToString(removedTriples, {
format: "N-Triples",
})} }`;
}
if (changes.added && changes.removed) {
output += "; ";
}
if (changes.added) {
const addedTriples = changes.added.map((quad) =>
createQuad(quad.subject, quad.predicate, quad.object)
);
output += `INSERT DATA { ${await datasetToString(addedTriples, {
format: "N-Triples",
})} }`;
}
return output.replaceAll("\n", " ");
}

@ -0,0 +1,33 @@
import React, {
createContext,
FunctionComponent,
useContext,
Context,
PropsWithChildren,
} from "react";
export function createGlobalHook<ReturnValues>(useHook: () => ReturnValues): {
Provider: FunctionComponent<PropsWithChildren>;
useGlobal: () => ReturnValues;
Context: Context<ReturnValues>;
} {
// The initial value will be set immediately
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const CreatedContext = createContext<ReturnValues>(undefined);
const Provider: FunctionComponent<PropsWithChildren> = ({ children }) => {
const returnValues = useHook();
return (
<CreatedContext.Provider value={returnValues}>
{children}
</CreatedContext.Provider>
);
};
const useGlobal = () => {
return useContext(CreatedContext);
};
return { Provider, useGlobal, Context: CreatedContext };
}

@ -0,0 +1,51 @@
import { createDataset, DatasetChanges } from "o-dataset-pack";
import { GraphType } from "jsonld-dataset-proxy";
import { Quad } from "@rdfjs/types";
import { defaultGraph, namedNode, quad as createQuad } from "@rdfjs/data-model";
export function graphNodeToString(graphNode: GraphType): string {
return graphNode.termType === "DefaultGraph"
? "defaultGraph()"
: graphNode.value;
}
export function stringToGraphNode(input: string): GraphType {
return input === "defaultGraph()" ? defaultGraph() : namedNode(input);
}
export function splitChangesByGraph(
changes: DatasetChanges<Quad>
): Map<GraphType, DatasetChanges<Quad>> {
const changesMap: Record<string, DatasetChanges<Quad>> = {};
changes.added?.forEach((quad) => {
const graphHash = graphNodeToString(quad.graph as GraphType);
if (!changesMap[graphHash]) {
changesMap[graphHash] = {};
}
if (!changesMap[graphHash].added) {
changesMap[graphHash].added = createDataset();
}
changesMap[graphHash].added?.add(
createQuad(quad.subject, quad.predicate, quad.object, quad.graph)
);
});
changes.removed?.forEach((quad) => {
const graphHash = graphNodeToString(quad.graph as GraphType);
if (!changesMap[graphHash]) {
changesMap[graphHash] = {};
}
if (!changesMap[graphHash].removed) {
changesMap[graphHash].removed = createDataset();
}
changesMap[graphHash].removed?.add(
createQuad(quad.subject, quad.predicate, quad.object, quad.graph)
);
});
const finalMap = new Map<GraphType, DatasetChanges<Quad>>();
Object.entries(changesMap).forEach(([key, value]) => {
finalMap.set(stringToGraphNode(key), value);
});
return finalMap;
}

@ -0,0 +1,6 @@
import { useState } from "react";
export function useForceUpdate() {
const [, setValue] = useState(0);
return () => setValue((value) => value + 1);
}

@ -0,0 +1,5 @@
describe("Trivial", () => {
it("Trivial", () => {
expect(true).toBe(true);
});
});

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"jsx": "react-jsx"
},
"include": ["./src"]
}

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": ".",
"strict": true,
"declaration": true,
"esModuleInterop": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"sourceMap": true,
"jsx": "react-jsx",
},
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}

@ -1,22 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"strict": true,
"declaration": true,
"esModuleInterop": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"lib": [
"es6"
]
},
"exclude": [
"node_modules",
"**/*.spec.ts"
]
"paths": {
"@ldo/*": ["packages/*/src"]
}
}
}
Loading…
Cancel
Save