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.
Files changed (112) hide show
  1. package/.dockerignore +20 -0
  2. package/.editorconfig +9 -0
  3. package/.github/workflows/docker.yml +59 -0
  4. package/.github/workflows/gh-pages.yml +23 -0
  5. package/.github/workflows/gh-release.yml +19 -0
  6. package/.github/workflows/test.yml +39 -0
  7. package/.husky/pre-commit +1 -0
  8. package/CHANGELOG.md +18 -0
  9. package/LICENSE +21 -0
  10. package/README.md +2710 -0
  11. package/bin/extra.js +81 -0
  12. package/bin/import.js +438 -0
  13. package/bin/mongodb.js +21 -0
  14. package/bin/reset.js +257 -0
  15. package/bin/upgrade.js +34 -0
  16. package/config/config.default.json +88 -0
  17. package/config/config.schema.json +877 -0
  18. package/config/config.test.json +107 -0
  19. package/config/index.js +77 -0
  20. package/config/setup.js +212 -0
  21. package/depdendencies.png +0 -0
  22. package/docker/.env +1 -0
  23. package/docker/Dockerfile +20 -0
  24. package/docker/README.md +175 -0
  25. package/docker/docker-compose.yml +29 -0
  26. package/docker/docker-entrypoint.sh +8 -0
  27. package/docker/mongo-initdb.d/mongo_setup.sh +22 -0
  28. package/ecosystem.example.json +7 -0
  29. package/errors/index.js +94 -0
  30. package/eslint.config.js +17 -0
  31. package/index.js +10 -0
  32. package/models/annotations.js +13 -0
  33. package/models/concepts.js +12 -0
  34. package/models/concordances.js +12 -0
  35. package/models/index.js +33 -0
  36. package/models/mappings.js +20 -0
  37. package/models/meta.js +21 -0
  38. package/models/registries.js +12 -0
  39. package/models/schemes.js +12 -0
  40. package/package.json +91 -0
  41. package/routes/annotations.js +83 -0
  42. package/routes/common.js +86 -0
  43. package/routes/concepts.js +64 -0
  44. package/routes/concordances.js +86 -0
  45. package/routes/data.js +19 -0
  46. package/routes/mappings.js +108 -0
  47. package/routes/registries.js +24 -0
  48. package/routes/schemes.js +72 -0
  49. package/routes/validate.js +37 -0
  50. package/server.js +190 -0
  51. package/services/abstract.js +328 -0
  52. package/services/annotations.js +237 -0
  53. package/services/concepts.js +459 -0
  54. package/services/concordances.js +264 -0
  55. package/services/data.js +30 -0
  56. package/services/index.js +34 -0
  57. package/services/mappings.js +978 -0
  58. package/services/registries.js +319 -0
  59. package/services/schemes.js +318 -0
  60. package/services/validate.js +39 -0
  61. package/status.schema.json +145 -0
  62. package/test/abstract-service.js +36 -0
  63. package/test/annotations/annotation.json +13 -0
  64. package/test/api.js +2481 -0
  65. package/test/chai.js +14 -0
  66. package/test/changes.js +179 -0
  67. package/test/concepts/conceptNoFileEnding +4 -0
  68. package/test/concepts/concepts-ddc-6-60-61-62.json +123 -0
  69. package/test/concordances/concordances.ndjson +2 -0
  70. package/test/config.js +26 -0
  71. package/test/configs/complex-config.json +90 -0
  72. package/test/configs/empty-object.json +1 -0
  73. package/test/configs/fail-array.json +1 -0
  74. package/test/configs/fail-empty.json +0 -0
  75. package/test/configs/fail-mapping-only-props1.json +5 -0
  76. package/test/configs/fail-mapping-only-props2.json +5 -0
  77. package/test/configs/fail-mapping-only-props3.json +5 -0
  78. package/test/configs/fail-mapping-only-props4.json +5 -0
  79. package/test/configs/fail-nonexisting-prop.json +3 -0
  80. package/test/configs/fail-port-string.json +3 -0
  81. package/test/configs/fail-registry-types.json +16 -0
  82. package/test/configs/registry-types.json +16 -0
  83. package/test/data-write.js +784 -0
  84. package/test/eslint.js +22 -0
  85. package/test/import-reset.js +287 -0
  86. package/test/infer-mappings.js +340 -0
  87. package/test/ipcheck.js +287 -0
  88. package/test/mappings/README.md +1 -0
  89. package/test/mappings/ddc-gnd-1.mapping.json +33 -0
  90. package/test/mappings/ddc-gnd-2.mapping.json +67 -0
  91. package/test/mappings/mapping-ddc-gnd-noScheme.json +145 -0
  92. package/test/mappings/mapping-ddc-gnd.json +175 -0
  93. package/test/mappings/mappings-ddc.json +214 -0
  94. package/test/registries/registries.ndjson +2 -0
  95. package/test/services.js +557 -0
  96. package/test/terminologies/terminologies.json +94 -0
  97. package/test/test-utils.js +182 -0
  98. package/test/utils.js +425 -0
  99. package/test/validate.js +226 -0
  100. package/utils/adjust.js +206 -0
  101. package/utils/auth.js +154 -0
  102. package/utils/changes.js +88 -0
  103. package/utils/db.js +106 -0
  104. package/utils/ipcheck.js +76 -0
  105. package/utils/middleware.js +636 -0
  106. package/utils/searchHelper.js +153 -0
  107. package/utils/status.js +77 -0
  108. package/utils/users.js +7 -0
  109. package/utils/utils.js +114 -0
  110. package/utils/uuid.js +6 -0
  111. package/utils/version.js +324 -0
  112. 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
+ }
@@ -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
@@ -0,0 +1,7 @@
1
+ export const getUrisOfUser = user => {
2
+ if (!user) {
3
+ return []
4
+ }
5
+ return [user.uri].concat(Object.values(user.identities || {})
6
+ .map(identity => identity.uri)).filter(uri => uri)
7
+ }
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
@@ -0,0 +1,6 @@
1
+ import { v4 as uuid } from "uuid"
2
+
3
+ const uuidRegex = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i)
4
+ const isValidUuid = id => id.match(uuidRegex) != null
5
+
6
+ export { uuid, isValidUuid }
@@ -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
+ }