diff --git a/.changeset/polite-buckets-glow.md b/.changeset/polite-buckets-glow.md new file mode 100644 index 0000000..fe333b4 --- /dev/null +++ b/.changeset/polite-buckets-glow.md @@ -0,0 +1,5 @@ +--- +"@kopflos-cms/hydra": minor +--- + +Configurable SPARQL Endpoint to use for fetching collection members diff --git a/.changeset/six-camels-cough.md b/.changeset/six-camels-cough.md new file mode 100644 index 0000000..1fa8167 --- /dev/null +++ b/.changeset/six-camels-cough.md @@ -0,0 +1,5 @@ +--- +"@kopflos-cms/core": patch +--- + +Added `kl:endpoint` predicate diff --git a/example/kopflos.config.ts b/example/kopflos.config.ts index dd28504..0d9795c 100644 --- a/example/kopflos.config.ts +++ b/example/kopflos.config.ts @@ -10,6 +10,7 @@ export default { updateUrl: 'http://localhost:7878/update', storeUrl: 'http://localhost:7878/store', }, + lindas: 'https://lindas.admin.ch/query', }, watch: ['lib'], plugins: { diff --git a/example/resources/countries.ttl b/example/resources/countries.ttl new file mode 100644 index 0000000..eb2493f --- /dev/null +++ b/example/resources/countries.ttl @@ -0,0 +1,21 @@ +PREFIX kl: +PREFIX schema: +PREFIX sh: +PREFIX rdf: +PREFIX hydra: +prefix kl-hydra: + +<> + a hydra:Collection ; + hydra:memberAssertion + [ + hydra:property rdf:type ; + hydra:object schema:Country ; + ] ; + kl:endpoint "lindas" ; + kl-hydra:memberShape + [ + sh:property [ sh:path schema:name ; sh:languageIn ("en" "de") ] ; + sh:property [ sh:path schema:identifier ] ; + ] ; +. diff --git a/package-lock.json b/package-lock.json index b5db457..360535f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6504,11 +6504,11 @@ } }, "node_modules/@hydrofoil/shape-to-query": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@hydrofoil/shape-to-query/-/shape-to-query-0.13.6.tgz", - "integrity": "sha512-5ItQHHO710NTx8TvJdK6uJI5BGNoZKcG9KQZbrfEz6EHZz+BKWYVIDStfd+pooDfM4yGNtnuw+mTrP2l4U/kWQ==", + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@hydrofoil/shape-to-query/-/shape-to-query-0.13.8.tgz", + "integrity": "sha512-Ox3i4JgjXj7n8+pFYlVOw0qtXQF4eCrKUjawjMTr4Z8W5EfSlrH/pLNNS1VsecHyTTLAWqfO1PO6z7x/I/3hkA==", "dependencies": { - "@hydrofoil/sparql-processor": "^0.1.3", + "@hydrofoil/sparql-processor": "^0.1.4", "@tpluscode/rdf-ns-builders": ">=3.0.2", "@tpluscode/rdf-string": "^1.3.4", "@types/sparqljs": "^3.1.11", @@ -6521,17 +6521,17 @@ "is-graph-pointer": "^2.0.0", "rdf-literal": "^1.3.2", "sparqljs": "^3.6.1", - "ts-pattern": "^5.2.0" + "ts-pattern": "^5.6.0" } }, "node_modules/@hydrofoil/sparql-processor": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@hydrofoil/sparql-processor/-/sparql-processor-0.1.3.tgz", - "integrity": "sha512-QVgIy+H3038t0r2EtPuknn1I9Cn2foVKk36uU3Wk+npceFu995kTkH1T77AN4KRxsSZBUyENN9XUxaCvUwiN+Q==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@hydrofoil/sparql-processor/-/sparql-processor-0.1.4.tgz", + "integrity": "sha512-pyHmlatvb9sVhJ8LobZcUsbs8fjsHZaDCBDMBUv4crmiCbaGk5vz2A9W/s/TmMzjcUEOUqzm6jksVWi0jEGOOw==", "dependencies": { "@types/sparqljs": "^3.1.11", - "@zazuko/prefixes": "^2.1.0", - "ts-pattern": "^5.2.0" + "@zazuko/prefixes": "^2.2.0", + "ts-pattern": "^5.6.0" } }, "node_modules/@hydrofoil/talos-core": { @@ -19391,9 +19391,9 @@ } }, "node_modules/ts-pattern": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.5.0.tgz", - "integrity": "sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.6.0.tgz", + "integrity": "sha512-SL8u60X5+LoEy9tmQHWCdPc2hhb2pKI6I1tU5Jue3v8+iRqZdcT3mWPwKKJy1fMfky6uha82c8ByHAE8PMhKHw==", "license": "MIT" }, "node_modules/tsconfig-paths": { @@ -20590,15 +20590,17 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@hydrofoil/shape-to-query": "^0.13.6", + "@hydrofoil/shape-to-query": "^0.13.8", "@kopflos-cms/core": "^0.3.1", "@sindresorhus/merge-streams": "^4.0.0", "@tpluscode/rdf-ns-builders": "^4.3.0", + "http-errors": "^2.0.0", "is-graph-pointer": "^2.1.0", "rdf-literal": "^1.3.2" }, "devDependencies": { "@rdfjs-elements/formats-pretty": "^0.6.8", + "@types/http-errors": "^2.0.4", "@zazuko/env-node": "^2.1.4", "chai": "^5.1.2", "mocha-chai-rdf": "^0.1.6" diff --git a/packages/core/ns.ts b/packages/core/ns.ts index 032c630..bfbfdfb 100644 --- a/packages/core/ns.ts +++ b/packages/core/ns.ts @@ -1,6 +1,6 @@ import rdf from '@zazuko/env-node' -type Properties = 'api' | 'resourceLoader' | 'handler' | 'method' | 'config' +type Properties = 'api' | 'resourceLoader' | 'handler' | 'method' | 'config' | 'endpoint' type Classes = 'Config' | 'Api' | 'ResourceShape' | 'Handler' type Shorthands = 'DescribeLoader' | 'OwnGraphLoader' diff --git a/packages/hydra/handlers/collection.ts b/packages/hydra/handlers/collection.ts index 5be294b..73904f8 100644 --- a/packages/hydra/handlers/collection.ts +++ b/packages/hydra/handlers/collection.ts @@ -13,15 +13,22 @@ constraints.set(kl['hydra#MemberAssertionConstraintComponent'], HydraMemberAsser export function get(): Handler { return ({ env, subject }) => { + const endpoint = subject.out(kl.endpoint).value || 'default' + if (!isReadable(env, subject)) { return new error.MethodNotAllowed('Collection is not readable') } const memberQuery = constructQuery(memberQueryShape({ env, collection: subject })) - const members = env.sparql.default.stream.query.construct(memberQuery) + const sparqlClient = env.sparql[endpoint] + if (!sparqlClient) { + return new error.InternalServerError(`SPARQL endpoint '${endpoint}' not found`) + } + + const members = sparqlClient.stream.query.construct(memberQuery) const totalQuery = constructQuery(totalsQueryShape({ env, collection: subject })) - const totals = env.sparql.default.stream.query.construct(totalQuery) + const totals = sparqlClient.stream.query.construct(totalQuery) return { status: 200, diff --git a/packages/hydra/lib/queryShapes.ts b/packages/hydra/lib/queryShapes.ts index b66faf4..5ed5b69 100644 --- a/packages/hydra/lib/queryShapes.ts +++ b/packages/hydra/lib/queryShapes.ts @@ -7,17 +7,22 @@ import { isGraphPointer } from 'is-graph-pointer' // eslint-disable-next-line import/no-unresolved import { kl } from '@kopflos-cms/core/ns.js' import { log } from '@kopflos-cms/core' +import type { DatasetCoreFactory } from '@rdfjs/types' interface MemberQueryShapeArgs { - env: Environment + env: Environment collection: GraphPointer limit?: number offset?: number } -export function memberQueryShape({ env, collection, limit, offset }: MemberQueryShapeArgs): GraphPointer { +export function memberQueryShape({ env, collection: original, limit, offset }: MemberQueryShapeArgs): GraphPointer { const { hydra, sh, rdf } = env.ns + const collection = env.clownface({ + dataset: env.dataset([...original.dataset]), + term: original.term, + }) const shape = collection .blankNode() .addOut(rdf.type, sh.NodeShape) @@ -105,13 +110,17 @@ export function memberQueryShape({ env, collection, limit, offset }: MemberQuery } interface TotalsQueryShapeArgs { - env: Environment + env: Environment collection: GraphPointer } -export function totalsQueryShape({ env, collection }: TotalsQueryShapeArgs) { +export function totalsQueryShape({ env, collection: original }: TotalsQueryShapeArgs) { const { hydra, sh, rdf } = env.ns + const collection = env.clownface({ + dataset: env.dataset([...original.dataset]), + term: original.term, + }) const filterShape = collection.blankNode() filterShape.addOut(hydra.memberAssertion, collection.out(hydra.memberAssertion)) diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 43a656a..104defa 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -20,7 +20,7 @@ "handlers/*.d.ts" ], "dependencies": { - "@hydrofoil/shape-to-query": "^0.13.6", + "@hydrofoil/shape-to-query": "^0.13.8", "@kopflos-cms/core": "^0.3.1", "@sindresorhus/merge-streams": "^4.0.0", "@tpluscode/rdf-ns-builders": "^4.3.0", diff --git a/packages/hydra/test/collection.test.ts b/packages/hydra/test/collection.test.ts index e0acf1e..1714266 100644 --- a/packages/hydra/test/collection.test.ts +++ b/packages/hydra/test/collection.test.ts @@ -32,6 +32,7 @@ describe('@kopflos-cms/hydra', () => { baseIri, sparql: { default: inMemoryClients(this.rdf), + lindas: 'https://lindas.admin.ch/query', }, } }) @@ -136,6 +137,46 @@ describe('@kopflos-cms/hydra', () => { expect(pointer.out(ns.hydra.totalItems).term).to.deep.eq(toRdf(14)) }) }) + + context('when collection is sourced from another endpoint', () => { + it('should return a stream of members', async function () { + // given + const kopflos = await startKopflos() + + // when + const collection = ex['countries/from-lindas'] + const res = await kopflos.handleRequest({ + method: 'GET', + iri: collection, + headers: {}, + query: {}, + body: {} as Body, + }) + + // then + const dataset = await $rdf.dataset().import(res.body as Stream) + const pointer = $rdf.clownface({ dataset }).node(collection) + expect(pointer.out(ns.hydra.member).terms).to.have.length.above(200) + }) + + it('should fail when endpoint is not configured', async function () { + // given + const kopflos = await startKopflos() + + // when + const collection = ex['countries/wrong-endpoint'] + const res = await kopflos.handleRequest({ + method: 'GET', + iri: collection, + headers: {}, + query: {}, + body: {} as Body, + }) + + // then + expect(res.status).to.equal(500) + }) + }) }) context('post', () => { diff --git a/packages/hydra/test/collection.test.ts.trig b/packages/hydra/test/collection.test.ts.trig index a52f310..53cb507 100644 --- a/packages/hydra/test/collection.test.ts.trig +++ b/packages/hydra/test/collection.test.ts.trig @@ -75,3 +75,37 @@ GRAPH { ] ; . } + +GRAPH { + + a hydra:Collection ; + hydra:memberAssertion + [ + hydra:property rdf:type ; + hydra:object schema:Country ; + ] ; + kl:endpoint "lindas" ; + kl-hydra:memberShape + [ + sh:property [ sh:path schema:name ] ; + sh:property [ sh:path schema:identifier ] ; + ] ; + . +} + +GRAPH { + + a hydra:Collection ; + hydra:memberAssertion + [ + hydra:property rdf:type ; + hydra:object schema:Country ; + ] ; + kl:endpoint "foobar" ; + kl-hydra:memberShape + [ + sh:property [ sh:path schema:name ] ; + sh:property [ sh:path schema:identifier ] ; + ] ; + . +}