graph-explorer
Version:
Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.
1,141 lines (1,025 loc) • 32.3 kB
text/typescript
import {
objectValues,
getOrCreateArrayInMap,
} from "../../viewUtils/collections";
import { DataProvider, LinkElementsParams, FilterParams } from "../provider";
import {
Dictionary,
ClassModel,
LinkType,
ElementModel,
LinkModel,
LinkCount,
PropertyModel,
ElementIri,
ElementTypeIri,
LinkTypeIri,
PropertyTypeIri,
LocalizedString,
} from "../model";
import {
prependAdditionalBindings,
enrichElementsWithImages,
flattenClassTree,
getClassTree,
getClassInfo,
getPropertyInfo,
getLinkTypes,
getElementsInfo,
getElementTypes,
getLinksInfo,
getLinksTypeIds,
getFilteredData,
getLinksTypesOf,
getLinkStatistics,
triplesToElementBinding,
isDirectLink,
isDirectProperty,
} from "./responseHandler";
import {
ClassBinding,
ElementBinding,
LinkBinding,
PropertyBinding,
BlankBinding,
FilterBinding,
LinkCountBinding,
LinkTypeBinding,
ElementImageBinding,
ElementTypeBinding,
SparqlResponse,
Triple,
} from "./sparqlModels";
import {
SparqlDataProviderSettings,
OWLStatsSettings,
LinkConfiguration,
PropertyConfiguration,
} from "./sparqlDataProviderSettings";
import * as BlankNodes from "./blankNodes";
import { parseTurtleText } from "./turtle";
export enum SparqlQueryMethod {
GET = 1,
POST,
}
export type QueryFunction = (params: {
url: string;
body?: string;
headers: Record<string, string>;
method: string;
}) => Promise<Response>;
/**
* Runtime settings of SPARQL data provider
*/
export interface SparqlDataProviderOptions {
/**
* If it's true then blank nodes will be present on the paper
* By default blank nodes wont be shown
*/
acceptBlankNodes?: boolean;
/**
* sparql endpoint URL to use
*/
endpointUrl: string;
// there are two options for fetching images: specify imagePropertyUris
// to use as image properties or specify a function to fetch image URLs
/**
* properties to use as image URLs
*/
imagePropertyUris?: string[];
/**
* Allows to extract/fetch image URLs externally instead of using `imagePropertyUris` option.
*/
prepareImages?: (
elementInfo: Dictionary<ElementModel>
) => Promise<Dictionary<string>>;
/**
* Allows to extract/fetch labels separately from SPARQL query as an alternative or
* in addition to `label` output binding.
*/
prepareLabels?: (
resources: Set<string>
) => Promise<Map<string, LocalizedString[]>>;
/**
* wether to use GET (more compatible (Virtuozo), more error-prone due to large request URLs)
* or POST(less compatible, better on large data sets)
*/
queryMethod?: SparqlQueryMethod;
/*
* function to send sparql requests
*/
queryFunction?: QueryFunction;
}
export class SparqlDataProvider implements DataProvider {
readonly options: SparqlDataProviderOptions;
readonly settings: SparqlDataProviderSettings;
private linkByPredicate = new Map<string, LinkConfiguration[]>();
private linkById = new Map<LinkTypeIri, LinkConfiguration>();
private openWorldLinks: boolean;
private propertyByPredicate = new Map<string, PropertyConfiguration[]>();
private openWorldProperties: boolean;
constructor(
options: SparqlDataProviderOptions,
settings: SparqlDataProviderSettings = OWLStatsSettings
) {
const { queryFunction = queryInternal } = options;
this.options = { ...options, queryFunction };
this.settings = settings;
for (const link of settings.linkConfigurations) {
this.linkById.set(link.id as LinkTypeIri, link);
const predicate = isDirectLink(link) ? link.path : link.id;
getOrCreateArrayInMap(this.linkByPredicate, predicate).push(link);
}
this.openWorldLinks =
settings.linkConfigurations.length === 0 ||
Boolean(settings.openWorldLinks);
for (const property of settings.propertyConfigurations) {
const predicate = isDirectProperty(property)
? property.path
: property.id;
getOrCreateArrayInMap(this.propertyByPredicate, predicate).push(property);
}
this.openWorldProperties =
settings.propertyConfigurations.length === 0 ||
Boolean(settings.openWorldProperties);
}
async classTree(): Promise<ClassModel[]> {
const { defaultPrefix, schemaLabelProperty, classTreeQuery } =
this.settings;
if (!classTreeQuery) {
return [];
}
const query =
defaultPrefix +
resolveTemplate(classTreeQuery, {
schemaLabelProperty,
});
const result = await this.executeSparqlQuery<ClassBinding>(query);
const classTree = getClassTree(result);
if (this.options.prepareLabels) {
await attachLabels(
flattenClassTree(classTree),
this.options.prepareLabels
);
}
return classTree;
}
async propertyInfo(params: {
propertyIds: PropertyTypeIri[];
}): Promise<Dictionary<PropertyModel>> {
const { defaultPrefix, schemaLabelProperty, propertyInfoQuery } =
this.settings;
let properties: Dictionary<PropertyModel>;
if (propertyInfoQuery) {
const ids = params.propertyIds
.map(escapeIri)
.map((id) => ` ( ${id} )`)
.join(" ");
const query =
defaultPrefix +
resolveTemplate(propertyInfoQuery, {
ids,
schemaLabelProperty,
});
const result = await this.executeSparqlQuery<PropertyBinding>(query);
properties = getPropertyInfo(result);
} else {
properties = {};
for (const id of params.propertyIds) {
properties[id] = { id, label: { values: [] } };
}
}
if (this.options.prepareLabels) {
await attachLabels(objectValues(properties), this.options.prepareLabels);
}
return properties;
}
async classInfo(params: {
classIds: ElementTypeIri[];
}): Promise<ClassModel[]> {
const { defaultPrefix, schemaLabelProperty, classInfoQuery } =
this.settings;
let classes: ClassModel[];
if (classInfoQuery) {
const ids = params.classIds
.map(escapeIri)
.map((id) => ` ( ${id} )`)
.join(" ");
const query =
defaultPrefix +
resolveTemplate(classInfoQuery, {
ids,
schemaLabelProperty,
});
const result = await this.executeSparqlQuery<ClassBinding>(query);
classes = getClassInfo(result);
} else {
classes = params.classIds.map(
(id): ClassModel => ({ id, label: { values: [] }, children: [] })
);
}
if (this.options.prepareLabels) {
await attachLabels(classes, this.options.prepareLabels);
}
return classes;
}
async linkTypesInfo(params: {
linkTypeIds: LinkTypeIri[];
}): Promise<LinkType[]> {
const { defaultPrefix, schemaLabelProperty, linkTypesInfoQuery } =
this.settings;
let linkTypes: LinkType[];
if (linkTypesInfoQuery) {
const ids = params.linkTypeIds
.map(escapeIri)
.map((id) => ` ( ${id} )`)
.join(" ");
const query =
defaultPrefix +
resolveTemplate(linkTypesInfoQuery, {
ids,
schemaLabelProperty,
});
const result = await this.executeSparqlQuery<LinkTypeBinding>(query);
linkTypes = getLinkTypes(result);
} else {
linkTypes = params.linkTypeIds.map(
(id): LinkType => ({ id, label: { values: [] } })
);
}
if (this.options.prepareLabels) {
await attachLabels(linkTypes, this.options.prepareLabels);
}
return linkTypes;
}
async linkTypes(): Promise<LinkType[]> {
const {
defaultPrefix,
schemaLabelProperty,
linkTypesQuery,
linkTypesPattern,
} = this.settings;
if (!linkTypesQuery) {
return [];
}
const query =
defaultPrefix +
resolveTemplate(linkTypesQuery, {
linkTypesPattern,
schemaLabelProperty,
});
const result = await this.executeSparqlQuery<LinkTypeBinding>(query);
const linkTypes = getLinkTypes(result);
if (this.options.prepareLabels) {
await attachLabels(linkTypes, this.options.prepareLabels);
}
return linkTypes;
}
async elementInfo(params: {
elementIds: ElementIri[];
}): Promise<Dictionary<ElementModel>> {
const nonBlankResources = params.elementIds.filter(
(id) => !BlankNodes.isEncodedBlank(id)
);
const blankNodeResponse = this.options.acceptBlankNodes
? BlankNodes.elementInfo(params.elementIds)
: undefined;
let triples: Triple[];
if (nonBlankResources.length > 0) {
const ids = nonBlankResources
.map(escapeIri)
.map((id) => ` (${id})`)
.join(" ");
const { defaultPrefix, dataLabelProperty, elementInfoQuery } =
this.settings;
const query =
defaultPrefix +
resolveTemplate(elementInfoQuery, {
ids,
dataLabelProperty,
propertyConfigurations: this.formatPropertyInfo(),
});
triples = await this.executeSparqlConstruct(query);
} else {
triples = [];
}
const types = this.queryManyElementTypes(
this.settings.propertyConfigurations.length > 0 ? params.elementIds : []
);
const bindings = triplesToElementBinding(triples);
const bindingsWithBlanks = prependAdditionalBindings(
bindings,
blankNodeResponse
);
const elementModels = getElementsInfo(
bindingsWithBlanks,
await types,
this.propertyByPredicate,
this.openWorldProperties
);
if (this.options.prepareLabels) {
await attachLabels(
objectValues(elementModels),
this.options.prepareLabels
);
}
if (this.options.prepareImages) {
await prepareElementImages(this.options.prepareImages, elementModels);
} else if (
this.options.imagePropertyUris &&
this.options.imagePropertyUris.length
) {
await this.attachImages(elementModels, this.options.imagePropertyUris);
}
return elementModels;
}
private async attachImages(
elementsInfo: Dictionary<ElementModel>,
types: string[]
): Promise<void> {
const ids = Object.keys(elementsInfo)
.filter((id) => !BlankNodes.isEncodedBlank(id))
.map(escapeIri)
.map((id) => ` ( ${id} )`)
.join(" ");
const typesString = types
.map(escapeIri)
.map((id) => ` ( ${id} )`)
.join(" ");
const query =
this.settings.defaultPrefix +
`
SELECT ?inst ?linkType ?image
WHERE {{
VALUES (?inst) {${ids}}
VALUES (?linkType) {${typesString}}
${this.settings.imageQueryPattern}
}}
`;
try {
const bindings = await this.executeSparqlQuery<ElementImageBinding>(
query
);
enrichElementsWithImages(bindings, elementsInfo);
} catch (err) {
console.error(err);
}
}
async linksInfo(params: {
elementIds: ElementIri[];
linkTypeIds: LinkTypeIri[];
}): Promise<LinkModel[]> {
const nonBlankResources = params.elementIds.filter(
(id) => !BlankNodes.isEncodedBlank(id)
);
const blankNodeResponse = this.options.acceptBlankNodes
? BlankNodes.linksInfo(params.elementIds)
: undefined;
const linkConfigurations = this.formatLinkLinks();
let bindings: Promise<SparqlResponse<LinkBinding>>;
let types: Promise<Map<ElementIri, Set<ElementTypeIri>>>;
if (nonBlankResources.length > 0) {
const ids = nonBlankResources
.map(escapeIri)
.map((id) => ` ( ${id} )`)
.join(" ");
const linksInfoQuery =
this.settings.defaultPrefix +
resolveTemplate(this.settings.linksInfoQuery, {
ids,
linkConfigurations,
});
bindings = this.executeSparqlQuery<LinkBinding>(linksInfoQuery);
types = this.queryManyElementTypes(params.elementIds);
} else {
bindings = Promise.resolve({
head: { vars: [] },
results: { bindings: [] },
});
types = this.queryManyElementTypes([]);
}
const bindingsWithBlanks = prependAdditionalBindings(
await bindings,
blankNodeResponse
);
const linksInfo = getLinksInfo(
bindingsWithBlanks,
await types,
this.linkByPredicate,
this.openWorldLinks
);
return linksInfo;
}
async linkTypesOf(params: { elementId: ElementIri }): Promise<LinkCount[]> {
if (
this.options.acceptBlankNodes &&
BlankNodes.isEncodedBlank(params.elementId)
) {
return Promise.resolve(getLinksTypesOf(BlankNodes.linkTypesOf(params)));
}
const {
defaultPrefix,
linkTypesOfQuery,
linkTypesStatisticsQuery,
filterTypePattern,
} = this.settings;
const elementIri = escapeIri(params.elementId);
const forAll = this.formatLinkUnion(
params.elementId,
undefined,
undefined,
"?outObject",
"?inObject",
false
);
if (forAll.usePredicatePart) {
forAll.unionParts.push(`{ ${elementIri} ?link ?outObject }`);
forAll.unionParts.push(`{ ?inObject ?link ${elementIri} }`);
}
const query =
defaultPrefix +
resolveTemplate(linkTypesOfQuery, {
elementIri,
linkConfigurations: forAll.unionParts.join("\nUNION\n"),
});
const linkTypeBindings = await this.executeSparqlQuery<LinkTypeBinding>(
query
);
const linkTypeIds = getLinksTypeIds(
linkTypeBindings,
this.linkByPredicate,
this.openWorldLinks
);
const navigateElementFilterOut = this.options.acceptBlankNodes
? `FILTER (IsIri(?outObject) || IsBlank(?outObject))`
: `FILTER IsIri(?outObject)`;
const navigateElementFilterIn = this.options.acceptBlankNodes
? `FILTER (IsIri(?inObject) || IsBlank(?inObject))`
: `FILTER IsIri(?inObject)`;
const foundLinkStats: LinkCount[] = [];
await Promise.all(
linkTypeIds.map(async (linkId) => {
const linkConfig = this.linkById.get(linkId);
let linkConfigurationOut: string;
let linkConfigurationIn: string;
if (!linkConfig || isDirectLink(linkConfig)) {
const predicate = escapeIri(
linkConfig && isDirectLink(linkConfig) ? linkConfig.path : linkId
);
linkConfigurationOut = `${elementIri} ${predicate} ?outObject`;
linkConfigurationIn = `?inObject ${predicate} ${elementIri}`;
} else {
linkConfigurationOut = this.formatLinkPath(
linkConfig.path,
elementIri,
"?outObject"
);
linkConfigurationIn = this.formatLinkPath(
linkConfig.path,
"?inObject",
elementIri
);
}
if (linkConfig && linkConfig.domain?.length > 0) {
const commaSeparatedDomains = linkConfig.domain
.map(escapeIri)
.join(", ");
const restrictionOut = filterTypePattern.replace(
/[?$]inst\b/g,
elementIri
);
const restrictionIn = filterTypePattern.replace(
/[?$]inst\b/g,
"?inObject"
);
linkConfigurationOut += ` { ${restrictionOut} FILTER(?class IN (${commaSeparatedDomains})) }`;
linkConfigurationIn += ` { ${restrictionIn} FILTER(?class IN (${commaSeparatedDomains})) }`;
}
const statsQuery =
defaultPrefix +
resolveTemplate(linkTypesStatisticsQuery, {
linkId: escapeIri(linkId),
elementIri,
linkConfigurationOut,
linkConfigurationIn,
navigateElementFilterOut,
navigateElementFilterIn,
});
const bindings = await this.executeSparqlQuery<LinkCountBinding>(
statsQuery
);
const linkStats = getLinkStatistics(bindings);
if (linkStats) {
foundLinkStats.push(linkStats);
}
})
);
return foundLinkStats;
}
linkElements(params: LinkElementsParams): Promise<Dictionary<ElementModel>> {
// for sparql we have rich filtering features and we just reuse filter.
return this.filter({
refElementId: params.elementId,
refElementLinkId: params.linkId,
linkDirection: params.direction,
limit: params.limit,
offset: params.offset,
languageCode: "",
});
}
async filter(baseParams: FilterParams): Promise<Dictionary<ElementModel>> {
const params: FilterParams = { ...baseParams };
if (params.limit === undefined) {
params.limit = 100;
}
// query types to match link configuration domains
const types = this.querySingleElementTypes(
params.refElementId && this.settings.linkConfigurations.length > 0
? params.refElementId
: undefined
);
const blankFiltration = this.options.acceptBlankNodes
? BlankNodes.filter(params)
: undefined;
if (blankFiltration && blankFiltration.results.bindings.length > 0) {
return getFilteredData(
blankFiltration,
await types,
this.linkByPredicate,
this.openWorldLinks
);
}
const filterQuery = this.createFilterQuery(params);
const bindings = await this.executeSparqlQuery<
ElementBinding & FilterBinding
>(filterQuery);
let bindingsWithBlanks: SparqlResponse<ElementBinding & FilterBinding>;
if (this.options.acceptBlankNodes) {
bindingsWithBlanks = await BlankNodes.updateFilterResults(
bindings,
(blankQuery) => this.executeSparqlQuery<BlankBinding>(blankQuery),
this.settings
);
} else {
bindingsWithBlanks = bindings as SparqlResponse<
ElementBinding & FilterBinding
>;
}
const elementModels = getFilteredData(
bindingsWithBlanks,
await types,
this.linkByPredicate,
this.openWorldLinks
);
if (this.options.prepareLabels) {
await attachLabels(
objectValues(elementModels),
this.options.prepareLabels
);
}
return elementModels;
}
private createFilterQuery(params: FilterParams): string {
if (!params.refElementId && params.refElementLinkId) {
throw new Error(
"Cannot execute refElementLink filter without refElement"
);
}
let outerProjection = "?inst ?class ?label ?blankType";
let innerProjection = "?inst";
let refQueryPart = "";
let refQueryTypes = "";
if (params.refElementId) {
outerProjection += " ?link ?direction";
innerProjection += " ?link ?direction";
refQueryPart = this.createRefQueryPart({
elementId: params.refElementId,
linkId: params.refElementLinkId,
direction: params.linkDirection,
});
if (this.settings.linkConfigurations.length > 0) {
outerProjection += " ?classAll";
refQueryTypes = this.settings.filterTypePattern.replace(
/[?$]class\b/g,
"?classAll"
);
}
}
let elementTypePart = "";
if (params.elementTypeId) {
const elementTypeIri = escapeIri(params.elementTypeId);
elementTypePart = this.settings.filterTypePattern.replace(
/[?$]class\b/g,
elementTypeIri
);
elementTypePart += " .";
}
const { defaultPrefix, fullTextSearch, dataLabelProperty } = this.settings;
let textSearchPart = "";
if (params.text) {
innerProjection += " ?score";
if (this.settings.fullTextSearch.extractLabel) {
textSearchPart += sparqlExtractLabel("?inst", "?extractedLabel");
}
textSearchPart = resolveTemplate(fullTextSearch.queryPattern, {
text: params.text,
dataLabelProperty,
});
}
const blankNodes = this.options.acceptBlankNodes;
if (blankNodes) {
outerProjection += ` ${BlankNodes.BLANK_NODE_QUERY_PARAMETERS}`;
}
return `${defaultPrefix}
${fullTextSearch.prefix}
SELECT ${outerProjection}
WHERE {
{
SELECT DISTINCT ${innerProjection} WHERE {
${fullTextSearch.elementFirst ? "" : textSearchPart}
${elementTypePart}
${refQueryPart}
${fullTextSearch.elementFirst ? textSearchPart : ""}
${this.settings.filterAdditionalRestriction}
}
${textSearchPart ? "ORDER BY DESC(?score)" : ""}
LIMIT ${params.limit} OFFSET ${params.offset}
}
${refQueryTypes}
${resolveTemplate(this.settings.filterElementInfoPattern, {
dataLabelProperty,
})}
${blankNodes ? BlankNodes.BLANK_NODE_QUERY : ""}
} ${textSearchPart ? "ORDER BY DESC(?score)" : ""}
`;
}
executeSparqlQuery<Binding>(query: string) {
const method = this.options.queryMethod
? this.options.queryMethod
: SparqlQueryMethod.GET;
return executeSparqlQuery<Binding>(
this.options.endpointUrl,
query,
method,
this.options.queryFunction
);
}
executeSparqlConstruct(query: string): Promise<Triple[]> {
const method = this.options.queryMethod
? this.options.queryMethod
: SparqlQueryMethod.GET;
return executeSparqlConstruct(
this.options.endpointUrl,
query,
method,
this.options.queryFunction
);
}
protected createRefQueryPart(params: {
elementId: ElementIri;
linkId?: LinkTypeIri;
direction?: "in" | "out";
}) {
const { elementId, linkId, direction } = params;
const { unionParts, usePredicatePart } = this.formatLinkUnion(
elementId,
linkId,
direction,
"?inst",
"?inst",
true
);
if (usePredicatePart) {
const refElementIRI = escapeIri(params.elementId);
let refLinkType: string | undefined;
if (linkId) {
const link = this.linkById.get(linkId);
refLinkType =
link && isDirectLink(link) ? escapeIri(link.path) : escapeIri(linkId);
}
const linkPattern = refLinkType || "?link";
const bindType = refLinkType ? `BIND(${refLinkType} as ?link)` : "";
// FILTER ISIRI is used to prevent blank nodes appearing in results
const blankFilter = this.options.acceptBlankNodes
? "FILTER(isIri(?inst) || isBlank(?inst))"
: "FILTER(isIri(?inst))";
if (!direction || direction === "out") {
unionParts.push(
`{ ${refElementIRI} ${linkPattern} ?inst BIND("out" as ?direction) ${bindType} ${blankFilter} }`
);
}
if (!direction || direction === "in") {
unionParts.push(
`{ ?inst ${linkPattern} ${refElementIRI} BIND("in" as ?direction) ${bindType} ${blankFilter} }`
);
}
}
let resultPattern =
unionParts.length === 0 ? "FILTER(false)" : unionParts.join(`\nUNION\n`);
const useAllLinksPattern =
!linkId && this.settings.filterRefElementLinkPattern.length > 0;
if (useAllLinksPattern) {
resultPattern += `\n${this.settings.filterRefElementLinkPattern}`;
}
return resultPattern;
}
private formatLinkUnion(
refElementIri: ElementIri,
linkIri: LinkTypeIri | undefined,
direction: "in" | "out" | undefined,
outElementVar: string,
inElementVar: string,
bindDirection: boolean
) {
const { linkConfigurations } = this.settings;
const fixedIri = escapeIri(refElementIri);
const unionParts: string[] = [];
let hasDirectLink = false;
for (const link of linkConfigurations) {
if (linkIri && link.id !== linkIri) {
continue;
}
if (isDirectLink(link)) {
hasDirectLink = true;
} else {
const linkType = escapeIri(link.id);
if (!direction || direction === "out") {
const path = this.formatLinkPath(link.path, fixedIri, outElementVar);
const boundedDirection = bindDirection
? `BIND("out" as ?direction) `
: "";
unionParts.push(
`{ ${path} BIND(${linkType} as ?link) ${boundedDirection}}`
);
}
if (!direction || direction === "in") {
const path = this.formatLinkPath(link.path, inElementVar, fixedIri);
const boundedDirection = bindDirection
? `BIND("in" as ?direction) `
: "";
unionParts.push(
`{ ${path} BIND(${linkType} as ?link) ${boundedDirection}}`
);
}
}
}
const usePredicatePart = this.openWorldLinks || hasDirectLink;
return { unionParts, usePredicatePart };
}
formatLinkLinks(): string {
const unionParts: string[] = [];
let hasDirectLink = false;
for (const link of this.settings.linkConfigurations) {
if (isDirectLink(link)) {
hasDirectLink = true;
} else {
const linkType = escapeIri(link.id);
unionParts.push(
`{ ${this.formatLinkPath(
link.path,
"?source",
"?target"
)} BIND(${linkType} as ?type) }`
);
}
}
const usePredicatePart = this.openWorldLinks || hasDirectLink;
if (usePredicatePart) {
unionParts.push(`{ ?source ?type ?target }`);
}
return unionParts.join("\nUNION\n");
}
formatLinkPath(path: string, source: string, target: string): string {
return path
.replace(/[?$]source\b/g, source)
.replace(/[?$]target\b/g, target);
}
formatPropertyInfo(): string {
const unionParts: string[] = [];
let hasDirectProperty = false;
for (const property of this.settings.propertyConfigurations) {
if (isDirectProperty(property)) {
hasDirectProperty = true;
} else {
const propType = escapeIri(property.id);
const formatted = this.formatPropertyPath(
property.path,
"?inst",
"?propValue"
);
unionParts.push(`{ ${formatted} BIND(${propType} as ?propType) }`);
}
}
const usePredicatePart = this.openWorldProperties || hasDirectProperty;
if (usePredicatePart) {
unionParts.push(`{ ?inst ?propType ?propValue }`);
}
return unionParts.join("\nUNION\n");
}
formatPropertyPath(path: string, subject: string, value: string): string {
return path.replace(/[?$]inst\b/g, subject).replace(/[?$]value\b/g, value);
}
private async querySingleElementTypes(
element: ElementIri | undefined
): Promise<Set<ElementTypeIri> | undefined> {
const types = await this.queryManyElementTypes(element ? [element] : []);
return types.get(element);
}
private async queryManyElementTypes(
elements: readonly ElementIri[]
): Promise<Map<ElementIri, Set<ElementTypeIri>>> {
if (elements.length === 0) {
return new Map();
}
const { filterTypePattern } = this.settings;
const ids = elements
.filter((iri) => !BlankNodes.isEncodedBlank(iri))
.map((iri) => `(${escapeIri(iri)})`)
.join(" ");
const queryTemplate =
this.settings.defaultPrefix +
`SELECT ?inst ?class { VALUES(?inst) { \${ids} } \${filterTypePattern} }`;
const query = resolveTemplate(queryTemplate, { ids, filterTypePattern });
let response = await this.executeSparqlQuery<ElementTypeBinding>(query);
if (
this.options.acceptBlankNodes &&
elements.find(BlankNodes.isEncodedBlank)
) {
const blankResponse = BlankNodes.getElementTypes(elements);
response = prependAdditionalBindings(response, blankResponse);
}
return getElementTypes(response);
}
}
interface LabeledItem {
id: string;
label: { values: LocalizedString[] };
}
async function attachLabels(
items: readonly LabeledItem[],
fetchLabels: SparqlDataProviderOptions["prepareLabels"]
): Promise<void> {
const resources = new Set<string>();
for (const item of items) {
if (BlankNodes.isEncodedBlank(item.id)) {
continue;
}
resources.add(item.id);
}
const labels = await fetchLabels(resources);
for (const item of items) {
if (labels.has(item.id)) {
item.label = { values: labels.get(item.id) };
}
}
}
function prepareElementImages(
fetchImages: SparqlDataProviderOptions["prepareImages"],
elementsInfo: Dictionary<ElementModel>
): Promise<void> {
return fetchImages(elementsInfo).then((images) => {
for (const iri in images) {
if (
Object.prototype.hasOwnProperty.call(images, iri) &&
elementsInfo[iri]
) {
elementsInfo[iri].image = images[iri];
}
}
});
}
function resolveTemplate(template: string, values: Dictionary<string>) {
let result = template;
for (const replaceKey in values) {
if (!Object.prototype.hasOwnProperty.call(values, replaceKey)) {
continue;
}
const replaceValue = values[replaceKey];
if (replaceValue) {
result = result.replace(
new RegExp("\\${" + replaceKey + "}", "g"),
replaceValue
);
}
}
return result;
}
export function executeSparqlQuery<Binding>(
endpoint: string,
query: string,
method: SparqlQueryMethod,
queryFunction: QueryFunction
): Promise<SparqlResponse<Binding>> {
let internalQuery: Promise<Response>;
if (method === SparqlQueryMethod.GET) {
internalQuery = queryFunction({
url: appendQueryParams(endpoint, { query }),
headers: {
Accept: "application/sparql-results+json",
},
method: "GET",
});
} else {
internalQuery = queryFunction({
url: endpoint,
body: query,
headers: {
Accept: "application/sparql-results+json",
"Content-Type": "application/sparql-query; charset=UTF-8",
},
method: "POST",
});
}
return internalQuery.then((response): Promise<SparqlResponse<Binding>> => {
if (response.ok) {
return response.json();
} else {
const error = new Error(response.statusText);
(error as any).response = response;
throw error;
}
});
}
export function executeSparqlConstruct(
endpoint: string,
query: string,
method: SparqlQueryMethod,
queryFunction: QueryFunction
): Promise<Triple[]> {
let internalQuery: Promise<Response>;
if (method === SparqlQueryMethod.GET) {
internalQuery = queryFunction({
url: appendQueryParams(endpoint, { query }),
headers: {
Accept: "text/turtle",
},
method: "GET",
});
} else {
internalQuery = queryFunction({
url: endpoint,
body: query,
headers: {
Accept: "text/turtle",
"Content-Type": "application/sparql-query; charset=UTF-8",
},
method: "POST",
});
}
return internalQuery
.then((response) => {
if (response.ok) {
return response.text();
} else {
const error = new Error(response.statusText);
(error as any).response = response;
throw error;
}
})
.then(parseTurtleText);
}
function appendQueryParams(
endpoint: string,
queryParams: Record<string, string> = {}
) {
const initialSeparator = endpoint.indexOf("?") < 0 ? "?" : "&";
const additionalParams =
initialSeparator +
Object.keys(queryParams)
.map((key) => `${key}=${encodeURIComponent(queryParams[key])}`)
.join("&");
return endpoint + additionalParams;
}
function queryInternal(params: {
url: string;
body?: string;
headers: any;
method: string;
}) {
return fetch(params.url, {
method: params.method,
body: params.body,
credentials: "same-origin",
mode: "cors",
cache: "default",
headers: params.headers,
});
}
function sparqlExtractLabel(subject: string, label: string): string {
return `
BIND ( str( ${subject} ) as ?uriStr)
BIND ( strafter(?uriStr, "#") as ?label3)
BIND ( strafter(strafter(?uriStr, "//"), "/") as ?label6)
BIND ( strafter(?label6, "/") as ?label5)
BIND ( strafter(?label5, "/") as ?label4)
BIND (if (?label3 != "", ?label3,
if (?label4 != "", ?label4,
if (?label5 != "", ?label5, ?label6))) as ${label})
`;
}
function escapeIri(iri: string) {
if (typeof iri !== "string") {
throw new Error(`Cannot escape IRI of type "${typeof iri}"`);
}
return `<${iri}>`;
}