@ulb-darmstadt/shacl-form 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/dist/assets/mode-BriAnl-6.js +13 -0
- package/dist/assets/mode-f6xgDLiM.js +13 -0
- package/dist/assets/query-CpoZo0uf.js +501 -0
- package/dist/assets/query-DLp_5-vp.js +84 -0
- package/dist/bundle.d.ts +3 -3
- package/dist/bundle.js +34 -447
- package/dist/config.d.ts +13 -5
- package/dist/constants.d.ts +3 -0
- package/dist/constraints.d.ts +3 -3
- package/dist/exports.d.ts +11 -10
- package/dist/form.d.ts +12 -6
- package/dist/graph-loader.d.ts +2 -2
- package/dist/group.d.ts +1 -1
- package/dist/index.js +6 -87
- package/dist/linker.d.ts +4 -4
- package/dist/node-template.d.ts +2 -2
- package/dist/node.d.ts +9 -4
- package/dist/plugin.d.ts +3 -1
- package/dist/plugins/file-upload.d.ts +2 -2
- package/dist/plugins/leaflet.d.ts +7 -4
- package/dist/plugins/leaflet.js +18 -10
- package/dist/property-template.d.ts +2 -2
- package/dist/property.d.ts +4 -2
- package/dist/query/editor.d.ts +3 -0
- package/dist/query/index.d.ts +55 -0
- package/dist/query/mode.d.ts +24 -0
- package/dist/query/sparql.d.ts +75 -0
- package/dist/rdf-loader.d.ts +1 -1
- package/dist/serialize.d.ts +1 -1
- package/dist/sparql.js +9 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.default.d.ts +2 -2
- package/dist/util.d.ts +2 -2
- package/package.json +36 -17
- package/src/query/query-mode.md +319 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Query mode
|
|
2
|
+
|
|
3
|
+
Query mode turns a SHACL node shape into a search form. Instead of producing RDF data, the component exposes the active filters as a backend-neutral JavaScript object. The host application can translate that object into SPARQL, Solr, Elasticsearch, SQL, or another query language.
|
|
4
|
+
|
|
5
|
+
## Enable query mode
|
|
6
|
+
|
|
7
|
+
Provide shapes as usual and set `data-mode="query"`:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<shacl-form
|
|
11
|
+
data-mode="query"
|
|
12
|
+
data-shapes-url=".../shapes.ttl"
|
|
13
|
+
data-shape-subject="http://example.org/DocumentShape"
|
|
14
|
+
></shacl-form>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`data-shape-subject` selects the root node shape. If it is omitted, the component uses its normal root-shape resolution rules. `data-mode="edit"` is the default. Use `data-mode="view"`, or the legacy `data-view` attribute, for view mode.
|
|
18
|
+
|
|
19
|
+
Query mode uses the shape hierarchy, property labels, descriptions, datatypes, `sh:in`, `sh:class`, `sh:languageIn`, `sh:node`, and supported `sh:or` and `sh:xone` alternatives to build controls. Editing concerns such as required values, default values, minimum counts, and generated subjects do not create query criteria.
|
|
20
|
+
|
|
21
|
+
Listen for the `query` event to run a search:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const form = document.querySelector('shacl-form')
|
|
25
|
+
|
|
26
|
+
form.addEventListener('query', event => {
|
|
27
|
+
search(event.detail)
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The event is emitted after the form initializes and whenever a criterion changes. Text input is debounced by 300 ms. It bubbles through the DOM and crosses the component's shadow boundary.
|
|
32
|
+
|
|
33
|
+
## The query object
|
|
34
|
+
|
|
35
|
+
`form.getQuery()` and `event.detail` have this structure (RDF values are RDF/JS `Term` objects):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
type Query = {
|
|
39
|
+
rootShapeId: string
|
|
40
|
+
targetClass?: string
|
|
41
|
+
criteria: QueryCriterion[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type QueryCriterion = {
|
|
45
|
+
field: QueryField
|
|
46
|
+
operator: 'contains' | 'equals' | 'range'
|
|
47
|
+
value?: Term
|
|
48
|
+
min?: Term
|
|
49
|
+
max?: Term
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type QueryField = {
|
|
53
|
+
id: string
|
|
54
|
+
path: string[]
|
|
55
|
+
shapePath?: string[]
|
|
56
|
+
datatype?: string
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `rootShapeId` identifies the selected root node shape.
|
|
61
|
+
- `targetClass` is its `sh:targetClass`, when present.
|
|
62
|
+
- `criteria` contains only controls with an active value. An empty form therefore has an empty array.
|
|
63
|
+
- `field.id` is an opaque, form-generated identifier. Use it to correlate a field with facet results; do not derive backend meaning from it.
|
|
64
|
+
- `field.path` is the complete RDF predicate path from the root resource to the value. A nested field can therefore have a path such as `[ex:author, ex:name]`.
|
|
65
|
+
- `field.shapePath` identifies the property-shape branch. It distinguishes qualified or alternative branches that have the same RDF predicate path.
|
|
66
|
+
- `field.datatype` is the field's datatype IRI when the shape supplies one.
|
|
67
|
+
|
|
68
|
+
All criteria are combined with logical AND and apply to the same root resource. RDF path segments describe traversal from that root; intermediate resources are not result rows.
|
|
69
|
+
|
|
70
|
+
The built-in editors choose operators as follows:
|
|
71
|
+
|
|
72
|
+
| Shape/value kind | Control | Criterion |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| String or language-tagged text | Text input, plus a language chooser for language-tagged text | `contains` with `value` |
|
|
75
|
+
| `sh:in`, `sh:class`, or boolean | Select | `equals` with `value` |
|
|
76
|
+
| Numeric, `xsd:date`, or `xsd:dateTime` | Min/max inputs or facet-backed slider | `range` with `min` and/or `max` |
|
|
77
|
+
| Other IRI or typed value | Text-compatible input | `equals` with `value` |
|
|
78
|
+
|
|
79
|
+
Language input produces a language-tagged RDF literal. `sh:languageIn` supplies the available language choices; otherwise the chooser starts with the form's preferred language and remains editable. Numeric and temporal bounds are typed literals. A range criterion may contain only one bound. A facet-backed slider is inactive until the user changes it, so merely displaying the full available range does not create a criterion.
|
|
80
|
+
|
|
81
|
+
Outside query mode, `getQuery()` still returns the root metadata when available but always returns an empty `criteria` array.
|
|
82
|
+
|
|
83
|
+
## Facets and field availability
|
|
84
|
+
|
|
85
|
+
A facet provider is optional. Without one, all shape-derived fields remain visible and query events still work. Register a provider to populate choices and ranges and hide filters that cannot match:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
form.setQueryFacetProvider({
|
|
89
|
+
async getFacets({ query, fields, signal }) {
|
|
90
|
+
const response = await fetch('/api/search/facets', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
signal,
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ query, fields }),
|
|
95
|
+
})
|
|
96
|
+
return response.json()
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The provider receives the current `query`, every queryable `field`, and an `AbortSignal`. Honor the signal when doing network work. When a criterion changes, the component emits `query`, aborts the preceding facet request, and requests fresh facets. Results from superseded requests are ignored.
|
|
102
|
+
|
|
103
|
+
Return one facet per field that the backend can describe:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
type QueryFacet = {
|
|
107
|
+
fieldId: string
|
|
108
|
+
count: number
|
|
109
|
+
buckets?: Array<{
|
|
110
|
+
value: Term
|
|
111
|
+
label?: string
|
|
112
|
+
count: number
|
|
113
|
+
}>
|
|
114
|
+
min?: Term
|
|
115
|
+
max?: Term
|
|
116
|
+
error?: boolean
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- `fieldId` must equal the corresponding `QueryField.id`.
|
|
121
|
+
- `count` is the number of matching root resources for which the field is available. Providers should count distinct roots when RDF joins can produce duplicates.
|
|
122
|
+
- `buckets` supplies discrete RDF values, optional display labels, and counts. For a field with `sh:in`, buckets outside the shape's allowed values are discarded. An active value missing from refreshed buckets remains selectable with count zero so the user can remove it.
|
|
123
|
+
- `min` and `max` supply typed bounds for numeric and temporal fields. When they form a usable interval, the two bound inputs become a range slider.
|
|
124
|
+
- `error: true` marks a field-level failure.
|
|
125
|
+
|
|
126
|
+
An inactive leaf with `count: 0` is given the `query-unavailable` class and hidden by the default theme. An active field remains visible even when its count becomes zero. Structural parent branches are hidden when they contain no available leaf. The host element receives `query-facets-empty` when no filter is available and affected properties receive `query-facet-error` for field-level failures.
|
|
127
|
+
|
|
128
|
+
If a provider is installed before initialization, the form has `query-facets-pending` and displays the configured loading text until the first facets arrive. Call `form.refreshQueryFacets()` after filters maintained outside the component change; it makes a new facet request without changing or emitting the query. The method is a no-op outside query mode.
|
|
129
|
+
|
|
130
|
+
If `getFacets` throws, the component emits a bubbling, composed `queryerror` event whose `detail` is the thrown value. Aborted requests do not emit this event.
|
|
131
|
+
|
|
132
|
+
## SPARQL support
|
|
133
|
+
|
|
134
|
+
The optional `@ulb-darmstadt/shacl-form/sparql` entry point exports `SparqlQueryProvider` and `SparqlQueryBuilder`. Despite its broad name, `SparqlQueryProvider` implements the form's `QueryFacetProvider` interface. It can also execute the result `SELECT` for the current query.
|
|
135
|
+
|
|
136
|
+
### Connect directly to an endpoint
|
|
137
|
+
|
|
138
|
+
Create the provider, register it with the form, and use the same provider to retrieve results:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { SparqlQueryProvider } from '@ulb-darmstadt/shacl-form/sparql'
|
|
142
|
+
|
|
143
|
+
const form = document.querySelector('shacl-form')!
|
|
144
|
+
const sparql = new SparqlQueryProvider('/api/sparql')
|
|
145
|
+
let latestRequest = 0
|
|
146
|
+
|
|
147
|
+
form.setQueryFacetProvider(sparql)
|
|
148
|
+
|
|
149
|
+
form.addEventListener('query', async event => {
|
|
150
|
+
const request = ++latestRequest
|
|
151
|
+
try {
|
|
152
|
+
const result = await sparql.select(event.detail, {
|
|
153
|
+
orderBy: 'ASC(?root)',
|
|
154
|
+
limit: 20,
|
|
155
|
+
offset: 0,
|
|
156
|
+
})
|
|
157
|
+
if (request === latestRequest) {
|
|
158
|
+
renderResults(result.results.bindings)
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (request === latestRequest) {
|
|
162
|
+
showSearchError(error)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The provider plays two roles:
|
|
169
|
+
|
|
170
|
+
1. On initialization and after every filter change, the form calls `sparql.getFacets(...)`. The returned counts, buckets, and bounds update the available controls.
|
|
171
|
+
2. The application can call `sparql.select(query, options)` to retrieve matching root resources. The form does not run this result query automatically. The request counter in the example prevents a slow, older request from replacing newer results.
|
|
172
|
+
|
|
173
|
+
An endpoint provider sends `POST` requests with an `application/x-www-form-urlencoded` body containing the generated `query`. It requests and parses `application/sparql-results+json`. The endpoint must therefore accept SPARQL query requests from the browser, including any required CORS and authentication configuration.
|
|
174
|
+
|
|
175
|
+
### Configure the endpoint provider
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { DataFactory } from 'n3'
|
|
179
|
+
import { SparqlQueryProvider } from '@ulb-darmstadt/shacl-form/sparql'
|
|
180
|
+
|
|
181
|
+
const sparql = new SparqlQueryProvider('/api/sparql', {
|
|
182
|
+
headers: {
|
|
183
|
+
authorization: 'Bearer token',
|
|
184
|
+
},
|
|
185
|
+
dataset: {
|
|
186
|
+
type: 'named',
|
|
187
|
+
graph: DataFactory.namedNode('http://example.org/data'),
|
|
188
|
+
},
|
|
189
|
+
bucketLimit: 50,
|
|
190
|
+
caseSensitive: false,
|
|
191
|
+
onError(error, field) {
|
|
192
|
+
console.error(`Could not load facet ${field.id}`, error)
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
form.setQueryFacetProvider(sparql)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The endpoint options are:
|
|
200
|
+
|
|
201
|
+
| Option | Meaning |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `headers` | Additional request headers, for example an authorization header. They are merged with the provider's content type and accept headers. |
|
|
204
|
+
| `dataset` | Selects the default graph, one fixed named graph, or any named graph. See below. |
|
|
205
|
+
| `bucketLimit` | Maximum number of buckets per discrete field. The default is `100`. |
|
|
206
|
+
| `caseSensitive` | Whether `contains` comparisons preserve case. The default is `false`, which compares `LCASE(STR(...))` values. |
|
|
207
|
+
| `rootPattern` | Replaces the default SPARQL pattern that identifies root resources. |
|
|
208
|
+
| `onError` | Receives facet-loading errors and the affected field. |
|
|
209
|
+
|
|
210
|
+
`dataset` accepts the following values:
|
|
211
|
+
|
|
212
|
+
- `{ type: 'default' }` queries the endpoint's default graph. This is the default.
|
|
213
|
+
- `{ type: 'named', graph: DataFactory.namedNode('...') }` wraps the generated patterns in a `GRAPH <...>` clause.
|
|
214
|
+
- `{ type: 'named' }` wraps them in `GRAPH ?graph`, allowing any named graph to match.
|
|
215
|
+
|
|
216
|
+
### Choose the root resources
|
|
217
|
+
|
|
218
|
+
Every generated query starts with a pattern that selects `?root`. By default, the provider uses:
|
|
219
|
+
|
|
220
|
+
- `?root rdf:type <targetClass>` when the root shape declares `sh:targetClass`; or
|
|
221
|
+
- `?root dcterms:conformsTo <rootShapeId>` otherwise.
|
|
222
|
+
|
|
223
|
+
Use `rootPattern` if the dataset identifies searchable resources differently:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const sparql = new SparqlQueryProvider('/api/sparql', {
|
|
227
|
+
rootPattern: ({ rootVariable }) => {
|
|
228
|
+
return `${rootVariable} <http://example.org/status> <http://example.org/Published> .`
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Return a SPARQL graph pattern without surrounding braces. The context also contains the current `query` and, for `{ type: 'named' }`, `graphVariable: '?graph'`. The provider adds the configured `GRAPH` wrapper.
|
|
234
|
+
|
|
235
|
+
### How SPARQL facets are calculated
|
|
236
|
+
|
|
237
|
+
For each refresh, the provider builds one request containing a `UNION` branch for every queryable field and executes it as a single SPARQL query. Each branch includes the root pattern, all active criteria, and the path of the field being faceted.
|
|
238
|
+
|
|
239
|
+
- Discrete fields are grouped by value. Bucket counts and the total field count use `COUNT(DISTINCT ?root)`.
|
|
240
|
+
- Numeric and temporal fields return `COUNT(DISTINCT ?root)`, `MIN(?facetValue)`, and `MAX(?facetValue)`.
|
|
241
|
+
- `bucketLimit` is applied independently to each discrete field.
|
|
242
|
+
- Nested `field.path` values become a sequence of triple patterns from `?root` to the facet value.
|
|
243
|
+
|
|
244
|
+
Because all active criteria are included, facet results describe the already-filtered result set. This includes a criterion on the field currently being faceted.
|
|
245
|
+
|
|
246
|
+
The SPARQL builder translates `field.path`, but not `field.shapePath`. If multiple qualified shape branches use the same RDF predicate path, they generate the same SPARQL traversal. Use a custom facet provider or query translation when the backend must distinguish those branches using additional type or shape conditions.
|
|
247
|
+
|
|
248
|
+
Facet failures are handled as field-level availability errors: `getFacets()` returns `{ count: 0, error: true }` for every requested field and invokes `onError(error, field)` for each field. It does not rethrow the endpoint error, so these failures do not produce the form's `queryerror` event. By contrast, `select()` rejects when its endpoint request fails; handle that rejection in the application.
|
|
249
|
+
|
|
250
|
+
### Use a custom SPARQL transport
|
|
251
|
+
|
|
252
|
+
Construct `SparqlQueryProvider` directly when requests must go through an SDK, server-side proxy, or custom authentication flow. The executor receives the generated SPARQL string and an `AbortSignal`, and must return SPARQL Results JSON:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { SparqlQueryProvider } from '@ulb-darmstadt/shacl-form/sparql'
|
|
256
|
+
|
|
257
|
+
const sparql = new SparqlQueryProvider(async (query, signal) => {
|
|
258
|
+
const response = await fetch('/api/run-sparql', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
signal,
|
|
261
|
+
headers: { 'content-type': 'application/json' },
|
|
262
|
+
body: JSON.stringify({ query }),
|
|
263
|
+
})
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error(`SPARQL request failed: ${response.status}`)
|
|
266
|
+
}
|
|
267
|
+
return response.json()
|
|
268
|
+
}, {
|
|
269
|
+
bucketLimit: 50,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
form.setQueryFacetProvider(sparql)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Pass the supplied signal to abortable I/O. A newer form query aborts the previous facet request. Calls to `select()` use the same executor but are not cancelled when the form changes.
|
|
276
|
+
|
|
277
|
+
### Build SPARQL without the provider
|
|
278
|
+
|
|
279
|
+
Use `SparqlQueryBuilder` when the application manages transport itself:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { SparqlQueryBuilder } from '@ulb-darmstadt/shacl-form/sparql'
|
|
283
|
+
|
|
284
|
+
const builder = new SparqlQueryBuilder({
|
|
285
|
+
dataset: { type: 'default' },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const select = builder.buildSelect(form.getQuery(), {
|
|
289
|
+
projection: ['?root'],
|
|
290
|
+
limit: 20,
|
|
291
|
+
})
|
|
292
|
+
const where = builder.buildWhere(form.getQuery())
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
By default, `buildSelect` returns distinct `?root` bindings. Its options are `projection`, `distinct`, `orderBy`, `limit`, and `offset`; `select()` returns the endpoint's SPARQL Results JSON unchanged. `buildWhere` returns only the graph pattern. `buildFacetSelect(request, field, bucketLimit)` builds one field's facet query, while `buildFacetsSelect(request, bucketLimit)` combines all requested fields. User-provided `projection` and `orderBy` strings are inserted as SPARQL syntax and should not be populated directly from untrusted input.
|
|
296
|
+
|
|
297
|
+
## Custom query editors
|
|
298
|
+
|
|
299
|
+
Registered form plugins may provide `createQueryEditor(field, template)` for specialized query controls. The returned element must expose:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
{
|
|
303
|
+
queryField: QueryField
|
|
304
|
+
getQueryCriteria(): QueryCriterion[]
|
|
305
|
+
setQueryFacet(facet?: QueryFacet): void
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The editor should emit a bubbling `change` event when its criteria change. The component then emits the new query and refreshes facets. If no plugin supplies a query editor, the built-in editor described above is used.
|
|
310
|
+
|
|
311
|
+
## Differences from edit and view modes
|
|
312
|
+
|
|
313
|
+
Query mode does not bind form controls to `data-values`, construct RDF, validate SHACL constraints, or submit data. The following methods throw an error in query mode:
|
|
314
|
+
|
|
315
|
+
- `toRDF()`
|
|
316
|
+
- `serialize()`
|
|
317
|
+
- `validate()`
|
|
318
|
+
|
|
319
|
+
Use `getQuery()` to read filters, the `query` event to react to changes, and the facet provider only to describe current availability. Result retrieval remains the responsibility of the hosting application.
|