@netwerk-digitaal-erfgoed/network-of-terms-query 6.2.7 → 6.2.9
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/CHANGELOG.md +3 -0
- package/{build → dist}/catalog.d.ts.map +1 -1
- package/{build → dist}/catalog.js +6 -7
- package/dist/config.d.ts.map +1 -0
- package/{build → dist}/config.js +0 -1
- package/{build → dist}/distributions.d.ts.map +1 -1
- package/{build → dist}/distributions.js +0 -1
- package/{build → dist}/helpers/logger-pino.d.ts.map +1 -1
- package/{build → dist}/helpers/logger-pino.js +0 -1
- package/{build → dist}/helpers/logger.js +0 -1
- package/{build → dist}/index.d.ts.map +1 -1
- package/{build → dist}/index.js +0 -1
- package/{build → dist}/instrumentation.js +0 -1
- package/{build → dist}/literal.d.ts +1 -1
- package/{build → dist}/literal.d.ts.map +1 -1
- package/{build → dist}/literal.js +7 -7
- package/{build → dist}/lookup/lookup.d.ts.map +1 -1
- package/{build → dist}/lookup/lookup.js +6 -4
- package/dist/query.d.ts.map +1 -0
- package/{build → dist}/query.js +3 -4
- package/{build → dist}/search/query-mode.js +2 -3
- package/{build → dist}/terms.d.ts +1 -1
- package/{build → dist}/terms.js +2 -3
- package/{build/server-test.d.ts → dist/test-utils.d.ts} +1 -1
- package/dist/test-utils.d.ts.map +1 -0
- package/{build/server-test.js → dist/test-utils.js} +8 -3
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/eslint.config.mjs +22 -0
- package/package.json +33 -36
- package/src/catalog.ts +157 -0
- package/src/config.ts +24 -0
- package/src/distributions.ts +94 -0
- package/src/helpers/logger-pino.ts +45 -0
- package/src/helpers/logger.ts +52 -0
- package/src/index.ts +12 -0
- package/src/instrumentation.ts +51 -0
- package/src/literal.ts +42 -0
- package/src/lookup/lookup.ts +147 -0
- package/src/query.ts +247 -0
- package/src/search/query-mode.ts +54 -0
- package/src/terms.ts +141 -0
- package/src/test-utils.ts +207 -0
- package/test/fixtures/terms.ttl +46 -0
- package/test/query.test.ts +67 -0
- package/test/search/query-mode.test.ts +71 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +20 -0
- package/tsconfig.test.json +27 -0
- package/vite.config.ts +26 -0
- package/build/catalog.js.map +0 -1
- package/build/config.d.ts.map +0 -1
- package/build/config.js.map +0 -1
- package/build/distributions.js.map +0 -1
- package/build/helpers/logger-pino.js.map +0 -1
- package/build/helpers/logger.js.map +0 -1
- package/build/index.js.map +0 -1
- package/build/instrumentation.js.map +0 -1
- package/build/literal.js.map +0 -1
- package/build/lookup/lookup.js.map +0 -1
- package/build/query.d.ts.map +0 -1
- package/build/query.js.map +0 -1
- package/build/search/query-mode.js.map +0 -1
- package/build/server-test.d.ts.map +0 -1
- package/build/server-test.js.map +0 -1
- package/build/terms.js.map +0 -1
- /package/{build → dist}/catalog.d.ts +0 -0
- /package/{build → dist}/config.d.ts +0 -0
- /package/{build → dist}/distributions.d.ts +0 -0
- /package/{build → dist}/helpers/logger-pino.d.ts +0 -0
- /package/{build → dist}/helpers/logger.d.ts +0 -0
- /package/{build → dist}/helpers/logger.d.ts.map +0 -0
- /package/{build → dist}/index.d.ts +0 -0
- /package/{build → dist}/instrumentation.d.ts +0 -0
- /package/{build → dist}/instrumentation.d.ts.map +0 -0
- /package/{build → dist}/lookup/lookup.d.ts +0 -0
- /package/{build → dist}/query.d.ts +0 -0
- /package/{build → dist}/search/query-mode.d.ts +0 -0
- /package/{build → dist}/search/query-mode.d.ts.map +0 -0
- /package/{build → dist}/terms.d.ts.map +0 -0
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { URL } from 'url';
|
|
2
|
+
|
|
3
|
+
export class Catalog {
|
|
4
|
+
private readonly prefixToDataset: Map<string, IRI>;
|
|
5
|
+
|
|
6
|
+
constructor(readonly datasets: ReadonlyArray<Dataset>) {
|
|
7
|
+
this.prefixToDataset = this.indexPrefixesByStringLength();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get dataset by IRI, accepting distribution IRIs too for BC.
|
|
12
|
+
*/
|
|
13
|
+
public getDatasetByIri(iri: IRI): Dataset | undefined {
|
|
14
|
+
return (
|
|
15
|
+
this.datasets.find(
|
|
16
|
+
(dataset) => dataset.iri.toString() === iri.toString(),
|
|
17
|
+
) ?? this.getDatasetByDistributionIri(iri)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public getDatasetsSortedByName(languageCode: string): Dataset[] {
|
|
22
|
+
return [...this.datasets].sort((a, b) =>
|
|
23
|
+
a.name[languageCode].localeCompare(b.name[languageCode]),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public getDatasetByDistributionIri(iri: IRI): Dataset | undefined {
|
|
28
|
+
return this.datasets.find(
|
|
29
|
+
(dataset) => dataset.getDistributionByIri(iri) !== undefined,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public getDatasetByTermIri(iri: IRI): Dataset | undefined {
|
|
34
|
+
for (const [prefix, datasetIri] of this.prefixToDataset) {
|
|
35
|
+
if (iri.toString().startsWith(prefix)) {
|
|
36
|
+
return this.getDatasetByIri(datasetIri);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public getDistributionsProvidingFeature(
|
|
44
|
+
featureType: FeatureType,
|
|
45
|
+
): Distribution[] {
|
|
46
|
+
return this.datasets.reduce<Distribution[]>((acc, dataset) => {
|
|
47
|
+
return [
|
|
48
|
+
...acc,
|
|
49
|
+
...dataset.distributions.filter((distribution) =>
|
|
50
|
+
distribution.hasFeature(featureType),
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
}, []);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all languages provided by datasets in this catalog.
|
|
58
|
+
*/
|
|
59
|
+
public getLanguages(): string[] {
|
|
60
|
+
return [
|
|
61
|
+
...new Set(
|
|
62
|
+
this.datasets.reduce<string[]>((acc, dataset) => {
|
|
63
|
+
return [...acc, ...dataset.inLanguage];
|
|
64
|
+
}, []),
|
|
65
|
+
),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Index the prefixes of all datasets by their string length in descending order for matching
|
|
71
|
+
* term IRIs against during lookup. When looking up terms, we want to match the longest possible prefix
|
|
72
|
+
* in case prefixes overlap.
|
|
73
|
+
*/
|
|
74
|
+
private indexPrefixesByStringLength() {
|
|
75
|
+
return new Map(
|
|
76
|
+
[
|
|
77
|
+
...this.datasets
|
|
78
|
+
.reduce((acc, dataset) => {
|
|
79
|
+
dataset.termsPrefixes.forEach((prefix) => {
|
|
80
|
+
acc.set(prefix.toString(), dataset.iri);
|
|
81
|
+
});
|
|
82
|
+
return acc;
|
|
83
|
+
}, new Map<string, IRI>())
|
|
84
|
+
.entries(),
|
|
85
|
+
].sort(([a], [b]) => b.localeCompare(a)),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type StringDictionary = Record<string, string>;
|
|
91
|
+
|
|
92
|
+
export class Dataset {
|
|
93
|
+
constructor(
|
|
94
|
+
readonly iri: IRI,
|
|
95
|
+
readonly name: StringDictionary,
|
|
96
|
+
readonly description: StringDictionary,
|
|
97
|
+
readonly genres: IRI[],
|
|
98
|
+
readonly termsPrefixes: IRI[],
|
|
99
|
+
readonly mainEntityOfPage: string,
|
|
100
|
+
readonly inLanguage: string[],
|
|
101
|
+
readonly creators: [Organization],
|
|
102
|
+
readonly distributions: [Distribution],
|
|
103
|
+
readonly alternateName: StringDictionary = {},
|
|
104
|
+
) {}
|
|
105
|
+
|
|
106
|
+
public getSparqlDistribution(): Distribution | undefined {
|
|
107
|
+
return this.distributions.find(
|
|
108
|
+
(distribution) => distribution instanceof SparqlDistribution,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public getDistributionByIri(iri: IRI): Distribution | undefined {
|
|
113
|
+
return this.distributions.find(
|
|
114
|
+
(distribution) => distribution.iri.toString() === iri.toString(),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class Organization {
|
|
120
|
+
constructor(
|
|
121
|
+
readonly iri: IRI,
|
|
122
|
+
readonly name: StringDictionary,
|
|
123
|
+
readonly alternateName: StringDictionary,
|
|
124
|
+
) {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class SparqlDistribution {
|
|
128
|
+
constructor(
|
|
129
|
+
readonly iri: IRI,
|
|
130
|
+
readonly endpoint: IRI,
|
|
131
|
+
readonly searchQuery: string,
|
|
132
|
+
readonly lookupQuery: string,
|
|
133
|
+
readonly features: Feature[] = [],
|
|
134
|
+
) {}
|
|
135
|
+
|
|
136
|
+
public hasFeature(feature: FeatureType) {
|
|
137
|
+
return this.features.some((value: Feature) => value.type === feature);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class Feature {
|
|
142
|
+
constructor(
|
|
143
|
+
readonly type: FeatureType,
|
|
144
|
+
readonly url: URL,
|
|
145
|
+
) {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export enum FeatureType {
|
|
149
|
+
RECONCILIATION = 'https://reconciliation-api.github.io/specs/latest/',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A union type to be extended in the future with other distribution types.
|
|
154
|
+
*/
|
|
155
|
+
export type Distribution = SparqlDistribution;
|
|
156
|
+
|
|
157
|
+
export type IRI = string;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { envSchema, JSONSchemaType } from 'env-schema';
|
|
2
|
+
|
|
3
|
+
const schema = {
|
|
4
|
+
type: 'object',
|
|
5
|
+
properties: {
|
|
6
|
+
MAX_QUERY_TIMEOUT: {
|
|
7
|
+
type: 'number',
|
|
8
|
+
default: 60000,
|
|
9
|
+
},
|
|
10
|
+
DEFAULT_QUERY_TIMEOUT: {
|
|
11
|
+
type: 'number',
|
|
12
|
+
default: 5000,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface Env {
|
|
18
|
+
MAX_QUERY_TIMEOUT: number;
|
|
19
|
+
DEFAULT_QUERY_TIMEOUT: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const config: JSONSchemaType<Env> = envSchema({
|
|
23
|
+
schema,
|
|
24
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { QueryEngine } from '@comunica/query-sparql';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
import Pino from 'pino';
|
|
4
|
+
import { QueryTermsService, TermsResponse } from './query.js';
|
|
5
|
+
import { QueryMode } from './search/query-mode.js';
|
|
6
|
+
import { Catalog, IRI } from './catalog.js';
|
|
7
|
+
import { comunica } from './index.js';
|
|
8
|
+
import { clientQueriesCounter } from './instrumentation.js';
|
|
9
|
+
|
|
10
|
+
interface BaseQueryOptions {
|
|
11
|
+
query: string;
|
|
12
|
+
queryMode: QueryMode;
|
|
13
|
+
limit: number;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QueryOptions extends BaseQueryOptions {
|
|
18
|
+
source: IRI;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface QueryAllOptions extends BaseQueryOptions {
|
|
22
|
+
sources: IRI[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const schemaBase = Joi.object({
|
|
26
|
+
query: Joi.string().required(),
|
|
27
|
+
queryMode: Joi.string().required(),
|
|
28
|
+
limit: Joi.number().integer(),
|
|
29
|
+
timeoutMs: Joi.number().integer(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const schemaQuery = schemaBase.append({
|
|
33
|
+
source: Joi.string().required(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const schemaQueryAll = schemaBase.append({
|
|
37
|
+
sources: Joi.array().items(Joi.string().required()).min(1).required(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export class DistributionsService {
|
|
41
|
+
private logger: Pino.Logger;
|
|
42
|
+
private catalog: Catalog;
|
|
43
|
+
private comunica: QueryEngine;
|
|
44
|
+
|
|
45
|
+
constructor(options: {
|
|
46
|
+
logger: Pino.Logger;
|
|
47
|
+
catalog: Catalog;
|
|
48
|
+
comunica?: QueryEngine;
|
|
49
|
+
}) {
|
|
50
|
+
this.logger = options.logger;
|
|
51
|
+
this.catalog = options.catalog;
|
|
52
|
+
this.comunica = options.comunica || comunica();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async query(options: QueryOptions): Promise<TermsResponse> {
|
|
56
|
+
const args = Joi.attempt(options, schemaQuery);
|
|
57
|
+
this.logger.info(`Preparing to query source "${args.source}"...`);
|
|
58
|
+
const dataset = this.catalog.getDatasetByIri(args.source);
|
|
59
|
+
if (dataset === undefined) {
|
|
60
|
+
throw Error(`Source with URI "${args.source}" not found`);
|
|
61
|
+
}
|
|
62
|
+
const distribution = dataset.getSparqlDistribution()!;
|
|
63
|
+
const queryService = new QueryTermsService({
|
|
64
|
+
logger: this.logger,
|
|
65
|
+
comunica: this.comunica,
|
|
66
|
+
});
|
|
67
|
+
return queryService.search(
|
|
68
|
+
args.query,
|
|
69
|
+
args.queryMode,
|
|
70
|
+
dataset,
|
|
71
|
+
distribution,
|
|
72
|
+
args.limit,
|
|
73
|
+
args.timeoutMs,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async queryAll(options: QueryAllOptions): Promise<TermsResponse[]> {
|
|
78
|
+
const args = Joi.attempt(options, schemaQueryAll);
|
|
79
|
+
clientQueriesCounter.add(1, {
|
|
80
|
+
numberOfSources: args.sources.length,
|
|
81
|
+
type: 'search',
|
|
82
|
+
});
|
|
83
|
+
const requests = args.sources.map((source: IRI) =>
|
|
84
|
+
this.query({
|
|
85
|
+
source,
|
|
86
|
+
query: args.query,
|
|
87
|
+
queryMode: args.queryMode,
|
|
88
|
+
limit: args.limit,
|
|
89
|
+
timeoutMs: args.timeoutMs,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
return Promise.all(requests);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Logger } from '@comunica/types';
|
|
2
|
+
import Pino from 'pino';
|
|
3
|
+
|
|
4
|
+
export interface ConstructorOptions {
|
|
5
|
+
logger: Pino.Logger;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class LoggerPino extends Logger {
|
|
9
|
+
protected logger: Pino.Logger;
|
|
10
|
+
|
|
11
|
+
constructor(options: ConstructorOptions) {
|
|
12
|
+
super();
|
|
13
|
+
this.logger = options.logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
debug(message: string, data?: any): void {
|
|
18
|
+
this.logger.debug(message, data);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
error(message: string, data?: any): void {
|
|
23
|
+
this.logger.error(message, data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
fatal(message: string, data?: any): void {
|
|
28
|
+
this.logger.fatal(message, data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
info(message: string, data?: any): void {
|
|
33
|
+
this.logger.info(message, data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
trace(message: string, data?: any): void {
|
|
38
|
+
this.logger.trace(message, data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
warn(message: string, data?: any): void {
|
|
43
|
+
this.logger.warn(message, data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Joi from 'joi';
|
|
2
|
+
import Pino from 'pino';
|
|
3
|
+
|
|
4
|
+
export interface GetLoggerOptions {
|
|
5
|
+
name: string;
|
|
6
|
+
level: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const schemaGetLogger = Joi.object({
|
|
10
|
+
name: Joi.string().required(),
|
|
11
|
+
level: Joi.string().required(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const baseOptions: Pino.LoggerOptions = {
|
|
15
|
+
base: {
|
|
16
|
+
name: undefined, // Don't log PID and hostname
|
|
17
|
+
},
|
|
18
|
+
level: 'warn',
|
|
19
|
+
messageKey: 'message',
|
|
20
|
+
formatters: {
|
|
21
|
+
level(label) {
|
|
22
|
+
return {
|
|
23
|
+
level: label,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function getCliLogger(options: GetLoggerOptions): Pino.Logger {
|
|
30
|
+
const args = Joi.attempt(options, schemaGetLogger);
|
|
31
|
+
const loggerOptions = Object.assign(baseOptions, {
|
|
32
|
+
base: {
|
|
33
|
+
name: args.name,
|
|
34
|
+
},
|
|
35
|
+
level: args.level,
|
|
36
|
+
prettyPrint: {
|
|
37
|
+
colorize: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const destinationStdErr = Pino.destination(2);
|
|
41
|
+
return Pino.pino(loggerOptions, destinationStdErr);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getHttpLogger(options: GetLoggerOptions): Pino.LoggerOptions {
|
|
45
|
+
const args = Joi.attempt(options, schemaGetLogger);
|
|
46
|
+
return Object.assign(baseOptions, {
|
|
47
|
+
base: {
|
|
48
|
+
name: args.name,
|
|
49
|
+
},
|
|
50
|
+
level: args.level,
|
|
51
|
+
});
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './query.js';
|
|
2
|
+
export * from './catalog.js';
|
|
3
|
+
export * from './literal.js';
|
|
4
|
+
export * from './lookup/lookup.js';
|
|
5
|
+
export * from './terms.js';
|
|
6
|
+
export * from './search/query-mode.js';
|
|
7
|
+
export * from './distributions.js';
|
|
8
|
+
export * from './helpers/logger.js';
|
|
9
|
+
|
|
10
|
+
import { QueryEngine } from '@comunica/query-sparql';
|
|
11
|
+
|
|
12
|
+
export const comunica = () => new QueryEngine();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MeterProvider,
|
|
3
|
+
PeriodicExportingMetricReader,
|
|
4
|
+
} from '@opentelemetry/sdk-metrics';
|
|
5
|
+
import {
|
|
6
|
+
defaultResource,
|
|
7
|
+
resourceFromAttributes,
|
|
8
|
+
} from '@opentelemetry/resources';
|
|
9
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
|
|
10
|
+
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
|
11
|
+
import { metrics, ValueType } from '@opentelemetry/api';
|
|
12
|
+
|
|
13
|
+
const sourceQueriesHistogramName = 'queries.source';
|
|
14
|
+
|
|
15
|
+
const meterProvider = new MeterProvider({
|
|
16
|
+
resource: defaultResource().merge(
|
|
17
|
+
resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'network-of-terms' }),
|
|
18
|
+
),
|
|
19
|
+
readers:
|
|
20
|
+
'test' === process.env.NODE_ENV
|
|
21
|
+
? []
|
|
22
|
+
: [
|
|
23
|
+
new PeriodicExportingMetricReader({
|
|
24
|
+
exporter: new OTLPMetricExporter(),
|
|
25
|
+
exportIntervalMillis:
|
|
26
|
+
(process.env.OTEL_METRIC_EXPORT_INTERVAL as unknown as number) ??
|
|
27
|
+
60000,
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
33
|
+
|
|
34
|
+
const meter = metrics.getMeter('default');
|
|
35
|
+
|
|
36
|
+
export const clientQueriesCounter = meter.createCounter(
|
|
37
|
+
'queries.client.counter',
|
|
38
|
+
{
|
|
39
|
+
description: 'Number of user queries',
|
|
40
|
+
valueType: ValueType.INT,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const sourceQueriesHistogram = meter.createHistogram(
|
|
45
|
+
sourceQueriesHistogramName,
|
|
46
|
+
{
|
|
47
|
+
description: 'Queries to terminology sources and their response times',
|
|
48
|
+
valueType: ValueType.INT,
|
|
49
|
+
unit: 'ms',
|
|
50
|
+
},
|
|
51
|
+
);
|
package/src/literal.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as RDF from '@rdfjs/types';
|
|
2
|
+
import { DataFactory } from 'rdf-data-factory';
|
|
3
|
+
|
|
4
|
+
const dataFactory = new DataFactory();
|
|
5
|
+
|
|
6
|
+
export function filterLiteralsByLanguage(
|
|
7
|
+
literals: RDF.Literal[],
|
|
8
|
+
languages: string[],
|
|
9
|
+
) {
|
|
10
|
+
const preferredLanguageLiterals = literals.filter((literal) =>
|
|
11
|
+
languages.includes(literal.language),
|
|
12
|
+
);
|
|
13
|
+
if (preferredLanguageLiterals.length > 0) {
|
|
14
|
+
return preferredLanguageLiterals;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// If literal has no language tag, we assume it is in the Network of Terms’ default language, Dutch.
|
|
18
|
+
return literals
|
|
19
|
+
.filter((literal) => literal.language === '')
|
|
20
|
+
.map((literal) => dataFactory.literal(literal.value, 'nl'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return value from {@link Literal} in the given languages.
|
|
25
|
+
*
|
|
26
|
+
* @param literals
|
|
27
|
+
* @param languages
|
|
28
|
+
*/
|
|
29
|
+
export function literalValues(
|
|
30
|
+
literals: RDF.Literal[],
|
|
31
|
+
languages: string[] = ['nl'],
|
|
32
|
+
) {
|
|
33
|
+
const languageLiterals = filterLiteralsByLanguage(literals, languages);
|
|
34
|
+
if (languageLiterals.length > 0) {
|
|
35
|
+
return languageLiterals.map((literal) => literal.value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fall back to English for sources that provide no Dutch labels.
|
|
39
|
+
return filterLiteralsByLanguage(literals, ['en']).map(
|
|
40
|
+
(literal) => literal.value,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Catalog, Dataset, Distribution, IRI } from '../catalog.js';
|
|
2
|
+
import {
|
|
3
|
+
Error,
|
|
4
|
+
QueryTermsService,
|
|
5
|
+
ServerError,
|
|
6
|
+
Terms,
|
|
7
|
+
TermsResponse,
|
|
8
|
+
TermsResult,
|
|
9
|
+
TimeoutError,
|
|
10
|
+
} from '../query.js';
|
|
11
|
+
import { Term } from '../terms.js';
|
|
12
|
+
import { clientQueriesCounter } from '../instrumentation.js';
|
|
13
|
+
|
|
14
|
+
export type LookupQueryResult = {
|
|
15
|
+
uri: string;
|
|
16
|
+
distribution: SourceResult;
|
|
17
|
+
result: LookupResult;
|
|
18
|
+
|
|
19
|
+
responseTimeMs: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SourceResult = Distribution | SourceNotFoundError;
|
|
23
|
+
|
|
24
|
+
export type LookupResult = Term | NotFoundError | TimeoutError | ServerError;
|
|
25
|
+
|
|
26
|
+
export class SourceNotFoundError {
|
|
27
|
+
readonly message: string;
|
|
28
|
+
|
|
29
|
+
constructor(readonly iri: string) {
|
|
30
|
+
this.message = `No source found that can provide term with URI ${iri}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class NotFoundError {
|
|
35
|
+
readonly message: string;
|
|
36
|
+
|
|
37
|
+
constructor(readonly iri: string) {
|
|
38
|
+
this.message = `No term found with URI ${iri}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class LookupService {
|
|
43
|
+
constructor(
|
|
44
|
+
private catalog: Catalog,
|
|
45
|
+
private queryService: QueryTermsService,
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
public async lookup(
|
|
49
|
+
iris: string[],
|
|
50
|
+
timeoutMs: number,
|
|
51
|
+
): Promise<LookupQueryResult[]> {
|
|
52
|
+
const irisToDataset = iris.reduce((acc, iri) => {
|
|
53
|
+
const dataset = this.catalog.getDatasetByTermIri(iri);
|
|
54
|
+
if (dataset) {
|
|
55
|
+
acc.set(iri.toString(), dataset);
|
|
56
|
+
}
|
|
57
|
+
return acc;
|
|
58
|
+
}, new Map<string, Dataset>());
|
|
59
|
+
|
|
60
|
+
const datasetToIris = [...irisToDataset].reduce(
|
|
61
|
+
(datasetMap, [iri, dataset]) => {
|
|
62
|
+
datasetMap.set(dataset, [...(datasetMap.get(dataset) ?? []), iri]);
|
|
63
|
+
return datasetMap;
|
|
64
|
+
},
|
|
65
|
+
new Map<Dataset, IRI[]>(),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const lookups = [...datasetToIris].map(([dataset]) =>
|
|
69
|
+
this.queryService.lookup(iris, dataset.distributions[0], timeoutMs),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const termsPerSource: TermsResponse[] = await Promise.all(lookups);
|
|
73
|
+
|
|
74
|
+
const datasetToTerms = termsPerSource.reduce(
|
|
75
|
+
(acc, response: TermsResponse) => {
|
|
76
|
+
let dataset = this.catalog.getDatasetByDistributionIri(
|
|
77
|
+
response.result.distribution.iri,
|
|
78
|
+
)!;
|
|
79
|
+
if (response.result instanceof Terms) {
|
|
80
|
+
const termsResult =
|
|
81
|
+
(acc.get(dataset)?.result as Terms) ??
|
|
82
|
+
new Terms(response.result.distribution, []);
|
|
83
|
+
for (const term of response.result.terms) {
|
|
84
|
+
if (term.datasetIri !== undefined) {
|
|
85
|
+
const termsDataset = this.catalog.getDatasetByIri(
|
|
86
|
+
term.datasetIri.value,
|
|
87
|
+
);
|
|
88
|
+
if (termsDataset !== undefined) {
|
|
89
|
+
dataset = termsDataset;
|
|
90
|
+
irisToDataset.set(term.id.value, dataset);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
termsResult.terms.push(term);
|
|
94
|
+
}
|
|
95
|
+
acc.set(
|
|
96
|
+
dataset,
|
|
97
|
+
new TermsResponse(termsResult, response.responseTimeMs),
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
const dataset = this.catalog.getDatasetByDistributionIri(
|
|
101
|
+
response.result.distribution.iri,
|
|
102
|
+
)!;
|
|
103
|
+
acc.set(dataset, response);
|
|
104
|
+
}
|
|
105
|
+
return acc;
|
|
106
|
+
},
|
|
107
|
+
new Map<Dataset, TermsResponse>(),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return iris.map((iri) => {
|
|
111
|
+
const dataset = irisToDataset.get(iri.toString());
|
|
112
|
+
if (dataset === undefined) {
|
|
113
|
+
clientQueriesCounter.add(1, {
|
|
114
|
+
type: 'lookup',
|
|
115
|
+
error: 'SourceNotFound',
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
uri: iri,
|
|
119
|
+
distribution: new SourceNotFoundError(iri),
|
|
120
|
+
result: new NotFoundError(iri),
|
|
121
|
+
responseTimeMs: 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = datasetToTerms.get(dataset)!;
|
|
126
|
+
clientQueriesCounter.add(1, { type: 'lookup' });
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
uri: iri,
|
|
130
|
+
distribution: dataset.distributions[0],
|
|
131
|
+
result: result(response.result, iri),
|
|
132
|
+
responseTimeMs: response.responseTimeMs,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function result(result: TermsResult, iri: string): LookupResult {
|
|
139
|
+
if (result instanceof Error) {
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
result.terms.find((term) => term.id.value === iri.toString()) ??
|
|
145
|
+
new NotFoundError(iri)
|
|
146
|
+
);
|
|
147
|
+
}
|