-
Notifications
You must be signed in to change notification settings - Fork 0
/
SfsApi.ts
297 lines (272 loc) · 8.81 KB
/
SfsApi.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import {DataFactory} from "rdf-data-factory";
import {IBindings, ISparqlEndpointFetcherArgs, SparqlEndpointFetcher} from "fetch-sparql-endpoint";
import {FilterPattern, Generator, Parser, Query, SelectQuery, VariableTerm} from "sparqljs";
import {Facet} from "./facets/Facet";
import {EventStream} from "./Events";
const dataFactory = new DataFactory();
/**
* Bindings represent SPARQL bindings of variables from results query.
*/
export type Bindings = IBindings;
/**
* Interface representing processed results fetched by results query.
*/
export interface Results {
variables: VariableTerm[],
bindings: Bindings[],
}
/**
* Interface representing SPARQL prefixes used in SPARQL queries.
*/
export type Prefixes = { [prefix: string]: string };
/**
* Interface representing {@link SfsApi} configuration.
* Extends {@link ISparqlEndpointFetcherArgs} which are passed to {@link SparqlEndpointFetcher}.
*/
export interface SfsApiConfig extends ISparqlEndpointFetcherArgs {
/**
* URL of SPARQL endpoint.
*/
endpointUrl: string,
/**
* SPARQL SELECT query used for fetching results. Will be enriched with active facet patterns.
*
* **IMPORTANT: baseQuery has to return _id and _label variable.**
* - _id variable is used as primary row identifier and other facets use this variable to build their own queries.
* - _label variable is used when using search query.
* - All SPARQL variables used by sfs-api are prefixed with "_" so you should not name your other variables like this.
*/
baseQuery: string,
/**
* Facets used in this SfsApi.
*/
facets: Facet[],
/**
* Language used for facet labels.
*/
language: string,
/**
* Prefixes used in results or facet queries.
*
* @remarks
*
* Use record-like object e.g. {
* rdfs: "http://www.w3.org/2000/01/rdf-schema#",
* skos: "http://www.w3.org/2004/02/skos/core#",
* owl: "https://www.w3.org/2002/07/owl#",
* dct: "http://purl.org/dc/terms/"
* }
*/
prefixes?: Prefixes,
}
/**
* Class representing whole facet search API. It is the core class of this library.
*/
export class SfsApi {
/**
* SPARQL Parser used for parsing text SPARQL queries to {@link Query} structure.
* {@link prefixes} passed to constructor of this class are passed used in this parser.
*/
public readonly sparqlParser
/**
* SPARQL Generator used to stringify {@link Query} structure to text query.
* {@link prefixes} passed to constructor of this class are used in this generator.
*/
public readonly sparqlGenerator;
/**
* Language used for filtering right labels for facets.
* Should be the same as used in {@link baseQuery}.
*/
public readonly language: string;
/**
* Sole event stream used in this library.
* Facets and API emit their events there and listen for other events.
*/
public readonly eventStream: EventStream;
private readonly endpointUrl: string;
private readonly baseQuery: SelectQuery;
private readonly facets: Facet[];
private readonly fetcher;
private searchPattern: string = "";
public constructor({endpointUrl, baseQuery, facets, language, prefixes, ...fetcherProps}: SfsApiConfig) {
this.sparqlGenerator = new Generator({prefixes: prefixes});
this.sparqlParser = new Parser({prefixes: prefixes});
this.eventStream = new EventStream();
this.eventStream.on("FACET_VALUE_CHANGED", () => {
this.fetchResults()
});
this.endpointUrl = endpointUrl;
this.baseQuery = this.sparqlParser.parse(baseQuery) as SelectQuery;
this.language = language;
this.facets = facets.map(facet => {
facet.sfsApi = this;
return facet;
});
this.fetcher = new SparqlEndpointFetcher(fetcherProps);
checkIfBaseQueryIsValid(this.baseQuery);
}
/**
* Builds results query from current state and fetches new results using it.
* Also streams events FETCH_RESULT_XXX to communicate its progress.
*
* @returns Promise containing the {@link Results}
*/
public async fetchResults() {
const query = this.buildResultsQuery();
this.eventStream.emit({
type: "FETCH_RESULTS_PENDING",
});
return this.fetchBindings(query)
.then(bindingsStream => {
return processResultsBindingsStream(bindingsStream)
.then(results => {
this.eventStream.emit({
type: "FETCH_RESULTS_SUCCESS",
results
});
return results;
})
})
.catch(error => {
this.eventStream.emit({
type: "FETCH_RESULTS_ERROR",
error: error,
});
throw error;
})
}
/**
* Initiates new search with provided {@link searchPattern}.
* Resets all facet states and returns new results via {@link fetchResults}.
*
* @param searchPattern - ?_label variable in baseQuery has to contain this search pattern
*
* @returns Promise containing the {@link Results}
*/
public async newSearch(searchPattern: string = "") {
this.searchPattern = searchPattern;
this.eventStream.emit({
type: "RESET_STATE",
});
this.eventStream.emit({
type: "NEW_SEARCH",
searchPattern,
});
return this.fetchResults();
}
/**
* Stringifies provided {@link query} and uses it to fetch bindings using {@link fetcher}.
*
* @param query - used for fetching bindings
* @returns promise of readable stream of fetched bindings
*/
public async fetchBindings(query: Query) {
const queryString = this.sparqlGenerator.stringify(query);
return this.fetcher.fetchBindings(this.endpointUrl, queryString);
}
/**
* Used to get API and all active facet constraints. Optionally, all active facets except facet of provided facetId.
*
* @param exceptFacetId - facet id of facet which should not be accounted in returned constraints.
*/
public getAllConstraints(exceptFacetId?: string) {
const query = this.getBaseQuery();
if (this.searchPattern) {
const filterPattern = generateSparqlFilterPattern("_label", this.searchPattern);
query.where?.push(filterPattern)
}
this.facets.forEach(facet => {
if (facet.isActive() && facet.id !== exceptFacetId) {
query.where?.push(...facet.getFacetConstraints() ?? []);
}
})
return query.where;
}
private buildResultsQuery() {
const query = this.getBaseQuery();
query.where = this.getAllConstraints();
return query;
}
private getBaseQuery(): SelectQuery {
return { // shallow copy (with deep "where" clone) is made to not mutate original baseQuery
...this.baseQuery,
where: this.baseQuery.where ? [...this.baseQuery.where] : []
};
}
}
/**
* Processes provided stream to {@link Results} structure.
* Returns all bindings present in provided stream.
*
* @param stream - stream to process to {@link Results} structure
*/
function processResultsBindingsStream(stream: NodeJS.ReadableStream): Promise<Results> {
return new Promise<Results>((resolve, reject) => {
let variables: VariableTerm[];
const bindings: Bindings[] = [];
stream.on("variables", fetchedVariables => {
variables = fetchedVariables;
})
stream.on("data", fetchedBindings => {
bindings.push(fetchedBindings);
});
stream.on("error", reject);
stream.on("end", () => {
resolve({variables, bindings});
});
});
}
/**
* Generates SPARQL FILTER() pattern.
* Both parameters are converted by SPARQL LCASE() to lowercase.
*
* @param variableToFilter - variable used in FILTER()
* @param filterValue - value used int FILTER()
*
* @returns pattern representing SPARQL FILTER pattern {@link FilterPattern}
*/
function generateSparqlFilterPattern(variableToFilter: string, filterValue: string): FilterPattern {
return {
"type": "filter",
"expression": {
"type": "operation",
"operator": "contains",
"args": [
{
"type": "operation",
"operator": "lcase",
"args": [
{
"type": "operation",
"operator": "str",
"args": [dataFactory.variable(variableToFilter)]
}
]
},
{
"type": "operation",
"operator": "lcase",
"args": [dataFactory.literal(filterValue)]
}
]
}
};
}
function checkIfBaseQueryIsValid(baseQuery: SelectQuery) {
let _idPresent = false;
let _labelPresent = false;
baseQuery.variables.forEach(variable => {
if ("value" in variable) {
if (variable.value === "_id") {
_idPresent = true;
} else if (variable.value === "_label") {
_labelPresent = true;
}
}
});
if (!_idPresent) {
throw new Error("SfsApi baseQuery has to SELECT ?_id variable. Check documentation for more info.");
} else if (!_labelPresent) {
throw new Error("SfsApi baseQuery has to SELECT ?_label variable. Check documentation for more info.");
}
}