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,153 @@
|
|
|
1
|
+
import _ from "lodash"
|
|
2
|
+
import jskos from "jskos-tools"
|
|
3
|
+
|
|
4
|
+
// from https://web.archive.org/web/20170609122132/http://jam.sg/blog/efficient-partial-keyword-searches/
|
|
5
|
+
export function makeSuffixes(values) {
|
|
6
|
+
const results = []
|
|
7
|
+
values.forEach(function (val) {
|
|
8
|
+
val = val.toUpperCase().trim()
|
|
9
|
+
let tmp, hasSuffix
|
|
10
|
+
for (let i = 0; i < val.length - 1; i++) {
|
|
11
|
+
tmp = val.substr(i)
|
|
12
|
+
hasSuffix = results.includes(tmp)
|
|
13
|
+
if (!hasSuffix) {
|
|
14
|
+
results.push(tmp)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
return results
|
|
19
|
+
}
|
|
20
|
+
// adapted from above
|
|
21
|
+
export function makePrefixes(values) {
|
|
22
|
+
const results = []
|
|
23
|
+
values.forEach(function (val) {
|
|
24
|
+
val = val.toUpperCase().trim()
|
|
25
|
+
let tmp, hasPrefix
|
|
26
|
+
results.push(val)
|
|
27
|
+
for (let i = 2; i < val.length; i++) {
|
|
28
|
+
tmp = val.substr(0, i)
|
|
29
|
+
hasPrefix = results.includes(tmp)
|
|
30
|
+
if (!hasPrefix) {
|
|
31
|
+
results.push(tmp)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
return results
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enumerates and sorts all labels (prefLabel and altLabel) in an item.
|
|
40
|
+
*
|
|
41
|
+
* Sorting:
|
|
42
|
+
* - prefLabel over altLabel
|
|
43
|
+
* - provided language over other languages
|
|
44
|
+
* - other languages by the order of the `languages` property of the item if available
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} item JSKOS item (scheme or concept)
|
|
47
|
+
*/
|
|
48
|
+
export function getAllLabelsSorted(item, language = "en") {
|
|
49
|
+
function extractAndSortLabels(labels, languages) {
|
|
50
|
+
return _.toPairs(labels).sort((a, b) => {
|
|
51
|
+
const bIndex = languages.indexOf(b[0]), aIndex = languages.indexOf(a[0])
|
|
52
|
+
if (bIndex === -1) {
|
|
53
|
+
return -1
|
|
54
|
+
}
|
|
55
|
+
if (aIndex === -1) {
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
return aIndex - bIndex
|
|
59
|
+
}).map(v => v[1])
|
|
60
|
+
}
|
|
61
|
+
const languages = [language].concat(item.languages || [])
|
|
62
|
+
return _.flattenDeep(extractAndSortLabels(item.prefLabel || {}, languages).concat(extractAndSortLabels(item.altLabel || {}, languages)))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Adds necessary properties required by indexes for search.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} item JSKOS item (scheme or concept)
|
|
69
|
+
*/
|
|
70
|
+
export function addKeywords(item) {
|
|
71
|
+
item._keywordsNotation = makePrefixes(item.notation || [])
|
|
72
|
+
// Do not write text index keywords for synthetic concepts
|
|
73
|
+
if (!item.type || !item.type.includes("http://rdf-vocabulary.ddialliance.org/xkos#CombinedConcept")) {
|
|
74
|
+
// Labels
|
|
75
|
+
// Assemble all labels
|
|
76
|
+
let labels = getAllLabelsSorted(item)
|
|
77
|
+
// Split labels by space and dash
|
|
78
|
+
item._keywordsLabels = makeSuffixes(labels)
|
|
79
|
+
// Other properties
|
|
80
|
+
item._keywordsOther = []
|
|
81
|
+
for (let map of (item.creator || []).concat(item.scopeNote, item.editorialNote, item.definition)) {
|
|
82
|
+
if (map) {
|
|
83
|
+
item._keywordsOther = item._keywordsOther.concat(Object.values(map))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Make sure to flatten both arrays and objects
|
|
87
|
+
item._keywordsOther = _.flattenDeep(item._keywordsOther)
|
|
88
|
+
item._keywordsOther = item._keywordsOther.map(v => {
|
|
89
|
+
if (_.isObject(v)) {
|
|
90
|
+
return Object.values(v)
|
|
91
|
+
}
|
|
92
|
+
return v
|
|
93
|
+
})
|
|
94
|
+
item._keywordsOther = _.flattenDeep(item._keywordsOther)
|
|
95
|
+
if (item.publisher) {
|
|
96
|
+
item._keywordsPublisher = _.flattenDeep(
|
|
97
|
+
item.publisher.map(publisher => {
|
|
98
|
+
return [publisher.uri].concat(Object.values(publisher.prefLabel || {}))
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Converts search results to Open Search Suggest Format for /suggest endpoints.
|
|
107
|
+
*
|
|
108
|
+
* @param {Object} options.query query object for request
|
|
109
|
+
* @param {Array} options.results results as JSKOS array
|
|
110
|
+
*/
|
|
111
|
+
export function toOpenSearchSuggestFormat({ query, results }) {
|
|
112
|
+
// Transform to OpenSearch Suggest Format
|
|
113
|
+
let labels = []
|
|
114
|
+
let descriptions = []
|
|
115
|
+
let uris = []
|
|
116
|
+
let currentOffset = query.offset
|
|
117
|
+
for (let result of results) {
|
|
118
|
+
// Skip if offset is not reached
|
|
119
|
+
if (currentOffset) {
|
|
120
|
+
currentOffset -= 1
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
// Skip if limit is reached
|
|
124
|
+
if (labels.length >= query.limit) {
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
// Determine prefLabel via `language` parameter
|
|
128
|
+
const language = (query.language || "").split(",").filter(lang => lang)
|
|
129
|
+
let prefLabel
|
|
130
|
+
for (const lang of language) {
|
|
131
|
+
prefLabel = _.get(result, `prefLabel.${lang}`)
|
|
132
|
+
if (prefLabel) {
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!prefLabel) {
|
|
137
|
+
prefLabel = jskos.prefLabel(result, { fallbackToUri: false })
|
|
138
|
+
}
|
|
139
|
+
let label = jskos.notation(result)
|
|
140
|
+
if (label && prefLabel) {
|
|
141
|
+
label += " "
|
|
142
|
+
}
|
|
143
|
+
label += prefLabel
|
|
144
|
+
labels.push(label) // + " (" + result.priority + ")")
|
|
145
|
+
descriptions.push("")
|
|
146
|
+
uris.push(result.uri)
|
|
147
|
+
}
|
|
148
|
+
const searchResults = [
|
|
149
|
+
query.search || "", labels, descriptions, uris,
|
|
150
|
+
]
|
|
151
|
+
searchResults.totalCount = results.length
|
|
152
|
+
return searchResults
|
|
153
|
+
}
|
package/utils/status.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import _ from "lodash"
|
|
2
|
+
|
|
3
|
+
export function serverStatus(config, ok) {
|
|
4
|
+
const { baseUrl } = config
|
|
5
|
+
const status = {
|
|
6
|
+
config: _.omit(_.cloneDeep(config), ["verbosity", "port", "mongo", "namespace", "proxies", "ips"]),
|
|
7
|
+
}
|
|
8
|
+
// Remove `ips` property from all actions
|
|
9
|
+
for (let type of ["schemes", "concepts", "mappings", "concordances", "annotations"]) {
|
|
10
|
+
if (status.config[type]) {
|
|
11
|
+
delete status.config[type].ips
|
|
12
|
+
for (let action of ["read", "create", "update", "delete"]) {
|
|
13
|
+
if (status.config[type][action]) {
|
|
14
|
+
delete status.config[type][action].ips
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Remove `key` from auth config if a symmetric algorithm is used
|
|
20
|
+
if (["HS256", "HS384", "HS512"].includes(status?.config?.auth?.algorithm)) {
|
|
21
|
+
delete status.config.auth.key
|
|
22
|
+
}
|
|
23
|
+
status.config.baseUrl = baseUrl
|
|
24
|
+
// Set all available endpoints to `null` first
|
|
25
|
+
for (let type of [
|
|
26
|
+
"data",
|
|
27
|
+
"schemes",
|
|
28
|
+
"top",
|
|
29
|
+
"voc-search",
|
|
30
|
+
"voc-suggest",
|
|
31
|
+
"voc-concepts",
|
|
32
|
+
"concepts",
|
|
33
|
+
"narrower",
|
|
34
|
+
"ancestors",
|
|
35
|
+
"search",
|
|
36
|
+
"suggest",
|
|
37
|
+
"mappings",
|
|
38
|
+
"concordances",
|
|
39
|
+
"annotations",
|
|
40
|
+
"registries",
|
|
41
|
+
]) {
|
|
42
|
+
status[type] = null
|
|
43
|
+
}
|
|
44
|
+
status.data = `${baseUrl}data`
|
|
45
|
+
if (status.config.schemes) {
|
|
46
|
+
status.schemes = `${baseUrl}voc`
|
|
47
|
+
status.top = `${baseUrl}voc/top`
|
|
48
|
+
status["voc-search"] = `${baseUrl}voc/search`
|
|
49
|
+
status["voc-suggest"] = `${baseUrl}voc/suggest`
|
|
50
|
+
status["voc-concepts"] = `${baseUrl}voc/concepts`
|
|
51
|
+
}
|
|
52
|
+
if (status.config.concepts) {
|
|
53
|
+
status.concepts = `${baseUrl}concepts`
|
|
54
|
+
status.narrower = `${baseUrl}concepts/narrower`
|
|
55
|
+
status.ancestors = `${baseUrl}concepts/ancestors`
|
|
56
|
+
status.search = `${baseUrl}concepts/search`
|
|
57
|
+
status.suggest = `${baseUrl}concepts/suggest`
|
|
58
|
+
}
|
|
59
|
+
if (status.config.mappings) {
|
|
60
|
+
status.mappings = `${baseUrl}mappings`
|
|
61
|
+
}
|
|
62
|
+
if (status.config.concordances) {
|
|
63
|
+
status.concordances = `${baseUrl}concordances`
|
|
64
|
+
}
|
|
65
|
+
if (status.config.annotations) {
|
|
66
|
+
status.annotations = `${baseUrl}annotations`
|
|
67
|
+
}
|
|
68
|
+
if (status.config.registries) {
|
|
69
|
+
status.registries = `${baseUrl}registries`
|
|
70
|
+
}
|
|
71
|
+
status.types = null // not supported in jskos-server yet
|
|
72
|
+
status.validate = `${baseUrl}validate`
|
|
73
|
+
|
|
74
|
+
status.ok = ok ? 1 : 0
|
|
75
|
+
|
|
76
|
+
return status
|
|
77
|
+
}
|
package/utils/users.js
ADDED
package/utils/utils.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import _ from "lodash"
|
|
2
|
+
import * as jskos from "jskos-tools"
|
|
3
|
+
|
|
4
|
+
// remove object properties when its value is null
|
|
5
|
+
export function removeNullProperties(obj) {
|
|
6
|
+
return Object.keys(obj).filter(key => obj[key] === null).forEach(key => delete obj[key])
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively remove certain fields from response
|
|
11
|
+
*
|
|
12
|
+
* Gets called in `returnJSON` and `handleDownload`. Shouldn't be used anywhere else.
|
|
13
|
+
*
|
|
14
|
+
* @param {(Object|Object[])} json JSON object or array of objects
|
|
15
|
+
* @param {number} [depth=0] Should not be set when called from outside
|
|
16
|
+
*/
|
|
17
|
+
export function cleanJSON(json, depth = 0, closedWorld = true) {
|
|
18
|
+
if (Array.isArray(json)) {
|
|
19
|
+
json.forEach(value => cleanJSON(value, depth, closedWorld))
|
|
20
|
+
} else if (_.isObject(json)) {
|
|
21
|
+
_.forOwn(json, (value, key) => {
|
|
22
|
+
if (
|
|
23
|
+
// Remove top level empty arrays/objects if closedWorldAssumption is set to false
|
|
24
|
+
(depth === 0 && !closedWorld && (_.isEqual(value, {}) || _.isEqual(value, [])))
|
|
25
|
+
// Remove all fields started with _
|
|
26
|
+
|| key.startsWith("_")
|
|
27
|
+
) {
|
|
28
|
+
_.unset(json, key)
|
|
29
|
+
} else {
|
|
30
|
+
cleanJSON(value, depth + 1, closedWorld)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function bulkOperationForEntities({ entities, replace = true }) {
|
|
37
|
+
return entities.map(e => (replace ? {
|
|
38
|
+
replaceOne: {
|
|
39
|
+
filter: { _id: e._id },
|
|
40
|
+
replacement: e,
|
|
41
|
+
upsert: true,
|
|
42
|
+
},
|
|
43
|
+
} : {
|
|
44
|
+
insertOne: {
|
|
45
|
+
document: e,
|
|
46
|
+
},
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Converts a MongoDB "find" query to an aggregation pipeline.
|
|
52
|
+
*
|
|
53
|
+
* In most cases, this will simply be a single $match stage, but there's special handling for
|
|
54
|
+
* $nearSquere queries on the field `location` that is converted into a $geoNear stage.
|
|
55
|
+
*
|
|
56
|
+
* @param {*} query
|
|
57
|
+
* @returns array with aggregation pipeline
|
|
58
|
+
*/
|
|
59
|
+
export function queryToAggregation(query) {
|
|
60
|
+
const pipeline = []
|
|
61
|
+
// Transform location $nearSphere query into $geoNear aggregation stage
|
|
62
|
+
if (query.location) {
|
|
63
|
+
const locationQuery = query.location.$nearSphere
|
|
64
|
+
pipeline.push({
|
|
65
|
+
$geoNear: {
|
|
66
|
+
spherical: true,
|
|
67
|
+
maxDistance: locationQuery.$maxDistance,
|
|
68
|
+
query: _.omit(query, ["location"]),
|
|
69
|
+
near: locationQuery.$geometry,
|
|
70
|
+
distanceField: "_distance",
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
} else {
|
|
74
|
+
pipeline.push({
|
|
75
|
+
$match: query,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
return pipeline
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} mapping mapping to be adjusted
|
|
84
|
+
* @param {Object} [options]
|
|
85
|
+
* @param {Object} [options.concordance] concordance object of mapping
|
|
86
|
+
* @param {Object} [options.fromScheme] manual override for `fromScheme`
|
|
87
|
+
* @param {Object} [options.toScheme] manual override for `toScheme`
|
|
88
|
+
* @returns
|
|
89
|
+
*/
|
|
90
|
+
export function addMappingSchemes(mapping, options = {}) {
|
|
91
|
+
mapping && ["from", "to"].forEach(side => {
|
|
92
|
+
const field = `${side}Scheme`
|
|
93
|
+
if (mapping[field]) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (options[field]) {
|
|
97
|
+
mapping[field] = options[field]
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
options.concordance = options.concordance || mapping.partOf?.[0]
|
|
101
|
+
if (options.concordance?.[field]) {
|
|
102
|
+
mapping[field] = options.concordance[field]
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const concepts = jskos.conceptsOfMapping(mapping, side)
|
|
106
|
+
const schemeFromConcept = concepts.find(concept => concept?.inScheme?.[0]?.uri)?.inScheme[0]
|
|
107
|
+
if (schemeFromConcept) {
|
|
108
|
+
mapping[field] = schemeFromConcept
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
return mapping
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
package/utils/uuid.js
ADDED
package/utils/version.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { addMappingSchemes } from "./utils.js"
|
|
2
|
+
import { uuid } from "./uuid.js"
|
|
3
|
+
import _ from "lodash"
|
|
4
|
+
import yesno from "yesno"
|
|
5
|
+
|
|
6
|
+
import { Scheme, Concordance, Mapping, Annotation } from "../models/index.js"
|
|
7
|
+
import { SchemeService } from "../services/schemes.js"
|
|
8
|
+
import { ConcordanceService } from "../services/concordances.js"
|
|
9
|
+
import { AnnotationService } from "../services/annotations.js"
|
|
10
|
+
import { MappingService } from "../services/mappings.js"
|
|
11
|
+
import { ConceptService } from "../services/concepts.js"
|
|
12
|
+
import { RegistryService } from "../services/registries.js"
|
|
13
|
+
|
|
14
|
+
import { addKeywords } from "./searchHelper.js"
|
|
15
|
+
|
|
16
|
+
export class Version {
|
|
17
|
+
|
|
18
|
+
constructor(version) {
|
|
19
|
+
this.version = version
|
|
20
|
+
const [major = 0, minor = 0, patch = 0] = version.split(".").map(v => parseInt(v))
|
|
21
|
+
this.major = major
|
|
22
|
+
this.minor = minor
|
|
23
|
+
this.patch = patch
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
gt(version) {
|
|
27
|
+
version = Version.from(version)
|
|
28
|
+
return this.major > version.major ||
|
|
29
|
+
(this.major == version.major && this.minor > version.minor) ||
|
|
30
|
+
(this.major == version.major && this.minor == version.minor && this.patch > version.patch)
|
|
31
|
+
}
|
|
32
|
+
gte(version) {
|
|
33
|
+
version = Version.from(version)
|
|
34
|
+
return this.major > version.major ||
|
|
35
|
+
(this.major == version.major && this.minor > version.minor) ||
|
|
36
|
+
(this.major == version.major && this.minor == version.minor && this.patch >= version.patch)
|
|
37
|
+
}
|
|
38
|
+
eq(version) {
|
|
39
|
+
return !this.lt(version) && !this.gt(version)
|
|
40
|
+
}
|
|
41
|
+
lt(version) {
|
|
42
|
+
return !this.gte(version)
|
|
43
|
+
}
|
|
44
|
+
lte(version) {
|
|
45
|
+
return !this.gt(version)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static from(version) {
|
|
49
|
+
return version instanceof Version ? version : new Version(version)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Upgrader {
|
|
55
|
+
|
|
56
|
+
constructor(config) {
|
|
57
|
+
this.baseUrl = config.baseUrl
|
|
58
|
+
this.schemeService = new SchemeService(config)
|
|
59
|
+
this.concordanceService = new ConcordanceService(config)
|
|
60
|
+
this.annotationService = new AnnotationService(config)
|
|
61
|
+
this.mappingService = new MappingService(config)
|
|
62
|
+
this.conceptService = new ConceptService(config)
|
|
63
|
+
this.registryService = new RegistryService(config)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Returns an array of version strings for which upgrades are necessary.
|
|
67
|
+
getUpgrades(fromVersion, { forceLatest = false }) {
|
|
68
|
+
const list = []
|
|
69
|
+
fromVersion = Version.from(fromVersion)
|
|
70
|
+
|
|
71
|
+
for (let version of Object.getOwnPropertyNames(Upgrader).filter(/^[0-9.]+$/)) {
|
|
72
|
+
if (fromVersion.lt(version) || forceLatest && fromVersion.eq(version)) {
|
|
73
|
+
list.push(version)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return list
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Methods that perform upgrades necessary for that version
|
|
80
|
+
|
|
81
|
+
async "1.2.0"() {
|
|
82
|
+
// 1. Additional fields for schemes (full-text search)
|
|
83
|
+
console.log("Creating additional fields for schemes...")
|
|
84
|
+
const schemes = await Scheme.find().lean()
|
|
85
|
+
for (let scheme of schemes) {
|
|
86
|
+
addKeywords(scheme)
|
|
87
|
+
await Scheme.findByIdAndUpdate(scheme._id, scheme)
|
|
88
|
+
}
|
|
89
|
+
console.log("... done.")
|
|
90
|
+
// 2. Create indexes for concordances
|
|
91
|
+
console.log("Creating indexes for concordances...")
|
|
92
|
+
await this.concordanceService.createIndexes()
|
|
93
|
+
console.log("... done.")
|
|
94
|
+
// 3. Create indexes for schemes
|
|
95
|
+
console.log("Creating indexes for schemes...")
|
|
96
|
+
await this.schemeService.createIndexes()
|
|
97
|
+
console.log("... done.")
|
|
98
|
+
}
|
|
99
|
+
async "1.2.2"() {
|
|
100
|
+
// Update text search fields for schemes (full-text search)
|
|
101
|
+
console.log("Updating text search fields for schemes...")
|
|
102
|
+
const schemes = await Scheme.find().lean()
|
|
103
|
+
for (let scheme of schemes) {
|
|
104
|
+
addKeywords(scheme)
|
|
105
|
+
// Also add modified if it doesn't exist
|
|
106
|
+
scheme.modified = scheme.modified || scheme.created
|
|
107
|
+
await Scheme.findByIdAndUpdate(scheme._id, scheme)
|
|
108
|
+
}
|
|
109
|
+
console.log("... done.")
|
|
110
|
+
}
|
|
111
|
+
async "1.2.3"() {
|
|
112
|
+
// Update text search fields for schemes (full-text search)
|
|
113
|
+
console.log("Updating text search fields for schemes...")
|
|
114
|
+
const schemes = await Scheme.find().lean()
|
|
115
|
+
for (let scheme of schemes) {
|
|
116
|
+
addKeywords(scheme)
|
|
117
|
+
await Scheme.findByIdAndUpdate(scheme._id, scheme)
|
|
118
|
+
}
|
|
119
|
+
console.log("... done.")
|
|
120
|
+
}
|
|
121
|
+
async "1.2.7"() {
|
|
122
|
+
// 1. Update publisher keywords field for schemes
|
|
123
|
+
console.log("Updating publisher keywords fields for schemes...")
|
|
124
|
+
const schemes = await Scheme.find().lean()
|
|
125
|
+
for (let scheme of schemes) {
|
|
126
|
+
addKeywords(scheme)
|
|
127
|
+
await Scheme.findByIdAndUpdate(scheme._id, scheme)
|
|
128
|
+
}
|
|
129
|
+
console.log("... done.")
|
|
130
|
+
// 2. Create indexes for schemes
|
|
131
|
+
console.log("Creating indexes for schemes...")
|
|
132
|
+
await this.schemeService.createIndexes()
|
|
133
|
+
console.log("... done.")
|
|
134
|
+
}
|
|
135
|
+
async "1.3"() {
|
|
136
|
+
console.log("Creating indexes for annotations...")
|
|
137
|
+
await this.annotationService.createIndexes()
|
|
138
|
+
console.log("... done.")
|
|
139
|
+
}
|
|
140
|
+
async "1.4"() {
|
|
141
|
+
console.log("Concordances will be upgraded:")
|
|
142
|
+
console.log("- _id will be changed to notation (if available) or a new UUID")
|
|
143
|
+
console.log(`- URI will be adjusted to start with ${this.baseUrl}`)
|
|
144
|
+
console.log("- Previous URI(s) will be moved to identifier")
|
|
145
|
+
console.log("- Distribtions which are served from this instance are removed (will be added dynamically)")
|
|
146
|
+
const ok = await yesno({
|
|
147
|
+
question: "Is that okay?",
|
|
148
|
+
defaultValue: false,
|
|
149
|
+
})
|
|
150
|
+
if (!ok) {
|
|
151
|
+
throw new Error("Aborted due to missing user confirmation.")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let adjusted = 0
|
|
155
|
+
let failed = 0
|
|
156
|
+
let skipped = 0
|
|
157
|
+
const concordances = await Concordance.find().lean()
|
|
158
|
+
for (const concordance of concordances) {
|
|
159
|
+
const previous_id = concordance._id
|
|
160
|
+
if (!previous_id.startsWith("http")) {
|
|
161
|
+
skipped += 1
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
let _id = concordance.notation[0]
|
|
165
|
+
if (!_id) {
|
|
166
|
+
_id = uuid()
|
|
167
|
+
concordance.notation = [_id].concat((concordance.notation || []).slice(1))
|
|
168
|
+
}
|
|
169
|
+
const identifier = []
|
|
170
|
+
// Add previous _id to identifier
|
|
171
|
+
identifier.push(previous_id)
|
|
172
|
+
// Add previous URI if necessary
|
|
173
|
+
if (previous_id != concordance.uri) {
|
|
174
|
+
identifier.push(concordance.uri)
|
|
175
|
+
}
|
|
176
|
+
// Set new _id and URI, add previous to identifier
|
|
177
|
+
concordance._id = _id
|
|
178
|
+
concordance.uri = `${this.baseUrl}concordances/${_id}`
|
|
179
|
+
console.log(`- Updating concordance ${previous_id} to _id ${_id} (${concordance.uri})`)
|
|
180
|
+
concordance.identifier = (concordance.identifier || []).concat(identifier)
|
|
181
|
+
// Remove distributions that are served by jskos-server and instead add them dynamically
|
|
182
|
+
concordance.distributions = (concordance.distributions || []).filter(dist => !dist.download || !dist.download.startsWith(this.baseUrl))
|
|
183
|
+
if (!concordance.distributions.length) {
|
|
184
|
+
delete concordance.distributions
|
|
185
|
+
}
|
|
186
|
+
// Save concordance
|
|
187
|
+
try {
|
|
188
|
+
await Concordance.insertMany([concordance])
|
|
189
|
+
await Concordance.deleteOne({ _id: previous_id })
|
|
190
|
+
adjusted += 1
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(error)
|
|
193
|
+
failed += 1
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`- Adjusted: ${adjusted}, skipped: ${skipped}, failed: ${failed}.`)
|
|
198
|
+
console.log("... done")
|
|
199
|
+
if (failed > 0) {
|
|
200
|
+
throw new Error("Not all concordances could be adjusted. Please check the errors and try again.")
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async "1.4.5"() {
|
|
204
|
+
console.log("Upgrades to annotations (see #173):")
|
|
205
|
+
|
|
206
|
+
console.log("- Update indexes for annotations...")
|
|
207
|
+
await this.annotationService.createIndexes()
|
|
208
|
+
console.log("... done.")
|
|
209
|
+
|
|
210
|
+
console.log("- Annotations will be updated to use an object for property `target` and to include mapping state if possible...")
|
|
211
|
+
const ok = await yesno({
|
|
212
|
+
question: "Is that okay?",
|
|
213
|
+
defaultValue: false,
|
|
214
|
+
})
|
|
215
|
+
if (!ok) {
|
|
216
|
+
throw new Error("Aborted due to missing user confirmation.")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let updatedCount = 0
|
|
220
|
+
const annotations = await Annotation.find({ "target.state.id": { $exists: false } }).exec()
|
|
221
|
+
for (const annotation of annotations) {
|
|
222
|
+
const target = _.get(annotation, "target.id", annotation.target)
|
|
223
|
+
const mapping = await Mapping.findOne({ uri: target })
|
|
224
|
+
const contentId = mapping && (mapping.identifier || []).find(id => id.startsWith("urn:jskos:mapping:content:"))
|
|
225
|
+
const update = contentId ? {
|
|
226
|
+
target: {
|
|
227
|
+
id: target,
|
|
228
|
+
state: {
|
|
229
|
+
id: contentId,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
} : {
|
|
233
|
+
target: { id: target },
|
|
234
|
+
}
|
|
235
|
+
await Annotation.updateOne({ _id: annotation._id }, update)
|
|
236
|
+
updatedCount += 1
|
|
237
|
+
}
|
|
238
|
+
console.log(`... done (${updatedCount} annotations updated).`)
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
async "1.5.3"() {
|
|
242
|
+
// Create indexes for mappings with index for mappingRelevance
|
|
243
|
+
console.log("Creating indexes for mappings...")
|
|
244
|
+
await this.mappingService.createIndexes()
|
|
245
|
+
console.log("... done.")
|
|
246
|
+
}
|
|
247
|
+
async "1.6.3"() {
|
|
248
|
+
// Create compound indexes for mappings to create a stable sorting order
|
|
249
|
+
console.log("Creating indexes for mappings...")
|
|
250
|
+
await this.mappingService.createIndexes()
|
|
251
|
+
console.log("... done.")
|
|
252
|
+
}
|
|
253
|
+
async "2.0.3"() {
|
|
254
|
+
console.log("Adding missing `fromScheme`/`toScheme` fields to mappings...")
|
|
255
|
+
const mappings = await Mapping.aggregate([
|
|
256
|
+
{
|
|
257
|
+
$match: { $or: [{ "fromScheme.uri": { $exists: false } }, { "toScheme.uri": { $exists: false } }] },
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
$lookup: {
|
|
261
|
+
from: Concordance.collection.name,
|
|
262
|
+
localField: "partOf.0.uri",
|
|
263
|
+
foreignField: "uri",
|
|
264
|
+
as: "CONCORDANCE",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
])
|
|
268
|
+
let adjustedCount = 0
|
|
269
|
+
for (const mapping of mappings) {
|
|
270
|
+
const concordance = mapping.CONCORDANCE?.[0]
|
|
271
|
+
const _id = mapping._id
|
|
272
|
+
const hasFromScheme = !!mapping.fromScheme, hasToScheme = !!mapping.toScheme
|
|
273
|
+
delete mapping._id
|
|
274
|
+
delete mapping.CONCORDANCE
|
|
275
|
+
addMappingSchemes(mapping, { concordance })
|
|
276
|
+
if (!hasFromScheme && mapping.fromScheme || !hasToScheme && mapping.toScheme) {
|
|
277
|
+
await Mapping.replaceOne({ _id }, mapping)
|
|
278
|
+
adjustedCount += 1
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
console.log(`... done (${adjustedCount} out of ${mappings.length} were adjusted).`)
|
|
282
|
+
}
|
|
283
|
+
async "2.1.0"() {
|
|
284
|
+
console.log("Creating indexes for concepts and annotations...")
|
|
285
|
+
await this.conceptService.createIndexes()
|
|
286
|
+
await this.annotationService.createIndexes()
|
|
287
|
+
console.log("... done.")
|
|
288
|
+
}
|
|
289
|
+
async "2.1.6"() {
|
|
290
|
+
console.log("Rewriting concordance URIs for mappings...")
|
|
291
|
+
|
|
292
|
+
// Find all concordances with "identifier" set
|
|
293
|
+
const concordances = await Concordance.find({
|
|
294
|
+
"identifier.0": { $exists: true },
|
|
295
|
+
})
|
|
296
|
+
console.log(`- Found ${concordances.length} concordances where updates might be necessary.`)
|
|
297
|
+
|
|
298
|
+
let updatedConcordaces = 0, updatedMappings = 0
|
|
299
|
+
for (const concordance of concordances) {
|
|
300
|
+
// Update concordance URIs
|
|
301
|
+
const result = await Mapping.updateMany({
|
|
302
|
+
"partOf.0.uri": {
|
|
303
|
+
$in: concordance.identifier,
|
|
304
|
+
},
|
|
305
|
+
}, {
|
|
306
|
+
"partOf.0.uri": concordance.uri,
|
|
307
|
+
})
|
|
308
|
+
if (result.modifiedCount) {
|
|
309
|
+
updatedConcordaces += 1
|
|
310
|
+
updatedMappings += result.modifiedCount
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (concordances.length) {
|
|
314
|
+
console.log(`- Updated ${updatedMappings} mappings in ${updatedConcordaces} concordances.`)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log("... done.")
|
|
318
|
+
}
|
|
319
|
+
async "2.4.0"() {
|
|
320
|
+
console.log("Creating index for registries...")
|
|
321
|
+
await this.registryService.createIndexes()
|
|
322
|
+
console.log("... done.")
|
|
323
|
+
}
|
|
324
|
+
}
|