diff --git a/.changeset/khaki-actors-shop.md b/.changeset/khaki-actors-shop.md new file mode 100644 index 00000000..5ed60700 --- /dev/null +++ b/.changeset/khaki-actors-shop.md @@ -0,0 +1,5 @@ +--- +"@hydrofoil/shape-to-query": patch +--- + +Node Expressions which only produce a constructed terms are inlined into the `CONSTRUCT` clause diff --git a/.github/workflows/netlify.yml b/.github/workflows/netlify.yml index 14a0c46e..4472413f 100644 --- a/.github/workflows/netlify.yml +++ b/.github/workflows/netlify.yml @@ -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 }} diff --git a/package-lock.json b/package-lock.json index 386ca281..466a18ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14613,7 +14613,7 @@ }, "packages/processor": { "name": "@hydrofoil/sparql-processor", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@types/sparqljs": "^3.1.11", "@zazuko/prefixes": "^2.1.0", @@ -14630,9 +14630,9 @@ }, "packages/shape-to-query": { "name": "@hydrofoil/shape-to-query", - "version": "0.13.4", + "version": "0.13.5", "dependencies": { - "@hydrofoil/sparql-processor": "^0.1.2", + "@hydrofoil/sparql-processor": "^0.1.3", "@tpluscode/rdf-ns-builders": ">=3.0.2", "@tpluscode/rdf-string": "^1.3.3", "@types/sparqljs": "^3.1.11", diff --git a/packages/demo/public/docs/example/rules/triple-rule-unrelated.ttl.rq b/packages/demo/public/docs/example/rules/triple-rule-unrelated.ttl.rq index 3db92c14..9118aec3 100644 --- a/packages/demo/public/docs/example/rules/triple-rule-unrelated.ttl.rq +++ b/packages/demo/public/docs/example/rules/triple-rule-unrelated.ttl.rq @@ -5,7 +5,7 @@ CONSTRUCT { ?resource1 rdf:type schema:Person. ?resource1 schema:givenName ?resource2. ?resource1 schema:familyName ?resource3. - ?resource4 ?resource5 ?resource6. + hydra:member ?resource4. } WHERE { ?resource1 rdf:type schema:Person. @@ -16,9 +16,7 @@ WHERE { } UNION { - ?resource1 rdf:type schema:Person. - BIND( AS ?resource4) - BIND(hydra:member AS ?resource5) - ?resource1 schema:jobTitle ?resource6. + ?resource1 rdf:type schema:Person; + schema:jobTitle ?resource4. } } \ No newline at end of file diff --git a/packages/demo/public/docs/example/rules/triple-rule.ttl.rq b/packages/demo/public/docs/example/rules/triple-rule.ttl.rq index 2aaaf81f..f6d287f7 100644 --- a/packages/demo/public/docs/example/rules/triple-rule.ttl.rq +++ b/packages/demo/public/docs/example/rules/triple-rule.ttl.rq @@ -2,18 +2,12 @@ PREFIX rdf: PREFIX schema: 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. } } \ No newline at end of file diff --git a/packages/shape-to-query/model/rule/TripleRule.ts b/packages/shape-to-query/model/rule/TripleRule.ts index 0010bcda..43a70e0b 100644 --- a/packages/shape-to-query/model/rule/TripleRule.ts +++ b/packages/shape-to-query/model/rule/TripleRule.ts @@ -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) { @@ -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 extends (infer U)[] ? U : never +type LimitedTerm = Term & { termType: ArrayToUnion } + +function getInlineTerm(expression: NodeExpression, args: Parameters, ...termType: T[]): Variable | LimitedTerm | 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 + } + } + + return undefined +} diff --git a/packages/shape-to-query/test/__snapshots__/index.test.ts.snap b/packages/shape-to-query/test/__snapshots__/index.test.ts.snap index 6276ffcd..81885ad5 100644 --- a/packages/shape-to-query/test/__snapshots__/index.test.ts.snap +++ b/packages/shape-to-query/test/__snapshots__/index.test.ts.snap @@ -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 { ?q1. } WHERE { - BIND( AS ?q1) - BIND( 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, . }) + 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, . }) } } }" diff --git a/packages/shape-to-query/test/__snapshots__/store.test.ts.snap b/packages/shape-to-query/test/__snapshots__/store.test.ts.snap index 4764a29b..72b5e15d 100644 --- a/packages/shape-to-query/test/__snapshots__/store.test.ts.snap +++ b/packages/shape-to-query/test/__snapshots__/store.test.ts.snap @@ -483,8 +483,8 @@ exports[`@hydrofoil/shape-to-query executing queries union of triple rules 1`] = PREFIX hydra: CONSTRUCT { ?resource1 rdf:type ?resource2. - ?resource1 ?resource4 ?resource5. - ?resource6 ?resource7 ?resource8. + ?resource1 hydra:view ?resource3. + ?resource4 rdf:type . } WHERE { VALUES ?resource1 { @@ -497,17 +497,14 @@ WHERE { VALUES ?resource1 { } - BIND(hydra:view AS ?resource4) - BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource5) + BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource3) } UNION { VALUES ?resource1 { } - BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource6) - BIND(rdf:type AS ?resource7) - BIND( AS ?resource8) + BIND(IRI(CONCAT(STR(?resource1), \\"#index\\")) AS ?resource4) } } }" diff --git a/packages/shape-to-query/test/index.test.ts b/packages/shape-to-query/test/index.test.ts index 8e6518f0..dddbe3e9 100644 --- a/packages/shape-to-query/test/index.test.ts +++ b/packages/shape-to-query/test/index.test.ts @@ -374,34 +374,35 @@ describe('@hydrofoil/shape-to-query', () => { PREFIX skos: PREFIX rdfs: 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) { () } - ?resource1 schema:mainEntity ?resource2 . + ?q1 schema:mainEntity ?q2. } } UNION { - SELECT ?resource3 ?resource4 ?resource5 { - VALUES (?resource1) { + SELECT ?q3 ?q4 WHERE { + VALUES (?q1) { () } - ?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) } } }`)