Merge pull request #79 from o-development/feat/link-traversal

Feat/link traversal
main
jaxoncreed 4 months ago committed by GitHub
commit ff92c8b67a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      lerna.json
  2. 696
      package-lock.json
  3. 6
      packages/cli/package.json
  4. 12
      packages/connected-nextgraph/package.json
  5. 1
      packages/connected-solid/jest.config.js
  6. 11
      packages/connected-solid/package.json
  7. 7
      packages/connected-solid/src/SolidConnectedPlugin.ts
  8. 340
      packages/connected-solid/test/Integration.test.ts
  9. 3
      packages/connected-solid/test/setup-tests.ts
  10. 2
      packages/connected/jest.config.js
  11. 19
      packages/connected/package.json
  12. 43
      packages/connected/src/ConnectedLdoBuilder.ts
  13. 23
      packages/connected/src/ConnectedLdoDataset.ts
  14. 24
      packages/connected/src/ConnectedLdoTransactionDataset.ts
  15. 2
      packages/connected/src/createConntectedLdoDataset.ts
  16. 21
      packages/connected/src/index.ts
  17. 287
      packages/connected/src/linkTraversal/ResourceLinkQuery.ts
  18. 162
      packages/connected/src/linkTraversal/exploreLinks.ts
  19. 6
      packages/connected/src/notifications/NotificationSubscription.ts
  20. 0
      packages/connected/src/notifications/SubscriptionCallbacks.ts
  21. 17
      packages/connected/src/trackingProxy/TrackingProxyContext.ts
  22. 0
      packages/connected/src/trackingProxy/TrackingSetProxy.ts
  23. 0
      packages/connected/src/trackingProxy/TrackingSubjectProxy.ts
  24. 42
      packages/connected/src/trackingProxy/createTrackingProxy.ts
  25. 4
      packages/connected/src/types/ConnectedContext.ts
  26. 4
      packages/connected/src/types/ConnectedPlugin.ts
  27. 15
      packages/connected/src/types/IConnectedLdoBuilder.ts
  28. 36
      packages/connected/src/types/IConnectedLdoDataset.ts
  29. 126
      packages/connected/src/types/ILinkQuery.ts
  30. 459
      packages/connected/test/.ldo/solidProfile.context.ts
  31. 749
      packages/connected/test/.ldo/solidProfile.schema.ts
  32. 71
      packages/connected/test/.ldo/solidProfile.shapeTypes.ts
  33. 293
      packages/connected/test/.ldo/solidProfile.typings.ts
  34. 121
      packages/connected/test/.shapes/solidProfile.shex
  35. 6
      packages/connected/test/ErrorResult.test.ts
  36. 58
      packages/connected/test/LinkTraversalData.ts
  37. 216
      packages/connected/test/LinkTraversalIntegration.test.ts
  38. 73
      packages/connected/test/MockResource.ts
  39. 59
      packages/connected/test/mocks/MockResource.ts
  40. 3
      packages/connected/test/util/wait.ts
  41. 4
      packages/dataset/package.json
  42. 6
      packages/jsonld-dataset-proxy/package.json
  43. 10
      packages/ldo/package.json
  44. 4
      packages/ldo/src/LdoBuilder.ts
  45. 1
      packages/ldo/test/profileData.ts
  46. 2
      packages/rdf-utils/package.json
  47. 1
      packages/react/README.md
  48. 12
      packages/react/package.json
  49. 3
      packages/react/src/createLdoReactMethods.tsx
  50. 4
      packages/react/src/index.ts
  51. 72
      packages/react/src/methods/useLinkQuery.ts
  52. 26
      packages/react/src/util/useTrackingProxy.ts
  53. 6
      packages/schema-converter-shex/package.json
  54. 27
      packages/solid-react/README.md
  55. 10
      packages/solid-react/package.json
  56. 1
      packages/solid-react/src/defaultIntance.ts
  57. 459
      packages/solid-react/test/.ldo/solidProfile.context.ts
  58. 749
      packages/solid-react/test/.ldo/solidProfile.schema.ts
  59. 71
      packages/solid-react/test/.ldo/solidProfile.shapeTypes.ts
  60. 293
      packages/solid-react/test/.ldo/solidProfile.typings.ts
  61. 90
      packages/solid-react/test/Solid-Integration.test.tsx
  62. 14
      packages/solid-react/test/setUpServer.ts
  63. 7
      packages/solid-react/test/test-server/configs/template/wac/link-query/main-profile.ttl
  64. 7
      packages/solid-react/test/test-server/configs/template/wac/link-query/other-profile.ttl
  65. 7
      packages/solid-react/test/test-server/configs/template/wac/link-query/third-profile.ttl
  66. 8
      packages/solid-type-index/package.json
  67. 2
      packages/solid-type-index/src/react/useInstanceUris.ts
  68. 2
      packages/solid-type-index/src/util/Options.ts
  69. 12
      packages/solid-type-index/test/General.test.tsx
  70. 8
      packages/subscribable-dataset/package.json
  71. 29
      packages/subscribable-dataset/src/SubscribableDataset.ts
  72. 2
      packages/subscribable-dataset/src/types.ts
  73. 30
      packages/subscribable-dataset/test/SubscribableDataset.test.ts
  74. 5
      packages/test-solid-server/.eslintrc
  75. 2
      packages/test-solid-server/.gitignore
  76. 21
      packages/test-solid-server/LICENSE.txt
  77. 17
      packages/test-solid-server/Readme.md
  78. 34
      packages/test-solid-server/package.json
  79. 28
      packages/test-solid-server/src/authFetch.ts
  80. 44
      packages/test-solid-server/src/configs/server-config-without-websocket.json
  81. 0
      packages/test-solid-server/src/configs/server-config.json
  82. 0
      packages/test-solid-server/src/configs/solid-css-seed.json
  83. 15
      packages/test-solid-server/src/createServer.ts
  84. 3
      packages/test-solid-server/src/index.ts
  85. 75
      packages/test-solid-server/src/resourceUtils.ts
  86. 60
      packages/test-solid-server/src/setupTestServer.ts
  87. 13
      packages/test-solid-server/tsconfig.build.json
  88. 4
      packages/traverser-shexj/package.json
  89. 2
      packages/type-traverser/package.json
  90. 4
      tsconfig.json

@ -1,4 +1,4 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "1.0.0-alpha.8"
"version": "1.0.0-alpha.9"
}

696
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "@ldo/cli",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "A Command Line Interface for Linked Data Objects",
"main": "./dist/index.js",
"bin": {
@ -40,8 +40,8 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"@ldo/ldo": "^1.0.0-alpha.3",
"@ldo/schema-converter-shex": "^1.0.0-alpha.3",
"@ldo/ldo": "^1.0.0-alpha.9",
"@ldo/schema-converter-shex": "^1.0.0-alpha.9",
"@shexjs/parser": "^1.0.0-alpha.24",
"child-process-promise": "^2.2.1",
"commander": "^9.3.0",

@ -1,6 +1,6 @@
{
"name": "@ldo/connected-nextgraph",
"version": "1.0.0-alpha.8",
"version": "1.0.0-alpha.9",
"description": "A plugin for @ldo/connected to work with the Solid ecosystem.",
"main": "dist/index.js",
"scripts": {
@ -26,7 +26,7 @@
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid#readme",
"devDependencies": {
"@inrupt/solid-client-authn-core": "^2.2.6",
"@ldo/cli": "^1.0.0-alpha.3",
"@ldo/cli": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"@solid-notifications/types": "^0.1.2",
@ -42,10 +42,10 @@
"typedoc-plugin-markdown": "^3.17.1"
},
"dependencies": {
"@ldo/connected": "^1.0.0-alpha.3",
"@ldo/dataset": "^1.0.0-alpha.3",
"@ldo/ldo": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/connected": "^1.0.0-alpha.9",
"@ldo/dataset": "^1.0.0-alpha.9",
"@ldo/ldo": "^1.0.0-alpha.9",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@solid-notifications/subscription": "^0.1.2",
"cross-fetch": "^3.1.6",
"http-link-header": "^1.1.1",

@ -3,7 +3,6 @@ const sharedConfig = require("../../jest.config.js");
module.exports = {
...sharedConfig,
rootDir: "./",
setupFiles: ["<rootDir>/test/setup-tests.ts"],
transform: {
"^.+\\.(ts|tsx)?$": "ts-jest",
"^.+\\.(js|jsx)$": "babel-jest",

@ -1,6 +1,6 @@
{
"name": "@ldo/connected-solid",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "A plugin for @ldo/connected to work with the Solid ecosystem.",
"main": "dist/index.js",
"scripts": {
@ -25,7 +25,8 @@
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid#readme",
"devDependencies": {
"@inrupt/solid-client-authn-core": "^2.2.6",
"@ldo/cli": "^1.0.0-alpha.3",
"@ldo/cli": "^1.0.0-alpha.9",
"@ldo/test-solid-server": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"@solid-notifications/types": "^0.1.2",
@ -38,9 +39,9 @@
"typedoc-plugin-markdown": "^3.17.1"
},
"dependencies": {
"@ldo/dataset": "^1.0.0-alpha.3",
"@ldo/ldo": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/dataset": "^1.0.0-alpha.9",
"@ldo/ldo": "^1.0.0-alpha.9",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@solid-notifications/subscription": "^0.1.2",
"cross-fetch": "^3.1.6",
"http-link-header": "^1.1.1",

@ -80,4 +80,11 @@ export const solidConnectedPlugin: SolidConnectedPlugin = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore "Types" only exists for the typing system
types: {},
normalizeUri(uri: SolidUri): SolidUri {
const url = new URL(uri);
url.hash = "";
url.search = "";
return url.toString() as SolidUri;
},
};

@ -1,5 +1,3 @@
import type { App } from "@solid/community-server";
import { ROOT_CONTAINER, WEB_ID, createApp } from "./solidServer.helper";
import {
namedNode,
quad as createQuad,
@ -16,9 +14,7 @@ import type {
} from "../src/requester/results/error/HttpErrorResult";
import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError";
import type { GetStorageContainerFromWebIdSuccess } from "../src/requester/results/success/CheckRootContainerSuccess";
import { generateAuthFetch } from "./authFetch.helper";
import { wait } from "./utils.helper";
import fs from "fs/promises";
import path from "path";
import type { GetWacRuleSuccess, UpdateResultError, WacRule } from "../src";
import {
@ -45,7 +41,11 @@ import {
ConnectedLdoTransactionDataset,
} from "@ldo/connected";
import { getStorageFromWebId } from "../src/getStorageFromWebId";
import type { ResourceInfo } from "@ldo/test-solid-server";
import { createApp, setupServer } from "@ldo/test-solid-server";
const ROOT_CONTAINER = "http://localhost:3001/";
const WEB_ID = "http://localhost:3001/example/profile/card#me";
const TEST_CONTAINER_SLUG = "test_ldo/";
const TEST_CONTAINER_URI =
`${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as SolidContainerUri;
@ -93,7 +93,7 @@ const TEST_CONTAINER_TTL = `@prefix dc: <http://purl.org/dc/terms/>.
posix:size 522.
<sample.txt> posix:mtime 1697810234;
posix:size 10.`;
const TEST_CONTAINER_ACL_URI = `${TEST_CONTAINER_URI}.acl`;
const _TEST_CONTAINER_ACL_URI = `${TEST_CONTAINER_URI}.acl`;
const TEST_CONTAINER_ACL = `<#b30e3fd1-b5a8-4763-ad9d-e95de9cf7933> a <http://www.w3.org/ns/auth/acl#Authorization>;
<http://www.w3.org/ns/auth/acl#accessTo> <${TEST_CONTAINER_URI}>;
<http://www.w3.org/ns/auth/acl#default> <${TEST_CONTAINER_URI}>;
@ -106,6 +106,57 @@ const SAMPLE_PROFILE_TTL = `
<${SAMPLE_PROFILE_URI}> pim:storage <https://example.com/A/>, <https://example.com/B/> .
`;
const resourceInfo: ResourceInfo = {
slug: TEST_CONTAINER_SLUG,
isContainer: true,
contains: [
{
slug: ".acl",
isContainer: false,
mimeType: "text/turtle",
data: TEST_CONTAINER_ACL,
},
{
slug: "sample.ttl",
isContainer: false,
mimeType: "text/turtle",
data: SPIDER_MAN_TTL,
},
{
slug: "sample.txt",
isContainer: false,
mimeType: "text/plain",
data: "some text.",
},
{
slug: "profile.ttl",
isContainer: false,
mimeType: "text/turtle",
data: SAMPLE_PROFILE_TTL,
},
{
slug: "sample_container/",
isContainer: true,
shouldNotInit: true,
contains: [],
},
{
slug: SAMPLE2_DATA_SLUG,
isContainer: false,
shouldNotInit: true,
mimeType: "text/turtle",
data: "",
},
{
slug: SAMPLE2_BINARY_SLUG,
isContainer: false,
shouldNotInit: true,
mimeType: "text/plain",
data: "",
},
],
};
async function testRequestLoads<ReturnVal>(
request: () => Promise<ReturnVal>,
loadingResource: SolidLeaf | SolidContainer,
@ -149,98 +200,13 @@ async function testRequestLoads<ReturnVal>(
}
describe("Integration", () => {
let app: App;
let authFetch: typeof fetch;
let fetchMock: jest.Mock<
Promise<Response>,
[input: RequestInfo | URL, init?: RequestInit | undefined]
>;
let solidLdoDataset: ConnectedLdoDataset<SolidConnectedPlugin[]>;
let previousJestId: string | undefined;
let previousNodeEnv: string | undefined;
beforeAll(async () => {
// Remove Jest ID so that community solid server doesn't use the Jest Import
previousJestId = process.env.JEST_WORKER_ID;
previousNodeEnv = process.env.NODE_ENV;
delete process.env.JEST_WORKER_ID;
process.env.NODE_ENV = "other_test";
// Start up the server
app = await createApp();
await app.start();
authFetch = await generateAuthFetch();
});
afterAll(async () => {
app.stop();
process.env.JEST_WORKER_ID = previousJestId;
process.env.NODE_ENV = previousNodeEnv;
const testDataPath = path.join(__dirname, "./data");
await fs.rm(testDataPath, { recursive: true, force: true });
});
const s = setupServer(3001, resourceInfo);
beforeEach(async () => {
fetchMock = jest.fn(authFetch);
solidLdoDataset = createSolidLdoDataset();
solidLdoDataset.setContext("solid", { fetch: fetchMock });
// Create a new document called sample.ttl
await authFetch(ROOT_CONTAINER, {
method: "POST",
headers: {
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"',
slug: TEST_CONTAINER_SLUG,
},
});
await authFetch(TEST_CONTAINER_ACL_URI, {
method: "PUT",
headers: {
"content-type": "text/turtle",
},
body: TEST_CONTAINER_ACL,
});
await Promise.all([
authFetch(TEST_CONTAINER_URI, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "sample.ttl" },
body: SPIDER_MAN_TTL,
}),
authFetch(TEST_CONTAINER_URI, {
method: "POST",
headers: { "content-type": "text/plain", slug: "sample.txt" },
body: "some text.",
}),
authFetch(TEST_CONTAINER_URI, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "profile.ttl" },
body: SAMPLE_PROFILE_TTL,
}),
]);
});
afterEach(async () => {
await Promise.all([
authFetch(SAMPLE_DATA_URI, {
method: "DELETE",
}),
authFetch(SAMPLE2_DATA_URI, {
method: "DELETE",
}),
authFetch(SAMPLE_BINARY_URI, {
method: "DELETE",
}),
authFetch(SAMPLE2_BINARY_URI, {
method: "DELETE",
}),
authFetch(SAMPLE_PROFILE_URI, {
method: "DELETE",
}),
authFetch(SAMPLE_CONTAINER_URI, {
method: "DELETE",
}),
]);
await authFetch(TEST_CONTAINER_URI, {
method: "DELETE",
});
solidLdoDataset.setContext("solid", { fetch: s.fetchMock });
});
/**
@ -338,7 +304,7 @@ describe("Integration", () => {
});
it("Returns an ServerError when an 500 error is returned", async () => {
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
const result = await testRequestLoads(() => resource.read(), resource, {
isLoading: true,
@ -350,7 +316,7 @@ describe("Integration", () => {
});
it("Returns an Unauthorized error if a 403 error is returned", async () => {
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 403 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 403 }));
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
const result = await testRequestLoads(() => resource.read(), resource, {
isLoading: true,
@ -362,7 +328,7 @@ describe("Integration", () => {
});
it("Returns an UnauthenticatedError on an 401 error is returned", async () => {
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 401 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 401 }));
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
const result = await testRequestLoads(() => resource.read(), resource, {
isLoading: true,
@ -374,7 +340,7 @@ describe("Integration", () => {
});
it("Returns an UnexpectedHttpError on a strange number error is returned", async () => {
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 399 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 399 }));
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
const result = await testRequestLoads(() => resource.read(), resource, {
isLoading: true,
@ -386,7 +352,7 @@ describe("Integration", () => {
});
it("Returns a NoncompliantPod error when no content type is returned", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(undefined, { status: 200, headers: {} }),
);
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
@ -404,7 +370,7 @@ describe("Integration", () => {
});
it("Returns a NoncompliantPod error if invalid turtle is provided", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("Error", {
status: 200,
headers: new Headers({ "content-type": "text/turtle" }),
@ -425,7 +391,7 @@ describe("Integration", () => {
});
it("Parses Turtle even when the content type contains parameters", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(SPIDER_MAN_TTL, {
status: 200,
headers: new Headers({ "content-type": "text/turtle;charset=utf-8" }),
@ -443,7 +409,7 @@ describe("Integration", () => {
});
it("Returns an UnexpectedResourceError if an unknown error is triggered", async () => {
fetchMock.mockRejectedValueOnce(new Error("Something happened."));
s.fetchMock.mockRejectedValueOnce(new Error("Something happened."));
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
const result = await testRequestLoads(() => resource.read(), resource, {
isLoading: true,
@ -457,7 +423,7 @@ describe("Integration", () => {
});
it("Does not return an error if there is no link header for a container request", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 200,
headers: new Headers({ "content-type": "text/turtle" }),
@ -489,7 +455,7 @@ describe("Integration", () => {
resource.read(),
]);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(s.fetchMock).toHaveBeenCalledTimes(1);
expect(result.type).toBe("dataReadSuccess");
expect(result1.type).toBe("dataReadSuccess");
});
@ -502,7 +468,7 @@ describe("Integration", () => {
resource.read(),
]);
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(s.fetchMock).toHaveBeenCalledTimes(3);
expect(result.type).toBe("dataReadSuccess");
expect(result1.type).toBe("dataReadSuccess");
});
@ -551,9 +517,9 @@ describe("Integration", () => {
it("returns a cached existing container", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
await resource.read();
fetchMock.mockClear();
s.fetchMock.mockClear();
const result = await resource.readIfUnfetched();
expect(fetchMock).not.toHaveBeenCalled();
expect(s.fetchMock).not.toHaveBeenCalled();
expect(result.type).toBe("containerReadSuccess");
expect(resource.children().length).toBe(3);
});
@ -561,7 +527,7 @@ describe("Integration", () => {
it("returns a cached existing data leaf", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
await resource.read();
fetchMock.mockClear();
s.fetchMock.mockClear();
const result = await resource.readIfUnfetched();
expect(result.type).toBe("dataReadSuccess");
expect(
@ -576,7 +542,7 @@ describe("Integration", () => {
it("returns a cached existing binary leaf", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI);
await resource.read();
fetchMock.mockClear();
s.fetchMock.mockClear();
const result = await resource.readIfUnfetched();
expect(result.type).toBe("binaryReadSuccess");
});
@ -584,18 +550,18 @@ describe("Integration", () => {
it("returns a cached absent container", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI);
await resource.read();
fetchMock.mockClear();
s.fetchMock.mockClear();
const result = await resource.readIfUnfetched();
expect(fetchMock).not.toHaveBeenCalled();
expect(s.fetchMock).not.toHaveBeenCalled();
expect(result.type).toBe("absentReadSuccess");
});
it("returns a cached absent leaf", async () => {
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
await resource.read();
fetchMock.mockClear();
s.fetchMock.mockClear();
const result = await resource.readIfUnfetched();
expect(fetchMock).not.toHaveBeenCalled();
expect(s.fetchMock).not.toHaveBeenCalled();
expect(result.type).toBe("absentReadSuccess");
});
});
@ -614,19 +580,19 @@ describe("Integration", () => {
});
it("Returns an error if there is no root container", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 200,
headers: new Headers({ "content-type": "text/turtle" }),
}),
);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 200,
headers: new Headers({ "content-type": "text/turtle" }),
}),
);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 200,
headers: new Headers({ "content-type": "text/turtle" }),
@ -641,7 +607,7 @@ describe("Integration", () => {
});
it("An error to be returned if a common http error is encountered", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 500,
}),
@ -653,7 +619,7 @@ describe("Integration", () => {
});
it("Returns an UnexpectedResourceError if an unknown error is triggered", async () => {
fetchMock.mockRejectedValueOnce(new Error("Something happened."));
s.fetchMock.mockRejectedValueOnce(new Error("Something happened."));
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
const result = await resource.getRootContainer();
expect(result.isError).toBe(true);
@ -663,7 +629,7 @@ describe("Integration", () => {
});
it("returns a NonCompliantPodError when there is no root", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 200,
headers: new Headers({
@ -711,7 +677,7 @@ describe("Integration", () => {
});
it("Passes any errors returned from the read method", async () => {
fetchMock.mockRejectedValueOnce(new Error("Something happened."));
s.fetchMock.mockRejectedValueOnce(new Error("Something happened."));
const result = await getStorageFromWebId(
SAMPLE_DATA_URI,
solidLdoDataset,
@ -720,8 +686,8 @@ describe("Integration", () => {
});
it("Passes any errors returned from the getRootContainer method", async () => {
fetchMock.mockResolvedValueOnce(new Response(""));
fetchMock.mockRejectedValueOnce(new Error("Something happened."));
s.fetchMock.mockResolvedValueOnce(new Response(""));
s.fetchMock.mockRejectedValueOnce(new Error("Something happened."));
const result = await getStorageFromWebId(
SAMPLE_DATA_URI,
solidLdoDataset,
@ -828,7 +794,7 @@ describe("Integration", () => {
it("returns and error if creating a container", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 500,
}),
@ -840,7 +806,7 @@ describe("Integration", () => {
it("returns a delete error if delete failed", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 500,
}),
@ -852,10 +818,10 @@ describe("Integration", () => {
it("returns an error if the create fetch fails", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockImplementationOnce(async (...args) => {
return authFetch(...args);
s.fetchMock.mockImplementationOnce(async (...args) => {
return s.authFetch(...args);
});
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 500,
}),
@ -867,10 +833,10 @@ describe("Integration", () => {
it("returns an unexpected error if some unknown error is triggered", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockImplementationOnce(async (...args) => {
return authFetch(...args);
s.fetchMock.mockImplementationOnce(async (...args) => {
return s.authFetch(...args);
});
fetchMock.mockImplementationOnce(async () => {
s.fetchMock.mockImplementationOnce(async () => {
throw new Error("Some Unknown");
});
const result = await resource.createAndOverwrite();
@ -889,7 +855,7 @@ describe("Integration", () => {
expect(result1.type).toBe("createSuccess");
expect(result2.type).toBe("createSuccess");
// 1 for read, 1 for delete in createAndOverwrite, 1 for create
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(s.fetchMock).toHaveBeenCalledTimes(3);
});
it("batches the create request while waiting on a similar request", async () => {
@ -902,7 +868,7 @@ describe("Integration", () => {
expect(result1.type).toBe("createSuccess");
expect(result2.type).toBe("createSuccess");
// 1 for delete in createAndOverwrite, 1 for create
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(s.fetchMock).toHaveBeenCalledTimes(2);
});
});
@ -999,7 +965,7 @@ describe("Integration", () => {
it("returns an error if creating a container", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(SAMPLE_CONTAINER_URI, {
status: 500,
}),
@ -1011,7 +977,7 @@ describe("Integration", () => {
it("returns an error if creating a leaf", async () => {
const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(SAMPLE2_DATA_URI, {
status: 500,
}),
@ -1028,7 +994,7 @@ describe("Integration", () => {
describe("deleteResource", () => {
it("returns an unexpected http error if an unexpected value is returned", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 214,
}),
@ -1040,7 +1006,7 @@ describe("Integration", () => {
it("returns an unexpected resource error if an unknown error is triggered", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockImplementationOnce(async () => {
s.fetchMock.mockImplementationOnce(async () => {
throw new Error("Some unknwon");
});
const result = await resource.delete();
@ -1056,7 +1022,7 @@ describe("Integration", () => {
it("returns an error on container read when deleting a container", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockImplementation(async (input, init) => {
s.fetchMock.mockImplementation(async (input, init) => {
if (
(init?.method === "get" || !init?.method) &&
input === TEST_CONTAINER_URI
@ -1065,7 +1031,7 @@ describe("Integration", () => {
status: 500,
});
}
return authFetch(input, init);
return s.authFetch(input, init);
});
const result = await resource.delete();
expect(result.isError).toBe(true);
@ -1082,13 +1048,13 @@ describe("Integration", () => {
it("returns an error on child delete when deleting a container", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockImplementation(async (input, init) => {
s.fetchMock.mockImplementation(async (input, init) => {
if (init?.method === "delete" && input === SAMPLE_DATA_URI) {
return new Response(SAMPLE_DATA_URI, {
status: 500,
});
}
return authFetch(input, init);
return s.authFetch(input, init);
});
const result = await resource.delete();
expect(result.isError).toBe(true);
@ -1105,13 +1071,13 @@ describe("Integration", () => {
it("returns an error on container delete when deleting a container", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockImplementation(async (input, init) => {
s.fetchMock.mockImplementation(async (input, init) => {
if (init?.method === "delete" && input === TEST_CONTAINER_URI) {
return new Response(SAMPLE_DATA_URI, {
status: 500,
});
}
return authFetch(input, init);
return s.authFetch(input, init);
});
const result = await resource.delete();
expect(result.isError).toBe(true);
@ -1184,7 +1150,7 @@ describe("Integration", () => {
});
it("handles an HTTP error", async () => {
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
const transaction = solidLdoDataset.startTransaction();
transaction.add(normanQuad);
@ -1202,7 +1168,7 @@ describe("Integration", () => {
});
it("handles an unknown request", async () => {
fetchMock.mockImplementationOnce(() => {
s.fetchMock.mockImplementationOnce(() => {
throw new Error("Some Error");
});
const transaction = solidLdoDataset.startTransaction();
@ -1286,7 +1252,7 @@ describe("Integration", () => {
]);
expect(updateResult1.type).toBe("aggregateSuccess");
expect(updateResult2.type).toBe("aggregateSuccess");
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(s.fetchMock).toHaveBeenCalledTimes(2);
expect(
solidLdoDataset.has(
createQuad(
@ -1394,7 +1360,7 @@ describe("Integration", () => {
it("returns a delete error if delete failed", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 500,
}),
@ -1409,10 +1375,10 @@ describe("Integration", () => {
it("returns an error if the create fetch fails", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI);
fetchMock.mockImplementationOnce(async (...args) => {
return authFetch(...args);
s.fetchMock.mockImplementationOnce(async (...args) => {
return s.authFetch(...args);
});
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(TEST_CONTAINER_TTL, {
status: 500,
}),
@ -1427,10 +1393,10 @@ describe("Integration", () => {
it("returns an unexpected error if some unknown error is triggered", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI);
fetchMock.mockImplementationOnce(async (...args) => {
return authFetch(...args);
s.fetchMock.mockImplementationOnce(async (...args) => {
return s.authFetch(...args);
});
fetchMock.mockImplementationOnce(async () => {
s.fetchMock.mockImplementationOnce(async () => {
throw new Error("Some Unknown");
});
const result = await resource.uploadAndOverwrite(
@ -1458,7 +1424,7 @@ describe("Integration", () => {
expect(result1.type).toBe("createSuccess");
expect(result2.type).toBe("createSuccess");
// 1 for read, 1 for delete in createAndOverwrite, 1 for create
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(s.fetchMock).toHaveBeenCalledTimes(3);
expect(resource.getBlob()?.toString()).toBe("some text 2.");
});
@ -1478,7 +1444,7 @@ describe("Integration", () => {
expect(result1.type).toBe("createSuccess");
expect(result2.type).toBe("createSuccess");
// 1 for delete in createAndOverwrite, 1 for create
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(s.fetchMock).toHaveBeenCalledTimes(2);
expect(resource.getBlob()?.toString()).toBe("some text 2.");
});
});
@ -1552,7 +1518,7 @@ describe("Integration", () => {
it("returns an error if an error is encountered", async () => {
const resource = solidLdoDataset.getResource(SAMPLE2_BINARY_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(SAMPLE2_BINARY_URI, {
status: 500,
}),
@ -1596,7 +1562,7 @@ describe("Integration", () => {
});
it("handles an error when committing data", async () => {
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response(SAMPLE_DATA_URI, {
status: 500,
}),
@ -1847,7 +1813,7 @@ describe("Integration", () => {
it("returns an error when an error is encountered fetching the aclUri", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
const wacResult = await resource.getWac();
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
@ -1862,7 +1828,7 @@ describe("Integration", () => {
it("returns a non-compliant error if a response is returned without a link header", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("Error", {
status: 200,
}),
@ -1879,7 +1845,7 @@ describe("Integration", () => {
it("returns a non-compliant error if a response is returned without an ACL link", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("Error", {
status: 200,
headers: { link: `<card.meta>; rel="describedBy"` },
@ -1896,7 +1862,7 @@ describe("Integration", () => {
});
it("Returns an UnexpectedResourceError if an unknown error is triggered while getting the wac URI", async () => {
fetchMock.mockRejectedValueOnce(new Error("Something happened."));
s.fetchMock.mockRejectedValueOnce(new Error("Something happened."));
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
const result = await resource.getWac();
expect(result.isError).toBe(true);
@ -1907,13 +1873,13 @@ describe("Integration", () => {
it("Returns an error if the request to get the ACL fails", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
const wacResult = await resource.getWac();
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
@ -1923,13 +1889,13 @@ describe("Integration", () => {
it("Returns an error if the request to the ACL resource returns invalid turtle", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("BAD TURTLE", { status: 200 }),
);
const wacResult = await resource.getWac();
@ -1944,14 +1910,14 @@ describe("Integration", () => {
it("Returns an error if there was a problem getting the parent resource", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(new Response("", { status: 404 }));
fetchMock.mockResolvedValueOnce(new Response("", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("", { status: 404 }));
s.fetchMock.mockResolvedValueOnce(new Response("", { status: 500 }));
const wacResult = await resource.getWac();
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
@ -1959,13 +1925,13 @@ describe("Integration", () => {
it("returns a NonCompliantPodError when this is the root resource and it doesn't have an ACL", async () => {
const resource = solidLdoDataset.getResource(ROOT_CONTAINER);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(new Response("", { status: 404 }));
s.fetchMock.mockResolvedValueOnce(new Response("", { status: 404 }));
const wacResult = await resource.getWac();
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("noncompliantPodError");
@ -2039,7 +2005,7 @@ describe("Integration", () => {
it("returns an error when an error is encountered fetching the aclUri", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
const wacResult = await resource.setWac(newRules);
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
@ -2047,13 +2013,13 @@ describe("Integration", () => {
it("Returns an error when the request to write the access rules throws an error", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockResolvedValueOnce(
s.fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(new Response("", { status: 500 }));
s.fetchMock.mockResolvedValueOnce(new Response("", { status: 500 }));
const wacResult = await resource.setWac(newRules);
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
@ -2083,7 +2049,7 @@ describe("Integration", () => {
expect(resource.isSubscribedToNotifications()).toBe(true);
await authFetch(SAMPLE_DATA_URI, {
await s.authFetch(SAMPLE_DATA_URI, {
method: "PATCH",
body: 'INSERT DATA { <http://example.org/#spiderman> <http://xmlns.com/foaf/0.1/name> "Peter Parker" . }',
headers: {
@ -2105,7 +2071,7 @@ describe("Integration", () => {
spidermanCallback.mockClear();
await resource.unsubscribeFromNotifications(subscriptionId);
expect(resource.isSubscribedToNotifications()).toBe(false);
await authFetch(SAMPLE_DATA_URI, {
await s.authFetch(SAMPLE_DATA_URI, {
method: "PATCH",
body: 'INSERT DATA { <http://example.org/#spiderman> <http://xmlns.com/foaf/0.1/name> "Miles Morales" . }',
headers: {
@ -2143,7 +2109,7 @@ describe("Integration", () => {
await resource.subscribeToNotifications();
await authFetch(SAMPLE_DATA_URI, {
await s.authFetch(SAMPLE_DATA_URI, {
method: "DELETE",
});
await wait(1000);
@ -2177,7 +2143,7 @@ describe("Integration", () => {
await testContainer.subscribeToNotifications();
await authFetch(SAMPLE_DATA_URI, {
await s.authFetch(SAMPLE_DATA_URI, {
method: "DELETE",
});
await wait(1000);
@ -2211,7 +2177,7 @@ describe("Integration", () => {
await testContainer.subscribeToNotifications();
await authFetch(TEST_CONTAINER_URI, {
await s.authFetch(TEST_CONTAINER_URI, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "sample2.ttl" },
body: SPIDER_MAN_TTL,
@ -2230,22 +2196,25 @@ describe("Integration", () => {
await testContainer.unsubscribeFromAllNotifications();
});
it("returns an error when it cannot subscribe to a notification", async () => {
it.skip("returns an error when it cannot subscribe to a notification", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
const onError = jest.fn();
await app.stop();
await resource.subscribeToNotifications({ onNotificationError: onError });
await s.app.stop();
await resource.subscribeToNotifications({
onNotificationError: onError,
});
expect(onError).toHaveBeenCalledTimes(2);
await app.start();
await s.app.start();
});
it("returns an error when the server doesnt support websockets", async () => {
it.skip("returns an error when the server doesnt support websockets", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
const onError = jest.fn();
await app.stop();
await s.app.stop();
const disabledWebsocketsApp = await createApp(
3001,
path.join(__dirname, "./configs/server-config-without-websocket.json"),
);
await disabledWebsocketsApp.start();
@ -2254,15 +2223,16 @@ describe("Integration", () => {
expect(onError).toHaveBeenCalledTimes(2);
await disabledWebsocketsApp.stop();
await app.start();
await s.app.start();
});
it("attempts to reconnect multiple times before giving up.", async () => {
it.skip("attempts to reconnect multiple times before giving up.", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
const onError = jest.fn();
await app.stop();
await s.app.stop();
const disabledWebsocketsApp = await createApp(
3001,
path.join(__dirname, "./configs/server-config-without-websocket.json"),
);
await disabledWebsocketsApp.start();
@ -2282,7 +2252,7 @@ describe("Integration", () => {
);
await disabledWebsocketsApp.stop();
await app.start();
await s.app.start();
});
it("causes no problems when unsubscribing when not subscribed", async () => {

@ -1,3 +0,0 @@
import { config } from "dotenv";
config();

@ -5,6 +5,6 @@ module.exports = {
rootDir: "./",
transform: {
"^.+\\.(ts|tsx)?$": "ts-jest",
"^.+\\.(js|jsx)$": "babel-jest",
},
coveragePathIgnorePatterns: ["/node_modules/", "/dist/"],
};

@ -1,16 +1,17 @@
{
"name": "@ldo/connected",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "A library for connecting LDO to resources outside the LDO environment",
"main": "dist/index.js",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"watch": "tsc --watch",
"test": "jest --coverage",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"test:watch": "jest --watch",
"prepublishOnly": "npm run test && npm run build",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
"docs": "typedoc --plugin typedoc-plugin-markdown"
"docs": "typedoc --plugin typedoc-plugin-markdown",
"build:ldo": "ldo build --input test/.shapes --output test/.ldo"
},
"repository": {
"type": "git",
@ -23,19 +24,23 @@
},
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid#readme",
"devDependencies": {
"@ldo/connected-solid": "^1.0.0-alpha.9",
"@ldo/test-solid-server": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"cross-env": "^7.0.3",
"jest-rdf": "^1.8.0",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.1",
"typed-emitter": "^2.1.0",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1"
"typedoc-plugin-markdown": "^3.17.1",
"uuid": "^11.1.0"
},
"dependencies": {
"@ldo/dataset": "^1.0.0-alpha.3",
"@ldo/ldo": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.3"
"@ldo/dataset": "^1.0.0-alpha.9",
"@ldo/ldo": "^1.0.0-alpha.9",
"@ldo/rdf-utils": "^1.0.0-alpha.9"
},
"files": [
"dist",

@ -0,0 +1,43 @@
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { LdoBuilder } from "@ldo/ldo";
import type { IConnectedLdoBuilder } from "./types/IConnectedLdoBuilder";
import type { JsonldDatasetProxyBuilder } from "@ldo/jsonld-dataset-proxy";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { LQInput } from "./types/ILinkQuery";
import { ResourceLinkQuery } from "./linkTraversal/ResourceLinkQuery";
import type { ConnectedPlugin } from "./types/ConnectedPlugin";
import type { IConnectedLdoDataset } from "./types/IConnectedLdoDataset";
export class ConnectedLdoBuilder<
Type extends LdoBase,
Plugins extends ConnectedPlugin[],
>
extends LdoBuilder<Type>
implements IConnectedLdoBuilder<Type, Plugins>
{
protected parentDataset: IConnectedLdoDataset<Plugins>;
constructor(
parentDataset: IConnectedLdoDataset<Plugins>,
jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder,
shapeType: ShapeType<Type>,
) {
super(jsonldDatasetProxyBuilder, shapeType);
this.parentDataset = parentDataset;
}
startLinkQuery<Input extends LQInput<Type>>(
startingResource: Plugins[number]["types"]["resource"],
startingSubject: SubjectNode | string,
linkQueryInput: Input,
): ResourceLinkQuery<Type, Input, Plugins> {
return new ResourceLinkQuery(
this.parentDataset,
this.shapeType,
this,
startingResource,
startingSubject,
linkQueryInput,
);
}
}

@ -1,17 +1,19 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { LdoDataset, startTransaction } from "@ldo/ldo";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { ConnectedPlugin } from "./types/ConnectedPlugin";
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset";
import { InvalidIdentifierResource } from "./InvalidIdentifierResource";
import type { ConnectedContext } from "./ConnectedContext";
import type { ConnectedContext } from "./types/ConnectedContext";
import type {
GetResourceReturnType,
IConnectedLdoDataset,
} from "./IConnectedLdoDataset";
} from "./types/IConnectedLdoDataset";
import { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset";
import type { SubjectNode } from "@ldo/rdf-utils";
import { ConnectedLdoBuilder } from "./ConnectedLdoBuilder";
import jsonldDatasetProxy from "@ldo/jsonld-dataset-proxy";
/**
* A ConnectedLdoDataset has all the functionality of a LdoDataset with the
@ -160,6 +162,14 @@ export class ConnectedLdoDataset<
return resource as any;
}
getResources(): GetResourceReturnType<Plugins[number], string>[] {
return Array.from(this.resourceMap.values());
}
getFetchedResources(): GetResourceReturnType<Plugins[number], string>[] {
return this.getResources().filter((resource) => resource.isFetched());
}
/**
* Generates a random uri and creates a resource.
*
@ -277,6 +287,13 @@ export class ConnectedLdoDataset<
this.context[pluginName] = { ...this.context[pluginName], ...context };
}
public usingType<Type extends LdoBase>(
shapeType: ShapeType<Type>,
): ConnectedLdoBuilder<Type, Plugins> {
const proxyBuilder = jsonldDatasetProxy(this, shapeType.context);
return new ConnectedLdoBuilder(this, proxyBuilder, shapeType);
}
public startTransaction(): ConnectedLdoTransactionDataset<Plugins> {
return new ConnectedLdoTransactionDataset(
this,

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { LdoTransactionDataset } from "@ldo/ldo";
import type { DatasetFactory, Quad } from "@rdfjs/types";
import {
@ -6,12 +7,12 @@ import {
type ITransactionDatasetFactory,
} from "@ldo/subscribable-dataset";
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { ConnectedContext } from "./ConnectedContext";
import type { ConnectedPlugin } from "./types/ConnectedPlugin";
import type { ConnectedContext } from "./types/ConnectedContext";
import type {
GetResourceReturnType,
IConnectedLdoDataset,
} from "./IConnectedLdoDataset";
} from "./types/IConnectedLdoDataset";
import { splitChangesByGraph } from "./util/splitChangesByGraph";
import type { IgnoredInvalidUpdateSuccess } from "./results/success/UpdateSuccess";
import { UpdateDefaultGraphSuccess } from "./results/success/UpdateSuccess";
@ -21,6 +22,8 @@ import type {
AggregateSuccess,
SuccessResult,
} from "./results/success/SuccessResult";
import { ConnectedLdoBuilder } from "./ConnectedLdoBuilder";
import jsonldDatasetProxy from "@ldo/jsonld-dataset-proxy";
/**
* A ConnectedLdoTransactionDataset has all the functionality of a
@ -98,6 +101,14 @@ export class ConnectedLdoTransactionDataset<Plugins extends ConnectedPlugin[]>
return this.context.dataset.getResource(uri, pluginName);
}
getResources(): Plugins[number]["types"]["resource"][] {
return this.context.dataset.getResources();
}
getFetchedResources(): Plugins[number]["types"]["resource"][] {
return this.context.dataset.getFetchedResources();
}
createResource<
Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>,
@ -225,4 +236,11 @@ export class ConnectedLdoTransactionDataset<Plugins extends ConnectedPlugin[]>
results: results.map((result) => result[2]) as any,
};
}
public usingType<Type extends LdoBase>(
shapeType: ShapeType<Type>,
): ConnectedLdoBuilder<Type, Plugins> {
const proxyBuilder = jsonldDatasetProxy(this, shapeType.context);
return new ConnectedLdoBuilder(this, proxyBuilder, shapeType);
}
}

@ -1,6 +1,6 @@
import { createDatasetFactory } from "@ldo/dataset";
import { ConnectedLdoDataset } from "./ConnectedLdoDataset";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { ConnectedPlugin } from "./types/ConnectedPlugin";
import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset";
/**

@ -1,13 +1,17 @@
export * from "./IConnectedLdoDataset";
export * from "./ConnectedLdoBuilder";
export * from "./ConnectedLdoDataset";
export * from "./ConnectedLdoTransactionDataset";
export * from "./ConnectedPlugin";
export * from "./Resource";
export * from "./InvalidIdentifierResource";
export * from "./ConnectedContext";
export * from "./methods";
export * from "./createConntectedLdoDataset";
export * from "./SubscriptionCallbacks";
export * from "./types/ConnectedContext";
export * from "./types/ConnectedPlugin";
export * from "./types/IConnectedLdoDataset";
export * from "./types/IConnectedLdoBuilder";
export * from "./types/ILinkQuery";
export * from "./util/splitChangesByGraph";
@ -21,3 +25,12 @@ export * from "./results/success/ReadSuccess";
export * from "./results/success/UpdateSuccess";
export * from "./notifications/NotificationSubscription";
export * from "./notifications/SubscriptionCallbacks";
export * from "./trackingProxy/TrackingProxyContext";
export * from "./trackingProxy/TrackingSetProxy";
export * from "./trackingProxy/TrackingSubjectProxy";
export * from "./trackingProxy/createTrackingProxy";
export * from "./linkTraversal/ResourceLinkQuery";
export * from "./linkTraversal/exploreLinks";

@ -0,0 +1,287 @@
import type { LdoBase, ShapeType } from "@ldo/ldo";
import type {
ExpandDeep,
ILinkQuery,
LQInput,
LQReturn,
} from "../types/ILinkQuery";
import type { ConnectedPlugin } from "../types/ConnectedPlugin";
import type { SubjectNode } from "@ldo/rdf-utils";
import { exploreLinks } from "./exploreLinks";
import type { IConnectedLdoDataset } from "../types/IConnectedLdoDataset";
import type { IConnectedLdoBuilder } from "../types/IConnectedLdoBuilder";
import { v4 } from "uuid";
import type { nodeEventListener } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
/**
* Represents a query over multiple datasources and constituting muliple
* resources.
*
* @example
* ```typescript
* import { ProfileShapeType } from "./.ldo/Profile.shapeType.ts";
*
* // Create a link query
* const linkQuery = ldoDataset
* .usingType(ProfileShapeType)
* .startLinkQuery(
* "http://example.com/profile/card",
* "http://example.com/profile/card#me",
* {
* name: true,
* knows: {
* name: true,
* },
* },
* }
* );
* // Susbscribe to this link query, automaticically updating the dataset when
* // something from the link query is changed.
* await linkQuery.subscribe();
* ```
*/
export class ResourceLinkQuery<
Type extends LdoBase,
QueryInput extends LQInput<Type>,
Plugins extends ConnectedPlugin[],
> implements ILinkQuery<Type, QueryInput>
{
protected previousTransactionId: string = "INIT";
// Resource Subscriptions uri -> unsubscribeId
protected activeResourceSubscriptions: Record<string, string> = {};
// Unsubscribe IDs for this ResourceLinkQuery
protected thisUnsubscribeIds = new Set<string>();
protected curOnDataChanged: nodeEventListener<Quad> | undefined;
protected resourcesWithSubscriptionInProgress: Record<
string,
Promise<void> | undefined
> = {};
/**
* @internal
* @param parentDataset The dataset for which this link query is a part
* @param shapeType A ShapeType for the link query to follow
* @param ldoBuilder An LdoBuilder associated with the dataset
* @param startingResource The resource to explore first in the link query
* @param startingSubject The starting point of the link query
* @param linkQueryInput A definition of the link query
*/
constructor(
protected parentDataset: IConnectedLdoDataset<Plugins>,
protected shapeType: ShapeType<Type>,
protected ldoBuilder: IConnectedLdoBuilder<Type, Plugins>,
protected startingResource: Plugins[number]["types"]["resource"],
protected startingSubject: SubjectNode | string,
protected linkQueryInput: QueryInput,
) {}
/**
* Runs this link query, returning the result
* @param options Options for how to run the link query
* @returns A subset of the ShapeType as defined by the LinkQuery
*
* @example
* ```
* import { ProfileShapeType } from "./.ldo/Profile.shapeType.ts";
*
* // Create a link query
* const linkQuery = ldoDataset
* .usingType(ProfileShapeType)
* .startLinkQuery(
* "http://example.com/profile/card",
* "http://example.com/profile/card#me",
* {
* name: true,
* knows: {
* name: true,
* },
* },
* }
* );
* // Susbscribe to this link query, automaticically updating the dataset when
* // something from the link query is changed.
* const result = await linkQuery.read();
* console.log(result.name);
* result.knows.forEach((person) => console.log(person.name));
* // The following will type-error. Despite "phone" existing on a Profile,
* // it was not covered by the link query.
* console.log(result.phone);
* ```
*/
async run(options?: {
reload?: boolean;
}): Promise<ExpandDeep<LQReturn<Type, QueryInput>>> {
await exploreLinks(
this.parentDataset,
this.shapeType,
this.startingResource,
this.startingSubject,
this.linkQueryInput,
{ shouldRefreshResources: options?.reload },
);
return this.fromSubject();
}
/**
* Subscribes to the data defined by the link query, updating the dataset if
* any changes are made.
* @returns An unsubscribeId
*
* @example
* ```
* import { ProfileShapeType } from "./.ldo/Profile.shapeType.ts";
*
* // Create a link query
* const linkQuery = ldoDataset
* .usingType(ProfileShapeType)
* .startLinkQuery(
* "http://example.com/profile/card",
* "http://example.com/profile/card#me",
* {
* name: true,
* knows: {
* name: true,
* },
* },
* }
* );
* // Susbscribe to this link query, automaticically updating the dataset when
* // something from the link query is changed.
* const unsubscribeId = await linkQuery.subscribe();
*
* // Now, let's imagine the following triple was added to
* "http://example.com/profile/card":
* <http://example.com/profile/card#me> <http://xmlns.com/foaf/0.1/knows> <http://example2.com/profile/card#me>
* Because you're subscribed, the dataset will automatically be updated.
*
* // End subscription
* linkQuery.unsubscribe(unsubscribeId);
* ```
*/
async subscribe(): Promise<string> {
const subscriptionId = v4();
this.thisUnsubscribeIds.add(subscriptionId);
// If there's already a registered onDataChange, we don't need to make a new
// on for this new subscription
if (this.curOnDataChanged) {
return subscriptionId;
}
this.curOnDataChanged = async (
_changes,
transactionId: string,
_triggering,
) => {
// Set a transaction Id, so that we only trigger one re-render
if (transactionId === this.previousTransactionId) return;
this.previousTransactionId = transactionId;
// Remove previous registration
this.parentDataset.removeListenerFromAllEvents(this.curOnDataChanged!);
// Explore the links, with a subscription to re-explore the links if any
// covered information changes
const resourcesToUnsubscribeFrom = new Set(
Object.keys(this.activeResourceSubscriptions),
);
// Only add the listeners if we're currently subscribed
const exploreOptions = this.curOnDataChanged
? {
onCoveredDataChanged: this.curOnDataChanged,
onResourceEncountered: async (resource) => {
// Wait for the the in progress registration to complete. Once it
// is complete, you're subscribed, so we can remove this from the
// resources to unsubscribe from.
if (this.resourcesWithSubscriptionInProgress[resource.uri]) {
await this.resourcesWithSubscriptionInProgress[resource.uri];
resourcesToUnsubscribeFrom.delete(resource.uri);
return;
}
// No need to do anything if we're already subscribed
if (resourcesToUnsubscribeFrom.has(resource.uri)) {
resourcesToUnsubscribeFrom.delete(resource.uri);
return;
}
// Otherwise begin the subscription
let resolve;
this.resourcesWithSubscriptionInProgress[resource.uri] =
new Promise<void>((res) => {
resolve = res;
});
const unsubscribeId = await resource.subscribeToNotifications();
this.activeResourceSubscriptions[resource.uri] = unsubscribeId;
// Unsubscribe in case unsubscribe call came in mid subscription
if (!this.curOnDataChanged) {
await this.unsubscribeFromResource(resource.uri);
}
resolve();
this.resourcesWithSubscriptionInProgress[resource.uri] =
undefined;
},
}
: {};
await exploreLinks(
this.parentDataset,
this.shapeType,
this.startingResource,
this.startingSubject,
this.linkQueryInput,
exploreOptions,
);
// Clean up unused subscriptions
await Promise.all(
Array.from(resourcesToUnsubscribeFrom).map(async (uri) =>
this.unsubscribeFromResource(uri),
),
);
};
await this.curOnDataChanged({}, "BEGIN_SUB", [null, null, null, null]);
return subscriptionId;
}
private async unsubscribeFromResource(uri) {
const resource = this.parentDataset.getResource(uri);
const unsubscribeId = this.activeResourceSubscriptions[uri];
delete this.activeResourceSubscriptions[uri];
await resource.unsubscribeFromNotifications(unsubscribeId);
}
private async fullUnsubscribe(): Promise<void> {
if (this.curOnDataChanged) {
this.parentDataset.removeListenerFromAllEvents(this.curOnDataChanged);
this.curOnDataChanged = undefined;
}
await Promise.all(
Object.keys(this.activeResourceSubscriptions).map(async (uri) => {
this.unsubscribeFromResource(uri);
}),
);
}
async unsubscribe(unsubscribeId: string): Promise<void> {
this.thisUnsubscribeIds.delete(unsubscribeId);
if (this.thisUnsubscribeIds.size === 0) {
await this.fullUnsubscribe();
}
}
async unsubscribeAll() {
this.thisUnsubscribeIds.clear();
await this.fullUnsubscribe();
}
fromSubject(): ExpandDeep<LQReturn<Type, QueryInput>> {
return this.ldoBuilder.fromSubject(
this.startingSubject,
) as unknown as ExpandDeep<LQReturn<Type, QueryInput>>;
}
getSubscribedResources(): Plugins[number]["types"]["resource"][] {
return Object.keys(this.activeResourceSubscriptions).map((uri) =>
this.parentDataset.getResource(uri),
);
}
}

@ -0,0 +1,162 @@
import type { LdoBase, ShapeType } from "@ldo/ldo";
import type { ConnectedPlugin } from "../types/ConnectedPlugin";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { LQInput } from "../types/ILinkQuery";
import { BasicLdSet } from "@ldo/jsonld-dataset-proxy";
import type { IConnectedLdoDataset } from "../types/IConnectedLdoDataset";
import { createTrackingProxyBuilder } from "../trackingProxy/createTrackingProxy";
import type { nodeEventListener } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
/**
* @internal
*/
interface ExploreLinksOptions<Plugins extends ConnectedPlugin[]> {
onResourceEncountered?: (
resource: Plugins[number]["types"]["resource"],
) => Promise<void>;
onCoveredDataChanged?: nodeEventListener<Quad>;
shouldRefreshResources?: boolean;
}
/**
* @internal
*/
export async function exploreLinks<
Type extends LdoBase,
Plugins extends ConnectedPlugin[],
>(
dataset: IConnectedLdoDataset<Plugins>,
shapeType: ShapeType<Type>,
startingResource: Plugins[number]["types"]["resource"],
startingSubject: SubjectNode | string,
queryInput: LQInput<Type>,
options?: ExploreLinksOptions<Plugins>,
): Promise<void> {
// Do an initial check of the resources.
const readResult = options?.shouldRefreshResources
? await startingResource.read()
: await startingResource.readIfUnfetched();
if (readResult.isError) return;
if (options?.onResourceEncountered)
await options?.onResourceEncountered(startingResource);
const proxyBuilder = options?.onCoveredDataChanged
? createTrackingProxyBuilder(
dataset,
shapeType,
options?.onCoveredDataChanged,
)
: dataset.usingType(shapeType);
const ldObject = proxyBuilder.fromSubject(startingSubject);
const encounteredDuringThisExploration = new Set<string>([
startingResource.uri,
]);
// Recursively explore the rest
await exploreLinksRecursive(
dataset,
ldObject,
queryInput,
encounteredDuringThisExploration,
options,
);
}
export async function exploreLinksRecursive<
Type extends LdoBase,
Plugins extends ConnectedPlugin[],
>(
dataset: IConnectedLdoDataset<Plugins>,
ldObject: Type,
queryInput: LQInput<Type>,
encounteredDuringThisExploration: Set<string>,
options?: ExploreLinksOptions<Plugins>,
): Promise<void> {
const shouldFetch = shouldFetchResource(
dataset,
ldObject,
queryInput,
encounteredDuringThisExploration,
);
const resourceToFetch = dataset.getResource(ldObject["@id"]);
if (shouldFetch) {
const readResult = options?.shouldRefreshResources
? await resourceToFetch.read()
: await resourceToFetch.readIfUnfetched();
// If there was an error with the read, the traversal is done.
if (readResult.isError) {
return;
}
}
if (!encounteredDuringThisExploration.has(resourceToFetch.uri)) {
encounteredDuringThisExploration.add(resourceToFetch.uri);
if (options?.onResourceEncountered)
await options.onResourceEncountered(resourceToFetch);
}
// Recurse through the other elemenets
await Promise.all(
Object.entries(queryInput).map(async ([queryKey, queryValue]) => {
if (
queryValue != undefined &&
queryValue !== true &&
ldObject[queryKey] != undefined
) {
if (ldObject[queryKey] instanceof BasicLdSet) {
await Promise.all(
ldObject[queryKey].map(async (item) => {
await exploreLinksRecursive(
dataset,
item,
queryValue,
encounteredDuringThisExploration,
options,
);
}),
);
} else {
await exploreLinksRecursive(
dataset,
ldObject[queryKey],
queryValue,
encounteredDuringThisExploration,
options,
);
}
}
}),
);
}
/**
* Determines if a resource needs to be fetched based on given data
*/
export function shouldFetchResource<
Type extends LdoBase,
Plugins extends ConnectedPlugin[],
>(
dataset: IConnectedLdoDataset<Plugins>,
ldObject: Type,
queryInput: LQInput<Type>,
encounteredDuringThisExploration: Set<string>,
): boolean {
const linkedResourceUri: string | undefined = ldObject["@id"];
// If it's a blank node, no need to fetch
if (!linkedResourceUri) return false;
const linkedResource = dataset.getResource(linkedResourceUri);
// If we've already explored the resource in this exporation, do not fetch
if (encounteredDuringThisExploration.has(linkedResource.uri)) return false;
return Object.entries(queryInput).some(([queryKey, queryValue]) => {
// If value is undefined then no need to fetch
if (!queryValue) return false;
// Always fetch if there's a set in the object
if (ldObject[queryKey] instanceof BasicLdSet) return true;
// Fetch if a singleton set is not present
if (ldObject[queryKey] == undefined) return true;
// Otherwise no need t to fetch
return false;
});
}

@ -1,7 +1,7 @@
import { v4 } from "uuid";
import type { ConnectedPlugin } from "../ConnectedPlugin";
import type { ConnectedContext } from "../ConnectedContext";
import type { SubscriptionCallbacks } from "../SubscriptionCallbacks";
import type { ConnectedPlugin } from "../types/ConnectedPlugin";
import type { ConnectedContext } from "../types/ConnectedContext";
import type { SubscriptionCallbacks } from "./SubscriptionCallbacks";
import type { NotificationCallbackError } from "../results/error/NotificationErrors";
/**

@ -5,7 +5,10 @@ import type {
} from "@ldo/jsonld-dataset-proxy";
import { ProxyContext } from "@ldo/jsonld-dataset-proxy";
import type { QuadMatch } from "@ldo/rdf-utils";
import type { SubscribableDataset } from "@ldo/subscribable-dataset";
import type {
nodeEventListener,
SubscribableDataset,
} from "@ldo/subscribable-dataset";
import type { BlankNode, NamedNode, Quad } from "@rdfjs/types";
import { createTrackingSubjectProxy } from "./TrackingSubjectProxy";
import { createTrackingSetProxy } from "./TrackingSetProxy";
@ -18,6 +21,11 @@ export interface TrackingProxyContextOptions extends ProxyContextOptions {
dataset: SubscribableDataset<Quad>;
}
/**
* @internal
* A listener that gets triggered whenever there's an update
*/
/**
* @internal
* This proxy exists to ensure react components rerender at the right time. It
@ -25,10 +33,13 @@ export interface TrackingProxyContextOptions extends ProxyContextOptions {
* dataset is updated with that key does it rerender the react component.
*/
export class TrackingProxyContext extends ProxyContext {
private listener: () => void;
private listener: nodeEventListener<Quad>;
private subscribableDataset: SubscribableDataset<Quad>;
constructor(options: TrackingProxyContextOptions, listener: () => void) {
constructor(
options: TrackingProxyContextOptions,
listener: nodeEventListener<Quad>,
) {
super(options);
this.subscribableDataset = options.dataset;
this.listener = listener;

@ -0,0 +1,42 @@
import {
ContextUtil,
JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy";
import { LdoBuilder } from "@ldo/ldo";
import type { LdoBase, LdoDataset, ShapeType } from "@ldo/ldo";
import { TrackingProxyContext } from "./TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
import type { nodeEventListener } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
/**
* @internal
* Creates a Linked Data Object builder that when creating linked data objects
* it tracks when something that was read from it is updated and triggers some
* action based on that.
*/
export function createTrackingProxyBuilder<Type extends LdoBase>(
dataset: LdoDataset,
shapeType: ShapeType<Type>,
onUpdate: nodeEventListener<Quad>,
): LdoBuilder<Type> {
// Remove all current subscriptions
// dataset.removeListenerFromAllEvents(onUpdate);
// 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"],
},
onUpdate,
);
const builder = new LdoBuilder(
new JsonldDatasetProxyBuilder(proxyContext),
shapeType,
);
return builder;
}

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ConnectedLdoDataset } from "./ConnectedLdoDataset";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { IConnectedLdoDataset } from "./IConnectedLdoDataset";
/**
* Each Plugin comes with a context. This is the aggregate of all those contexts
@ -9,7 +9,7 @@ import type { ConnectedPlugin } from "./ConnectedPlugin";
export type ConnectedContext<
Plugins extends ConnectedPlugin<any, any, any, any>[],
> = {
dataset: ConnectedLdoDataset<Plugins>;
dataset: IConnectedLdoDataset<Plugins>;
} & {
[P in Plugins[number] as P["name"]]: P["types"]["context"];
};

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ConnectedContext } from "./ConnectedContext";
import type { Resource } from "./Resource";
import type { ErrorResult } from "./results/error/ErrorResult";
import type { Resource } from "../Resource";
import type { ErrorResult } from "../results/error/ErrorResult";
/**
* A ConnectedPlugin can be passed to a ConnectedDataset to allow it to connect

@ -0,0 +1,15 @@
import type { LdoBase, LdoBuilder } from "@ldo/ldo";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { ILinkQuery, LQInput } from "./ILinkQuery";
export interface IConnectedLdoBuilder<
Type extends LdoBase,
Plugins extends ConnectedPlugin[],
> extends LdoBuilder<Type> {
startLinkQuery<Input extends LQInput<Type>>(
startingResource: Plugins[number]["types"]["resource"],
startingSubject: SubjectNode | string,
linkQueryInput: Input,
): ILinkQuery<Type, Input>;
}

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LdoDataset } from "@ldo/ldo";
import type { LdoBase, LdoDataset, ShapeType } from "@ldo/ldo";
import type { ConnectedPlugin } from "./ConnectedPlugin";
import type { InvalidIdentifierResource } from "./InvalidIdentifierResource";
import type { InvalidIdentifierResource } from "../InvalidIdentifierResource";
import type { IConnectedLdoBuilder } from "./IConnectedLdoBuilder";
export type ReturnTypeFromArgs<Func, Arg> = Func extends (
arg: Arg,
@ -78,6 +79,33 @@ export interface IConnectedLdoDataset<Plugins extends ConnectedPlugin[]>
pluginName?: Name,
): GetResourceReturnType<Plugin, UriType>;
/**
* Retireves a representation of all Resources referenced by this dataset
* This does not necessarily mean that it's been fetched (use the
* `getFetchedResources` method for that). It simply means that at one point
* it was referenced.
*
* @returns a Resource array
*
* @example
* ```typescript
* const allResources = connectedLdoDataset.getResources();
* ```
*/
getResources(): Plugins[number]["types"]["resource"][];
/**
* Retireves a representation of all Resources that have been fetched.
*
* @returns a Resource array
*
* @example
* ```typescript
* const allResources = connectedLdoDataset.getFetchedResources();
* ```
*/
getFetchedResources(): Plugins[number]["types"]["resource"][];
/**
* Generates a random uri and creates a resource.
*
@ -136,4 +164,8 @@ export interface IConnectedLdoDataset<Plugins extends ConnectedPlugin[]>
name: Name,
context: Plugin["types"]["context"],
);
usingType<Type extends LdoBase>(
shapeType: ShapeType<Type>,
): IConnectedLdoBuilder<Type, Plugins>;
}

@ -0,0 +1,126 @@
// This file is a stripped down version of a full-implmentation of a global
// query interface found here https://github.com/o-development/ldo-query/blob/main/lib/ShapeQuery.ts
// If I ever want to implement a global query interface, this is a good place
// to start.
import type { LdoBase, LdSet } from "@ldo/ldo";
// import { SolidProfileShapeShapeType } from "../../test/.ldo/solidProfile.shapeTypes";
// import type { SolidProfileShape } from "../../test/.ldo/solidProfile.typings";
/**
* Link Query Input
*/
export type LQInput<Type> = LQInputObject<Type>;
export type LQInputObject<Type> = Partial<{
[key in Exclude<keyof Type, "@context">]: LQInputFlattenSet<Type[key]>;
}>;
export type LQInputSubSet<Type> = Type extends object
? LQInputObject<Type>
: true;
export type LQInputFlattenSet<Type> = Type extends LdSet<infer SetSubType>
? LQInputSubSet<SetSubType>
: LQInputSubSet<Type>;
/**
* Link Query Input Default
*/
// TODO: I don't remember why I need this. Delete if unneeded
// export type LQInputDefaultType<Type> = {
// [key in keyof Type]: Type[key] extends object ? undefined : true;
// };
// export type LQInputDefault<Type> =
// LQInputDefaultType<Type> extends LQInput<Type>
// ? LQInputDefaultType<Type>
// : never;
/**
* Link Query Return
*/
export type LQReturn<Type, Input extends LQInput<Type>> = LQReturnObject<
Type,
Input
>;
export type LQReturnObject<Type, Input extends LQInputObject<Type>> = {
[key in Exclude<
keyof Required<Type>,
"@context"
> as undefined extends Input[key]
? never
: key]: Input[key] extends LQInputFlattenSet<Type[key]>
? undefined extends Type[key]
? LQReturnExpandSet<Type[key], Input[key]> | undefined
: LQReturnExpandSet<Type[key], Input[key]>
: never;
};
export type LQReturnSubSet<Type, Input> = Input extends LQInputSubSet<Type>
? Input extends LQInputObject<Type>
? Input extends true
? Type
: LQReturnObject<Type, Input>
: Type
: never;
export type LQReturnExpandSet<
Type,
Input extends LQInputFlattenSet<Type>,
> = NonNullable<Type> extends LdSet<infer SetSubType>
? LdSet<LQReturnSubSet<SetSubType, Input>>
: LQReturnSubSet<Type, Input>;
export type ExpandDeep<T> = T extends LdSet<infer U>
? LdSet<ExpandDeep<U>> // recursively expand sets
: T extends object
? { [K in keyof T]: ExpandDeep<T[K]> } // recursively expand objects
: T; // base case (primitive types)
/**
* ILinkQuery: Manages resources in a link query
*/
export interface LinkQueryRunOptions {
reload?: boolean;
}
export interface ILinkQuery<Type extends LdoBase, Input extends LQInput<Type>> {
run(
options?: LinkQueryRunOptions,
): Promise<ExpandDeep<LQReturn<Type, Input>>>;
subscribe(): Promise<string>;
unsubscribe(subscriptionId: string): Promise<void>;
unsubscribeAll(): Promise<void>;
fromSubject(): ExpandDeep<LQReturn<Type, Input>>;
}
// function test<Type extends LdoBase, Input extends LQInput<Type>>(
// shapeType: ShapeType<Type>,
// input: Input,
// ): ExpandDeep<LQReturn<Type, Input>> {
// throw new Error("Not Implemented");
// }
// type TestLQInput = {
// name: true;
// knows: {
// name: true;
// };
// };
// type testReturn = LQReturn<SolidProfileShape, TestLQInput>;
// type test2 = LQReturnSubSet<string | undefined, true>;
// type lqInputObject = LQInputObject<string | undefined>;
// type meh = TestLQInput extends true ? true : false;
// const thing = test(SolidProfileShapeShapeType, {
// name: true,
// knows: {
// name: true,
// },
// });

@ -0,0 +1,459 @@
import { LdoJsonldContext } from "@ldo/ldo";
/**
* =============================================================================
* solidProfileContext: JSONLD Context for solidProfile
* =============================================================================
*/
export const solidProfileContext: LdoJsonldContext = {
type: {
"@id": "@type",
},
Person: {
"@id": "http://schema.org/Person",
"@context": {
type: {
"@id": "@type",
},
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",
"@isCollection": true,
},
hasEmail: {
"@id": "http://www.w3.org/2006/vcard/ns#hasEmail",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
key: {
"@id": "http://www.w3.org/ns/auth/cert#key",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@isCollection": true,
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@isCollection": true,
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@isCollection": true,
},
},
},
Person2: {
"@id": "http://xmlns.com/foaf/0.1/Person",
"@context": {
type: {
"@id": "@type",
},
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",
"@isCollection": true,
},
hasEmail: {
"@id": "http://www.w3.org/2006/vcard/ns#hasEmail",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
key: {
"@id": "http://www.w3.org/ns/auth/cert#key",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@isCollection": true,
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@isCollection": true,
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@isCollection": true,
},
},
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
Dom: {
"@id": "http://www.w3.org/2006/vcard/ns#Dom",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Home: {
"@id": "http://www.w3.org/2006/vcard/ns#Home",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
ISDN: {
"@id": "http://www.w3.org/2006/vcard/ns#ISDN",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Internet: {
"@id": "http://www.w3.org/2006/vcard/ns#Internet",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Intl: {
"@id": "http://www.w3.org/2006/vcard/ns#Intl",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Label: {
"@id": "http://www.w3.org/2006/vcard/ns#Label",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Parcel: {
"@id": "http://www.w3.org/2006/vcard/ns#Parcel",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Postal: {
"@id": "http://www.w3.org/2006/vcard/ns#Postal",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Pref: {
"@id": "http://www.w3.org/2006/vcard/ns#Pref",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Work: {
"@id": "http://www.w3.org/2006/vcard/ns#Work",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
X400: {
"@id": "http://www.w3.org/2006/vcard/ns#X400",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
mode: {
"@id": "http://www.w3.org/ns/auth/acl#mode",
"@isCollection": true,
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@isCollection": true,
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@isCollection": true,
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@isCollection": true,
},
};

@ -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/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 { LdoJsonldContext, LdSet } from "@ldo/ldo";
/**
* =============================================================================
* Typescript Typings for solidProfile
* =============================================================================
*/
/**
* SolidProfileShape Type
*/
export interface SolidProfileShape {
"@id"?: string;
"@context"?: LdoJsonldContext;
/**
* Defines the node as a Person (from Schema.org) | Defines the node as a Person (from foaf)
*/
type: LdSet<
| {
"@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?: LdSet<AddressShape>;
/**
* The person's email.
*/
hasEmail?: LdSet<EmailShape>;
/**
* A link to the person's photo
*/
hasPhoto?: {
"@id": string;
};
/**
* Photo link but in string form
*/
img?: string;
/**
* Person's telephone number
*/
hasTelephone?: LdSet<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?: LdSet<TrustedAppShape>;
/**
* A list of RSA public keys that are associated with private keys the user holds.
*/
key?: LdSet<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?: LdSet<{
"@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?: LdSet<{
"@id": string;
}>;
/**
* A registry of all types used on the user's Pod (for public access)
*/
publicTypeIndex?: LdSet<{
"@id": string;
}>;
/**
* A list of WebIds for all the people this user knows.
*/
knows?: LdSet<SolidProfileShape>;
}
/**
* AddressShape Type
*/
export interface AddressShape {
"@id"?: string;
"@context"?: LdoJsonldContext;
/**
* 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"?: LdoJsonldContext;
/**
* 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"?: LdoJsonldContext;
/**
* 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"?: LdoJsonldContext;
/**
* The level of access provided to this origin
*/
mode: LdSet<
| {
"@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"?: LdoJsonldContext;
/**
* 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" ;
}

@ -5,9 +5,9 @@ import {
UnexpectedResourceError,
} from "../src/results/error/ErrorResult";
import { InvalidUriError } from "../src/results/error/InvalidUriError";
import { MockResouce } from "./MockResource";
import { MockResource } from "./mocks/MockResource";
const mockResource = new MockResouce("https://example.com/");
const mockResource = new MockResource("https://example.com/");
describe("ErrorResult", () => {
describe("fromThrown", () => {
@ -38,7 +38,7 @@ describe("ErrorResult", () => {
});
describe("default messages", () => {
class ConcreteResourceError extends ResourceError<MockResouce> {
class ConcreteResourceError extends ResourceError<MockResource> {
readonly type = "concreteResourceError" as const;
}
class ConcreteErrorResult extends ErrorResult {

@ -0,0 +1,58 @@
import type { ResourceInfo } from "@ldo/test-solid-server";
export const BASE_CONTAINER = "http://localhost:3005/test-container/";
export const MAIN_PROFILE_URI = `${BASE_CONTAINER}mainProfile.ttl`;
export const MAIN_PROFILE_SUBJECT = `${MAIN_PROFILE_URI}#me`;
export const OTHER_PROFILE_URI = `${BASE_CONTAINER}otherProfile.ttl`;
export const OTHER_PROFILE_SUBJECT = `${OTHER_PROFILE_URI}#me`;
export const THIRD_PROFILE_URI = `${BASE_CONTAINER}thirdProfile.ttl`;
export const THIRD_PROFILE_SUBJECT = `${THIRD_PROFILE_URI}#me`;
export const linkTraversalData: ResourceInfo = {
slug: "test-container/",
isContainer: true,
contains: [
{
slug: "mainProfile.ttl",
isContainer: false,
mimeType: "text/turtle",
data: `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix : <#> .
:me a foaf:Person ;
foaf:name "Main User" ;
foaf:mbox <mailto:main@example.org> ;
foaf:knows <http://localhost:3005/test-container/otherProfile.ttl#me> .
`,
},
{
slug: "otherProfile.ttl",
isContainer: false,
mimeType: "text/turtle",
data: `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix : <#> .
:me a foaf:Person ;
foaf:name "Other User" ;
foaf:mbox <mailto:other@example.org> ;
foaf:knows <http://localhost:3005/test-container/mainProfile.ttl#me> .
`,
},
{
slug: "thirdProfile.ttl",
isContainer: false,
mimeType: "text/turtle",
data: `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix : <#> .
:me a foaf:Person ;
foaf:name "Third User" ;
foaf:mbox <mailto:third@example.org> ;
foaf:knows <http://localhost:3005/test-container/mainProfile.ttl#me> .
`,
},
],
};

@ -0,0 +1,216 @@
import type { ConnectedLdoDataset } from "../src/ConnectedLdoDataset";
import { changeData, commitData, createConnectedLdoDataset } from "../src";
import {
solidConnectedPlugin,
type SolidConnectedPlugin,
} from "@ldo/connected-solid";
import { setupServer } from "@ldo/test-solid-server";
import {
linkTraversalData,
MAIN_PROFILE_SUBJECT,
MAIN_PROFILE_URI,
OTHER_PROFILE_URI,
THIRD_PROFILE_SUBJECT,
THIRD_PROFILE_URI,
} from "./LinkTraversalData";
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes";
import { wait } from "./util/wait";
describe("Link Traversal", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let solidLdoDataset: ConnectedLdoDataset<SolidConnectedPlugin[]>;
const s = setupServer(3005, linkTraversalData);
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
solidLdoDataset = createConnectedLdoDataset([solidConnectedPlugin]);
solidLdoDataset.setContext("solid", { fetch: s.fetchMock });
});
it("does a simple run to traverse data", async () => {
const mainProfileResource = solidLdoDataset.getResource(MAIN_PROFILE_URI);
const data = await solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.startLinkQuery(mainProfileResource, MAIN_PROFILE_SUBJECT, {
name: true,
knows: {
name: true,
},
})
.run();
const resourceUris = solidLdoDataset
.getResources()
.map((resource) => resource.uri);
expect(resourceUris.length).toBe(3);
expect(resourceUris).toContain(MAIN_PROFILE_URI);
expect(resourceUris).toContain(OTHER_PROFILE_URI);
expect(data.name).toBe("Main User");
expect(data.knows?.toArray()[0].name).toBe("Other User");
});
it("handles subscriptions if data changes locally", async () => {
const mainProfileResource = solidLdoDataset.getResource(MAIN_PROFILE_URI);
const linkQuery = solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.startLinkQuery(mainProfileResource, MAIN_PROFILE_SUBJECT, {
name: true,
knows: {
name: true,
},
});
await linkQuery.subscribe();
// Should have regular information
let mainProfile = solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.fromSubject(MAIN_PROFILE_SUBJECT);
let resourceUris = solidLdoDataset
.getResources()
.map((resource) => resource.uri);
expect(resourceUris.length).toBe(3);
expect(resourceUris).toContain(MAIN_PROFILE_URI);
expect(resourceUris).toContain(OTHER_PROFILE_URI);
expect(mainProfile.name).toBe("Main User");
expect(mainProfile.knows?.size).toBe(1);
expect(mainProfile.knows?.toArray()[0].name).toBe("Other User");
// Update to include a new document
const cMainProfile = changeData(mainProfile, mainProfileResource);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
cMainProfile.knows?.add({ "@id": THIRD_PROFILE_SUBJECT });
await commitData(cMainProfile);
// Wait for 200ms to allow the other file to be fetched
await wait(200);
// After the data is committed, the third profile should be present
mainProfile = solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.fromSubject(MAIN_PROFILE_SUBJECT);
resourceUris = solidLdoDataset
.getResources()
.map((resource) => resource.uri);
expect(resourceUris.length).toBe(4);
expect(resourceUris).toContain(MAIN_PROFILE_URI);
expect(resourceUris).toContain(OTHER_PROFILE_URI);
expect(resourceUris).toContain(THIRD_PROFILE_URI);
expect(mainProfile.name).toBe("Main User");
expect(mainProfile.knows?.size).toBe(2);
const knowNames = mainProfile.knows?.map((knowsPerson) => knowsPerson.name);
expect(knowNames).toContain("Other User");
expect(knowNames).toContain("Third User");
// Unsubscribe
});
it.only("handles subscriptions if data changes on the Pod", async () => {
const mainProfileResource = solidLdoDataset.getResource(MAIN_PROFILE_URI);
const linkQuery = solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.startLinkQuery(mainProfileResource, MAIN_PROFILE_SUBJECT, {
name: true,
knows: {
name: true,
},
});
const unsubscribeId = await linkQuery.subscribe();
// Should have regular information
let mainProfile = solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.fromSubject(MAIN_PROFILE_SUBJECT);
let resourceUris = solidLdoDataset
.getResources()
.map((resource) => resource.uri);
expect(resourceUris.length).toBe(3);
expect(resourceUris).toContain(MAIN_PROFILE_URI);
expect(resourceUris).toContain(OTHER_PROFILE_URI);
expect(mainProfile.name).toBe("Main User");
expect(mainProfile.knows?.size).toBe(1);
expect(mainProfile.knows?.toArray()[0].name).toBe("Other User");
let subscribedResources = linkQuery
.getSubscribedResources()
.map((resource) => resource.uri);
expect(subscribedResources.length).toBe(2);
expect(subscribedResources).toContain(MAIN_PROFILE_URI);
expect(subscribedResources).toContain(OTHER_PROFILE_URI);
// Update data on the Pod
await s.authFetch(MAIN_PROFILE_URI, {
method: "PATCH",
body: "INSERT DATA { <http://localhost:3005/test-container/mainProfile.ttl#me> <http://xmlns.com/foaf/0.1/knows> <http://localhost:3005/test-container/thirdProfile.ttl#me> . }",
headers: {
"Content-Type": "application/sparql-update",
},
});
await wait(1000);
// After the data is committed, the third profile should be present
mainProfile = solidLdoDataset
.usingType(SolidProfileShapeShapeType)
.fromSubject(MAIN_PROFILE_SUBJECT);
resourceUris = solidLdoDataset
.getResources()
.map((resource) => resource.uri);
expect(resourceUris.length).toBe(4);
expect(resourceUris).toContain(MAIN_PROFILE_URI);
expect(resourceUris).toContain(OTHER_PROFILE_URI);
expect(resourceUris).toContain(THIRD_PROFILE_URI);
expect(mainProfile.name).toBe("Main User");
expect(mainProfile.knows?.size).toBe(2);
const knowNames = mainProfile.knows?.map((knowsPerson) => knowsPerson.name);
expect(knowNames).toContain("Other User");
expect(knowNames).toContain("Third User");
subscribedResources = linkQuery
.getSubscribedResources()
.map((resource) => resource.uri);
expect(subscribedResources.length).toBe(3);
expect(subscribedResources).toContain(MAIN_PROFILE_URI);
expect(subscribedResources).toContain(OTHER_PROFILE_URI);
expect(subscribedResources).toContain(THIRD_PROFILE_URI);
// Unsubscribe
await linkQuery.unsubscribe(unsubscribeId);
await wait(200);
s.fetchMock.mockClear();
// Does not update when unsubscribed
await s.authFetch(MAIN_PROFILE_URI, {
method: "PATCH",
body: "INSERT DATA { <http://localhost:3005/test-container/mainProfile.ttl#me> <http://xmlns.com/foaf/0.1/knows> <http://localhost:3005/test-container/fourthProfile.ttl#me> . }",
headers: {
"Content-Type": "application/sparql-update",
},
});
await wait(1000);
expect(s.fetchMock).not.toHaveBeenCalled();
subscribedResources = linkQuery
.getSubscribedResources()
.map((resource) => resource.uri);
expect(subscribedResources.length).toBe(0);
// Check that all resources are unsubscribed from notifications
const resources = solidLdoDataset.getResources();
resources.forEach((resource) => {
expect(resource.isSubscribedToNotifications()).toBe(false);
});
const cMainProfile = changeData(mainProfile, mainProfileResource);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
cMainProfile.knows?.add({
"@id": "http://localhost:3005/test-container/fifthProfile.ttl#me",
});
await commitData(cMainProfile);
});
});

@ -1,73 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import EventEmitter from "events";
import type { ResourceError } from "../src";
import {
Unfetched,
type ConnectedResult,
type Resource,
type ResourceEventEmitter,
} from "../src";
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { ReadSuccess } from "../src/results/success/ReadSuccess";
import type { UpdateSuccess } from "../src/results/success/UpdateSuccess";
export class MockResouce
extends (EventEmitter as new () => ResourceEventEmitter)
implements Resource
{
isError = false as const;
uri: string;
type = "mock" as const;
status: ConnectedResult;
constructor(uri: string) {
super();
this.uri = uri;
this.status = new Unfetched(this);
}
isLoading(): boolean {
throw new Error("Method not implemented.");
}
isFetched(): boolean {
throw new Error("Method not implemented.");
}
isUnfetched(): boolean {
throw new Error("Method not implemented.");
}
isDoingInitialFetch(): boolean {
throw new Error("Method not implemented.");
}
isPresent(): boolean | undefined {
throw new Error("Method not implemented.");
}
isAbsent(): boolean | undefined {
throw new Error("Method not implemented.");
}
isSubscribedToNotifications(): boolean {
throw new Error("Method not implemented.");
}
read(): Promise<ReadSuccess<any> | ResourceError<any>> {
throw new Error("Method not implemented.");
}
readIfUnfetched(): Promise<ReadSuccess<any> | ResourceError<any>> {
throw new Error("Method not implemented.");
}
update(
_datasetChanges: DatasetChanges,
): Promise<UpdateSuccess<any> | ResourceError<any>> {
throw new Error("Method not implemented.");
}
subscribeToNotifications(_callbacks?: {
onNotification: (message: any) => void;
onNotificationError: (err: Error) => void;
}): Promise<string> {
throw new Error("Method not implemented.");
}
unsubscribeFromNotifications(_subscriptionId: string): Promise<void> {
throw new Error("Method not implemented.");
}
unsubscribeFromAllNotifications(): Promise<void> {
throw new Error("Method not implemented.");
}
}

@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import EventEmitter from "events";
import type { ResourceError } from "../../src";
import {
Unfetched,
type ConnectedResult,
type Resource,
type ResourceEventEmitter,
} from "../../src";
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { ReadSuccess } from "../../src/results/success/ReadSuccess";
import type { UpdateSuccess } from "../../src/results/success/UpdateSuccess";
export class MockResource
extends (EventEmitter as new () => ResourceEventEmitter)
implements Resource
{
isError = false as const;
uri: string;
type = "mock" as const;
status: ConnectedResult;
constructor(uri: string) {
super();
this.uri = uri;
this.status = new Unfetched(this);
}
isLoading = jest.fn<boolean, []>();
isFetched = jest.fn<boolean, []>();
isUnfetched = jest.fn<boolean, []>();
isDoingInitialFetch = jest.fn<boolean, []>();
isPresent = jest.fn<boolean | undefined, []>();
isAbsent = jest.fn<boolean | undefined, []>();
isSubscribedToNotifications = jest.fn<boolean, []>();
read = jest.fn<Promise<ReadSuccess<any> | ResourceError<any>>, []>();
readIfUnfetched = jest.fn<
Promise<ReadSuccess<any> | ResourceError<any>>,
[]
>();
update = jest.fn<
Promise<UpdateSuccess<any> | ResourceError<any>>,
[DatasetChanges]
>();
subscribeToNotifications = jest.fn<
Promise<string>,
[
{
onNotification: (message: any) => void;
onNotificationError: (err: Error) => void;
}?,
]
>();
unsubscribeFromNotifications = jest.fn<Promise<void>, [string]>();
unsubscribeFromAllNotifications = jest.fn<Promise<void>, []>();
}

@ -0,0 +1,3 @@
export async function wait(time: number) {
return new Promise((resolve) => setTimeout(resolve, time));
}

@ -1,6 +1,6 @@
{
"name": "@ldo/dataset",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "An RDFJS dataset implementation",
"main": "dist/index.js",
"scripts": {
@ -31,7 +31,7 @@
"ts-node": "^9.1.1"
},
"dependencies": {
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@rdfjs/dataset": "^1.1.0",
"buffer": "^6.0.3",
"readable-stream": "^4.2.0"

@ -1,6 +1,6 @@
{
"name": "@ldo/jsonld-dataset-proxy",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "",
"main": "dist/index.js",
"scripts": {
@ -37,8 +37,8 @@
"src"
],
"dependencies": {
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/subscribable-dataset": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@ldo/subscribable-dataset": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/dataset": "^1.1.0",
"jsonld2graphobject": "^0.0.4"

@ -1,6 +1,6 @@
{
"name": "@ldo/ldo",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "",
"main": "dist/index.js",
"scripts": {
@ -23,7 +23,7 @@
},
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/ldo#readme",
"devDependencies": {
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@rdfjs/types": "^1.0.1",
"@types/jsonld": "^1.5.6",
"@types/n3": "^1.10.4",
@ -34,9 +34,9 @@
"typedoc-plugin-markdown": "^3.17.1"
},
"dependencies": {
"@ldo/dataset": "^1.0.0-alpha.3",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.3",
"@ldo/subscribable-dataset": "^1.0.0-alpha.3",
"@ldo/dataset": "^1.0.0-alpha.9",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.9",
"@ldo/subscribable-dataset": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"buffer": "^6.0.3",
"readable-stream": "^4.3.0"

@ -31,8 +31,8 @@ export class LdoBuilder<Type extends LdoBase> {
/**
* @internal
*/
private jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder;
private shapeType: ShapeType<Type>;
protected jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder;
protected shapeType: ShapeType<Type>;
/**
* Initializes the LdoBuilder

@ -974,7 +974,6 @@ export interface RSAPublicKeyShape {
export interface SolidProfileShape {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* Defines the node as a Person (from Schema.org) | Defines the node as a Person (from foaf)
*/

@ -1,6 +1,6 @@
{
"name": "@ldo/rdf-utils",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "Some RDF Utilities to support LDO librariers",
"main": "dist/index.js",
"scripts": {

@ -127,6 +127,7 @@ Hooks
- [useMatchSubject](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useMatchObject](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useSubscribeToResource](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useLinkQuery](https://ldo.js.org/latest/api/react/useLinkQuery/)
## Sponsorship
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).

@ -1,6 +1,6 @@
{
"name": "@ldo/react",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "A React library for LDO.",
"main": "dist/index.js",
"scripts": {
@ -29,11 +29,11 @@
"ts-node": "^10.9.2"
},
"dependencies": {
"@ldo/connected": "^1.0.0-alpha.3",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.3",
"@ldo/ldo": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/subscribable-dataset": "^1.0.0-alpha.3",
"@ldo/connected": "^1.0.0-alpha.9",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.9",
"@ldo/ldo": "^1.0.0-alpha.9",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@ldo/subscribable-dataset": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6"
},

@ -9,6 +9,7 @@ import { createUseMatchSubject } from "./methods/useMatchSubject";
import { createUseResource } from "./methods/useResource";
import { createUseSubject } from "./methods/useSubject";
import { createUseSubscribeToResource } from "./methods/useSubscribeToResource";
import { createUseLinkQuery } from "./methods/useLinkQuery";
/**
* A function that creates all common react functions given specific plugin.
@ -29,6 +30,7 @@ import { createUseSubscribeToResource } from "./methods/useSubscribeToResource";
* useResource,
* useSubject,
* useSubscribeToResource,
* useLinkQuery,
* } = createLdoReactMethods([
* solidConnectedPlugin,
* nextGraphConnectedPlugin
@ -70,5 +72,6 @@ export function createLdoReactMethods<
useResource: createUseResource(dataset),
useSubject: createUseSubject(dataset),
useSubscribeToResource: createUseSubscribeToResource(dataset),
useLinkQuery: createUseLinkQuery(dataset),
};
}

@ -6,8 +6,6 @@ export * from "./methods/useMatchSubject";
export * from "./methods/useResource";
export * from "./methods/useSubject";
export * from "./methods/useSubscribeToResource";
export * from "./methods/useLinkQuery";
export * from "./util/TrackingProxyContext";
export * from "./util/TrackingSetProxy";
export * from "./util/TrackingSubjectProxy";
export * from "./util/useTrackingProxy";

@ -0,0 +1,72 @@
import type {
ConnectedLdoDataset,
ConnectedPlugin,
ExpandDeep,
LQInput,
LQReturn,
ResourceLinkQuery,
} from "@ldo/connected";
import type { LdoBase, LdoBuilder, ShapeType } from "@ldo/ldo";
import type { SubjectNode } from "@ldo/rdf-utils";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTrackingProxy } from "../util/useTrackingProxy";
/**
* @internal
*
* Creates a useMatchSubject function.
*/
export function createUseLinkQuery<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
) {
/**
* Returns an array of matching linked data objects. Triggers a rerender if
* the data is updated.
*/
return function useQueryLink<
Type extends LdoBase,
QueryInput extends LQInput<Type>,
>(
shapeType: ShapeType<Type>,
startingResource: string,
startingSubject: SubjectNode | string,
linkQuery: QueryInput,
): ExpandDeep<LQReturn<Type, QueryInput>> | undefined {
const linkQueryRef = useRef<
ResourceLinkQuery<Type, QueryInput, Plugins> | undefined
>();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (linkQueryRef.current) {
linkQueryRef.current.unsubscribeAll();
}
const resource = dataset.getResource(startingResource);
setIsLoading(true);
linkQueryRef.current = dataset
.usingType(shapeType)
.startLinkQuery(resource, startingSubject, linkQuery);
linkQueryRef.current.subscribe().then(() => setIsLoading(false));
return () => {
linkQueryRef.current?.unsubscribeAll();
};
}, [shapeType, startingResource, startingSubject, linkQuery]);
const fromSubject = useCallback(
(builder: LdoBuilder<Type>) => {
if (!startingSubject) return;
return builder.fromSubject(startingSubject);
},
[startingSubject],
);
const linkedDataObject = useTrackingProxy(shapeType, fromSubject, dataset);
return isLoading
? undefined
: (linkedDataObject as unknown as ExpandDeep<LQReturn<Type, QueryInput>>);
};
}

@ -1,12 +1,7 @@
import {
ContextUtil,
JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy";
import { LdoBuilder } from "@ldo/ldo";
import type { LdoBuilder } from "@ldo/ldo";
import type { LdoBase, LdoDataset, ShapeType } from "@ldo/ldo";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TrackingProxyContext } from "./TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
import { createTrackingProxyBuilder } from "@ldo/connected";
/**
* @internal
@ -28,22 +23,7 @@ export function useTrackingProxy<Type extends LdoBase, ReturnType>(
const linkedDataObject = useMemo(() => {
// Remove all current subscriptions
dataset.removeListenerFromAllEvents(forceUpdate);
// Rebuild the LdoBuilder from scratch to inject TrackingProxyContext
const contextUtil = new ContextUtil(shapeType.context);
const proxyContext = new TrackingProxyContext(
{
dataset,
contextUtil,
writeGraphs: [defaultGraph()],
languageOrdering: ["none", "en", "other"],
},
forceUpdate,
);
const builder = new LdoBuilder(
new JsonldDatasetProxyBuilder(proxyContext),
shapeType,
);
const builder = createTrackingProxyBuilder(dataset, shapeType, forceUpdate);
return createLdo(builder);
}, [shapeType, dataset, forceUpdateCounter, forceUpdate, createLdo]);

@ -1,6 +1,6 @@
{
"name": "@ldo/schema-converter-shex",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "",
"main": "dist/index.js",
"scripts": {
@ -20,7 +20,7 @@
},
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/schema-converter-shex#readme",
"devDependencies": {
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.3",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.9",
"@shexjs/parser": "^1.0.0-alpha.24",
"@types/jsonld": "^1.5.6",
"@types/shexj": "^2.1.3",
@ -31,7 +31,7 @@
"dist"
],
"dependencies": {
"@ldo/traverser-shexj": "^1.0.0-alpha.3",
"@ldo/traverser-shexj": "^1.0.0-alpha.9",
"dts-dom": "~3.6.0",
"jsonld2graphobject": "^0.0.5"
},

@ -20,17 +20,13 @@ Now install the @ldo/solid library
npm i @ldo/solid @ldo/solid-react
```
<details>
<summary>
Manual Installation
</summary>
### Manual Installation
If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently.
```
npm i @ldo/ldo @ldo/solid @ldo/solid-react
```
</details>
## Simple Example
@ -115,18 +111,19 @@ export default App;
Providers
- [BrowserSolidLdoProvider](https://ldo.js.org/latest/api/react/BrowserSolidLdoProvider/)
- [SolidLdoProvider](https://ldo.js.org/latest/api/react/SolidLdoProvider/)
- [BrowserSolidLdoProvider](https://ldo.js.org/latest/api/solid-react/BrowserSolidLdoProvider/)
- [SolidLdoProvider](https://ldo.js.org/latest/api/solid-react/SolidLdoProvider/)
Hooks
- [useLdo](https://ldo.js.org/latest/api/react/useLdo/)
- [useResource](https://ldo.js.org/latest/api/react/useResource/)
- [useRootContainer](https://ldo.js.org/latest/api/react/useRootContainer/)
- [useSolidAuth](https://ldo.js.org/latest/api/react/useSolidAuth/)
- [useSubject](https://ldo.js.org/latest/api/react/useSubject/)
- [useMatchSubject](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useMatchObject](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useSubscribeToResource](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useLdo](https://ldo.js.org/latest/api/solid-react/useLdo/)
- [useResource](https://ldo.js.org/latest/api/solid-react/useResource/)
- [useRootContainer](https://ldo.js.org/latest/api/solid-react/useRootContainer/)
- [useSolidAuth](https://ldo.js.org/latest/api/solid-react/useSolidAuth/)
- [useSubject](https://ldo.js.org/latest/api/solid-react/useSubject/)
- [useMatchSubject](https://ldo.js.org/latest/api/solid-react/useMatchSubject/)
- [useMatchObject](https://ldo.js.org/latest/api/solid-react/useMatchSubject/)
- [useSubscribeToResource](https://ldo.js.org/latest/api/solid-react/useMatchSubject/)
- [useLinkQUery](https://ldo.js.org/latest/api/solid-react/useLinkQuery/)
## Sponsorship
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).

@ -1,6 +1,6 @@
{
"name": "@ldo/solid-react",
"version": "1.0.0-alpha.5",
"version": "1.0.0-alpha.9",
"description": "A React library for LDO and Solid",
"main": "dist/index.js",
"scripts": {
@ -29,7 +29,7 @@
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@inrupt/jest-jsdom-polyfills": "^3.2.6",
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@rdfjs/types": "^1.0.1",
"@testing-library/react": "^14.1.2",
"babel-jest": "^29.7.0",
@ -41,9 +41,9 @@
},
"dependencies": {
"@inrupt/solid-client-authn-browser": "2.3.0",
"@ldo/connected": "^1.0.0-alpha.3",
"@ldo/connected-solid": "^1.0.0-alpha.3",
"@ldo/react": "^1.0.0-alpha.3",
"@ldo/connected": "^1.0.0-alpha.9",
"@ldo/connected-solid": "^1.0.0-alpha.9",
"@ldo/react": "^1.0.0-alpha.9",
"@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6"
},

@ -13,6 +13,7 @@ export const {
useResource,
useSubject,
useSubscribeToResource,
useLinkQuery,
} = createLdoReactMethods([solidConnectedPlugin]);
export const { BrowserSolidLdoProvider, useSolidAuth, useRootContainerFor } =

@ -0,0 +1,459 @@
import { LdoJsonldContext } from "@ldo/ldo";
/**
* =============================================================================
* solidProfileContext: JSONLD Context for solidProfile
* =============================================================================
*/
export const solidProfileContext: LdoJsonldContext = {
type: {
"@id": "@type",
},
Person: {
"@id": "http://schema.org/Person",
"@context": {
type: {
"@id": "@type",
},
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",
"@isCollection": true,
},
hasEmail: {
"@id": "http://www.w3.org/2006/vcard/ns#hasEmail",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
key: {
"@id": "http://www.w3.org/ns/auth/cert#key",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@isCollection": true,
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@isCollection": true,
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@isCollection": true,
},
},
},
Person2: {
"@id": "http://xmlns.com/foaf/0.1/Person",
"@context": {
type: {
"@id": "@type",
},
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",
"@isCollection": true,
},
hasEmail: {
"@id": "http://www.w3.org/2006/vcard/ns#hasEmail",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
key: {
"@id": "http://www.w3.org/ns/auth/cert#key",
"@type": "@id",
"@isCollection": true,
},
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",
"@isCollection": true,
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@isCollection": true,
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@isCollection": true,
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@isCollection": true,
},
},
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
Dom: {
"@id": "http://www.w3.org/2006/vcard/ns#Dom",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Home: {
"@id": "http://www.w3.org/2006/vcard/ns#Home",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
ISDN: {
"@id": "http://www.w3.org/2006/vcard/ns#ISDN",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Internet: {
"@id": "http://www.w3.org/2006/vcard/ns#Internet",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Intl: {
"@id": "http://www.w3.org/2006/vcard/ns#Intl",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Label: {
"@id": "http://www.w3.org/2006/vcard/ns#Label",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Parcel: {
"@id": "http://www.w3.org/2006/vcard/ns#Parcel",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Postal: {
"@id": "http://www.w3.org/2006/vcard/ns#Postal",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Pref: {
"@id": "http://www.w3.org/2006/vcard/ns#Pref",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
Work: {
"@id": "http://www.w3.org/2006/vcard/ns#Work",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
X400: {
"@id": "http://www.w3.org/2006/vcard/ns#X400",
"@context": {
type: {
"@id": "@type",
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
},
},
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
mode: {
"@id": "http://www.w3.org/ns/auth/acl#mode",
"@isCollection": true,
},
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",
"@isCollection": true,
},
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",
"@isCollection": true,
},
account: {
"@id": "http://www.w3.org/ns/solid/terms#account",
"@type": "@id",
},
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",
"@isCollection": true,
},
publicTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#publicTypeIndex",
"@type": "@id",
"@isCollection": true,
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@isCollection": true,
},
};

@ -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/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 { LdoJsonldContext, LdSet } from "@ldo/ldo";
/**
* =============================================================================
* Typescript Typings for solidProfile
* =============================================================================
*/
/**
* SolidProfileShape Type
*/
export interface SolidProfileShape {
"@id"?: string;
"@context"?: LdoJsonldContext;
/**
* Defines the node as a Person (from Schema.org) | Defines the node as a Person (from foaf)
*/
type: LdSet<
| {
"@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?: LdSet<AddressShape>;
/**
* The person's email.
*/
hasEmail?: LdSet<EmailShape>;
/**
* A link to the person's photo
*/
hasPhoto?: {
"@id": string;
};
/**
* Photo link but in string form
*/
img?: string;
/**
* Person's telephone number
*/
hasTelephone?: LdSet<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?: LdSet<TrustedAppShape>;
/**
* A list of RSA public keys that are associated with private keys the user holds.
*/
key?: LdSet<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?: LdSet<{
"@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?: LdSet<{
"@id": string;
}>;
/**
* A registry of all types used on the user's Pod (for public access)
*/
publicTypeIndex?: LdSet<{
"@id": string;
}>;
/**
* A list of WebIds for all the people this user knows.
*/
knows?: LdSet<SolidProfileShape>;
}
/**
* AddressShape Type
*/
export interface AddressShape {
"@id"?: string;
"@context"?: LdoJsonldContext;
/**
* 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"?: LdoJsonldContext;
/**
* 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"?: LdoJsonldContext;
/**
* 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"?: LdoJsonldContext;
/**
* The level of access provided to this origin
*/
mode: LdSet<
| {
"@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"?: LdoJsonldContext;
/**
* RSA Modulus
*/
modulus: string;
/**
* RSA Exponent
*/
exponent: number;
}

@ -2,10 +2,14 @@ import React, { useCallback, useEffect, useState } from "react";
import type { FunctionComponent } from "react";
import { render, screen, fireEvent, act } from "@testing-library/react";
import {
MAIN_PROFILE_SUBJECT,
MAIN_PROFILE_URI,
OTHER_PROFILE_URI,
SAMPLE_BINARY_URI,
SAMPLE_DATA_URI,
SERVER_DOMAIN,
setUpServer,
THIRD_PROFILE_SUBJECT,
} from "./setUpServer";
import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider";
import {
@ -17,9 +21,13 @@ import {
useRootContainerFor,
useSubject,
useSubscribeToResource,
useLinkQuery,
} from "../src";
import { PostShShapeType } from "./.ldo/post.shapeTypes";
import type { PostSh } from "./.ldo/post.typings";
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes";
import { changeData, commitData } from "@ldo/connected";
import type { SolidProfileShape } from "./.ldo/solidProfile.typings";
// Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000);
@ -650,4 +658,86 @@ describe("Integration Tests", () => {
unmount();
});
});
/**
* ===========================================================================
* useLinkQuery
* ===========================================================================
*/
describe("useLinkQuery", () => {
const linkQuery = {
name: true,
knows: {
name: true,
},
} as const;
it("Fetches a resource using useLinkQuery", async () => {
const UseLinkQueryTest: FunctionComponent = () => {
const profile = useLinkQuery(
SolidProfileShapeShapeType,
MAIN_PROFILE_URI,
MAIN_PROFILE_SUBJECT,
linkQuery,
);
const addProfile = useCallback(async () => {
const cProfile = changeData(
profile as SolidProfileShape,
dataset.getResource(MAIN_PROFILE_URI),
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
cProfile.knows?.add({ "@id": THIRD_PROFILE_SUBJECT });
await commitData(cProfile);
}, [profile]);
if (!profile) return <p>Loading</p>;
return (
<div>
<p role="profile-name">{profile.name}</p>
<ul role="list">
{profile.knows?.map((nestedProfile) => (
<li key={nestedProfile["@id"]}>{nestedProfile.name}</li>
))}
</ul>
<button role="add-profile" onClick={addProfile}>
Add Profile
</button>
</div>
);
};
const { unmount } = render(
<UnauthenticatedSolidLdoProvider>
<UseLinkQueryTest />
</UnauthenticatedSolidLdoProvider>,
);
await screen.findByText("Loading");
let profileNameElement = await screen.findByRole("profile-name");
expect(profileNameElement.textContent).toBe("Main User");
let list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe("Other User");
expect(list.children.length).toBe(1);
// Click button to add a publisher
await fireEvent.click(screen.getByText("Add Profile"));
// Give some time for notifications to propogate
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
});
profileNameElement = await screen.findByRole("profile-name");
expect(profileNameElement.textContent).toBe("Main User");
list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe("Other User");
expect(list.children[1].innerHTML).toBe("Third User");
expect(list.children.length).toBe(2);
unmount();
});
});
});

@ -20,6 +20,20 @@ export const SAMPLE2_BINARY_URI =
`${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as SolidLeafUri;
export const SAMPLE_CONTAINER_URI =
`${TEST_CONTAINER_URI}sample_container/` as SolidContainerUri;
export const LINK_QUERY_CONTAINER = `${ROOT_CONTAINER}link-query/`;
export const MAIN_PROFILE_URI =
`${LINK_QUERY_CONTAINER}main-profile.ttl` as SolidContainerUri;
export const MAIN_PROFILE_SUBJECT =
`${MAIN_PROFILE_URI}#me` as SolidContainerUri;
export const OTHER_PROFILE_URI =
`${LINK_QUERY_CONTAINER}other-profile.ttl` as SolidContainerUri;
export const OTHER_PROFILE_SUBJECT =
`${OTHER_PROFILE_URI}#me` as SolidContainerUri;
export const THIRD_PROFILE_URI =
`${LINK_QUERY_CONTAINER}third-profile.ttl` as SolidContainerUri;
export const THIRD_PROFILE_SUBJECT =
`${THIRD_PROFILE_URI}#me` as SolidContainerUri;
export const EXAMPLE_POST_TTL = `@prefix schema: <http://schema.org/> .
<#Post1>

@ -0,0 +1,7 @@
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix : <#> .
:me a foaf:Person ;
foaf:name "Main User" ;
foaf:mbox <mailto:main@example.org> ;
foaf:knows <http://localhost:3002/example/link-query/other-profile.ttl#me> .

@ -0,0 +1,7 @@
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix : <#> .
:me a foaf:Person ;
foaf:name "Other User" ;
foaf:mbox <mailto:other@example.org> ;
foaf:knows <http://localhost:3002/example/link-query/main-profile.ttl#me> .

@ -0,0 +1,7 @@
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix : <#> .
:me a foaf:Person ;
foaf:name "Third User" ;
foaf:mbox <mailto:third@example.org> ;
foaf:knows <http://localhost:3002/example/link-query/main-profile.ttl#me> .

@ -1,6 +1,6 @@
{
"name": "@ldo/solid-type-index",
"version": "1.0.0-alpha.5",
"version": "1.0.0-alpha.9",
"description": "Solid Type Index support for LDO",
"main": "dist/index.js",
"scripts": {
@ -26,7 +26,7 @@
},
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme",
"devDependencies": {
"@ldo/rdf-utils": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"@rdfjs/types": "^1.0.1",
"@testing-library/react": "^14.1.2",
"jest-environment-jsdom": "^29.7.0",
@ -34,8 +34,8 @@
"ts-node": "^10.9.2"
},
"dependencies": {
"@ldo/connected-solid": "^1.0.0-alpha.3",
"@ldo/solid-react": "^1.0.0-alpha.5"
"@ldo/connected-solid": "^1.0.0-alpha.9",
"@ldo/solid-react": "^1.0.0-alpha.9"
},
"files": [
"dist",

@ -37,6 +37,8 @@ export function useInstanceUris(classUri: string): SolidLeafUri[] {
useEffect(() => {
getInstanceUris(classUri, typeRegistrations.toArray(), {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
solidLdoDataset: dataset,
}).then(setLeafUris);
}, [typeRegistrations]);

@ -4,8 +4,6 @@ import type { SolidConnectedPlugin } from "@ldo/connected-solid";
import { createSolidLdoDataset, guaranteeFetch } from "@ldo/connected-solid";
export interface Options {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore I'm honestly just tired of dealing with this at this point
solidLdoDataset?: IConnectedLdoDataset<SolidConnectedPlugin[]>;
fetch?: typeof fetch;
}

@ -35,11 +35,15 @@ describe("General Tests", () => {
const solidLdoDataset = createSolidLdoDataset();
const typeRegistrations = await getTypeRegistrations(WEB_ID, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
solidLdoDataset,
});
const addressBookUris = await getInstanceUris(
ADDRESS_BOOK,
typeRegistrations.toArray(),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
{ solidLdoDataset },
);
expect(addressBookUris).toEqual(
@ -52,6 +56,8 @@ describe("General Tests", () => {
BOOKMARK,
typeRegistrations.toArray(),
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
solidLdoDataset,
},
);
@ -65,6 +71,8 @@ describe("General Tests", () => {
const solidLdoDataset = createSolidLdoDataset();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
await initTypeIndex(WEB_ID, { solidLdoDataset });
const profile = solidLdoDataset
@ -80,6 +88,8 @@ describe("General Tests", () => {
const solidLdoDataset = createSolidLdoDataset();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
await getTypeRegistrations(WEB_ID, { solidLdoDataset });
const transaction = solidLdoDataset.startTransaction();
@ -119,6 +129,8 @@ describe("General Tests", () => {
const solidLdoDataset = createSolidLdoDataset();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: come back and see if we can fix this
await getTypeRegistrations(WEB_ID, { solidLdoDataset });
const transaction = solidLdoDataset.startTransaction();

@ -1,6 +1,6 @@
{
"name": "@ldo/subscribable-dataset",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "An RDFJS dataset implementation that can be subscribed to for updates",
"main": "dist/index.js",
"scripts": {
@ -27,11 +27,13 @@
"@rdfjs/types": "^1.0.1",
"@types/jsonld": "^1.5.6",
"@types/rdfjs__dataset": "^1.0.4",
"@types/uuid": "^10.0.0",
"ts-node": "^9.1.1"
},
"dependencies": {
"@ldo/dataset": "^1.0.0-alpha.3",
"@ldo/rdf-utils": "^1.0.0-alpha.3"
"@ldo/dataset": "^1.0.0-alpha.9",
"@ldo/rdf-utils": "^1.0.0-alpha.9",
"uuid": "^11.1.0"
},
"files": [
"dist",

@ -16,6 +16,7 @@ import type {
ITransactionDatasetFactory,
} from "./types";
import { ExtendedDataset } from "@ldo/dataset";
import { v4 } from "uuid";
/**
* A wrapper for a dataset that allows subscriptions to be made on nodes to
@ -175,7 +176,10 @@ export class SubscribableDataset<InAndOutQuad extends BaseQuad = BaseQuad>
// A mapping of serialized QuadMatches to the changed quads
const matchingDatasetChanges: Record<
string,
DatasetChanges<InAndOutQuad>
{
changes: DatasetChanges<InAndOutQuad>;
triggerQuadMatch: QuadMatch;
}
> = {};
// Population MatchingDatasetChanges
@ -217,13 +221,18 @@ export class SubscribableDataset<InAndOutQuad extends BaseQuad = BaseQuad>
if (this.eventEmitter.listenerCount(eventName) > 0) {
// Set matchingDatasetChanges to include data to emit
if (!matchingDatasetChanges[eventName]) {
matchingDatasetChanges[eventName] = {};
matchingDatasetChanges[eventName] = {
triggerQuadMatch: quadMatch,
changes: {},
};
}
if (!matchingDatasetChanges[eventName][changeType]) {
matchingDatasetChanges[eventName][changeType] =
if (!matchingDatasetChanges[eventName].changes[changeType]) {
matchingDatasetChanges[eventName].changes[changeType] =
this.datasetFactory.dataset();
}
matchingDatasetChanges[eventName][changeType]?.add(changedQuad);
matchingDatasetChanges[eventName].changes[changeType]?.add(
changedQuad,
);
}
});
});
@ -231,10 +240,16 @@ export class SubscribableDataset<InAndOutQuad extends BaseQuad = BaseQuad>
populateMatchingDatasetChanges("added");
populateMatchingDatasetChanges("removed");
const transactionId = v4();
// Alert all listeners
Object.entries(matchingDatasetChanges).forEach(
([quadMatchString, changes]) => {
this.eventEmitter.emit(quadMatchString, changes);
([quadMatchString, info]) => {
this.eventEmitter.emit(
quadMatchString,
info.changes,
transactionId,
info.triggerQuadMatch,
);
},
);
}

@ -6,6 +6,8 @@ import type { Dataset, BaseQuad, DatasetFactory } from "@rdfjs/types";
*/
export type nodeEventListener<InAndOutQuad extends BaseQuad = BaseQuad> = (
changes: DatasetChanges<InAndOutQuad>,
transactionId: string,
triggeringQuadMatch: QuadMatch,
) => void;
/**

@ -62,6 +62,12 @@ describe("SubscribableDataset", () => {
expect(callbackFunc).toBeCalledTimes(1);
expect(callbackFunc.mock.calls[0][0].added.size).toBe(1);
expect(callbackFunc.mock.calls[0][0].added.has(tomColorQuad)).toBe(true);
expect(callbackFunc.mock.calls[0][2]).toEqual([
namedNode("http://example.org/cartoons#Tom"),
null,
null,
null,
]);
});
it("Alerts when a node is removed", () => {
@ -74,6 +80,12 @@ describe("SubscribableDataset", () => {
expect(callbackFunc).toBeCalledTimes(1);
expect(callbackFunc.mock.calls[0][0].removed.size).toBe(1);
expect(callbackFunc.mock.calls[0][0].removed.has(tomTypeQuad)).toBe(true);
expect(callbackFunc.mock.calls[0][2]).toEqual([
namedNode("http://example.org/cartoons#Tom"),
null,
null,
null,
]);
});
it("Alerts when multiple quads are added", () => {
@ -87,6 +99,12 @@ describe("SubscribableDataset", () => {
expect(callbackFunc.mock.calls[0][0].added.size).toBe(2);
expect(callbackFunc.mock.calls[0][0].added.has(lickyNameQuad)).toBe(true);
expect(callbackFunc.mock.calls[0][0].added.has(lickyTypeQuad)).toBe(true);
expect(callbackFunc.mock.calls[0][2]).toEqual([
namedNode("http://example.org/cartoons#Licky"),
null,
null,
null,
]);
});
it("Alerts when bulk updated by only adding", () => {
@ -105,6 +123,12 @@ describe("SubscribableDataset", () => {
),
).toBe(true);
expect(callbackFuncLicky.mock.calls[0][0].removed).toBe(undefined);
expect(callbackFuncLicky.mock.calls[0][2]).toEqual([
namedNode("http://example.org/cartoons#Licky"),
null,
null,
null,
]);
});
it("Alerts when bulk updated by only removing", () => {
@ -123,6 +147,12 @@ describe("SubscribableDataset", () => {
),
).toBe(true);
expect(callbackFuncTom.mock.calls[0][0].added).toBe(undefined);
expect(callbackFuncTom.mock.calls[0][2]).toEqual([
namedNode("http://example.org/cartoons#Tom"),
null,
null,
null,
]);
});
it("Alerts when emit is called", () => {

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

@ -0,0 +1,2 @@
node_modules
*/data

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Jackson Morgan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,17 @@
# @ldo/test-solid-server
This is a reusable Solid Server to be used in Jest integration tests.
## Setup
Install cross-env
```
npm i --save-dev cross-env
```
Use the following to run your tests
```
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
```

@ -0,0 +1,34 @@
{
"name": "@ldo/test-solid-server",
"version": "1.0.0-alpha.9",
"description": "A solid server to be used in jest tests",
"main": "dist/index.js",
"scripts": {
"build": "tsc --project tsconfig.build.json && npm run copy-configs",
"prepublishOnly": "npm run build",
"copy-configs": "cp -r src/configs dist/configs",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern"
},
"repository": {
"type": "git",
"url": "git+https://github.com/o-development/ldo.git"
},
"author": "Jackson Morgan",
"license": "MIT",
"bugs": {
"url": "https://github.com/o-development/ldo/issues"
},
"homepage": "https://github.com/o-development/ldo/tree/main/packages/solid#readme",
"dependencies": {
"@inrupt/solid-client-authn-core": "^2.2.6",
"@solid/community-server": "^7.1.3",
"cross-env": "^7.0.3"
},
"files": [
"dist",
"src"
],
"publishConfig": {
"access": "public"
}
}

@ -7,14 +7,14 @@ import {
import fetch from "cross-fetch";
const config = {
podName: process.env.USER_NAME || "example",
email: process.env.EMAIL || "hello@example.com",
password: process.env.PASSWORD || "abc123",
podName: "example",
email: "hello@example.com",
password: "abc123",
};
async function getAuthorization(): Promise<string> {
async function getAuthorization(port: number): Promise<string> {
// First we request the account API controls to find out where we can log in
const indexResponse = await fetch("http://localhost:3001/.account/");
const indexResponse = await fetch(`http://localhost:${port}/.account/`);
const { controls } = await indexResponse.json();
// And then we log in to the account API
@ -32,11 +32,12 @@ async function getAuthorization(): Promise<string> {
}
async function getSecret(
port: number,
authorization: string,
): Promise<{ id: string; secret: string; resource: string }> {
// Now that we are logged in, we need to request the updated controls from the server.
// These will now have more values than in the previous example.
const indexResponse = await fetch("http://localhost:3001/.account/", {
const indexResponse = await fetch(`http://localhost:${port}/.account/`, {
headers: { authorization: `CSS-Account-Token ${authorization}` },
});
const { controls } = await indexResponse.json();
@ -53,7 +54,7 @@ async function getSecret(
// Only WebIDs linked to your account can be used.
body: JSON.stringify({
name: "my-token",
webId: `http://localhost:3001/${config.podName}/profile/card#me`,
webId: `http://localhost:${port}/${config.podName}/profile/card#me`,
}),
});
@ -65,6 +66,7 @@ async function getSecret(
}
async function getAccessToken(
port: number,
id: string,
secret: string,
): Promise<{ accessToken: string; dpopKey: KeyPair }> {
@ -79,9 +81,9 @@ async function getAccessToken(
secret,
)}`;
// This URL can be found by looking at the "token_endpoint" field at
// http://localhost:3001/.well-known/openid-configuration
// http://localhost:PORT/.well-known/openid-configuration
// if your server is hosted at http://localhost:3000/.
const tokenUrl = "http://localhost:3001/.oidc/token";
const tokenUrl = `http://localhost:${port}/.oidc/token`;
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
@ -104,9 +106,9 @@ async function getAccessToken(
}
}
export async function generateAuthFetch() {
const authorization = await getAuthorization();
const { id, secret } = await getSecret(authorization);
const { accessToken, dpopKey } = await getAccessToken(id, secret);
export async function generateAuthFetch(port: number) {
const authorization = await getAuthorization(port);
const { id, secret } = await getSecret(port, authorization);
const { accessToken, dpopKey } = await getAccessToken(port, id, secret);
return await buildAuthenticatedFetch(accessToken, { dpopKey });
}

@ -0,0 +1,44 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"import": [
"css:config/app/init/initialize-root.json",
"css:config/app/main/default.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/webhooks.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/no-accounts.json",
"css:config/identity/oidc/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",
"css:config/ldp/metadata-writer/default.json",
"css:config/ldp/modes/default.json",
"css:config/storage/backend/file.json",
"css:config/storage/key-value/resource-store.json",
"css:config/storage/location/root.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/suffix.json",
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/file.json",
"css:config/util/variables/default.json"
],
"@graph": [
{
"comment": [
"A Solid server that stores its resources on disk and uses WAC for authorization.",
"No registration and the root container is initialized to allow full access for everyone so make sure to change this."
]
}
]
}

@ -4,19 +4,14 @@ import * as path from "path";
import type { App } from "@solid/community-server";
import { AppRunner, resolveModulePath } from "@solid/community-server";
import "jest-rdf";
import type { SolidContainerUri } from "../src";
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/";
export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "";
export const ROOT_CONTAINER =
`${SERVER_DOMAIN}${ROOT_ROUTE}` as SolidContainerUri;
export const WEB_ID =
process.env.WEB_ID || `${SERVER_DOMAIN}example/profile/card#me`;
// Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000);
export async function createApp(customConfigPath?: string): Promise<App> {
export async function createApp(
port: number,
customConfigPath?: string,
): Promise<App> {
if (process.env.SERVER) {
return {
start: () => {},
@ -33,7 +28,7 @@ export async function createApp(customConfigPath?: string): Promise<App> {
config: customConfigPath ?? resolveModulePath("config/file-root.json"),
variableBindings: {},
shorthand: {
port: 3_001,
port: port,
loggingLevel: "off",
seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"),
rootFilePath: path.join(__dirname, "./data"),

@ -0,0 +1,3 @@
export * from "./createServer";
export * from "./setupTestServer";
export * from "./resourceUtils";

@ -0,0 +1,75 @@
export type ResourceInfo = ContainerInfo | LeafInfo;
interface ContainerInfo {
slug: string;
isContainer: true;
shouldNotInit?: boolean;
contains: (ContainerInfo | LeafInfo)[];
}
interface LeafInfo {
slug: string;
isContainer: false;
data: string;
shouldNotInit?: boolean;
mimeType: string;
}
export async function initResources(
rootUri: string,
resourceInfo: ResourceInfo,
authFetch: typeof fetch,
): Promise<void> {
if (resourceInfo?.shouldNotInit) return;
if (resourceInfo.isContainer) {
await authFetch(rootUri, {
method: "POST",
headers: {
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"',
slug: resourceInfo.slug,
},
});
await Promise.all(
resourceInfo.contains.map((subResourceInfo) =>
initResources(
`${rootUri}${resourceInfo.slug}`,
subResourceInfo,
authFetch,
),
),
);
} else {
await authFetch(`${rootUri}${resourceInfo.slug}`, {
method: "PUT",
headers: {
"content-type": resourceInfo.mimeType,
},
body: resourceInfo.data,
});
}
}
export async function cleanResources(
rootUri: string,
resourceInfo: ResourceInfo,
authFetch: typeof fetch,
): Promise<void> {
if (resourceInfo.isContainer) {
await Promise.all(
resourceInfo.contains.map((subResourceInfo) =>
cleanResources(
`${rootUri}${resourceInfo.slug}`,
subResourceInfo,
authFetch,
),
),
);
await authFetch(`${rootUri}${resourceInfo.slug}`, {
method: "DELETE",
});
} else {
await authFetch(`${rootUri}${resourceInfo.slug}`, {
method: "DELETE",
});
}
}

@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { App } from "@solid/community-server";
import { createApp } from "./createServer";
import path from "path";
import type { ResourceInfo } from "./resourceUtils";
import { cleanResources, initResources } from "./resourceUtils";
import { generateAuthFetch } from "./authFetch";
import fs from "fs/promises";
export function setupServer(
port: number,
resourceInfo: ResourceInfo,
customConfigPath?: string,
) {
const data: {
app: App;
fetchMock: jest.Mock<
Promise<Response>,
[input: RequestInfo | URL, init?: RequestInit | undefined]
>;
authFetch: typeof fetch;
rootUri: string;
} = {
rootUri: `http://localhost:${port}/`,
} as any;
let previousJestId: string | undefined;
let previousNodeEnv: string | undefined;
beforeAll(async () => {
// Remove Jest ID so that community solid server doesn't use the Jest Import
previousJestId = process.env.JEST_WORKER_ID;
previousNodeEnv = process.env.NODE_ENV;
delete process.env.JEST_WORKER_ID;
process.env.NODE_ENV = "other_test";
// Start up the server
data.app = await createApp(port, customConfigPath);
await data.app.start();
data.authFetch = await generateAuthFetch(port);
});
afterAll(async () => {
data.app.stop();
process.env.JEST_WORKER_ID = previousJestId;
process.env.NODE_ENV = previousNodeEnv;
const testDataPath = path.join(__dirname, "./data");
await fs.rm(testDataPath, { recursive: true, force: true });
});
beforeEach(async () => {
data.fetchMock = jest.fn(data.authFetch);
// Create a new document called sample.ttl
await initResources(data.rootUri, resourceInfo, data.authFetch);
});
afterEach(async () => {
await cleanResources(data.rootUri, resourceInfo, data.authFetch);
});
return data;
}

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"./src"
],
"exclude": [
"./dist",
"./coverage"
]
}

@ -1,6 +1,6 @@
{
"name": "@ldo/traverser-shexj",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "A type-traverser for ShexJ",
"main": "dist/index.js",
"scripts": {
@ -24,7 +24,7 @@
"src"
],
"dependencies": {
"@ldo/type-traverser": "^1.0.0-alpha.3"
"@ldo/type-traverser": "^1.0.0-alpha.9"
},
"publishConfig": {
"access": "public"

@ -1,6 +1,6 @@
{
"name": "@ldo/type-traverser",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.9",
"description": "An organized way to traverse over objects using typescript",
"main": "dist/index.js",
"scripts": {

@ -2,7 +2,9 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"paths": {
"@ldo/*": ["packages/*/src"]
"@ldo/*": [
"packages/*/src"
]
},
"allowJs": true
}

Loading…
Cancel
Save