Finish react refactor and it builds

main
Jackson Morgan 6 months ago
parent c69fef3070
commit 39388bfe83
  1. 88
      package-lock.json
  2. 14
      packages/connected/src/ConnectedLdoDataset.ts
  3. 10
      packages/connected/src/ConnectedLdoTransactionDataset.ts
  4. 11
      packages/connected/src/IConnectedLdoDataset.ts
  5. 5
      packages/connected/src/InvalidIdentifierResource.ts
  6. 14
      packages/connected/src/createConntectedLdoDataset.ts
  7. 1
      packages/connected/src/index.ts
  8. 8
      packages/react/package.json
  9. 97
      packages/react/src/BrowserSolidLdoProvider.tsx
  10. 26
      packages/react/src/SolidAuthContext.ts
  11. 55
      packages/react/src/SolidLdoProvider.tsx
  12. 62
      packages/react/src/UnauthenticatedSolidLdoProvider.tsx
  13. 27
      packages/react/src/createLdoReactMethods.ts
  14. 13
      packages/react/src/index.ts
  15. 58
      packages/react/src/methods/useLdo.ts
  16. 26
      packages/react/src/methods/useMatchObject.ts
  17. 26
      packages/react/src/methods/useMatchSubject.ts
  18. 122
      packages/react/src/methods/useResource.ts
  19. 42
      packages/react/src/methods/useSubject.ts
  20. 55
      packages/react/src/methods/useSubscribeToResource.ts
  21. 21
      packages/react/src/useMatchObject.ts
  22. 21
      packages/react/src/useMatchSubject.ts
  23. 114
      packages/react/src/useResource.ts
  24. 31
      packages/react/src/useRootContainer.ts
  25. 30
      packages/react/src/useSubject.ts
  26. 52
      packages/react/src/useSubscribeToResource.ts
  27. 6
      packages/react/src/util/useTrackingProxy.ts
  28. 14
      packages/react/test/Solid-Integration.test.tsx

88
package-lock.json generated

@ -4632,6 +4632,10 @@
"resolved": "packages/rdf-utils", "resolved": "packages/rdf-utils",
"link": true "link": true
}, },
"node_modules/@ldo/react": {
"resolved": "packages/react",
"link": true
},
"node_modules/@ldo/schema-converter-shex": { "node_modules/@ldo/schema-converter-shex": {
"resolved": "packages/schema-converter-shex", "resolved": "packages/schema-converter-shex",
"link": true "link": true
@ -21375,6 +21379,88 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"packages/react": {
"name": "@ldo/react",
"version": "1.0.0-alpha.1",
"license": "MIT",
"dependencies": {
"@ldo/connected": "^1.0.0-alpha.1",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/rdf-utils": "^1.0.0-alpha.1",
"@ldo/subscribable-dataset": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6"
},
"devDependencies": {
"@rdfjs/types": "^1.0.1",
"@testing-library/react": "^14.1.2",
"@types/jest": "^27.0.3",
"jest-environment-jsdom": "^27.0.0",
"start-server-and-test": "^2.0.3",
"ts-jest": "^27.1.2",
"ts-node": "^10.9.2"
}
},
"packages/react/node_modules/ts-jest": {
"version": "27.1.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
"integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^27.0.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@types/jest": "^27.0.0",
"babel-jest": ">=27.0.0 <28",
"jest": "^27.0.0",
"typescript": ">=3.8 <5.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/jest": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"packages/react/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"packages/schema-converter-shex": { "packages/schema-converter-shex": {
"name": "@ldo/schema-converter-shex", "name": "@ldo/schema-converter-shex",
"version": "1.0.0-alpha.1", "version": "1.0.0-alpha.1",
@ -21525,10 +21611,10 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inrupt/solid-client-authn-browser": "^2.0.0", "@inrupt/solid-client-authn-browser": "^2.0.0",
"@ldo/connected": "^1.0.0-alpha.1",
"@ldo/dataset": "^1.0.0-alpha.1", "@ldo/dataset": "^1.0.0-alpha.1",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1", "@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1", "@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/solid": "^1.0.0-alpha.1",
"@ldo/subscribable-dataset": "^1.0.0-alpha.1", "@ldo/subscribable-dataset": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0", "@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6" "cross-fetch": "^3.1.6"

@ -7,12 +7,11 @@ import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset";
import { InvalidIdentifierResource } from "./InvalidIdentifierResource"; import { InvalidIdentifierResource } from "./InvalidIdentifierResource";
import type { ConnectedContext } from "./ConnectedContext"; import type { ConnectedContext } from "./ConnectedContext";
import type { import type {
GetResourceReturnType,
IConnectedLdoDataset, IConnectedLdoDataset,
ReturnTypeFromArgs,
} from "./IConnectedLdoDataset"; } from "./IConnectedLdoDataset";
import { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset"; import { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset";
import type { SubjectNode } from "@ldo/rdf-utils"; import type { SubjectNode } from "@ldo/rdf-utils";
import type { Resource } from "./Resource";
export class ConnectedLdoDataset< export class ConnectedLdoDataset<
Plugins extends ConnectedPlugin<any, any, any, any>[], Plugins extends ConnectedPlugin<any, any, any, any>[],
@ -69,12 +68,7 @@ export class ConnectedLdoDataset<
Name extends Plugins[number]["name"], Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>, Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string, UriType extends string,
>( >(uri: UriType, pluginName?: Name): GetResourceReturnType<Plugin, UriType> {
uri: UriType,
pluginName?: Name,
): UriType extends Plugin["types"]["uri"]
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ReturnType<Plugin["getResource"]> | InvalidIdentifierResource {
// Check for which plugins this uri is valid // Check for which plugins this uri is valid
const validPlugins = this.plugins const validPlugins = this.plugins
.filter((plugin) => plugin.isUriValid(uri)) .filter((plugin) => plugin.isUriValid(uri))
@ -129,8 +123,8 @@ export class ConnectedLdoDataset<
createData<Type extends LdoBase>( createData<Type extends LdoBase>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
subject: string | SubjectNode, subject: string | SubjectNode,
resource: Resource, resource: Plugins[number]["types"]["resource"],
...additionalResources: Resource[] ...additionalResources: Plugins[number]["types"]["resource"][]
): Type { ): Type {
const resources = [resource, ...additionalResources]; const resources = [resource, ...additionalResources];
const linkedDataObject = this.usingType(shapeType) const linkedDataObject = this.usingType(shapeType)

@ -8,10 +8,9 @@ import {
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils";
import type { ConnectedPlugin } from "./ConnectedPlugin"; import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { ConnectedContext } from "./ConnectedContext"; import type { ConnectedContext } from "./ConnectedContext";
import type { InvalidIdentifierResource } from "./InvalidIdentifierResource";
import type { import type {
GetResourceReturnType,
IConnectedLdoDataset, IConnectedLdoDataset,
ReturnTypeFromArgs,
} from "./IConnectedLdoDataset"; } from "./IConnectedLdoDataset";
import { splitChangesByGraph } from "./util/splitChangesByGraph"; import { splitChangesByGraph } from "./util/splitChangesByGraph";
import type { IgnoredInvalidUpdateSuccess } from "./results/success/UpdateSuccess"; import type { IgnoredInvalidUpdateSuccess } from "./results/success/UpdateSuccess";
@ -88,12 +87,7 @@ export class ConnectedLdoTransactionDataset<Plugins extends ConnectedPlugin[]>
Name extends Plugins[number]["name"], Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>, Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string, UriType extends string,
>( >(uri: UriType, pluginName?: Name): GetResourceReturnType<Plugin, UriType> {
uri: UriType,
pluginName?: Name,
): UriType extends Plugin["types"]["uri"]
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ReturnType<Plugin["getResource"]> | InvalidIdentifierResource {
return this.context.dataset.getResource(uri, pluginName); return this.context.dataset.getResource(uri, pluginName);
} }

@ -10,6 +10,13 @@ export type ReturnTypeFromArgs<Func, Arg> = Func extends (
? R ? R
: never; : never;
export type GetResourceReturnType<
Plugin extends ConnectedPlugin,
UriType extends string,
> = UriType extends Plugin["types"]["uri"]
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ReturnType<Plugin["getResource"]> | InvalidIdentifierResource;
export interface IConnectedLdoDataset<Plugins extends ConnectedPlugin[]> export interface IConnectedLdoDataset<Plugins extends ConnectedPlugin[]>
extends LdoDataset { extends LdoDataset {
/** /**
@ -36,9 +43,7 @@ export interface IConnectedLdoDataset<Plugins extends ConnectedPlugin[]>
>( >(
uri: UriType, uri: UriType,
pluginName?: Name, pluginName?: Name,
): UriType extends Plugin["types"]["uri"] ): GetResourceReturnType<Plugin, UriType>;
? ReturnTypeFromArgs<Plugin["getResource"], UriType>
: ReturnType<Plugin["getResource"]> | InvalidIdentifierResource;
createResource< createResource<
Name extends Plugins[number]["name"], Name extends Plugins[number]["name"],

@ -47,7 +47,10 @@ export class InvalidIdentifierResource
async update(): Promise<InvalidUriError<this>> { async update(): Promise<InvalidUriError<this>> {
return this.status; return this.status;
} }
async subscribeToNotifications(_callbacks): Promise<string> { async subscribeToNotifications(_callbacks?: {
onNotification: (message: unknown) => void;
onNotificationError: (err: Error) => void;
}): Promise<string> {
throw new Error("Cannot subscribe to an invalid resource."); throw new Error("Cannot subscribe to an invalid resource.");
} }
async unsubscribeFromNotifications(_subscriptionId: string): Promise<void> { async unsubscribeFromNotifications(_subscriptionId: string): Promise<void> {

@ -0,0 +1,14 @@
import { createDatasetFactory } from "@ldo/dataset";
import { ConnectedLdoDataset } from "./ConnectedLdoDataset";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset";
export function createConnectedLdoDataset<Plugins extends ConnectedPlugin[]>(
plugins: Plugins,
): ConnectedLdoDataset<Plugins> {
return new ConnectedLdoDataset(
plugins,
createDatasetFactory(),
createTransactionDatasetFactory(),
);
}

@ -6,6 +6,7 @@ export * from "./Resource";
export * from "./InvalidIdentifierResource"; export * from "./InvalidIdentifierResource";
export * from "./ConnectedContext"; export * from "./ConnectedContext";
export * from "./methods"; export * from "./methods";
export * from "./createConntectedLdoDataset";
export * from "./util/splitChangesByGraph"; export * from "./util/splitChangesByGraph";

@ -1,7 +1,7 @@
{ {
"name": "@ldo/react", "name": "@ldo/react",
"version": "1.0.0-alpha.1", "version": "1.0.0-alpha.1",
"description": "A React library for LDO and Solid", "description": "A React library for LDO.",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "tsc --project tsconfig.build.json", "build": "tsc --project tsconfig.build.json",
@ -26,7 +26,6 @@
}, },
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme", "homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme",
"devDependencies": { "devDependencies": {
"@ldo/rdf-utils": "^1.0.0-alpha.1",
"@rdfjs/types": "^1.0.1", "@rdfjs/types": "^1.0.1",
"@testing-library/react": "^14.1.2", "@testing-library/react": "^14.1.2",
"@types/jest": "^27.0.3", "@types/jest": "^27.0.3",
@ -36,11 +35,10 @@
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
}, },
"dependencies": { "dependencies": {
"@inrupt/solid-client-authn-browser": "^2.0.0", "@ldo/connected": "^1.0.0-alpha.1",
"@ldo/dataset": "^1.0.0-alpha.1",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1", "@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1", "@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/connected": "^1.0.0-alpha.1", "@ldo/rdf-utils": "^1.0.0-alpha.1",
"@ldo/subscribable-dataset": "^1.0.0-alpha.1", "@ldo/subscribable-dataset": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0", "@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6" "cross-fetch": "^3.1.6"

@ -1,97 +0,0 @@
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>
);
};

@ -1,26 +0,0 @@
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);
}

@ -1,55 +0,0 @@
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>
);
};

@ -1,62 +0,0 @@
/* 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,27 @@
import { createUseLdo } from "./methods/useLdo";
import {
createConnectedLdoDataset,
type ConnectedPlugin,
} from "@ldo/connected";
import { createUseMatchObject } from "./methods/useMatchObject";
import { createUseMatchSubject } from "./methods/useMatchSubject";
import { createUseResource } from "./methods/useResource";
import { createUseSubject } from "./methods/useSubject";
import { createUseSubscribeToResource } from "./methods/useSubscribeToResource";
export function createLdoReactMethods<Plugins extends ConnectedPlugin[]>(
plugins: Plugins,
) {
const dataset = createConnectedLdoDataset(plugins);
dataset.setMaxListeners(1000);
return {
dataset,
useLdo: createUseLdo(dataset),
useMatchObject: createUseMatchObject(dataset),
useMatchSubject: createUseMatchSubject(dataset),
useResource: createUseResource(dataset),
useSubject: createUseSubject(dataset),
useSubscribeToResource: createUseSubscribeToResource(dataset),
};
}

@ -1,12 +1 @@
export * from "./BrowserSolidLdoProvider"; export * from "./createLdoReactMethods";
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";

@ -1,15 +1,16 @@
import type { LdoBase, ShapeType } from "@ldo/ldo"; import {
changeData,
type ConnectedLdoDataset,
type ConnectedLdoTransactionDataset,
type ConnectedPlugin,
} from "@ldo/connected";
import { getDataset, type LdoBase, type ShapeType } from "@ldo/ldo";
import type { SubjectNode } from "@ldo/rdf-utils"; import type { SubjectNode } from "@ldo/rdf-utils";
import type {
Resource,
SolidLdoDataset,
SolidLdoTransactionDataset,
} from "@ldo/solid";
import { changeData, commitData } from "@ldo/solid";
export interface UseLdoMethods { export interface UseLdoMethods<Plugins extends ConnectedPlugin[]> {
dataset: SolidLdoDataset; dataset: ConnectedLdoDataset<Plugins>;
getResource: SolidLdoDataset["getResource"]; getResource: ConnectedLdoDataset<Plugins>["getResource"];
setContext: ConnectedLdoDataset<Plugins>["setContext"];
getSubject<Type extends LdoBase>( getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
subject: string | SubjectNode, subject: string | SubjectNode,
@ -17,26 +18,32 @@ export interface UseLdoMethods {
createData<Type extends LdoBase>( createData<Type extends LdoBase>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
subject: string | SubjectNode, subject: string | SubjectNode,
resource: Resource, resource: Plugins[number]["types"]["resource"],
...additionalResources: Resource[] ...additionalResources: Plugins[number]["types"]["resource"][]
): Type; ): Type;
changeData<Type extends LdoBase>( changeData<Type extends LdoBase>(
input: Type, input: Type,
resource: Resource, resource: Plugins[number]["types"]["resource"],
...additionalResources: Resource[] ...additionalResources: Plugins[number]["types"]["resource"][]
): Type; ): Type;
commitData( commitData(
input: LdoBase, input: LdoBase,
): ReturnType<SolidLdoTransactionDataset["commitToPod"]>; ): ReturnType<ConnectedLdoTransactionDataset<Plugins>["commitToRemote"]>;
} }
export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { export function createUseLdo<Plugins extends ConnectedPlugin[]>(
return { dataset: ConnectedLdoDataset<Plugins>,
dataset: dataset, ) {
return (): UseLdoMethods<Plugins> => ({
dataset,
/** /**
* Gets a resource * Gets a resource
*/ */
getResource: dataset.getResource.bind(dataset), getResource: dataset.getResource.bind(dataset),
/**
* Set the context
*/
setContext: dataset.setContext.bind(dataset),
/** /**
* Returns a Linked Data Object for a subject * Returns a Linked Data Object for a subject
* @param shapeType The shape type for the data * @param shapeType The shape type for the data
@ -59,8 +66,8 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
createData<Type extends LdoBase>( createData<Type extends LdoBase>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
subject: string | SubjectNode, subject: string | SubjectNode,
resource: Resource, resource: Plugins[number]["types"]["resource"],
...additionalResources: Resource[] ...additionalResources: Plugins[number]["types"]["resource"][]
): Type { ): Type {
return dataset.createData( return dataset.createData(
shapeType, shapeType,
@ -79,6 +86,13 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
* Commits the transaction to the global dataset, syncing all subscribing * Commits the transaction to the global dataset, syncing all subscribing
* components and Solid Pods * components and Solid Pods
*/ */
commitData: commitData, commitData(
}; input: LdoBase,
): ReturnType<ConnectedLdoTransactionDataset<Plugins>["commitToRemote"]> {
const inputDataset = getDataset(
input,
) as ConnectedLdoTransactionDataset<Plugins>;
return inputDataset.commitToRemote();
},
});
} }

@ -0,0 +1,26 @@
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";
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
export function createUseMatchObject<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
) {
return 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, dataset);
};
}

@ -0,0 +1,26 @@
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";
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
export function createUseMatchSubject<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
) {
return 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, dataset);
};
}

@ -0,0 +1,122 @@
import { useMemo, useEffect, useRef, useState, useCallback } from "react";
import type {
ConnectedLdoDataset,
ConnectedPlugin,
GetResourceReturnType,
Resource,
} from "@ldo/connected";
export interface UseResourceOptions<Name> {
pluginName?: Name;
suppressInitialRead?: boolean;
reloadOnMount?: boolean;
subscribe?: boolean;
}
export type useResourceType<Plugins extends ConnectedPlugin[]> = {
<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string,
>(
uri: UriType,
options?: UseResourceOptions<Name>,
): GetResourceReturnType<Plugin, UriType>;
<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string,
>(
uri?: UriType,
options?: UseResourceOptions<Name>,
): GetResourceReturnType<Plugin, UriType> | undefined;
};
export function createUseResource<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
): useResourceType<Plugins> {
return function useResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
UriType extends string,
>(
uri?: UriType,
options?: UseResourceOptions<Name>,
): GetResourceReturnType<Plugin, UriType> | undefined {
const subscriptionIdRef = useRef<string | undefined>();
// Get the resource
const resource = useMemo(() => {
if (uri) {
const resource = dataset.getResource(uri);
// Run read operations if necissary
if (!options?.suppressInitialRead) {
if (options?.reloadOnMount) {
resource.read();
} else {
resource.readIfUnfetched();
}
}
return resource;
}
return undefined;
}, [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 as
| GetResourceReturnType<Plugin, UriType>
| undefined;
};
}

@ -0,0 +1,42 @@
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";
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
export type useSubjectType = {
<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type;
<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject?: string | SubjectNode,
): Type | undefined;
<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject?: string | SubjectNode,
): Type | undefined;
};
export function createUseSubject<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
): useSubjectType {
return 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, dataset);
};
}

@ -0,0 +1,55 @@
import { useEffect, useRef } from "react";
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
export function createUseSubscribeToResource<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
) {
return function useSubscribeToResource(...uris: string[]): void {
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);
},
),
);
};
}, []);
};
}

@ -1,21 +0,0 @@
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);
}

@ -1,21 +0,0 @@
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);
}

@ -1,114 +0,0 @@
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;
}

@ -1,31 +0,0 @@
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);
}

@ -1,30 +0,0 @@
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);
}

@ -1,52 +0,0 @@
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);
},
),
);
};
}, []);
}

@ -3,18 +3,16 @@ import {
JsonldDatasetProxyBuilder, JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy"; } from "@ldo/jsonld-dataset-proxy";
import { LdoBuilder } from "@ldo/ldo"; import { LdoBuilder } from "@ldo/ldo";
import type { LdoBase, ShapeType } from "@ldo/ldo"; import type { LdoBase, LdoDataset, ShapeType } from "@ldo/ldo";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { TrackingProxyContext } from "./TrackingProxyContext"; import { TrackingProxyContext } from "./TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model"; import { defaultGraph } from "@rdfjs/data-model";
import { useLdo } from "../SolidLdoProvider";
export function useTrackingProxy<Type extends LdoBase, ReturnType>( export function useTrackingProxy<Type extends LdoBase, ReturnType>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
createLdo: (builder: LdoBuilder<Type>) => ReturnType, createLdo: (builder: LdoBuilder<Type>) => ReturnType,
dataset: LdoDataset,
): ReturnType { ): ReturnType {
const { dataset } = useLdo();
const [forceUpdateCounter, setForceUpdateCounter] = useState(0); const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
const forceUpdate = useCallback( const forceUpdate = useCallback(
() => setForceUpdateCounter((val) => val + 1), () => setForceUpdateCounter((val) => val + 1),

@ -8,15 +8,15 @@ import {
setUpServer, setUpServer,
} from "./setUpServer"; } from "./setUpServer";
import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider"; import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider";
import { useResource } from "../src/useResource"; import { useResource } from "../src/methods/useResource";
import { useRootContainerFor } from "../src/useRootContainer"; import { useRootContainerFor } from "../src/methods/useRootContainer";
import { useLdo } from "../src/SolidLdoProvider"; import { useLdo } from "../src/createLdoReactMethods";
import { PostShShapeType } from "./.ldo/post.shapeTypes"; import { PostShShapeType } from "./.ldo/post.shapeTypes";
import type { PostSh } from "./.ldo/post.typings"; import type { PostSh } from "./.ldo/post.typings";
import { useSubject } from "../src/useSubject"; import { useSubject } from "../src/methods/useSubject";
import { useMatchSubject } from "../src/useMatchSubject"; import { useMatchSubject } from "../src/methods/useMatchSubject";
import { useMatchObject } from "../src/useMatchObject"; import { useMatchObject } from "../src/methods/useMatchObject";
import { useSubscribeToResource } from "../src/useSubscribeToResource"; import { useSubscribeToResource } from "../src/methods/useSubscribeToResource";
// Use an increased timeout, since the CSS server takes too much setup time. // Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000); jest.setTimeout(40_000);

Loading…
Cancel
Save