jskos-server 2.4.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/.dockerignore +20 -0
- package/.editorconfig +9 -0
- package/.github/workflows/docker.yml +59 -0
- package/.github/workflows/gh-pages.yml +23 -0
- package/.github/workflows/gh-release.yml +19 -0
- package/.github/workflows/test.yml +39 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +2710 -0
- package/bin/extra.js +81 -0
- package/bin/import.js +438 -0
- package/bin/mongodb.js +21 -0
- package/bin/reset.js +257 -0
- package/bin/upgrade.js +34 -0
- package/config/config.default.json +88 -0
- package/config/config.schema.json +877 -0
- package/config/config.test.json +107 -0
- package/config/index.js +77 -0
- package/config/setup.js +212 -0
- package/depdendencies.png +0 -0
- package/docker/.env +1 -0
- package/docker/Dockerfile +20 -0
- package/docker/README.md +175 -0
- package/docker/docker-compose.yml +29 -0
- package/docker/docker-entrypoint.sh +8 -0
- package/docker/mongo-initdb.d/mongo_setup.sh +22 -0
- package/ecosystem.example.json +7 -0
- package/errors/index.js +94 -0
- package/eslint.config.js +17 -0
- package/index.js +10 -0
- package/models/annotations.js +13 -0
- package/models/concepts.js +12 -0
- package/models/concordances.js +12 -0
- package/models/index.js +33 -0
- package/models/mappings.js +20 -0
- package/models/meta.js +21 -0
- package/models/registries.js +12 -0
- package/models/schemes.js +12 -0
- package/package.json +91 -0
- package/routes/annotations.js +83 -0
- package/routes/common.js +86 -0
- package/routes/concepts.js +64 -0
- package/routes/concordances.js +86 -0
- package/routes/data.js +19 -0
- package/routes/mappings.js +108 -0
- package/routes/registries.js +24 -0
- package/routes/schemes.js +72 -0
- package/routes/validate.js +37 -0
- package/server.js +190 -0
- package/services/abstract.js +328 -0
- package/services/annotations.js +237 -0
- package/services/concepts.js +459 -0
- package/services/concordances.js +264 -0
- package/services/data.js +30 -0
- package/services/index.js +34 -0
- package/services/mappings.js +978 -0
- package/services/registries.js +319 -0
- package/services/schemes.js +318 -0
- package/services/validate.js +39 -0
- package/status.schema.json +145 -0
- package/test/abstract-service.js +36 -0
- package/test/annotations/annotation.json +13 -0
- package/test/api.js +2481 -0
- package/test/chai.js +14 -0
- package/test/changes.js +179 -0
- package/test/concepts/conceptNoFileEnding +4 -0
- package/test/concepts/concepts-ddc-6-60-61-62.json +123 -0
- package/test/concordances/concordances.ndjson +2 -0
- package/test/config.js +26 -0
- package/test/configs/complex-config.json +90 -0
- package/test/configs/empty-object.json +1 -0
- package/test/configs/fail-array.json +1 -0
- package/test/configs/fail-empty.json +0 -0
- package/test/configs/fail-mapping-only-props1.json +5 -0
- package/test/configs/fail-mapping-only-props2.json +5 -0
- package/test/configs/fail-mapping-only-props3.json +5 -0
- package/test/configs/fail-mapping-only-props4.json +5 -0
- package/test/configs/fail-nonexisting-prop.json +3 -0
- package/test/configs/fail-port-string.json +3 -0
- package/test/configs/fail-registry-types.json +16 -0
- package/test/configs/registry-types.json +16 -0
- package/test/data-write.js +784 -0
- package/test/eslint.js +22 -0
- package/test/import-reset.js +287 -0
- package/test/infer-mappings.js +340 -0
- package/test/ipcheck.js +287 -0
- package/test/mappings/README.md +1 -0
- package/test/mappings/ddc-gnd-1.mapping.json +33 -0
- package/test/mappings/ddc-gnd-2.mapping.json +67 -0
- package/test/mappings/mapping-ddc-gnd-noScheme.json +145 -0
- package/test/mappings/mapping-ddc-gnd.json +175 -0
- package/test/mappings/mappings-ddc.json +214 -0
- package/test/registries/registries.ndjson +2 -0
- package/test/services.js +557 -0
- package/test/terminologies/terminologies.json +94 -0
- package/test/test-utils.js +182 -0
- package/test/utils.js +425 -0
- package/test/validate.js +226 -0
- package/utils/adjust.js +206 -0
- package/utils/auth.js +154 -0
- package/utils/changes.js +88 -0
- package/utils/db.js +106 -0
- package/utils/ipcheck.js +76 -0
- package/utils/middleware.js +636 -0
- package/utils/searchHelper.js +153 -0
- package/utils/status.js +77 -0
- package/utils/users.js +7 -0
- package/utils/utils.js +114 -0
- package/utils/uuid.js +6 -0
- package/utils/version.js +324 -0
- package/views/base.ejs +172 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import _ from "lodash"
|
|
2
|
+
import jskos from "jskos-tools"
|
|
3
|
+
import { validate } from "jskos-validate"
|
|
4
|
+
import { bulkOperationForEntities, queryToAggregation } from "../utils/utils.js"
|
|
5
|
+
import { addKeywords } from "../utils/searchHelper.js"
|
|
6
|
+
import { Concept } from "../models/concepts.js"
|
|
7
|
+
import { SchemeService } from "../services/schemes.js"
|
|
8
|
+
import { MalformedBodyError, MalformedRequestError, EntityNotFoundError, InvalidBodyError, DatabaseAccessError } from "../errors/index.js"
|
|
9
|
+
|
|
10
|
+
import { AbstractService } from "./abstract.js"
|
|
11
|
+
|
|
12
|
+
export class ConceptService extends AbstractService {
|
|
13
|
+
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super(config)
|
|
16
|
+
this.schemeService = new SchemeService(config)
|
|
17
|
+
this.model = Concept
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
retrieveItems(query, $skip, $limit, narrower = true) {
|
|
21
|
+
const pipeline = queryToAggregation(query)
|
|
22
|
+
if (narrower) {
|
|
23
|
+
pipeline.push({
|
|
24
|
+
$lookup: {
|
|
25
|
+
from: Concept.collection.name,
|
|
26
|
+
localField: "uri",
|
|
27
|
+
foreignField: "broader.uri",
|
|
28
|
+
as: "narrower",
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
pipeline.push({
|
|
32
|
+
$addFields: {
|
|
33
|
+
narrower: {
|
|
34
|
+
$reduce: {
|
|
35
|
+
input: "$narrower",
|
|
36
|
+
initialValue: [],
|
|
37
|
+
in: [null],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
if (_.isNumber($skip)) {
|
|
44
|
+
pipeline.push({ $skip })
|
|
45
|
+
}
|
|
46
|
+
if (_.isNumber($limit)) {
|
|
47
|
+
pipeline.push({ $limit })
|
|
48
|
+
}
|
|
49
|
+
return Concept.aggregate(pipeline)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return a Promise with an array of concept data.
|
|
54
|
+
*/
|
|
55
|
+
async getTop(query) {
|
|
56
|
+
let criteria
|
|
57
|
+
if (query.uri) {
|
|
58
|
+
let uris
|
|
59
|
+
// Get scheme from database
|
|
60
|
+
let scheme = await this.schemeService.getScheme(query.uri)
|
|
61
|
+
if (scheme) {
|
|
62
|
+
uris = [scheme.uri].concat(scheme.identifier || [])
|
|
63
|
+
} else {
|
|
64
|
+
uris = [query.uri]
|
|
65
|
+
}
|
|
66
|
+
criteria = { $or: uris.map(uri => ({ "topConceptOf.uri": uri })) }
|
|
67
|
+
} else {
|
|
68
|
+
// Search for all top concepts in all vocabularies
|
|
69
|
+
criteria = { "topConceptOf.uri": { $type: 2 } }
|
|
70
|
+
}
|
|
71
|
+
const concepts = await this.retrieveItems(criteria, query.offset, query.limit)
|
|
72
|
+
concepts.totalCount = await this._count(Concept, [{ $match: criteria }])
|
|
73
|
+
return concepts
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Return a Promise with an array of concepts.
|
|
78
|
+
*/
|
|
79
|
+
async queryItems(query) {
|
|
80
|
+
if (!_.intersection(Object.keys(query), ["uri", "notation", "voc", "near"]).length) {
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
const criteria = []
|
|
84
|
+
const mongoQuery = { $and: criteria }
|
|
85
|
+
const uris = query.uri ? query.uri.split("|") : []
|
|
86
|
+
const notations = query.notation ? query.notation.split("|") : []
|
|
87
|
+
if (uris.length || notations.length) {
|
|
88
|
+
criteria.push({
|
|
89
|
+
$or: [].concat(uris.map(uri => ({ uri })), notations.map(notation => ({ notation }))),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
if (query.voc) {
|
|
93
|
+
let uris
|
|
94
|
+
// Get scheme from database
|
|
95
|
+
let scheme = await this.schemeService.getScheme(query.voc)
|
|
96
|
+
if (scheme) {
|
|
97
|
+
uris = [scheme.uri].concat(scheme.identifier || [])
|
|
98
|
+
} else {
|
|
99
|
+
uris = [query.voc]
|
|
100
|
+
}
|
|
101
|
+
criteria.push({ $or: uris.map(uri => ({ "inScheme.uri": uri })) })
|
|
102
|
+
}
|
|
103
|
+
if (query.near) {
|
|
104
|
+
const [latitude, longitude] = query.near.split(",").map(parseFloat)
|
|
105
|
+
// distance is given in km, but MongoDB uses meters
|
|
106
|
+
const distance = (query.distance || 1) * 1000
|
|
107
|
+
if (!_.isFinite(latitude) || !_.isFinite(longitude) || !(latitude >= -90 && latitude <= 90) || !(longitude >= -180 && longitude <= 180)) {
|
|
108
|
+
throw new MalformedRequestError(`Parameter \`near\` (${query.near}) is malformed. The correct format is "latitude,longitude" with latitude between -90 and 90 and longitude between -180 and 180.`)
|
|
109
|
+
}
|
|
110
|
+
if (!distance) {
|
|
111
|
+
throw new MalformedRequestError(`Parameter \`distance\` (${query.distance}) is malformed. Please give a number in km (default: 1).`)
|
|
112
|
+
}
|
|
113
|
+
mongoQuery.location = {
|
|
114
|
+
$nearSphere: {
|
|
115
|
+
$geometry: {
|
|
116
|
+
type: "Point",
|
|
117
|
+
coordinates: [longitude, latitude],
|
|
118
|
+
},
|
|
119
|
+
$maxDistance: distance,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (query.download) {
|
|
124
|
+
return this.retrieveItems(mongoQuery, null, null, false).cursor()
|
|
125
|
+
}
|
|
126
|
+
const concepts = await this.retrieveItems(mongoQuery, query.offset, query.limit)
|
|
127
|
+
concepts.totalCount = await this._count(Concept, queryToAggregation(mongoQuery))
|
|
128
|
+
return concepts
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Return a Promise with an array of concept data.
|
|
133
|
+
*/
|
|
134
|
+
async getNarrower(query) {
|
|
135
|
+
if (!query.uri) {
|
|
136
|
+
return []
|
|
137
|
+
}
|
|
138
|
+
return await this.retrieveItems({ broader: { $elemMatch: { uri: query.uri } } })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Return a Promise with an array of concept data.
|
|
143
|
+
*/
|
|
144
|
+
async getAncestors(query, root = true) {
|
|
145
|
+
if (!query.uri) {
|
|
146
|
+
return []
|
|
147
|
+
}
|
|
148
|
+
const uri = query.uri
|
|
149
|
+
// First retrieve the concept object from database
|
|
150
|
+
const concept = await this.getItem(uri)
|
|
151
|
+
if (!concept) {
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
if (concept.broader && concept.broader.length) {
|
|
155
|
+
// Load next parent
|
|
156
|
+
let parentUri = concept.broader[0].uri
|
|
157
|
+
// Temporary fix for self-referencing broader
|
|
158
|
+
parentUri = parentUri == uri ? (concept.broader[1] && concept.broader[1].uri) : parentUri
|
|
159
|
+
if (!parentUri) {
|
|
160
|
+
if (root) {
|
|
161
|
+
return []
|
|
162
|
+
} else {
|
|
163
|
+
return [concept]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const ancestors = await this.getAncestors({ uri: parentUri }, false)
|
|
167
|
+
if (root) {
|
|
168
|
+
return ancestors
|
|
169
|
+
} else {
|
|
170
|
+
return [concept].concat(ancestors)
|
|
171
|
+
}
|
|
172
|
+
} else if (!root) {
|
|
173
|
+
return [concept]
|
|
174
|
+
} else {
|
|
175
|
+
return []
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Return a Promise with an array of suggestions in JSKOS format.
|
|
181
|
+
*/
|
|
182
|
+
async search(query) {
|
|
183
|
+
let search = query.query || query.search || ""
|
|
184
|
+
let results = await this.searchItems({ search, voc: query.voc })
|
|
185
|
+
const { limit, offset } = this._getLimitAndOffset(query)
|
|
186
|
+
const searchResults = results.slice(offset, offset + limit)
|
|
187
|
+
searchResults.totalCount = results.length
|
|
188
|
+
return searchResults
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Write endpoints start here
|
|
192
|
+
|
|
193
|
+
async createItem({ bodyStream, bulk = false, setApi = false, bulkReplace = true, scheme }) {
|
|
194
|
+
if (!bodyStream) {
|
|
195
|
+
throw new MalformedBodyError()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let isMultiple = true
|
|
199
|
+
bodyStream.on("isSingleObject", () => {
|
|
200
|
+
isMultiple = false
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
let response, preparation
|
|
204
|
+
|
|
205
|
+
if (bulk) {
|
|
206
|
+
preparation = await new Promise((resolve) => {
|
|
207
|
+
const preparation = {
|
|
208
|
+
concepts: [],
|
|
209
|
+
schemeUrisToAdjust: [],
|
|
210
|
+
errors: [],
|
|
211
|
+
}
|
|
212
|
+
let current = []
|
|
213
|
+
const saveObjects = async (objects) => {
|
|
214
|
+
const { concepts, errors, schemeUrisToAdjust } = await this.prepareAndCheckConcepts(objects, { scheme })
|
|
215
|
+
concepts.length && await Concept.bulkWrite(bulkOperationForEntities({ entities: concepts, replace: bulkReplace }))
|
|
216
|
+
preparation.concepts = preparation.concepts.concat(concepts.map(c => ({ uri: c.uri })))
|
|
217
|
+
preparation.errors = preparation.errors.concat(errors.map(c => ({ uri: c.uri })))
|
|
218
|
+
preparation.schemeUrisToAdjust = _.uniq(preparation.schemeUrisToAdjust.concat(schemeUrisToAdjust))
|
|
219
|
+
}
|
|
220
|
+
const promises = []
|
|
221
|
+
bodyStream.on("data", (concept) => {
|
|
222
|
+
current.push(concept)
|
|
223
|
+
if (current.length % 5000 == 0) {
|
|
224
|
+
promises.push(saveObjects(current))
|
|
225
|
+
current = []
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
bodyStream.on("end", async () => {
|
|
229
|
+
promises.push(saveObjects(current))
|
|
230
|
+
await Promise.all(promises)
|
|
231
|
+
preparation.errors.length && this.warn(`Warning on bulk import of concepts: ${preparation.errors.length} concepts were not imported due to validation errors.`)
|
|
232
|
+
resolve(preparation)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
response = preparation.concepts
|
|
236
|
+
} else {
|
|
237
|
+
// Fully assemble body for non-bulk operations
|
|
238
|
+
let { items } = await this._readBodyStream(bodyStream)
|
|
239
|
+
// Prepare
|
|
240
|
+
preparation = await this.prepareAndCheckConcepts(items, { scheme })
|
|
241
|
+
items = preparation.concepts
|
|
242
|
+
if (preparation.errors.length) {
|
|
243
|
+
throw preparation.errors[0]
|
|
244
|
+
}
|
|
245
|
+
// Insert concepts
|
|
246
|
+
response = await Concept.insertMany(items, { lean: true })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
preparation.setApi = setApi
|
|
250
|
+
await this.postAdjustmentsForItems(preparation)
|
|
251
|
+
|
|
252
|
+
return isMultiple ? response : response[0]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async updateItem({ body, existing }) {
|
|
256
|
+
if (!body) {
|
|
257
|
+
throw new MalformedBodyError()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!_.isObject(body)) {
|
|
261
|
+
throw new MalformedBodyError()
|
|
262
|
+
}
|
|
263
|
+
let concept = body
|
|
264
|
+
|
|
265
|
+
// Prepare
|
|
266
|
+
const preparation = await this.prepareAndCheckConcepts([concept])
|
|
267
|
+
|
|
268
|
+
// Throw error if necessary
|
|
269
|
+
if (preparation.errors.length) {
|
|
270
|
+
throw preparation.errors[0]
|
|
271
|
+
}
|
|
272
|
+
concept = preparation.concepts[0]
|
|
273
|
+
|
|
274
|
+
// Override _id, uri, and created properties
|
|
275
|
+
concept._id = existing._id
|
|
276
|
+
concept.uri = existing.uri
|
|
277
|
+
concept.created = existing.created
|
|
278
|
+
|
|
279
|
+
// Write concept to database
|
|
280
|
+
const result = await Concept.replaceOne({ _id: existing._id }, concept)
|
|
281
|
+
if (!result.acknowledged) {
|
|
282
|
+
throw new DatabaseAccessError()
|
|
283
|
+
}
|
|
284
|
+
if (!result.matchedCount) {
|
|
285
|
+
throw new EntityNotFoundError()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ? Can we return the request without waiting for this step?
|
|
289
|
+
await this.postAdjustmentsForItems(preparation)
|
|
290
|
+
|
|
291
|
+
return concept
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async deleteItem({ uri, existing, setApi = false }) {
|
|
295
|
+
if (!uri) {
|
|
296
|
+
throw new MalformedRequestError()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
super.deleteItem({ existing })
|
|
300
|
+
|
|
301
|
+
await this.postAdjustmentsForItems({
|
|
302
|
+
// Adjust scheme in case it was its last concept
|
|
303
|
+
schemeUrisToAdjust: [existing?.inScheme?.[0]?.uri],
|
|
304
|
+
conceptUrisWithNarrower: [],
|
|
305
|
+
setApi,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async deleteConceptsFromScheme({ uri, scheme, setApi = false }) {
|
|
310
|
+
if (!uri && !scheme) {
|
|
311
|
+
throw new MalformedRequestError()
|
|
312
|
+
}
|
|
313
|
+
if (uri && !scheme) {
|
|
314
|
+
scheme = await this.schemeService.getScheme(uri)
|
|
315
|
+
}
|
|
316
|
+
if (!scheme) {
|
|
317
|
+
throw new EntityNotFoundError(`Could not find scheme with URI ${uri} to delete concepts from.`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const result = await Concept.deleteMany({ "inScheme.uri": { $in: [scheme.uri].concat(scheme.identifier || []) } })
|
|
321
|
+
if (!result) {
|
|
322
|
+
throw new DatabaseAccessError()
|
|
323
|
+
}
|
|
324
|
+
if (!result.deletedCount) {
|
|
325
|
+
throw new EntityNotFoundError("No concepts found to delete.")
|
|
326
|
+
}
|
|
327
|
+
await this.postAdjustmentsForItems({
|
|
328
|
+
schemeUrisToAdjust: [uri],
|
|
329
|
+
conceptUrisWithNarrower: [],
|
|
330
|
+
setApi,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Prepares and checks a list of concepts before inserting/updating (see `prepareAndCheckConcept`).
|
|
336
|
+
*
|
|
337
|
+
* @param {Object} allConcept concept objects
|
|
338
|
+
* @returns {Object} preparation object with properties `concepts`, `errors`, `schemeUrisToAdjust`, and `conceptUrisWithNarrower`; needs to be provided to `postAdjustmentsForItems`
|
|
339
|
+
*/
|
|
340
|
+
async prepareAndCheckConcepts(allConcepts, { scheme } = {}) {
|
|
341
|
+
const getSchemeUri = c => c?.inScheme?.[0]?.uri || c?.topConceptOf?.[0]?.uri
|
|
342
|
+
const schemeUrisToAdjust = []
|
|
343
|
+
const concepts = []
|
|
344
|
+
const errors = []
|
|
345
|
+
// Set inScheme for concepts when `scheme` option is given
|
|
346
|
+
if (scheme) {
|
|
347
|
+
allConcepts.forEach(concept => {
|
|
348
|
+
if (!getSchemeUri(concept)) {
|
|
349
|
+
concept.inScheme = [{ uri: scheme }]
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
// Load all schemes for concepts
|
|
354
|
+
const schemes = await this.schemeService.queryItems({
|
|
355
|
+
uri: allConcepts.map(c => getSchemeUri(c)).filter(Boolean).join("|"),
|
|
356
|
+
})
|
|
357
|
+
for (let concept of allConcepts) {
|
|
358
|
+
try {
|
|
359
|
+
await this.prepareAndCheckConcept(concept, schemes)
|
|
360
|
+
let scheme = concept?.inScheme?.[0]?.uri
|
|
361
|
+
if (scheme && !schemeUrisToAdjust.includes(scheme)) {
|
|
362
|
+
schemeUrisToAdjust.push(scheme)
|
|
363
|
+
}
|
|
364
|
+
concepts.push(concept)
|
|
365
|
+
} catch (error) {
|
|
366
|
+
errors.push(error)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
concepts,
|
|
371
|
+
errors,
|
|
372
|
+
schemeUrisToAdjust,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Prepares and checks a concept before inserting/updating:
|
|
378
|
+
* - copies `topConceptOf` to `inScheme` if necessary
|
|
379
|
+
* - validates object, throws error if it doesn't
|
|
380
|
+
* - makes sure that it has a valid scheme, throws error if it doesn't
|
|
381
|
+
* - adjust scheme URI if necessary
|
|
382
|
+
* - adds certain keyword properties necessary for text indexes
|
|
383
|
+
*
|
|
384
|
+
* @param {Object} concept concept object
|
|
385
|
+
* @param {[Object]} schemes array of schemes
|
|
386
|
+
*/
|
|
387
|
+
async prepareAndCheckConcept(concept, schemes) {
|
|
388
|
+
concept._id = concept.uri
|
|
389
|
+
// Add "inScheme" for all top concepts
|
|
390
|
+
if (!concept.inScheme && concept.topConceptOf) {
|
|
391
|
+
concept.inScheme = concept.topConceptOf
|
|
392
|
+
}
|
|
393
|
+
// Remove `narrower` and `ancestors` properties => we're only using `broader` to build the concept hierarchy
|
|
394
|
+
delete concept.narrower
|
|
395
|
+
delete concept.ancestors
|
|
396
|
+
// Validate concept
|
|
397
|
+
if (!validate.concept(concept) || !concept.uri) {
|
|
398
|
+
throw new InvalidBodyError()
|
|
399
|
+
}
|
|
400
|
+
// Check concept scheme
|
|
401
|
+
const inScheme = concept.inScheme?.[0]
|
|
402
|
+
// Load scheme from database if necessary
|
|
403
|
+
if (!schemes || !schemes.length) {
|
|
404
|
+
schemes = await this.schemeService.queryItems({ uri: inScheme.uri })
|
|
405
|
+
}
|
|
406
|
+
const scheme = schemes.find(s => jskos.compare(s, inScheme))
|
|
407
|
+
if (!scheme) {
|
|
408
|
+
// Either no scheme at all or not found in database
|
|
409
|
+
let message = "Error when adding concept to database: "
|
|
410
|
+
if (inScheme) {
|
|
411
|
+
message += `Concept scheme with URI ${inScheme.uri} is not supported.`
|
|
412
|
+
} else {
|
|
413
|
+
message += "Concept has no concept scheme."
|
|
414
|
+
}
|
|
415
|
+
throw new MalformedRequestError(message)
|
|
416
|
+
}
|
|
417
|
+
// Adjust URIs of schemes
|
|
418
|
+
concept.inScheme[0].uri = scheme.uri
|
|
419
|
+
if (concept.topConceptOf && concept.topConceptOf.length) {
|
|
420
|
+
concept.topConceptOf[0].uri = scheme.uri
|
|
421
|
+
}
|
|
422
|
+
// Add index keywords
|
|
423
|
+
addKeywords(concept)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async postAdjustmentsForItems(preparation) {
|
|
427
|
+
// runs `postAdjustmentsForItems` for relevant schemes in `preparation.schemeUrisToAdjust`
|
|
428
|
+
// adds `narrower: [null]` for concepts in `preparation.conceptUrisWithNarrower`
|
|
429
|
+
await this.schemeService.postAdjustmentsForItems(preparation.schemeUrisToAdjust.map(uri => ({ uri })), { setApi: preparation.setApi })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async createIndexes() {
|
|
433
|
+
await this._createIndexes([
|
|
434
|
+
[{ "broader.uri": 1 }, {}],
|
|
435
|
+
[{ "topConceptOf.uri": 1 }, {}],
|
|
436
|
+
[{ "inScheme.uri": 1 }, {}],
|
|
437
|
+
[{ uri: 1 }, {}],
|
|
438
|
+
[{ notation: 1 }, {}],
|
|
439
|
+
[{ identifier: 1 }, {}],
|
|
440
|
+
[{ _keywordsLabels: 1 }, {}],
|
|
441
|
+
[{ location: "2dsphere" }, {}],
|
|
442
|
+
[
|
|
443
|
+
{
|
|
444
|
+
_keywordsNotation: "text",
|
|
445
|
+
_keywordsLabels: "text",
|
|
446
|
+
_keywordsOther: "text",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "text",
|
|
450
|
+
default_language: "german",
|
|
451
|
+
weights: {
|
|
452
|
+
_keywordsNotation: 10,
|
|
453
|
+
_keywordsLabels: 6,
|
|
454
|
+
_keywordsOther: 3,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
]])
|
|
458
|
+
}
|
|
459
|
+
}
|