commit
c30aa1b94d
@ -1,8 +1,8 @@ |
|||||||
import { ContextDefinition } from "jsonld"; |
import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy"; |
||||||
|
|
||||||
/** |
/** |
||||||
* ============================================================================= |
* ============================================================================= |
||||||
* <%- fileName %>Context: JSONLD Context for <%- fileName %> |
* <%- fileName %>Context: JSONLD Context for <%- fileName %> |
||||||
* ============================================================================= |
* ============================================================================= |
||||||
*/ |
*/ |
||||||
export const <%- fileName %>Context: ContextDefinition = <%- context %>; |
export const <%- fileName %>Context: LdoJsonldContext = <%- context %>; |
||||||
|
@ -0,0 +1,52 @@ |
|||||||
|
import { useLdo } from "./SolidLdoProvider"; |
||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
|
||||||
|
export function useSubscribeToResource(...uris: string[]): void { |
||||||
|
const { dataset } = useLdo(); |
||||||
|
const currentlySubscribed = useRef<Record<string, string>>({}); |
||||||
|
useEffect(() => { |
||||||
|
const resources = uris.map((uri) => dataset.getResource(uri)); |
||||||
|
const previousSubscriptions = { ...currentlySubscribed.current }; |
||||||
|
Promise.all<void>( |
||||||
|
resources.map(async (resource) => { |
||||||
|
if (!previousSubscriptions[resource.uri]) { |
||||||
|
// Prevent multiple triggers from created subscriptions while waiting
|
||||||
|
// for connection
|
||||||
|
currentlySubscribed.current[resource.uri] = "AWAITING"; |
||||||
|
// Read and subscribe
|
||||||
|
await resource.readIfUnfetched(); |
||||||
|
currentlySubscribed.current[resource.uri] = |
||||||
|
await resource.subscribeToNotifications(); |
||||||
|
} else { |
||||||
|
delete previousSubscriptions[resource.uri]; |
||||||
|
} |
||||||
|
}), |
||||||
|
).then(async () => { |
||||||
|
// Unsubscribe from all remaining previous subscriptions
|
||||||
|
await Promise.all( |
||||||
|
Object.entries(previousSubscriptions).map( |
||||||
|
async ([resourceUri, subscriptionId]) => { |
||||||
|
// Unsubscribe
|
||||||
|
delete currentlySubscribed.current[resourceUri]; |
||||||
|
const resource = dataset.getResource(resourceUri); |
||||||
|
await resource.unsubscribeFromNotifications(subscriptionId); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
}); |
||||||
|
}, [uris]); |
||||||
|
|
||||||
|
// Cleanup Subscriptions
|
||||||
|
useEffect(() => { |
||||||
|
return () => { |
||||||
|
Promise.all( |
||||||
|
Object.entries(currentlySubscribed.current).map( |
||||||
|
async ([resourceUri, subscriptionId]) => { |
||||||
|
const resource = dataset.getResource(resourceUri); |
||||||
|
await resource.unsubscribeFromNotifications(subscriptionId); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"extends": ["../../.eslintrc"] |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
test/test-server/data |
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2023 Jackson Morgan |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,49 @@ |
|||||||
|
# @ldo/solid-type-index |
||||||
|
|
||||||
|
A library to handle [type indexes](https://solid.github.io/type-indexes/index.html) with [LDO](https://ldo.js.org). |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
``` |
||||||
|
npm i @ldo/solid-type-index @ldo/solid |
||||||
|
``` |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
|
||||||
|
```typescript |
||||||
|
import { initTypeIndex } from "@ldo/solid-type-index"; |
||||||
|
import { createSolidLdoDataset } from "@ldo/solid"; |
||||||
|
|
||||||
|
async function main() { |
||||||
|
const myWebId = "https://example.com/profile/card#me"; |
||||||
|
const solidLdoDataset = createSolidLodDataset(); |
||||||
|
|
||||||
|
// Initialize a type index for a webId in case it isn't initialized |
||||||
|
await initTypeIndex(myWebId, { solidLdoDataset }); |
||||||
|
|
||||||
|
// Get Type Registrations |
||||||
|
const typeRegistrations = await getTypeRegistrations(WEB_ID, { |
||||||
|
solidLdoDataset, |
||||||
|
}); |
||||||
|
|
||||||
|
// Get Instance Uris (the URIs for resources that contain an instance of a |
||||||
|
// class) |
||||||
|
const bookmarkUris: string[] = await getInstanceUris( |
||||||
|
"http://www.w3.org/2002/01/bookmark#Bookmark", |
||||||
|
typeRegistrations, |
||||||
|
{ solidLdoDataset } |
||||||
|
); |
||||||
|
|
||||||
|
} |
||||||
|
main(); |
||||||
|
``` |
||||||
|
|
||||||
|
## Sponsorship |
||||||
|
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/). |
||||||
|
|
||||||
|
[<img src="https://nlnet.nl/logo/banner.png" alt="nlnet foundation logo" width="300" />](https://nlnet.nl/) |
||||||
|
[<img src="https://nlnet.nl/image/logos/NGI0Entrust_tag.svg" alt="NGI Zero Entrust Logo" width="300" />](https://nlnet.nl/) |
||||||
|
|
||||||
|
## Liscense |
||||||
|
MIT |
@ -0,0 +1,6 @@ |
|||||||
|
const sharedConfig = require("../../jest.config.js"); |
||||||
|
module.exports = { |
||||||
|
...sharedConfig, |
||||||
|
rootDir: "./", |
||||||
|
testEnvironment: "jsdom", |
||||||
|
}; |
@ -0,0 +1,2 @@ |
|||||||
|
import "@inrupt/jest-jsdom-polyfills"; |
||||||
|
globalThis.fetch = async () => new Response(); |
@ -0,0 +1,52 @@ |
|||||||
|
{ |
||||||
|
"name": "@ldo/solid-type-index", |
||||||
|
"version": "0.0.1-alpha.28", |
||||||
|
"description": "Solid Type Index support for LDO", |
||||||
|
"main": "dist/index.js", |
||||||
|
"scripts": { |
||||||
|
"build": "tsc --project tsconfig.build.json", |
||||||
|
"watch": "tsc --watch", |
||||||
|
"test": "npm run test:integration", |
||||||
|
"test:watch": "jest --watch", |
||||||
|
"prepublishOnly": "npm run test && npm run build", |
||||||
|
"build:ldo": "ldo build --input src/.shapes --output src/.ldo", |
||||||
|
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern", |
||||||
|
"test:integration": "start-server-and-test start-test-server http://localhost:3003 start-integration-test", |
||||||
|
"start-test-server": "ts-node ./test/test-server/runServer.ts", |
||||||
|
"start-integration-test": "jest --coverage" |
||||||
|
}, |
||||||
|
"repository": { |
||||||
|
"type": "git", |
||||||
|
"url": "git+https://github.com/o-development/ldobjects.git" |
||||||
|
}, |
||||||
|
"author": "Jackson Morgan", |
||||||
|
"license": "MIT", |
||||||
|
"bugs": { |
||||||
|
"url": "https://github.com/o-development/ldobjects/issues" |
||||||
|
}, |
||||||
|
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme", |
||||||
|
"devDependencies": { |
||||||
|
"@ldo/rdf-utils": "^0.0.1-alpha.24", |
||||||
|
"@rdfjs/types": "^1.0.1", |
||||||
|
"@testing-library/react": "^14.1.2", |
||||||
|
"@types/jest": "^27.0.3", |
||||||
|
"@types/uuid": "^10.0.0", |
||||||
|
"jest-environment-jsdom": "^27.0.0", |
||||||
|
"start-server-and-test": "^2.0.3", |
||||||
|
"ts-jest": "^27.1.2", |
||||||
|
"ts-node": "^10.9.2" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@ldo/solid": "^0.0.1-alpha.28", |
||||||
|
"@ldo/solid-react": "^0.0.1-alpha.28", |
||||||
|
"uuid": "^11.0.5" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"dist", |
||||||
|
"src" |
||||||
|
], |
||||||
|
"publishConfig": { |
||||||
|
"access": "public" |
||||||
|
}, |
||||||
|
"gitHead": "c63f055aab22155b60a5fdee4172979b9c287dfa" |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* profileContext: JSONLD Context for profile |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
export const profileContext: LdoJsonldContext = { |
||||||
|
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, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,64 @@ |
|||||||
|
import { Schema } from "shexj"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* profileSchema: ShexJ Schema for profile |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
export const profileSchema: Schema = { |
||||||
|
type: "Schema", |
||||||
|
shapes: [ |
||||||
|
{ |
||||||
|
id: "https://shaperepo.com/schemas/solidProfile#TypeIndexProfile", |
||||||
|
type: "ShapeDecl", |
||||||
|
shapeExpr: { |
||||||
|
type: "Shape", |
||||||
|
expression: { |
||||||
|
type: "EachOf", |
||||||
|
expressions: [ |
||||||
|
{ |
||||||
|
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)", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
@ -0,0 +1,19 @@ |
|||||||
|
import { ShapeType } from "@ldo/ldo"; |
||||||
|
import { profileSchema } from "./profile.schema"; |
||||||
|
import { profileContext } from "./profile.context"; |
||||||
|
import { TypeIndexProfile } from "./profile.typings"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* LDO ShapeTypes profile |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* TypeIndexProfile ShapeType |
||||||
|
*/ |
||||||
|
export const TypeIndexProfileShapeType: ShapeType<TypeIndexProfile> = { |
||||||
|
schema: profileSchema, |
||||||
|
shape: "https://shaperepo.com/schemas/solidProfile#TypeIndexProfile", |
||||||
|
context: profileContext, |
||||||
|
}; |
@ -0,0 +1,27 @@ |
|||||||
|
import { ContextDefinition } from "jsonld"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* Typescript Typings for profile |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* TypeIndexProfile Type |
||||||
|
*/ |
||||||
|
export interface TypeIndexProfile { |
||||||
|
"@id"?: string; |
||||||
|
"@context"?: ContextDefinition; |
||||||
|
/** |
||||||
|
* A registry of all types used on the user's Pod (for private access only) |
||||||
|
*/ |
||||||
|
privateTypeIndex?: { |
||||||
|
"@id": string; |
||||||
|
}[]; |
||||||
|
/** |
||||||
|
* A registry of all types used on the user's Pod (for public access) |
||||||
|
*/ |
||||||
|
publicTypeIndex?: { |
||||||
|
"@id": string; |
||||||
|
}[]; |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* typeIndexContext: JSONLD Context for typeIndex |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
export const typeIndexContext: LdoJsonldContext = { |
||||||
|
type: { |
||||||
|
"@id": "@type" |
||||||
|
}, |
||||||
|
TypeIndex: { |
||||||
|
"@id": "http://www.w3.org/ns/solid/terms#TypeIndex", |
||||||
|
"@context": { |
||||||
|
type: { |
||||||
|
"@id": "@type", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
ListedDocument: { |
||||||
|
"@id": "http://www.w3.org/ns/solid/terms#ListedDocument", |
||||||
|
"@context": { |
||||||
|
type: { |
||||||
|
"@id": "@type", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
TypeRegistration: { |
||||||
|
"@id": "http://www.w3.org/ns/solid/terms#TypeRegistration", |
||||||
|
"@context": { |
||||||
|
type: { |
||||||
|
"@id": "@type", |
||||||
|
}, |
||||||
|
forClass: { |
||||||
|
"@id": "http://www.w3.org/ns/solid/terms#forClass", |
||||||
|
"@type": "@id", |
||||||
|
}, |
||||||
|
instance: { |
||||||
|
"@id": "http://www.w3.org/ns/solid/terms#instance", |
||||||
|
"@type": "@id", |
||||||
|
"@isCollection": true, |
||||||
|
}, |
||||||
|
instanceContainer: { |
||||||
|
"@id": "http://www.w3.org/ns/solid/terms#instanceContainer", |
||||||
|
"@type": "@id", |
||||||
|
"@isCollection": true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,144 @@ |
|||||||
|
import { Schema } from "shexj"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* typeIndexSchema: ShexJ Schema for typeIndex |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
export const typeIndexSchema: Schema = { |
||||||
|
type: "Schema", |
||||||
|
shapes: [ |
||||||
|
{ |
||||||
|
id: "https://shaperepo.com/schemas/solidProfile#TypeIndexDocument", |
||||||
|
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/ns/solid/terms#TypeIndex"], |
||||||
|
}, |
||||||
|
annotations: [ |
||||||
|
{ |
||||||
|
type: "Annotation", |
||||||
|
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||||
|
object: { |
||||||
|
value: "Defines the node as a TypeIndex", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "TripleConstraint", |
||||||
|
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||||
|
valueExpr: { |
||||||
|
type: "NodeConstraint", |
||||||
|
values: ["http://www.w3.org/ns/solid/terms#ListedDocument"], |
||||||
|
}, |
||||||
|
annotations: [ |
||||||
|
{ |
||||||
|
type: "Annotation", |
||||||
|
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||||
|
object: { |
||||||
|
value: "Defines the node as a Listed Document", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "https://shaperepo.com/schemas/solidProfile#TypeRegistration", |
||||||
|
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/ns/solid/terms#TypeRegistration"], |
||||||
|
}, |
||||||
|
annotations: [ |
||||||
|
{ |
||||||
|
type: "Annotation", |
||||||
|
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||||
|
object: { |
||||||
|
value: "Defines this node as a Type Registration", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "TripleConstraint", |
||||||
|
predicate: "http://www.w3.org/ns/solid/terms#forClass", |
||||||
|
valueExpr: { |
||||||
|
type: "NodeConstraint", |
||||||
|
nodeKind: "iri", |
||||||
|
}, |
||||||
|
annotations: [ |
||||||
|
{ |
||||||
|
type: "Annotation", |
||||||
|
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||||
|
object: { |
||||||
|
value: "The class of object at this type.", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "TripleConstraint", |
||||||
|
predicate: "http://www.w3.org/ns/solid/terms#instance", |
||||||
|
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 specific resource that contains the class.", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: "TripleConstraint", |
||||||
|
predicate: "http://www.w3.org/ns/solid/terms#instanceContainer", |
||||||
|
valueExpr: { |
||||||
|
type: "NodeConstraint", |
||||||
|
nodeKind: "iri", |
||||||
|
}, |
||||||
|
min: 0, |
||||||
|
max: -1, |
||||||
|
annotations: [ |
||||||
|
{ |
||||||
|
type: "Annotation", |
||||||
|
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||||
|
object: { |
||||||
|
value: "Containers that contain resources with the class.", |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
@ -0,0 +1,28 @@ |
|||||||
|
import { ShapeType } from "@ldo/ldo"; |
||||||
|
import { typeIndexSchema } from "./typeIndex.schema"; |
||||||
|
import { typeIndexContext } from "./typeIndex.context"; |
||||||
|
import { TypeIndexDocument, TypeRegistration } from "./typeIndex.typings"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* LDO ShapeTypes typeIndex |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* TypeIndexDocument ShapeType |
||||||
|
*/ |
||||||
|
export const TypeIndexDocumentShapeType: ShapeType<TypeIndexDocument> = { |
||||||
|
schema: typeIndexSchema, |
||||||
|
shape: "https://shaperepo.com/schemas/solidProfile#TypeIndexDocument", |
||||||
|
context: typeIndexContext, |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* TypeRegistration ShapeType |
||||||
|
*/ |
||||||
|
export const TypeRegistrationShapeType: ShapeType<TypeRegistration> = { |
||||||
|
schema: typeIndexSchema, |
||||||
|
shape: "https://shaperepo.com/schemas/solidProfile#TypeRegistration", |
||||||
|
context: typeIndexContext, |
||||||
|
}; |
@ -0,0 +1,58 @@ |
|||||||
|
import { ContextDefinition } from "jsonld"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* Typescript Typings for typeIndex |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* TypeIndexDocument Type |
||||||
|
*/ |
||||||
|
export interface TypeIndexDocument { |
||||||
|
"@id"?: string; |
||||||
|
"@context"?: ContextDefinition; |
||||||
|
/** |
||||||
|
* Defines the node as a TypeIndex | Defines the node as a Listed Document |
||||||
|
*/ |
||||||
|
type: ( |
||||||
|
| { |
||||||
|
"@id": "TypeIndex"; |
||||||
|
} |
||||||
|
| { |
||||||
|
"@id": "ListedDocument"; |
||||||
|
} |
||||||
|
)[]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* TypeRegistration Type |
||||||
|
*/ |
||||||
|
export interface TypeRegistration { |
||||||
|
"@id"?: string; |
||||||
|
"@context"?: ContextDefinition; |
||||||
|
/** |
||||||
|
* Defines this node as a Type Registration |
||||||
|
*/ |
||||||
|
type: { |
||||||
|
"@id": "TypeRegistration"; |
||||||
|
}; |
||||||
|
/** |
||||||
|
* The class of object at this type. |
||||||
|
*/ |
||||||
|
forClass: { |
||||||
|
"@id": string; |
||||||
|
}; |
||||||
|
/** |
||||||
|
* A specific resource that contains the class. |
||||||
|
*/ |
||||||
|
instance?: { |
||||||
|
"@id": string; |
||||||
|
}[]; |
||||||
|
/** |
||||||
|
* Containers that contain resources with the class. |
||||||
|
*/ |
||||||
|
instanceContainer?: { |
||||||
|
"@id": string; |
||||||
|
}[]; |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
PREFIX srs: <https://shaperepo.com/schemas/solidProfile#> |
||||||
|
PREFIX solid: <http://www.w3.org/ns/solid/terms#> |
||||||
|
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> |
||||||
|
|
||||||
|
srs:TypeIndexProfile { |
||||||
|
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)" ; |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
PREFIX srs: <https://shaperepo.com/schemas/solidProfile#> |
||||||
|
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> |
||||||
|
PREFIX solid: <http://www.w3.org/ns/solid/terms#> |
||||||
|
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#> |
||||||
|
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> |
||||||
|
|
||||||
|
srs:TypeIndexDocument EXTRA a { |
||||||
|
a [ solid:TypeIndex ] |
||||||
|
// rdfs:comment "Defines the node as a TypeIndex" ; |
||||||
|
a [ solid:ListedDocument ] |
||||||
|
// rdfs:comment "Defines the node as a Listed Document" ; |
||||||
|
} |
||||||
|
|
||||||
|
srs:TypeRegistration EXTRA a { |
||||||
|
a [ solid:TypeRegistration ] |
||||||
|
// rdfs:comment "Defines this node as a Type Registration" ; |
||||||
|
solid:forClass IRI |
||||||
|
// rdfs:comment "The class of object at this type." ; |
||||||
|
solid:instance IRI * |
||||||
|
// rdfs:comment "A specific resource that contains the class." ; |
||||||
|
solid:instanceContainer IRI * |
||||||
|
// rdfs:comment "Containers that contain resources with the class." ; |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
export const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; |
||||||
|
export const TYPE_REGISTRATION = |
||||||
|
"http://www.w3.org/ns/solid/terms#TypeRegistration"; |
||||||
|
export const FOR_CLASS = "http://www.w3.org/ns/solid/terms#forClass"; |
||||||
|
export const INSTANCE = "http://www.w3.org/ns/solid/terms#instance"; |
||||||
|
export const INSTANCE_CONTAINER = |
||||||
|
"http://www.w3.org/ns/solid/terms#instanceContainer"; |
@ -0,0 +1,92 @@ |
|||||||
|
import type { ContainerUri, LeafUri } from "@ldo/solid"; |
||||||
|
import type { TypeRegistration } from "./.ldo/typeIndex.typings"; |
||||||
|
import type { TypeIndexProfile } from "./.ldo/profile.typings"; |
||||||
|
import { TypeIndexProfileShapeType } from "./.ldo/profile.shapeTypes"; |
||||||
|
import { TypeRegistrationShapeType } from "./.ldo/typeIndex.shapeTypes"; |
||||||
|
import { RDF_TYPE, TYPE_REGISTRATION } from "./constants"; |
||||||
|
import type { Options } from "./util/Options"; |
||||||
|
import { guaranteeOptions } from "./util/Options"; |
||||||
|
|
||||||
|
export async function getTypeRegistrations( |
||||||
|
webId: string, |
||||||
|
options?: Options, |
||||||
|
): Promise<TypeRegistration[]> { |
||||||
|
const { dataset } = guaranteeOptions(options); |
||||||
|
|
||||||
|
// Get Profile
|
||||||
|
const profile = await getProfile(webId, options); |
||||||
|
|
||||||
|
// Get Type Indexes
|
||||||
|
const typeIndexUris = getTypeIndexesUrisFromProfile(profile); |
||||||
|
|
||||||
|
// Fetch the type Indexes
|
||||||
|
await Promise.all( |
||||||
|
typeIndexUris.map(async (typeIndexUri) => { |
||||||
|
const typeIndexResource = dataset.getResource(typeIndexUri); |
||||||
|
const readResult = await typeIndexResource.readIfUnfetched(); |
||||||
|
if (readResult.isError) throw readResult; |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
// Get Type Registrations
|
||||||
|
return dataset |
||||||
|
.usingType(TypeRegistrationShapeType) |
||||||
|
.matchSubject(RDF_TYPE, TYPE_REGISTRATION); |
||||||
|
} |
||||||
|
|
||||||
|
export async function getProfile( |
||||||
|
webId: string, |
||||||
|
options?: Options, |
||||||
|
): Promise<TypeIndexProfile> { |
||||||
|
const { dataset } = guaranteeOptions(options); |
||||||
|
const profileResource = dataset.getResource(webId); |
||||||
|
const readResult = await profileResource.readIfUnfetched(); |
||||||
|
if (readResult.isError) throw readResult; |
||||||
|
return dataset.usingType(TypeIndexProfileShapeType).fromSubject(webId); |
||||||
|
} |
||||||
|
|
||||||
|
export function getTypeIndexesUrisFromProfile( |
||||||
|
profile: TypeIndexProfile, |
||||||
|
): LeafUri[] { |
||||||
|
const uris: LeafUri[] = []; |
||||||
|
profile.privateTypeIndex?.forEach((indexNode) => { |
||||||
|
uris.push(indexNode["@id"] as LeafUri); |
||||||
|
}); |
||||||
|
profile.publicTypeIndex?.forEach((indexNode) => { |
||||||
|
uris.push(indexNode["@id"] as LeafUri); |
||||||
|
}); |
||||||
|
return uris; |
||||||
|
} |
||||||
|
|
||||||
|
export async function getInstanceUris( |
||||||
|
classUri: string, |
||||||
|
typeRegistrations: TypeRegistration[], |
||||||
|
options?: Options, |
||||||
|
): Promise<LeafUri[]> { |
||||||
|
const { dataset } = guaranteeOptions(options); |
||||||
|
|
||||||
|
const leafUris = new Set<LeafUri>(); |
||||||
|
await Promise.all( |
||||||
|
typeRegistrations.map(async (registration) => { |
||||||
|
if (registration.forClass["@id"] === classUri) { |
||||||
|
// Individual registrations
|
||||||
|
registration.instance?.forEach((instance) => |
||||||
|
leafUris.add(instance["@id"] as LeafUri), |
||||||
|
); |
||||||
|
// Container registrations
|
||||||
|
await Promise.all( |
||||||
|
registration.instanceContainer?.map(async (instanceContainer) => { |
||||||
|
const containerResource = dataset.getResource( |
||||||
|
instanceContainer["@id"] as ContainerUri, |
||||||
|
); |
||||||
|
await containerResource.readIfUnfetched(); |
||||||
|
containerResource.children().forEach((child) => { |
||||||
|
if (child.type === "leaf") leafUris.add(child.uri); |
||||||
|
}); |
||||||
|
}) ?? [], |
||||||
|
); |
||||||
|
} |
||||||
|
}), |
||||||
|
); |
||||||
|
return Array.from(leafUris); |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
export * from "./getTypeIndex"; |
||||||
|
export * from "./setTypeIndex"; |
||||||
|
export * from "./react/useInstanceUris"; |
||||||
|
export * from "./react/useTypeIndexProfile"; |
@ -0,0 +1,45 @@ |
|||||||
|
import type { LeafUri } from "@ldo/solid"; |
||||||
|
import { useTypeIndexProfile } from "./useTypeIndexProfile"; |
||||||
|
import { useEffect, useMemo, useState } from "react"; |
||||||
|
import { useSubscribeToUris } from "./util/useSubscribeToUris"; |
||||||
|
import { useLdo, useMatchSubject } from "@ldo/solid-react"; |
||||||
|
import { TypeRegistrationShapeType } from "../.ldo/typeIndex.shapeTypes"; |
||||||
|
import { RDF_TYPE, TYPE_REGISTRATION } from "../constants"; |
||||||
|
import { |
||||||
|
getInstanceUris, |
||||||
|
getTypeIndexesUrisFromProfile, |
||||||
|
} from "../getTypeIndex"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides the LeafUris of everything in a type node for a specific class uri |
||||||
|
* |
||||||
|
* @param classUri - the class uri |
||||||
|
* @returns - URIs of all resources registered with this node |
||||||
|
*/ |
||||||
|
export function useInstanceUris(classUri: string): LeafUri[] { |
||||||
|
const { dataset } = useLdo(); |
||||||
|
const profile = useTypeIndexProfile(); |
||||||
|
|
||||||
|
const typeIndexUris: string[] = useMemo( |
||||||
|
() => (profile ? getTypeIndexesUrisFromProfile(profile) : []), |
||||||
|
[profile], |
||||||
|
); |
||||||
|
|
||||||
|
useSubscribeToUris(typeIndexUris); |
||||||
|
|
||||||
|
const [leafUris, setLeafUris] = useState<LeafUri[]>([]); |
||||||
|
|
||||||
|
const typeRegistrations = useMatchSubject( |
||||||
|
TypeRegistrationShapeType, |
||||||
|
RDF_TYPE, |
||||||
|
TYPE_REGISTRATION, |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
getInstanceUris(classUri, typeRegistrations, { |
||||||
|
solidLdoDataset: dataset, |
||||||
|
}).then(setLeafUris); |
||||||
|
}, [typeRegistrations]); |
||||||
|
|
||||||
|
return leafUris; |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
import { useResource, useSolidAuth, useSubject } from "@ldo/solid-react"; |
||||||
|
import type { TypeIndexProfile } from "../.ldo/profile.typings"; |
||||||
|
import { TypeIndexProfileShapeType } from "../.ldo/profile.shapeTypes"; |
||||||
|
|
||||||
|
export function useTypeIndexProfile(): TypeIndexProfile | undefined { |
||||||
|
const { session } = useSolidAuth(); |
||||||
|
useResource(session.webId, { subscribe: true }); |
||||||
|
const profile = useSubject(TypeIndexProfileShapeType, session.webId); |
||||||
|
return profile; |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
import { useLdo } from "@ldo/solid-react"; |
||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
|
||||||
|
export function useSubscribeToUris(uris: string[]) { |
||||||
|
const { dataset } = useLdo(); |
||||||
|
const currentlySubscribed = useRef<Record<string, string>>({}); |
||||||
|
useEffect(() => { |
||||||
|
const resources = uris.map((uri) => dataset.getResource(uri)); |
||||||
|
const previousSubscriptions = { ...currentlySubscribed.current }; |
||||||
|
Promise.all<void>( |
||||||
|
resources.map(async (resource) => { |
||||||
|
if (!previousSubscriptions[resource.uri]) { |
||||||
|
// Read and subscribe
|
||||||
|
await resource.readIfUnfetched(); |
||||||
|
currentlySubscribed.current[resource.uri] = |
||||||
|
await resource.subscribeToNotifications(); |
||||||
|
} else { |
||||||
|
delete previousSubscriptions[resource.uri]; |
||||||
|
} |
||||||
|
}), |
||||||
|
).then(async () => { |
||||||
|
// Unsubscribe from all remaining previous subscriptions
|
||||||
|
await Promise.all( |
||||||
|
Object.entries(previousSubscriptions).map( |
||||||
|
async ([resourceUri, subscriptionId]) => { |
||||||
|
// Unsubscribe
|
||||||
|
delete currentlySubscribed.current[resourceUri]; |
||||||
|
const resource = dataset.getResource(resourceUri); |
||||||
|
await resource.unsubscribeFromNotifications(subscriptionId); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
}); |
||||||
|
}, [uris]); |
||||||
|
} |
@ -0,0 +1,207 @@ |
|||||||
|
import { v4 } from "uuid"; |
||||||
|
import { |
||||||
|
TypeIndexDocumentShapeType, |
||||||
|
TypeRegistrationShapeType, |
||||||
|
} from "./.ldo/typeIndex.shapeTypes"; |
||||||
|
import { FOR_CLASS, RDF_TYPE, TYPE_REGISTRATION } from "./constants"; |
||||||
|
import { guaranteeOptions, type Options } from "./util/Options"; |
||||||
|
import { namedNode, quad } from "@rdfjs/data-model"; |
||||||
|
import type { TypeRegistration } from "./.ldo/typeIndex.typings"; |
||||||
|
import { getProfile } from "./getTypeIndex"; |
||||||
|
import { TypeIndexProfileShapeType } from "./.ldo/profile.shapeTypes"; |
||||||
|
import type { Container } from "@ldo/solid"; |
||||||
|
import type { ISolidLdoDataset } from "@ldo/solid"; |
||||||
|
import type { NamedNode } from "@rdfjs/types"; |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* INITIALIZERS |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
export async function initTypeIndex( |
||||||
|
webId: string, |
||||||
|
options?: Options, |
||||||
|
): Promise<void> { |
||||||
|
const { dataset } = guaranteeOptions(options); |
||||||
|
const profile = await getProfile(webId, options); |
||||||
|
if (!profile.privateTypeIndex?.length || !profile.publicTypeIndex?.length) { |
||||||
|
const profileFolder = await dataset.getResource(webId).getParentContainer(); |
||||||
|
if (profileFolder?.isError) throw profileFolder; |
||||||
|
if (!profileFolder) |
||||||
|
throw new Error("No folder to save the type indexes to."); |
||||||
|
if (!profile.privateTypeIndex?.length) { |
||||||
|
await createIndex(webId, profileFolder, dataset, true); |
||||||
|
} |
||||||
|
if (!profile.publicTypeIndex?.length) { |
||||||
|
await createIndex(webId, profileFolder, dataset, false); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* @param webId |
||||||
|
* @param profileFolder |
||||||
|
* @param dataset |
||||||
|
*/ |
||||||
|
export async function createIndex( |
||||||
|
webId, |
||||||
|
profileFolder: Container, |
||||||
|
dataset: ISolidLdoDataset, |
||||||
|
isPrivate: boolean, |
||||||
|
) { |
||||||
|
// Create a private type index
|
||||||
|
const createResult = await profileFolder.createChildAndOverwrite( |
||||||
|
`${isPrivate ? "private" : "public"}_index_${v4()}`, |
||||||
|
); |
||||||
|
if (createResult.isError) throw createResult; |
||||||
|
const indexResource = createResult.resource; |
||||||
|
const wacResult = await indexResource.setWac({ |
||||||
|
agent: { |
||||||
|
[webId]: { read: true, write: true, append: true, control: true }, |
||||||
|
}, |
||||||
|
public: { |
||||||
|
read: isPrivate ? false : true, |
||||||
|
write: true, |
||||||
|
append: true, |
||||||
|
control: true, |
||||||
|
}, |
||||||
|
authenticated: { |
||||||
|
read: isPrivate ? false : true, |
||||||
|
write: true, |
||||||
|
append: true, |
||||||
|
control: true, |
||||||
|
}, |
||||||
|
}); |
||||||
|
if (wacResult.isError) throw wacResult; |
||||||
|
const transaction = dataset.startTransaction(); |
||||||
|
const cProfile = transaction |
||||||
|
.usingType(TypeIndexProfileShapeType) |
||||||
|
.write(dataset.getResource(webId).uri) |
||||||
|
.fromSubject(webId); |
||||||
|
if (isPrivate) { |
||||||
|
cProfile.privateTypeIndex?.push({ "@id": indexResource.uri }); |
||||||
|
} else { |
||||||
|
cProfile.publicTypeIndex?.push({ "@id": indexResource.uri }); |
||||||
|
} |
||||||
|
const cTypeIndex = transaction |
||||||
|
.usingType(TypeIndexDocumentShapeType) |
||||||
|
.write(indexResource.uri) |
||||||
|
.fromSubject(indexResource.uri); |
||||||
|
|
||||||
|
cTypeIndex.type = [{ "@id": "ListedDocument" }, { "@id": "TypeIndex" }]; |
||||||
|
const commitResult = await transaction.commitToPod(); |
||||||
|
if (commitResult.isError) throw commitResult; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ============================================================================= |
||||||
|
* DATASET MODIFIERS |
||||||
|
* ============================================================================= |
||||||
|
*/ |
||||||
|
interface Instances { |
||||||
|
instance?: string[]; |
||||||
|
instanceContainer?: string[]; |
||||||
|
} |
||||||
|
|
||||||
|
export function addRegistration( |
||||||
|
indexUri: string, |
||||||
|
classUri: string, |
||||||
|
instances: Instances, |
||||||
|
options?: Options, |
||||||
|
): void { |
||||||
|
// Check to see if its already in the index
|
||||||
|
const typeRegistration = findAppropriateTypeRegistration( |
||||||
|
indexUri, |
||||||
|
classUri, |
||||||
|
options, |
||||||
|
); |
||||||
|
|
||||||
|
// Add instances to type registration
|
||||||
|
instances.instance?.forEach((instance) => { |
||||||
|
typeRegistration.instance?.push({ "@id": instance }); |
||||||
|
}); |
||||||
|
instances.instanceContainer?.forEach((instanceContainer) => { |
||||||
|
typeRegistration.instanceContainer?.push({ "@id": instanceContainer }); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export async function removeRegistration( |
||||||
|
indexUri: string, |
||||||
|
classUri: string, |
||||||
|
instances: Instances, |
||||||
|
options?: Options, |
||||||
|
) { |
||||||
|
// Check to see if its already in the index
|
||||||
|
const typeRegistration = findAppropriateTypeRegistration( |
||||||
|
indexUri, |
||||||
|
classUri, |
||||||
|
options, |
||||||
|
); |
||||||
|
|
||||||
|
console.log(typeRegistration["@id"]); |
||||||
|
|
||||||
|
// Add instances to type registration
|
||||||
|
instances.instance?.forEach((instance) => { |
||||||
|
typeRegistration.instance?.splice( |
||||||
|
typeRegistration.instance.findIndex((val) => val["@id"] === instance), |
||||||
|
1, |
||||||
|
); |
||||||
|
}); |
||||||
|
instances.instanceContainer?.forEach((instanceContainer) => { |
||||||
|
console.log("Splicing instanceContainers", instanceContainer); |
||||||
|
typeRegistration.instanceContainer?.splice( |
||||||
|
typeRegistration.instanceContainer.findIndex( |
||||||
|
(val) => val["@id"] === instanceContainer, |
||||||
|
), |
||||||
|
1, |
||||||
|
); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export function findAppropriateTypeRegistration( |
||||||
|
indexUri: string, |
||||||
|
classUri: string, |
||||||
|
options?: Options, |
||||||
|
) { |
||||||
|
const { dataset } = guaranteeOptions(options); |
||||||
|
// Check to see if its already in the index
|
||||||
|
const existingRegistrationsUris: NamedNode[] = dataset |
||||||
|
.match( |
||||||
|
null, |
||||||
|
namedNode(RDF_TYPE), |
||||||
|
namedNode(TYPE_REGISTRATION), |
||||||
|
namedNode(indexUri), |
||||||
|
) |
||||||
|
.toArray() |
||||||
|
.map((quad) => quad.subject) as NamedNode[]; |
||||||
|
|
||||||
|
const existingRegistrationForClassUri = existingRegistrationsUris.find( |
||||||
|
(registrationUri) => { |
||||||
|
return dataset.has( |
||||||
|
quad( |
||||||
|
registrationUri, |
||||||
|
namedNode(FOR_CLASS), |
||||||
|
namedNode(classUri), |
||||||
|
namedNode(indexUri), |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
)?.value; |
||||||
|
|
||||||
|
let typeRegistration: TypeRegistration; |
||||||
|
if (existingRegistrationForClassUri) { |
||||||
|
typeRegistration = dataset |
||||||
|
.usingType(TypeRegistrationShapeType) |
||||||
|
.write(indexUri) |
||||||
|
.fromSubject(existingRegistrationForClassUri); |
||||||
|
} else { |
||||||
|
typeRegistration = dataset |
||||||
|
.usingType(TypeRegistrationShapeType) |
||||||
|
.write(indexUri) |
||||||
|
.fromSubject(`${indexUri}#${v4()}`); |
||||||
|
typeRegistration.type = { "@id": "TypeRegistration" }; |
||||||
|
typeRegistration.forClass = { "@id": classUri }; |
||||||
|
} |
||||||
|
return typeRegistration; |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
import { createSolidLdoDataset } from "@ldo/solid"; |
||||||
|
import type { ISolidLdoDataset } from "@ldo/solid"; |
||||||
|
import { guaranteeFetch } from "@ldo/solid/dist/util/guaranteeFetch"; |
||||||
|
|
||||||
|
export interface Options { |
||||||
|
solidLdoDataset?: ISolidLdoDataset; |
||||||
|
fetch?: typeof fetch; |
||||||
|
} |
||||||
|
|
||||||
|
export function guaranteeOptions(options?: Options) { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
const dataset = options?.solidLdoDataset ?? createSolidLdoDataset({ fetch }); |
||||||
|
return { fetch, dataset }; |
||||||
|
} |
@ -0,0 +1,138 @@ |
|||||||
|
import { createSolidLdoDataset } from "@ldo/solid"; |
||||||
|
import { |
||||||
|
MY_BOOKMARKS_1_URI, |
||||||
|
MY_BOOKMARKS_2_URI, |
||||||
|
PRIVATE_TYPE_INDEX_URI, |
||||||
|
PUBLIC_TYPE_INDEX_URI, |
||||||
|
ROOT_CONTAINER, |
||||||
|
setupEmptyTypeIndex, |
||||||
|
setupFullTypeIndex, |
||||||
|
setUpServer, |
||||||
|
WEB_ID, |
||||||
|
} from "./setUpServer"; |
||||||
|
import { getInstanceUris, getTypeRegistrations } from "../src/getTypeIndex"; |
||||||
|
import { |
||||||
|
addRegistration, |
||||||
|
initTypeIndex, |
||||||
|
removeRegistration, |
||||||
|
} from "../src/setTypeIndex"; |
||||||
|
import { TypeIndexProfileShapeType } from "../src/.ldo/profile.shapeTypes"; |
||||||
|
import { namedNode } from "@rdfjs/dataset"; |
||||||
|
import { INSTANCE } from "../src/constants"; |
||||||
|
|
||||||
|
// Use an increased timeout, since the CSS server takes too much setup time.
|
||||||
|
jest.setTimeout(40_000); |
||||||
|
|
||||||
|
const ADDRESS_BOOK = "http://www.w3.org/2006/vcard/ns#AddressBook"; |
||||||
|
const BOOKMARK = "http://www.w3.org/2002/01/bookmark#Bookmark"; |
||||||
|
const EXAMPLE_THING = "https://example.com/ExampleThing"; |
||||||
|
|
||||||
|
describe("General Tests", () => { |
||||||
|
const s = setUpServer(); |
||||||
|
|
||||||
|
it("gets the current typeindex", async () => { |
||||||
|
await setupFullTypeIndex(s); |
||||||
|
|
||||||
|
const solidLdoDataset = createSolidLdoDataset(); |
||||||
|
const typeRegistrations = await getTypeRegistrations(WEB_ID, { |
||||||
|
solidLdoDataset, |
||||||
|
}); |
||||||
|
const addressBookUris = await getInstanceUris( |
||||||
|
ADDRESS_BOOK, |
||||||
|
typeRegistrations, |
||||||
|
{ solidLdoDataset }, |
||||||
|
); |
||||||
|
expect(addressBookUris).toEqual( |
||||||
|
expect.arrayContaining([ |
||||||
|
"https://example.com/myPrivateAddressBook.ttl", |
||||||
|
"https://example.com/myPublicAddressBook.ttl", |
||||||
|
]), |
||||||
|
); |
||||||
|
const bookmarkUris = await getInstanceUris(BOOKMARK, typeRegistrations, { |
||||||
|
solidLdoDataset, |
||||||
|
}); |
||||||
|
expect(bookmarkUris).toEqual( |
||||||
|
expect.arrayContaining([MY_BOOKMARKS_1_URI, MY_BOOKMARKS_2_URI]), |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it("initializes the type index", async () => { |
||||||
|
await setupEmptyTypeIndex(s); |
||||||
|
|
||||||
|
const solidLdoDataset = createSolidLdoDataset(); |
||||||
|
|
||||||
|
await initTypeIndex(WEB_ID, { solidLdoDataset }); |
||||||
|
|
||||||
|
const profile = solidLdoDataset |
||||||
|
.usingType(TypeIndexProfileShapeType) |
||||||
|
.fromSubject(WEB_ID); |
||||||
|
|
||||||
|
expect(profile.privateTypeIndex?.[0]?.["@id"]).toBeDefined(); |
||||||
|
expect(profile.publicTypeIndex?.[0]?.["@id"]).toBeDefined(); |
||||||
|
}); |
||||||
|
|
||||||
|
it("Adds to the typeIndex", async () => { |
||||||
|
await setupFullTypeIndex(s); |
||||||
|
|
||||||
|
const solidLdoDataset = createSolidLdoDataset(); |
||||||
|
|
||||||
|
await getTypeRegistrations(WEB_ID, { solidLdoDataset }); |
||||||
|
|
||||||
|
const transaction = solidLdoDataset.startTransaction(); |
||||||
|
addRegistration( |
||||||
|
PUBLIC_TYPE_INDEX_URI, |
||||||
|
ADDRESS_BOOK, |
||||||
|
{ instance: ["https://example.com/AdressBook3"] }, |
||||||
|
{ solidLdoDataset: transaction }, |
||||||
|
); |
||||||
|
addRegistration( |
||||||
|
PRIVATE_TYPE_INDEX_URI, |
||||||
|
EXAMPLE_THING, |
||||||
|
{ instanceContainer: ["https://example.com/ExampleInstance"] }, |
||||||
|
{ solidLdoDataset: transaction }, |
||||||
|
); |
||||||
|
const { added, removed } = transaction.getChanges(); |
||||||
|
|
||||||
|
const existingRegistration = namedNode( |
||||||
|
"http://localhost:3003/example/profile/publicTypeIndex.ttl#ab09fd", |
||||||
|
); |
||||||
|
|
||||||
|
expect(removed).not.toBeDefined(); |
||||||
|
expect(added?.size).toBe(4); |
||||||
|
expect(added?.match(existingRegistration).size).toBe(1); |
||||||
|
expect( |
||||||
|
added?.match( |
||||||
|
existingRegistration, |
||||||
|
namedNode(INSTANCE), |
||||||
|
namedNode("https://example.com/AdressBook3"), |
||||||
|
namedNode("http://localhost:3003/example/profile/publicTypeIndex.ttl"), |
||||||
|
).size, |
||||||
|
).toBe(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it("Removes from the typeIndex", async () => { |
||||||
|
await setupFullTypeIndex(s); |
||||||
|
|
||||||
|
const solidLdoDataset = createSolidLdoDataset(); |
||||||
|
|
||||||
|
await getTypeRegistrations(WEB_ID, { solidLdoDataset }); |
||||||
|
|
||||||
|
const transaction = solidLdoDataset.startTransaction(); |
||||||
|
removeRegistration( |
||||||
|
PUBLIC_TYPE_INDEX_URI, |
||||||
|
ADDRESS_BOOK, |
||||||
|
{ instance: ["https://example.com/myPublicAddressBook.ttl"] }, |
||||||
|
{ solidLdoDataset: transaction }, |
||||||
|
); |
||||||
|
removeRegistration( |
||||||
|
PRIVATE_TYPE_INDEX_URI, |
||||||
|
BOOKMARK, |
||||||
|
{ instanceContainer: [`${ROOT_CONTAINER}myBookmarks/`] }, |
||||||
|
{ solidLdoDataset: transaction }, |
||||||
|
); |
||||||
|
const { added, removed } = transaction.getChanges(); |
||||||
|
|
||||||
|
expect(added).not.toBeDefined(); |
||||||
|
expect(removed?.size).toBe(2); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,153 @@ |
|||||||
|
import fetch from "cross-fetch"; |
||||||
|
|
||||||
|
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3003/"; |
||||||
|
export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/"; |
||||||
|
export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`; |
||||||
|
export const PROFILE_CONTAINER = `${ROOT_CONTAINER}profile/`; |
||||||
|
export const WEB_ID = `${PROFILE_CONTAINER}card.ttl#me`; |
||||||
|
export const PUBLIC_TYPE_INDEX_URI = `${PROFILE_CONTAINER}publicTypeIndex.ttl`; |
||||||
|
export const PRIVATE_TYPE_INDEX_URI = `${PROFILE_CONTAINER}privateTypeIndex.ttl`; |
||||||
|
export const MY_BOOKMARKS_CONTAINER = `${ROOT_CONTAINER}myBookmarks/`; |
||||||
|
export const MY_BOOKMARKS_1_URI = `${ROOT_CONTAINER}myBookmarks/bookmark1.ttl`; |
||||||
|
export const MY_BOOKMARKS_2_URI = `${ROOT_CONTAINER}myBookmarks/bookmark2.ttl`; |
||||||
|
|
||||||
|
export const PROFILE_TTL = ` |
||||||
|
<#me> <http://www.w3.org/ns/solid/terms#publicTypeIndex> <${PROFILE_CONTAINER}publicTypeIndex.ttl> ; |
||||||
|
<http://www.w3.org/ns/solid/terms#privateTypeIndex> <${PROFILE_CONTAINER}privateTypeIndex.ttl> .`;
|
||||||
|
export const PUBLIC_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
||||||
|
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>. |
||||||
|
@prefix bk: <http://www.w3.org/2002/01/bookmark#>. |
||||||
|
|
||||||
|
<> |
||||||
|
a solid:TypeIndex ; |
||||||
|
a solid:ListedDocument. |
||||||
|
|
||||||
|
<#ab09fd> a solid:TypeRegistration; |
||||||
|
solid:forClass vcard:AddressBook; |
||||||
|
solid:instance <https://example.com/myPublicAddressBook.ttl>. |
||||||
|
|
||||||
|
<#bq1r5e> a solid:TypeRegistration; |
||||||
|
solid:forClass bk:Bookmark; |
||||||
|
solid:instanceContainer <${ROOT_CONTAINER}myBookmarks/>.`;
|
||||||
|
export const PRIVATE_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
||||||
|
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>. |
||||||
|
@prefix bk: <http://www.w3.org/2002/01/bookmark#>. |
||||||
|
|
||||||
|
<> |
||||||
|
a solid:TypeIndex ; |
||||||
|
a solid:UnlistedDocument. |
||||||
|
|
||||||
|
<#ab09fd> a solid:TypeRegistration; |
||||||
|
solid:forClass vcard:AddressBook; |
||||||
|
solid:instance <https://example.com/myPrivateAddressBook.ttl>. |
||||||
|
|
||||||
|
<#bq1r5e> a solid:TypeRegistration; |
||||||
|
solid:forClass bk:Bookmark; |
||||||
|
solid:instanceContainer <${ROOT_CONTAINER}myBookmarks/>.`;
|
||||||
|
|
||||||
|
export interface SetUpServerReturn { |
||||||
|
authFetch: typeof fetch; |
||||||
|
fetchMock: jest.Mock< |
||||||
|
Promise<Response>, |
||||||
|
[input: RequestInfo | URL, init?: RequestInit | undefined] |
||||||
|
>; |
||||||
|
} |
||||||
|
|
||||||
|
export async function setupFullTypeIndex(s: SetUpServerReturn) { |
||||||
|
// Create a new document called sample.ttl
|
||||||
|
await s.authFetch(WEB_ID, { method: "DELETE" }); |
||||||
|
await s.authFetch(ROOT_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { |
||||||
|
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"', |
||||||
|
slug: "myBookmarks/", |
||||||
|
}, |
||||||
|
}); |
||||||
|
await Promise.all([ |
||||||
|
s.authFetch(PROFILE_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "card.ttl" }, |
||||||
|
body: PROFILE_TTL, |
||||||
|
}), |
||||||
|
s.authFetch(PROFILE_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "publicTypeIndex.ttl" }, |
||||||
|
body: PUBLIC_TYPE_INDEX_TTL, |
||||||
|
}), |
||||||
|
s.authFetch(PROFILE_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { |
||||||
|
"content-type": "text/turtle", |
||||||
|
slug: "privateTypeIndex.ttl", |
||||||
|
}, |
||||||
|
body: PRIVATE_TYPE_INDEX_TTL, |
||||||
|
}), |
||||||
|
s.authFetch(MY_BOOKMARKS_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "bookmark1.ttl" }, |
||||||
|
body: "", |
||||||
|
}), |
||||||
|
s.authFetch(MY_BOOKMARKS_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "bookmark2.ttl" }, |
||||||
|
body: "", |
||||||
|
}), |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
export async function setupEmptyTypeIndex(s: SetUpServerReturn) { |
||||||
|
// Create a new document called sample.ttl
|
||||||
|
await s.authFetch(WEB_ID, { method: "DELETE" }); |
||||||
|
await s.authFetch(ROOT_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { |
||||||
|
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"', |
||||||
|
slug: "myBookmarks/", |
||||||
|
}, |
||||||
|
}); |
||||||
|
await Promise.all([ |
||||||
|
s.authFetch(PROFILE_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "card.ttl" }, |
||||||
|
body: "", |
||||||
|
}), |
||||||
|
s.authFetch(MY_BOOKMARKS_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "bookmark1.ttl" }, |
||||||
|
body: "", |
||||||
|
}), |
||||||
|
s.authFetch(MY_BOOKMARKS_CONTAINER, { |
||||||
|
method: "POST", |
||||||
|
headers: { "content-type": "text/turtle", slug: "bookmark2.ttl" }, |
||||||
|
body: "", |
||||||
|
}), |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
export function setUpServer(): SetUpServerReturn { |
||||||
|
// Ignore to build s
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const s: SetUpServerReturn = {}; |
||||||
|
|
||||||
|
beforeAll(async () => { |
||||||
|
// s.authFetch = await getAuthenticatedFetch();
|
||||||
|
s.authFetch = fetch; |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
s.fetchMock = jest.fn(s.authFetch); |
||||||
|
}); |
||||||
|
|
||||||
|
afterEach(async () => { |
||||||
|
await Promise.all([ |
||||||
|
await s.authFetch(WEB_ID, { method: "DELETE" }), |
||||||
|
await s.authFetch(PUBLIC_TYPE_INDEX_URI, { method: "DELETE" }), |
||||||
|
await s.authFetch(PRIVATE_TYPE_INDEX_URI, { method: "DELETE" }), |
||||||
|
await s.authFetch(MY_BOOKMARKS_1_URI, { method: "DELETE" }), |
||||||
|
await s.authFetch(MY_BOOKMARKS_2_URI, { method: "DELETE" }), |
||||||
|
]); |
||||||
|
}); |
||||||
|
|
||||||
|
return s; |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
{ |
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", |
||||||
|
"import": [ |
||||||
|
"css:config/app/init/initialize-intro.json", |
||||||
|
"css:config/app/main/default.json", |
||||||
|
"css:config/app/variables/default.json", |
||||||
|
"css:config/http/handler/default.json", |
||||||
|
"css:config/http/middleware/default.json", |
||||||
|
"css:config/http/notifications/all.json", |
||||||
|
"css:config/http/server-factory/http.json", |
||||||
|
"css:config/http/static/default.json", |
||||||
|
"css:config/identity/access/public.json", |
||||||
|
"css:config/identity/email/default.json", |
||||||
|
"css:config/identity/handler/default.json", |
||||||
|
"css:config/identity/oidc/default.json", |
||||||
|
"css:config/identity/ownership/token.json", |
||||||
|
"css:config/identity/pod/static.json", |
||||||
|
"css:config/ldp/authentication/dpop-bearer.json", |
||||||
|
"css:config/ldp/authorization/webacl.json", |
||||||
|
"css:config/ldp/handler/default.json", |
||||||
|
"css:config/ldp/metadata-parser/default.json", |
||||||
|
"css:config/ldp/metadata-writer/default.json", |
||||||
|
"css:config/ldp/modes/default.json", |
||||||
|
"css:config/storage/backend/memory.json", |
||||||
|
"css:config/storage/key-value/resource-store.json", |
||||||
|
"css:config/storage/location/root.json", |
||||||
|
"css:config/storage/middleware/default.json", |
||||||
|
"css:config/util/auxiliary/acl.json", |
||||||
|
"css:config/util/identifiers/suffix.json", |
||||||
|
"css:config/util/index/default.json", |
||||||
|
"css:config/util/logging/winston.json", |
||||||
|
"css:config/util/representation-conversion/default.json", |
||||||
|
"css:config/util/resource-locker/memory.json", |
||||||
|
"css:config/util/variables/default.json" |
||||||
|
], |
||||||
|
"@graph": [ |
||||||
|
{ |
||||||
|
"comment": "A Solid server that stores its resources in memory and uses WAC for authorization." |
||||||
|
}, |
||||||
|
{ |
||||||
|
"comment": "The location of the new pod templates folder.", |
||||||
|
"@type": "Override", |
||||||
|
"overrideInstance": { |
||||||
|
"@id": "urn:solid-server:default:PodResourcesGenerator" |
||||||
|
}, |
||||||
|
"overrideParameters": { |
||||||
|
"@type": "StaticFolderGenerator", |
||||||
|
"templateFolder": "./test/test-server/configs/template" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
[ |
||||||
|
{ |
||||||
|
"email": "hello@example.com", |
||||||
|
"password": "abc123", |
||||||
|
"pods": [ |
||||||
|
{ "name": "example" } |
||||||
|
] |
||||||
|
} |
||||||
|
] |
@ -0,0 +1,13 @@ |
|||||||
|
@prefix : <#>. |
||||||
|
@prefix acl: <http://www.w3.org/ns/auth/acl#>. |
||||||
|
@prefix foaf: <http://xmlns.com/foaf/0.1/>. |
||||||
|
@prefix eve: <./>. |
||||||
|
@prefix c: <./profile/card#>. |
||||||
|
|
||||||
|
:ControlReadWrite |
||||||
|
a acl:Authorization; |
||||||
|
acl:accessTo eve:; |
||||||
|
acl:agent c:me, <mailto:info@o.team>; |
||||||
|
acl:agentClass foaf:Agent; |
||||||
|
acl:default eve:; |
||||||
|
acl:mode acl:Control, acl:Read, acl:Write. |
@ -0,0 +1,19 @@ |
|||||||
|
# ACL resource for the WebID profile document |
||||||
|
@prefix acl: <http://www.w3.org/ns/auth/acl#>. |
||||||
|
@prefix foaf: <http://xmlns.com/foaf/0.1/>. |
||||||
|
|
||||||
|
# The WebID profile is readable by the public. |
||||||
|
# This is required for discovery and verification, |
||||||
|
# e.g. when checking identity providers. |
||||||
|
<#public> |
||||||
|
a acl:Authorization; |
||||||
|
acl:agentClass foaf:Agent; |
||||||
|
acl:accessTo <./card>; |
||||||
|
acl:mode acl:Read. |
||||||
|
|
||||||
|
# The owner has full access to the profile |
||||||
|
<#owner> |
||||||
|
a acl:Authorization; |
||||||
|
acl:agent <{{webId}}>; |
||||||
|
acl:accessTo <./card>; |
||||||
|
acl:mode acl:Read, acl:Write, acl:Control. |
@ -0,0 +1,7 @@ |
|||||||
|
import { createApp } from "./solidServer.helper"; |
||||||
|
|
||||||
|
async function run() { |
||||||
|
const app = await createApp(); |
||||||
|
await app.start(); |
||||||
|
} |
||||||
|
run(); |
@ -0,0 +1,39 @@ |
|||||||
|
// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9
|
||||||
|
|
||||||
|
import * as path from "path"; |
||||||
|
import type { App } from "@solid/community-server"; |
||||||
|
import { AppRunner, resolveModulePath } from "@solid/community-server"; |
||||||
|
|
||||||
|
export async function createApp(): Promise<App> { |
||||||
|
if (process.env.SERVER) { |
||||||
|
return { |
||||||
|
start: () => {}, |
||||||
|
stop: () => {}, |
||||||
|
} as App; |
||||||
|
} |
||||||
|
const appRunner = new AppRunner(); |
||||||
|
|
||||||
|
return appRunner.create({ |
||||||
|
loaderProperties: { |
||||||
|
mainModulePath: resolveModulePath(""), |
||||||
|
typeChecking: false, |
||||||
|
}, |
||||||
|
config: path.join( |
||||||
|
__dirname, |
||||||
|
"configs", |
||||||
|
"components-config", |
||||||
|
"unauthenticatedServer.json", |
||||||
|
), |
||||||
|
variableBindings: {}, |
||||||
|
shorthand: { |
||||||
|
port: 3_003, |
||||||
|
loggingLevel: "off", |
||||||
|
seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"), |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export interface ISecretData { |
||||||
|
id: string; |
||||||
|
secret: string; |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.base.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "./dist", |
||||||
|
"lib": ["dom"] |
||||||
|
}, |
||||||
|
"include": ["./src"] |
||||||
|
} |
@ -1,8 +0,0 @@ |
|||||||
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Returned when a notification has been successfully subscribed to for a resource |
|
||||||
*/ |
|
||||||
export interface SubscribeToNotificationSuccess extends ResourceSuccess { |
|
||||||
type: "subscribeToNotificationSuccess"; |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Returned when a notification has been successfully unsubscribed from for a resource |
|
||||||
*/ |
|
||||||
export interface UnsubscribeToNotificationSuccess extends ResourceSuccess { |
|
||||||
type: "unsubscribeFromNotificationSuccess"; |
|
||||||
} |
|
Loading…
Reference in new issue