diff --git a/utils/rdflib/CHANGELOG.md b/utils/rdflib/CHANGELOG.md index 23dcc820..0171ee86 100644 --- a/utils/rdflib/CHANGELOG.md +++ b/utils/rdflib/CHANGELOG.md @@ -2,7 +2,15 @@ All notable changes to this module will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- [discoverType](https://solid-contrib.github.io/data-modules/rdflib-utils/classes/index.ModuleSupport.html#discoverType) ## 0.2.0 @@ -11,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [ModuleSupport](https://solid-contrib.github.io/data-modules/rdflib-utils/classes/index.ModuleSupport.html) - [TypeIndexQuery](https://solid-contrib.github.io/data-modules/rdflib-utils/classes/index.TypeIndexQuery.html) - [addInstanceToTypeIndex](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/index.addInstanceToTypeIndex.html) -- helper functions to generate terms in common Solid namespaces: +- helper functions to generate terms in common Solid namespaces: - [ldp](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/index.ldp.html) - [pim](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/index.pim.html) - [rdf](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/index.rdf.html) @@ -19,8 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Breaking Change -- [generateId](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/identifier.generateId.html): Moved to submodule - +- [generateId](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/identifier.generateId.html): + Moved to submodule ## 0.1.1 @@ -42,4 +50,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [mockForbidden](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/test_support.mockForbidden.html) - [mockLdpContainer](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/test_support.mockLdpContainer.html) - [mockNotFound](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/test_support.mockNotFound.html) - - [mockTurtleDocument](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/test_support.mockTurtleDocument.html) \ No newline at end of file + - [mockTurtleDocument](https://solid-contrib.github.io/data-modules/rdflib-utils/functions/test_support.mockTurtleDocument.html) diff --git a/utils/rdflib/src/index.ts b/utils/rdflib/src/index.ts index 70e9fbd5..243916a6 100644 --- a/utils/rdflib/src/index.ts +++ b/utils/rdflib/src/index.ts @@ -1,4 +1,4 @@ -import { Fetcher, IndexedFormula, UpdateManager } from "rdflib"; +import { Fetcher, IndexedFormula, NamedNode, UpdateManager } from "rdflib"; export * from "./web-operations/index.js"; export * from "./queries/index.js"; @@ -11,3 +11,19 @@ export interface ModuleConfig { fetcher: Fetcher; updater: UpdateManager; } + +/** + * Lists instances and containers found in a type index + */ +export interface TypeRegistrations { + instanceContainers: NamedNode[]; + instances: NamedNode[]; +} + +/** + * Type registrations grouped by whether they have been discovered in private or public type index. + */ +export interface TypeRegistrationsByVisibility { + public: TypeRegistrations; + private: TypeRegistrations; +} diff --git a/utils/rdflib/src/module/ModuleSupport.integration.spec.ts b/utils/rdflib/src/module/ModuleSupport.integration.spec.ts index f239b3d8..83af20b4 100644 --- a/utils/rdflib/src/module/ModuleSupport.integration.spec.ts +++ b/utils/rdflib/src/module/ModuleSupport.integration.spec.ts @@ -82,6 +82,138 @@ describe("ModuleSupport", () => { ).toBe(true); }); + describe("discoverType", () => { + describe("given registrations in public type index", () => { + it("returns all instances and containers from that index", async () => { + const { authenticatedFetch, support } = givenModuleSupport(); + + mockTurtleDocument( + authenticatedFetch, + "https://pod.test/alice/profile/card", + ` + @prefix vcard: . + @prefix solid: . + + <#me> a vcard:Individual; + vcard:fn "Alice"; + solid:publicTypeIndex ; + . +`, + ); + + mockTurtleDocument( + authenticatedFetch, + "https://pod.test/alice/settings/publicTypeIndex.ttl", + ` + @prefix vcard: . + @prefix solid: . + @prefix ex: . + + <#registration-1> a solid:TypeRegistration ; + solid:forClass ex:Something ; + solid:instance , ; + . + + <#registration-2> a solid:TypeRegistration ; + solid:forClass ex:Something ; + solid:instanceContainer ; + . +`, + ); + + const result = await support.discoverType( + sym("https://pod.test/alice/profile/card#me"), + sym("http://vocab.example#Something"), + ); + + expect(result.private).toEqual({ + instances: [], + instanceContainers: [], + }); + + expect(result.public).toEqual({ + instances: [ + sym("https://pod.test/alice/something/1/index.ttl#this"), + sym("https://pod.test/alice/something/2/index.ttl#this"), + ], + instanceContainers: [sym("https://pod.test/alice/things/")], + }); + }); + }); + + describe("given registrations in private type index", () => { + it("returns all instances and containers from that index", async () => { + const { authenticatedFetch, support } = givenModuleSupport(); + + mockTurtleDocument( + authenticatedFetch, + "https://pod.test/alice/profile/card", + ` + @prefix vcard: . + @prefix solid: . + @prefix pim: . + + <#me> a vcard:Individual; + vcard:fn "Alice"; + pim:preferencesFile ; + . +`, + ); + + mockTurtleDocument( + authenticatedFetch, + "https://pod.test/alice/settings/prefs.ttl", + ` + @prefix vcard: . + @prefix solid: . + + + solid:privateTypeIndex ; + . +`, + ); + + mockTurtleDocument( + authenticatedFetch, + "https://pod.test/alice/settings/privateTypeIndex.ttl", + ` + @prefix vcard: . + @prefix solid: . + @prefix ex: . + + <#registration-1> a solid:TypeRegistration ; + solid:forClass ex:Something ; + solid:instance , ; + . + + <#registration-2> a solid:TypeRegistration ; + solid:forClass ex:Something ; + solid:instanceContainer ; + . +`, + ); + + const result = await support.discoverType( + sym("https://pod.test/alice/profile/card#me"), + sym("http://vocab.example#Something"), + ); + + expect(result.private).toEqual({ + instances: [ + sym("https://pod.test/alice/something/1/index.ttl#this"), + sym("https://pod.test/alice/something/2/index.ttl#this"), + ], + instanceContainers: [sym("https://pod.test/alice/things/")], + }); + + expect(result.public).toEqual({ + instances: [], + instanceContainers: [], + }); + }); + }); + }); + describe("isContainer", () => { it("returns true if url refers to a container", async () => { const { authenticatedFetch, support } = givenModuleSupport(); diff --git a/utils/rdflib/src/module/ModuleSupport.ts b/utils/rdflib/src/module/ModuleSupport.ts index 39692532..b80d58a7 100644 --- a/utils/rdflib/src/module/ModuleSupport.ts +++ b/utils/rdflib/src/module/ModuleSupport.ts @@ -1,5 +1,12 @@ -import { Fetcher, IndexedFormula, Node, sym, UpdateManager } from "rdflib"; -import { fetchNode, ModuleConfig } from "../index.js"; +import { Fetcher, IndexedFormula, NamedNode, Node, sym, UpdateManager } from "rdflib"; +import { + fetchNode, + ModuleConfig, + PreferencesQuery, + ProfileQuery, + TypeIndexQuery, + TypeRegistrationsByVisibility +} from "../index.js"; import { ldp, rdf } from "../namespaces/index.js"; /** @@ -32,6 +39,66 @@ export class ModuleSupport { return Promise.all(nodes.map((it) => this.fetchNode(it))); } + /** + * Discover storage locations (instances or instance containers) for a given type by fetching and querying private and public type indexes + * @param webId - The WebID to search for type indexes + * @param typeNode - a NamedNode representing the type to discover + */ + async discoverType( + webId: NamedNode, + typeNode: NamedNode, + ): Promise { + // 1. fetch webId + // 2.1 query profile for public type index + // 3.1 fetch public type index + // 4.1 query registrations + // 2.2 query profile for preferences file + // 3.2 fetch preferences file + // 4.2 query settings document for private type index + // 5. fetch private type index + // 6. query registrations + + // 1. + await this.fetchNode(webId); + // 2. + const profileQuery = new ProfileQuery(webId, this.store); + const publicTypeIndex = profileQuery.queryPublicTypeIndex(); + const preferencesFile = profileQuery.queryPreferencesFile(); + // 3. + await Promise.allSettled([ + this.fetchNode(publicTypeIndex), + this.fetchNode(preferencesFile), + ]); + // 4.1 + const noRegistrations = { instances: [], instanceContainers: [] }; + const publicRegistrations = publicTypeIndex + ? new TypeIndexQuery( + this.store, + publicTypeIndex, + ).queryRegistrationsForType(typeNode) + : noRegistrations; + // 4.2 + const privateTypeIndex = preferencesFile + ? new PreferencesQuery( + this.store, + webId, + preferencesFile, + ).queryPrivateTypeIndex() + : null; + // 5. + await this.fetchNode(privateTypeIndex); + const privateRegistrations = privateTypeIndex + ? new TypeIndexQuery( + this.store, + privateTypeIndex, + ).queryRegistrationsForType(typeNode) + : noRegistrations; + return { + private: privateRegistrations, + public: publicRegistrations, + }; + } + /** * Checks whether the resource identified by the given URL is a LDP container * @param storageUrl - The URL to check diff --git a/utils/rdflib/src/queries/TypeIndexQuery.spec.ts b/utils/rdflib/src/queries/TypeIndexQuery.spec.ts index 05699579..15b0f19f 100644 --- a/utils/rdflib/src/queries/TypeIndexQuery.spec.ts +++ b/utils/rdflib/src/queries/TypeIndexQuery.spec.ts @@ -5,18 +5,118 @@ const VCARD_ADDRESS_BOOK = sym("http://www.w3.org/2006/vcard/ns#AddressBook"); describe("TypeIndexQuery", () => { describe("query instances", () => { - it("returns nothing if store is empty", () => { + it("returns all instances listed in the type registrations for given class", () => { const store = graph(); + parse( + ` + @prefix : <#>. + @prefix vcard: . + @prefix solid: . + + :registration-1 a solid:TypeRegistration ; + solid:forClass vcard:AddressBook ; + solid:instance , ; + . + + :registration-2 a solid:TypeRegistration ; + solid:forClass vcard:AddressBook ; + solid:instance ; + . + + `, + store, + "https://pod.test/alice/setting/publicTypeIndex.ttl", + ); + const query = new TypeIndexQuery( store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([]); + expect(result).toEqual([ + sym("https://pod.test/alice/contacts/1/index.ttl#this"), + sym("https://pod.test/alice/contacts/2/index.ttl#this"), + sym("https://pod.test/alice/contacts/3/index.ttl#this"), + ]); }); + }); + + describe("query registrations for type", () => { + describe("returns nothing", () => { + it("if store is empty", () => { + const store = graph(); + + const query = new TypeIndexQuery( + store, + sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), + ); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ instances: [], instanceContainers: [] }); + }); - it("returns nothing if type registration does not list instances", () => { + it("if type registration does not list anything", () => { + const store = graph(); + + parse( + ` + @prefix : <#>. + @prefix vcard: . + @prefix solid: . + + :registration-1 a solid:TypeRegistration ; + solid:forClass vcard:AddressBook . + + `, + store, + "https://pod.test/alice/setting/publicTypeIndex.ttl", + ); + + const query = new TypeIndexQuery( + store, + sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), + ); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [], + instanceContainers: [], + }); + }); + + it("if registration is for wrong class", () => { + const store = graph(); + + parse( + ` + @prefix : <#>. + @prefix vcard: . + @prefix solid: . + + :registration-1 a solid:TypeRegistration ; + solid:forClass :Anything ; + solid:instance , ; + solid:instanceContainer ; + . + + `, + store, + "https://pod.test/alice/setting/publicTypeIndex.ttl", + ); + + const query = new TypeIndexQuery( + store, + sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), + ); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [], + instanceContainers: [], + }); + }); + }); + + it("even returns instances from a registration, that is not explicitly typed", () => { + // see https://github.com/solid/type-indexes/issues/32#issuecomment-2013540668 const store = graph(); parse( @@ -25,8 +125,10 @@ describe("TypeIndexQuery", () => { @prefix vcard: . @prefix solid: . - :registration-1 a solid:TypeRegistration ; - solid:forClass vcard:AddressBook . + :registration-1 + solid:forClass vcard:AddressBook ; + solid:instance , ; + . `, store, @@ -37,11 +139,17 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [ + sym("https://pod.test/alice/contacts/1/index.ttl#this"), + sym("https://pod.test/alice/contacts/2/index.ttl#this"), + ], + instanceContainers: [], + }); }); - it("returns nothing if registration is for wrong class", () => { + it("returns all instances listed in the type registration for given class", () => { const store = graph(); parse( @@ -51,7 +159,7 @@ describe("TypeIndexQuery", () => { @prefix solid: . :registration-1 a solid:TypeRegistration ; - solid:forClass :Anything ; + solid:forClass vcard:AddressBook ; solid:instance , ; . @@ -64,12 +172,17 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [ + sym("https://pod.test/alice/contacts/1/index.ttl#this"), + sym("https://pod.test/alice/contacts/2/index.ttl#this"), + ], + instanceContainers: [], + }); }); - it("even returns instances from a registration, that is not explicitly typed", () => { - // see https://github.com/solid/type-indexes/issues/32#issuecomment-2013540668 + it("does not return instances or containers that are not a named node", () => { const store = graph(); parse( @@ -78,9 +191,10 @@ describe("TypeIndexQuery", () => { @prefix vcard: . @prefix solid: . - :registration-1 + :registration-1 a solid:TypeRegistration ; solid:forClass vcard:AddressBook ; - solid:instance , ; + solid:instanceContainer "literal" ; + solid:instance "literal" ; . `, @@ -92,14 +206,14 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([ - "https://pod.test/alice/contacts/1/index.ttl#this", - "https://pod.test/alice/contacts/2/index.ttl#this", - ]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [], + instanceContainers: [], + }); }); - it("returns all instances listed in the type registration for given class", () => { + it("returns all instance containers listed in the type registration for given class", () => { const store = graph(); parse( @@ -110,7 +224,7 @@ describe("TypeIndexQuery", () => { :registration-1 a solid:TypeRegistration ; solid:forClass vcard:AddressBook ; - solid:instance , ; + solid:instanceContainer , ; . `, @@ -122,14 +236,17 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([ - "https://pod.test/alice/contacts/1/index.ttl#this", - "https://pod.test/alice/contacts/2/index.ttl#this", - ]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [], + instanceContainers: [ + sym("https://pod.test/alice/contacts/"), + sym("https://pod.test/alice/address-books/"), + ], + }); }); - it("combines instances from multiple type registrations for given class", () => { + it("combines instances and containers from multiple type registrations for given class", () => { // see https://github.com/solid/type-indexes/issues/32#issuecomment-2013540668 const store = graph(); @@ -150,6 +267,18 @@ describe("TypeIndexQuery", () => { :registration-3 a solid:TypeRegistration ; solid:forClass :SomeThingElse ; solid:instance . + + :registration-4 a solid:TypeRegistration ; + solid:forClass vcard:AddressBook ; + solid:instanceContainer . + + :registration-5 a solid:TypeRegistration ; + solid:forClass vcard:AddressBook ; + solid:instanceContainer . + + :registration-6 a solid:TypeRegistration ; + solid:forClass :SomeThingElse ; + solid:instanceContainer . `, store, @@ -160,15 +289,21 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([ - "https://pod.test/alice/contacts/1/index.ttl#this", - "https://pod.test/alice/contacts/2/index.ttl#this", - ]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [ + sym("https://pod.test/alice/contacts/1/index.ttl#this"), + sym("https://pod.test/alice/contacts/2/index.ttl#this"), + ], + instanceContainers: [ + sym("https://pod.test/alice/contacts-container-1/"), + sym("https://pod.test/alice/contacts-container-2/"), + ], + }); }); describe("use correct source documents", () => { - it("returns no instances from a wrong document", () => { + it("returns no instances or containers from a wrong document", () => { const store = graph(); parse( @@ -180,6 +315,7 @@ describe("TypeIndexQuery", () => { a solid:TypeRegistration ; solid:forClass vcard:AddressBook ; solid:instance , ; + solid:instanceContainer ; . `, @@ -196,6 +332,7 @@ describe("TypeIndexQuery", () => { :registration-1 a solid:TypeRegistration ; solid:forClass vcard:AddressBook ; solid:instance , ; + solid:instanceContainer ; . `, @@ -207,11 +344,16 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([ - "https://pod.test/alice/contacts/1/index.ttl#this", - "https://pod.test/alice/contacts/2/index.ttl#this", - ]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ + instances: [ + sym("https://pod.test/alice/contacts/1/index.ttl#this"), + sym("https://pod.test/alice/contacts/2/index.ttl#this"), + ], + instanceContainers: [ + sym("https://pod.test/alice/contacts-container/"), + ], + }); }); it("do not accept solid:forClass statement from wrong document", () => { @@ -250,8 +392,8 @@ describe("TypeIndexQuery", () => { store, sym("https://pod.test/alice/setting/publicTypeIndex.ttl"), ); - const result = query.queryInstancesForClass(VCARD_ADDRESS_BOOK); - expect(result).toEqual([]); + const result = query.queryRegistrationsForType(VCARD_ADDRESS_BOOK); + expect(result).toEqual({ instances: [], instanceContainers: [] }); }); }); }); diff --git a/utils/rdflib/src/queries/TypeIndexQuery.ts b/utils/rdflib/src/queries/TypeIndexQuery.ts index 7adc726a..303e4093 100644 --- a/utils/rdflib/src/queries/TypeIndexQuery.ts +++ b/utils/rdflib/src/queries/TypeIndexQuery.ts @@ -1,5 +1,6 @@ import { IndexedFormula, isNamedNode, NamedNode } from "rdflib"; import { solid } from "../namespaces/index.js"; +import { TypeRegistrations } from "../index.js"; /** * Used query data from a type index document @@ -16,21 +17,47 @@ export class TypeIndexQuery { * @returns A list of the URIs of the found instances */ queryInstancesForClass(type: NamedNode) { + return this.queryRegistrationsForType(type).instances; + } + + private getNamedNodes( + which: "instance" | "instanceContainer", + registration: NamedNode, + ): NamedNode[] { + return this.store + .each(registration, solid(which), null, this.typeIndexDoc) + .filter((it) => isNamedNode(it)) + .map((it) => it as NamedNode); + } + + queryRegistrationsForType(type: NamedNode): TypeRegistrations { const registrations = this.store.each( null, solid("forClass"), type, this.typeIndexDoc, ); - return registrations.flatMap((registration) => { - if (!isNamedNode(registration)) return []; - return this.getInstanceValues(registration as NamedNode); - }); - } - - private getInstanceValues(registration: NamedNode) { - return this.store - .each(registration, solid("instance"), null, this.typeIndexDoc) - .map((it) => it.value); + return registrations + .filter((it) => isNamedNode(it)) + .map((it) => it as NamedNode) + .map((registration: NamedNode) => { + return { + instances: this.getNamedNodes("instance", registration), + instanceContainers: this.getNamedNodes( + "instanceContainer", + registration, + ), + }; + }) + .reduce( + (acc, current) => ({ + instances: [...acc.instances, ...current.instances], + instanceContainers: [ + ...acc.instanceContainers, + ...current.instanceContainers, + ], + }), + { instanceContainers: [], instances: [] }, + ); } }