Skip to content
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/khaki-actors-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shape-to-query": patch
---

Node Expressions which only produce a constructed terms are inlined into the `CONSTRUCT` clause
4 changes: 2 additions & 2 deletions .github/workflows/netlify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20

- uses: nelonoel/branch-name@v1.0.1

- name: Publish preview
uses: tpluscode/action-netlify-deploy@monorepo-filter
uses: jsmrcaga/action-netlify-deploy@v2.4.0
if: env.NETLIFY_AUTH_TOKEN
with:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CONSTRUCT {
?resource1 rdf:type schema:Person.
?resource1 schema:givenName ?resource2.
?resource1 schema:familyName ?resource3.
?resource4 ?resource5 ?resource6.
<http://example.org/JobTitles> hydra:member ?resource4.
}
WHERE {
?resource1 rdf:type schema:Person.
Expand All @@ -16,9 +16,7 @@ WHERE {
}
UNION
{
?resource1 rdf:type schema:Person.
BIND(<http://example.org/JobTitles> AS ?resource4)
BIND(hydra:member AS ?resource5)
?resource1 schema:jobTitle ?resource6.
?resource1 rdf:type schema:Person;
schema:jobTitle ?resource4.
}
}
14 changes: 4 additions & 10 deletions packages/demo/public/docs/example/rules/triple-rule.ttl.rq
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX schema: <http://schema.org/>
CONSTRUCT {
?resource1 rdf:type schema:Person.
?resource2 ?resource3 ?resource1.
?resource1 ?resource6 ?resource7.
?resource2 schema:relatedTo ?resource1.
?resource1 schema:relatedTo ?resource3.
}
WHERE {
?resource1 rdf:type schema:Person.
{
?resource1 (schema:spouse|schema:children|schema:parent) ?resource2.
BIND(schema:relatedTo AS ?resource3)
}
{ ?resource1 (schema:spouse|schema:children|schema:parent) ?resource2. }
UNION
{
BIND(schema:relatedTo AS ?resource6)
?resource1 (schema:spouse|schema:children|schema:parent) ?resource7.
}
{ ?resource1 (schema:spouse|schema:children|schema:parent) ?resource3. }
}
73 changes: 51 additions & 22 deletions packages/shape-to-query/model/rule/TripleRule.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/* eslint-disable camelcase */
import $rdf from '@zazuko/env/web.js'
import type { GraphPointer } from 'clownface'
import { sh } from '@tpluscode/rdf-ns-builders/loose'
import { isGraphPointer } from 'is-graph-pointer'
import type { Pattern } from 'sparqljs'
import type { Term, Variable } from '@rdfjs/types'
import type { NodeExpression } from '../nodeExpression/NodeExpression.js'
import { PatternBuilder } from '../nodeExpression/NodeExpression.js'
import type { ShapePatterns } from '../../lib/shapePatterns.js'
import type { ModelFactory } from '../ModelFactory.js'
import type { Rule, Parameters } from './Rule.js'
import type { Rule, Parameters as BuildParameters } from './Rule.js'

export default class TripleRule implements Rule {
constructor(public subject: NodeExpression, public predicate: NodeExpression, public object: NodeExpression) {
Expand All @@ -31,32 +33,59 @@ export default class TripleRule implements Rule {
)
}

buildPatterns({ focusNode, variable, rootPatterns }: Parameters): ShapePatterns {
buildPatterns({ focusNode, variable, rootPatterns }: BuildParameters): ShapePatterns {
const args = { subject: focusNode, variable, rootPatterns }
const builder = new PatternBuilder()

const subject = builder.build(this.subject, { subject: focusNode, variable, rootPatterns })
const predicate = builder.build(this.predicate, { subject: focusNode, variable, rootPatterns })
const object = builder.build(this.object, { subject: focusNode, variable, rootPatterns })

const whereClause: Pattern[] = [
...rootPatterns,
...subject.patterns,
...predicate.patterns,
...object.patterns,
].map(pattern => {
if (pattern.type === 'query') {
return {
type: 'group',
patterns: [pattern],
}
}
let constructSubject = getInlineTerm(this.subject, [args, builder], 'NamedNode', 'BlankNode')
let constructPredicate = getInlineTerm(this.predicate, [args, builder], 'NamedNode')
let constructObject = getInlineTerm(this.object, [args, builder], 'NamedNode', 'BlankNode', 'Literal')

const whereClause: Pattern[] = []
if (!constructSubject || !constructPredicate || !constructObject) {
whereClause.push(...rootPatterns)
}

function buildExpression(expression: NodeExpression): Variable {
const { patterns, object } = builder.build(expression, args)
whereClause.push(...patterns)
return object
}

return pattern
})
constructSubject = constructSubject || buildExpression(this.subject)
constructPredicate = constructPredicate || buildExpression(this.predicate)
constructObject = constructObject || buildExpression(this.object)

return {
constructClause: [$rdf.quad(subject.object, predicate.object, object.object)],
whereClause,
constructClause: [$rdf.quad(constructSubject, constructPredicate, constructObject)],
whereClause: whereClause.map(pattern => {
if (pattern.type === 'query') {
return {
type: 'group',
patterns: [pattern],
}
}

return pattern
}),
}
}
}

type ArrayToUnion<T> = T extends (infer U)[] ? U : never
type LimitedTerm<T extends Term['termType']> = Term & { termType: ArrayToUnion<T> }

function getInlineTerm<T extends Term['termType']>(expression: NodeExpression, args: Parameters<NodeExpression['buildInlineExpression']>, ...termType: T[]): Variable | LimitedTerm<T> | undefined {
const result = expression.buildInlineExpression?.(...args)
if (result && !result.patterns && 'termType' in result.inline) {
if (result.inline.termType === 'Variable') {
return result.inline
}

if ((termType as string[]).includes(result.inline.termType)) {
return result.inline as LimitedTerm<T>
}
}

return undefined
}
14 changes: 6 additions & 8 deletions packages/shape-to-query/test/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@hydrofoil/shape-to-query shape with count rule generates correct query 1`] = `
"CONSTRUCT { ?q1 ?q2 ?q3. }
"CONSTRUCT { <http://example.org/collection> <http://example.org/numberOfArticles> ?q1. }
WHERE {
BIND(<http://example.org/collection> AS ?q1)
BIND(<http://example.org/numberOfArticles> AS ?q2)
{
SELECT (COUNT(DISTINCT ?q4) AS ?q3) WHERE {
?q4 schema:name ?q5.
FILTER(REGEX(?q5, \\"Tech\\", \\"i\\"))
?q4 rdf:type ?q6.
FILTER(EXISTS { ?q4 rdf:type schema:DefinedTermSet, <https://cube.link/meta/SharedDimension>. })
SELECT (COUNT(DISTINCT ?q2) AS ?q1) WHERE {
?q2 schema:name ?q3.
FILTER(REGEX(?q3, \\"Tech\\", \\"i\\"))
?q2 rdf:type ?q4.
FILTER(EXISTS { ?q2 rdf:type schema:DefinedTermSet, <https://cube.link/meta/SharedDimension>. })
}
}
}"
Expand Down
11 changes: 4 additions & 7 deletions packages/shape-to-query/test/__snapshots__/store.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,8 @@ exports[`@hydrofoil/shape-to-query executing queries union of triple rules 1`] =
PREFIX hydra: <http://www.w3.org/ns/hydra/core#>
CONSTRUCT {
?resource1 rdf:type ?resource2.
?resource1 ?resource4 ?resource5.
?resource6 ?resource7 ?resource8.
?resource1 hydra:view ?resource3.
?resource4 rdf:type <http://example.org/AlphabeticallyPagedView>.
}
WHERE {
VALUES ?resource1 {
Expand All @@ -497,17 +497,14 @@ WHERE {
VALUES ?resource1 {
<http://example.org/people>
}
BIND(hydra:view AS ?resource4)
BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource5)
BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource3)
}
UNION
{
VALUES ?resource1 {
<http://example.org/people>
}
BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource6)
BIND(rdf:type AS ?resource7)
BIND(<http://example.org/AlphabeticallyPagedView> AS ?resource8)
BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource4)
}
}
}"
Expand Down
35 changes: 18 additions & 17 deletions packages/shape-to-query/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,34 +374,35 @@ describe('@hydrofoil/shape-to-query', () => {
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT {
?resource1 schema:mainEntity ?resource2.
?resource3 ?resource4 ?resource5.
?q1 schema:mainEntity ?q2.
?q3 rdfs:label ?q4.
}
WHERE {
{
SELECT ?resource1 ?resource2 {
VALUES (?resource1) {
SELECT ?q1 ?q2 WHERE {
VALUES (?q1) {
(<https://new.wikibus.org/page/brands>)
}
?resource1 schema:mainEntity ?resource2 .
?q1 schema:mainEntity ?q2.
}
}
UNION
{
SELECT ?resource3 ?resource4 ?resource5 {
VALUES (?resource1) {
SELECT ?q3 ?q4 WHERE {
VALUES (?q1) {
(<https://new.wikibus.org/page/brands>)
}
?resource1 schema:mainEntity ?resource2.
?resource2 rdf:type*/hydra:memberAssertion ?resource9.
?resource9 hydra:property ?resource11.
VALUES ?resource11 { rdf:type }
?resource9 hydra:object ?resource8.
?resource8 ^rdf:type ?resource7.
?resource7 skos:prefLabel ?resource6.
BIND(IRI((CONCAT((str(?resource2)), "?i=", (ENCODE_FOR_URI((LCASE((SUBSTR(?resource6, 1, 1))))))))) as ?resource3)
BIND(rdfs:label as ?resource4)
BIND(UCASE((SUBSTR(?resource6, 1 , 1 ))) as ?resource5)
?q1 schema:mainEntity ?q2.
?q2 ((rdf:type*)/hydra:memberAssertion) ?q5.
?q5 hydra:property ?q6.
VALUES ?q6 {
rdf:type
}
?q5 hydra:object ?q7.
?q7 ^rdf:type ?q8.
?q8 skos:prefLabel ?q9.
BIND(IRI(CONCAT(STR(?q2), "?q10=", ENCODE_FOR_URI(LCASE(SUBSTR(?q9, 1 , 1 ))))) AS ?q3)
BIND(UCASE(SUBSTR(?q9, 1 , 1 )) AS ?q4)
}
}
}`)
Expand Down
Loading