Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: separate member shapes #223

Merged
merged 1 commit into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-foxes-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kopflos-cms/hydra": patch
---

Collections: support `kl-hydra:memberCreateShape` and `kl-hydra:memberQueryShape` which can be used instead of `kl-hydra:memberShape` to separate querying from validation
2 changes: 2 additions & 0 deletions packages/hydra/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import limitOffsetStrategy from './lib/partialCollection/limitOffsetStrategy.js'
import pageIndexStrategy from './lib/partialCollection/pageIndexStrategy.js'

type ExtendingTerms = 'hydra#memberShape'
| 'hydra#memberCreateShape'
| 'hydra#memberQueryShape'
| 'hydra#MemberAssertionConstraintComponent'
| 'hydra'
| 'hydra#DefaultCollectionShape'
Expand Down
5 changes: 4 additions & 1 deletion packages/hydra/lib/queryShapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ export function memberQueryShape({ env, collection: original, limit, offset }: M
})
})

let memberShape = collection.out(kl['hydra#memberShape'])
let memberShape = collection.out(kl['hydra#memberQueryShape'])
if (!isGraphPointer(memberShape)) {
memberShape = collection.out(kl['hydra#memberShape'])
}
if (!isGraphPointer(memberShape)) {
memberShape = collection.blankNode()
}
Expand Down
6 changes: 5 additions & 1 deletion packages/hydra/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { HandlerArgs } from '@kopflos-cms/core'
import type { MultiPointer } from 'clownface'
import { isGraphPointer } from 'is-graph-pointer'

export default function ({ env, subject }: HandlerArgs): MultiPointer {
const memberShape = subject.out(env.ns.kl('hydra#memberShape'))
let memberShape = subject.out(env.ns.kl('hydra#memberCreateShape'))
if (!isGraphPointer(memberShape)) {
memberShape = subject.out(env.ns.kl('hydra#memberShape'))
}

const collectionShape = subject
.blankNode()
Expand Down
6 changes: 6 additions & 0 deletions packages/hydra/test/__snapshots__/collection.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ exports[`@kopflos-cms/hydra hydra:Collection post when collection has validation
<http://example.org/municipality/valid-name> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://schema.ld.admin.ch/Municipality> <http://example.org/municipality/valid-name> .
"
`;

exports[`@kopflos-cms/hydra hydra:Collection post when collection has validation when collection uses :hydraCreateShape creates a new resource 1`] = `
"<http://example.org/municipality/valid-name> <http://schema.org/name> \\"Valid name\\" <http://example.org/municipality/valid-name> .
<http://example.org/municipality/valid-name> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://schema.ld.admin.ch/Municipality> <http://example.org/municipality/valid-name> .
"
`;
52 changes: 52 additions & 0 deletions packages/hydra/test/collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,58 @@ describe('@kopflos-cms/hydra', () => {
expect(res.status).to.equal(400)
})

context('when collection uses :hydraCreateShape', () => {
it('should return 400 when body is invalid', async function () {
// given
const kopflos = await startKopflos()
const collection = ex['municipalities/writable-with-create-shape']
const newMember = ex('invalid-municipality')
const dataset = loadGraph(newMember)
$rdf.clownface({ dataset })
.node(collection)
.addOut(ns.hydra.member, newMember)

// when
const res = await kopflos.handleRequest({
method: 'POST',
iri: collection,
headers: {},
query: {},
body: asBody(dataset, collection),
})

// then
expect(res.status).to.equal(400)
})

it('creates a new resource', async function () {
// given
const kopflos = await startKopflos()
const collection = ex['municipalities/writable-with-create-shape']
const newMember = ex('valid-municipality')
const dataset = loadGraph(newMember)
$rdf.clownface({ dataset })
.node(collection)
.addOut(ns.hydra.member, newMember)

// when
const res = await kopflos.handleRequest({
method: 'POST',
iri: collection,
headers: {},
query: {},
body: asBody(dataset, collection),
})

// then
expect(res.status).to.equal(201)
expect(res.headers?.Location).to.equal(`${ex('municipality/valid-name').value}`)
const newMemberDataset = await $rdf.dataset()
.import(clients.stream.store.get(ex('municipality/valid-name')))
expect(newMemberDataset).canonical.toMatchSnapshot()
})
})

it('should return 400 when body does not include hydra:member triple', async function () {
// given
const kopflos = await startKopflos()
Expand Down
42 changes: 42 additions & 0 deletions packages/hydra/test/collection.test.ts.trig
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,48 @@ GRAPH <municipalities/writable-with-validation> {
.
}

GRAPH <municipalities/writable-with-create-shape> {
<municipalities/writable-with-create-shape>
a hydra:Collection ;
hydra:memberAssertion
[
hydra:property rdf:type ;
hydra:object <https://schema.ld.admin.ch/Municipality> ;
] ;
hydra:writable true ;
kl-hydra:memberUriTemplate
(
"/municipality/"
[
sparql:encode_for_uri
(
[
sparql:replace ( [ sparql:lcase ( [ sh:path schema:name ] ) ] " " "-" "g" )
]
)
]
) ;
kl-hydra:memberShape
[
sh:property
[
sh:path schema:name ;
sh:dataType xsd:string ;
] ;
] ;
kl-hydra:memberCreateShape
[
sh:property
[
sh:path schema:name ;
sh:dataType xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
] ;
.
}

graph <municipality/already-exists> {
<municipality/already-exists>
schema:name "Already exists" .
Expand Down
54 changes: 54 additions & 0 deletions packages/hydra/test/lib/__snapshots__/queryShapes.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,60 @@ _:t5 a sh:NodeShape ;
"
`;

exports[`@kopflos-cms/hydra/lib/queryShapes.js memberQueryShape using member query shape ignores :memberShape 1`] = `
"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .
@prefix schema: <http://schema.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix kl: <https://kopflos.described.at/> .

_:t3 sh:property [
sh:path schema:name ;
] ;
sh:rule [
rdf:type sh:TripleRule ;
sh:predicate hydra:member ;
sh:subject <http://example.org/> ;
sh:object sh:this ;
], [
rdf:type sh:TripleRule ;
sh:predicate rdf:type ;
sh:subject sh:this ;
sh:object <https://schema.ld.admin.ch/Municipality> ;
] .

_:t7 hydra:object <https://schema.ld.admin.ch/Municipality> ;
hydra:property rdf:type .

<http://example.org/> a hydra:Collection ;
<https://kopflos.described.at/hydra#memberQueryShape> _:t3 ;
<https://kopflos.described.at/hydra#memberShape> [
sh:property [
sh:path <http://www.geonames.org/ontology#featureCode> ;
], [
sh:path schema:name ;
] ;
] ;
hydra:memberAssertion _:t7 .

_:t8 a sh:NodeShape ;
sh:target [
rdf:type <https://hypermedia.app/shape-to-query#NodeExpressionTarget> ;
sh:expression [
sh:distinct [
sh:filterShape [
hydra:memberAssertion _:t7 ;
] ;
] ;
] ;
] ;
sh:and (
_:t3
) .

"
`;

exports[`@kopflos-cms/hydra/lib/queryShapes.js totalsQueryShape multiple member assertions returns correct total count 1`] = `
"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix hydra: <http://www.w3.org/ns/hydra/core#> .
Expand Down
13 changes: 13 additions & 0 deletions packages/hydra/test/lib/queryShapes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ describe('@kopflos-cms/hydra/lib/queryShapes.js', () => {
})
})

context('using member query shape', () => {
it('ignores :memberShape', async function () {
// given
const collection = this.rdf.graph.namedNode(ex())

// when
const shape = memberQueryShape({ env, collection })

// then
expect(await serialize(shape.dataset)).toMatchSnapshot()
})
})

context('ordered collection', () => {
it('returns shape with offset', async function () {
// given
Expand Down
21 changes: 21 additions & 0 deletions packages/hydra/test/lib/queryShapes.test.ts.trig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PREFIX gn: <http://www.geonames.org/ontology#>
PREFIX ex: <http://example.org/>
PREFIX schema: <http://schema.org/>
PREFIX sh: <http://www.w3.org/ns/shacl#>
Expand All @@ -21,6 +22,26 @@ GRAPH <unordered-collection> {
.
}

GRAPH <using-member-query-shape> {
ex:
a hydra:Collection ;
hydra:memberAssertion
[
hydra:property rdf:type ;
hydra:object <https://schema.ld.admin.ch/Municipality> ;
] ;
kl-hydra:memberQueryShape
[
sh:property [ sh:path schema:name ] ;
] ;
kl-hydra:memberShape
[
sh:property [ sh:path schema:name ] ;
sh:property [ sh:path gn:featureCode ] ;
] ;
.
}

GRAPH <ordered-collection> {
ex:
a hydra:Collection ;
Expand Down