Skip to content

Commit

Permalink
feat: separate member shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Feb 14, 2025
1 parent df9e82f commit de05a9e
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 2 deletions.
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

0 comments on commit de05a9e

Please sign in to comment.