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,264 @@
1
+ import _ from "lodash"
2
+ import jskos from "jskos-tools"
3
+ import { validate } from "jskos-validate"
4
+
5
+ import { removeNullProperties } from "../utils/utils.js"
6
+ import { uuid } from "../utils/uuid.js"
7
+
8
+ import { Concordance } from "../models/concordances.js"
9
+ import { Mapping } from "../models/mappings.js"
10
+ import { SchemeService } from "./schemes.js"
11
+
12
+ const validateConcordance = validate.concordance
13
+
14
+ import { MalformedRequestError, EntityNotFoundError, InvalidBodyError, DatabaseAccessError } from "../errors/index.js"
15
+
16
+ import { AbstractService } from "./abstract.js"
17
+
18
+ export class ConcordanceService extends AbstractService {
19
+
20
+ constructor(config) {
21
+ super(config)
22
+ this.schemeService = new SchemeService(config)
23
+ this.uriBase = config.baseUrl + "concordances/"
24
+ this.model = Concordance
25
+ }
26
+
27
+ /**
28
+ * Return a Promise with an array of concordances.
29
+ */
30
+ async queryItems(query) {
31
+ let conditions = []
32
+ // Search by URI
33
+ if (query.uri) {
34
+ const uris = query.uri.split("|")
35
+ conditions.push({ $or: uris.map(uri => ({ uri: uri })).concat(uris.map(uri => ({ identifier: uri }))) })
36
+ }
37
+ // Search by fromScheme/toScheme (URI or notation)
38
+ for (let part of ["fromScheme", "toScheme"]) {
39
+ if (query[part]) {
40
+ let uris = []
41
+ for (let uriOrNotation of query[part].split("|")) {
42
+ let scheme = await this.schemeService.getScheme(uriOrNotation)
43
+ if (scheme) {
44
+ uris = uris.concat(scheme.uri, scheme.identifier || [])
45
+ } else {
46
+ uris = uris.concat(query[part])
47
+ }
48
+ }
49
+ conditions.push({ $or: uris.map(uri => ({ [`${part}.uri`]: uri })) })
50
+ }
51
+ }
52
+ // Search by creator
53
+ if (query.creator) {
54
+ const creators = query.creator.split("|")
55
+ conditions.push({
56
+ $or: _.flatten(creators.map(creator => [
57
+ jskos.isValidUri(creator) ? null : { "creator.prefLabel.de": new RegExp(_.escapeRegExp(creator), "i") },
58
+ jskos.isValidUri(creator) ? null : { "creator.prefLabel.en": new RegExp(_.escapeRegExp(creator), "i") },
59
+ jskos.isValidUri(creator) ? { "creator.uri": creator } : null,
60
+ ].filter(Boolean))),
61
+ })
62
+ }
63
+ // Set mode
64
+ let mode = query.mode
65
+ if (!["and", "or"].includes(mode)) {
66
+ mode = "and"
67
+ }
68
+
69
+ const mongoQuery = conditions.length ? { [`$${mode}`]: conditions } : {}
70
+
71
+ if (query.download) {
72
+ // For a download, return a stream
73
+ return this.model.find(mongoQuery).lean().cursor()
74
+ } else {
75
+ // Otherwise, return results
76
+ const { limit, offset } = this._getLimitAndOffset(query)
77
+ const concordances = await this.model.find(mongoQuery).lean().skip(offset).limit(limit).exec()
78
+ concordances.totalCount = await this._count(this.model, [{ $match: mongoQuery }])
79
+ return concordances
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Returns a promise with a single concordance with ObjectId in req.params._id.
85
+ */
86
+ async getItem(uriOrId) {
87
+ if (!uriOrId) {
88
+ throw new MalformedRequestError()
89
+ }
90
+ let result
91
+ // First look via ID
92
+ result = await this.model.findById(uriOrId).lean()
93
+ if (result) {
94
+ return result
95
+ }
96
+ // Then via URI
97
+ result = await this.model.findOne({ uri: uriOrId }).lean()
98
+ if (result) {
99
+ return result
100
+ }
101
+ // Then via identifier
102
+ result = await this.model.findOne({ identifier: uriOrId }).lean()
103
+ if (result) {
104
+ return result
105
+ }
106
+
107
+ throw new EntityNotFoundError(null, uriOrId)
108
+ }
109
+
110
+ async prepareAndCheckItemForAction(concordance, action, { bulk }) {
111
+ if (action !== "create") {
112
+ return concordance
113
+ }
114
+
115
+ // Add created and modified dates.
116
+ const now = (new Date()).toISOString()
117
+ if (!bulk || !concordance.created) {
118
+ concordance.created = now
119
+ }
120
+ concordance.modified = now
121
+ // Validate concordance
122
+ if (!validateConcordance(concordance)) {
123
+ throw new InvalidBodyError()
124
+ }
125
+ // Check if schemes are available and replace them with URI/notation only
126
+ await this.schemeService.replaceSchemeProperties(concordance, ["fromScheme", "toScheme"], false)
127
+
128
+ // _id and URI
129
+ delete concordance._id
130
+ if (concordance.uri) {
131
+ let uri = concordance.uri
132
+ // URI already exists, use if it's valid, otherwise move to identifier
133
+ if (uri.startsWith(this.uriBase)) {
134
+ concordance._id = uri.slice(this.uriBase.length, uri.length)
135
+ concordance.notation = [concordance._id].concat((concordance.notation || []).slice(1))
136
+ } else {
137
+ concordance.identifier = (concordance.identifier || []).concat([uri])
138
+ }
139
+ }
140
+ if (!concordance._id) {
141
+ concordance._id = concordance.notation && concordance.notation[0] || uuid()
142
+ concordance.uri = this.uriBase + concordance._id
143
+ concordance.notation = [concordance._id].concat((concordance.notation || []).slice(1))
144
+ }
145
+ // Extent should be 0 when added; will be updated in postAdjustmentForConcordance whenever there are changes
146
+ concordance.extent = "0"
147
+
148
+ return concordance
149
+ }
150
+
151
+ async putConcordance({ body, existing }) {
152
+ let concordance = body
153
+ if (!concordance) {
154
+ throw new InvalidBodyError()
155
+ }
156
+
157
+ // Override some properties from existing that shouldn't change
158
+ // TODO: Should we throw errors if user tries to change these?
159
+ for (const prop of ["_id", "uri", "notation", "fromScheme", "toScheme", "created"]) {
160
+ concordance[prop] = existing[prop]
161
+ }
162
+ // Add modified date.
163
+ concordance.modified = (new Date()).toISOString()
164
+ if (existing.extent) {
165
+ concordance.extent = existing.extent
166
+ } else {
167
+ delete concordance.extent
168
+ }
169
+ if (existing.distributions) {
170
+ concordance.distributions = existing.distributions
171
+ } else {
172
+ delete concordance.distributions
173
+ }
174
+ // Validate concordance
175
+ if (!validateConcordance(concordance)) {
176
+ throw new InvalidBodyError()
177
+ }
178
+
179
+ const result = await this.model.replaceOne({ _id: existing._id }, concordance)
180
+ if (result.acknowledged && result.matchedCount) {
181
+ await this.postAdjustmentForConcordance(existing._id)
182
+ return concordance
183
+ } else {
184
+ throw new DatabaseAccessError()
185
+ }
186
+ }
187
+
188
+ async patchConcordance({ body, existing }) {
189
+ if (!body) {
190
+ throw new InvalidBodyError()
191
+ }
192
+
193
+ // Certain properties that shouldn't change
194
+ let errorMessage = ""
195
+ for (const prop of ["_id", "uri", "notation", "fromScheme", "toScheme", "created", "extent", "distributions"]) {
196
+ if (body[prop]) {
197
+ errorMessage += `Field \`${prop}\` can't be changed via PATCH. `
198
+ }
199
+ }
200
+ if (errorMessage) {
201
+ throw new InvalidBodyError(errorMessage)
202
+ }
203
+
204
+ let concordance = body
205
+
206
+ // Add modified date.
207
+ concordance.modified = (new Date()).toISOString()
208
+
209
+ // Use lodash merge to merge concordance objects
210
+ _.assign(existing, concordance)
211
+
212
+ removeNullProperties(existing)
213
+
214
+ // Validate concordance after merge
215
+ if (!validateConcordance(existing)) {
216
+ throw new InvalidBodyError()
217
+ }
218
+
219
+ const result = await this.model.replaceOne({ _id: existing._id }, existing)
220
+ if (result.acknowledged) {
221
+ await this.postAdjustmentForConcordance(existing._id)
222
+ return existing
223
+ } else {
224
+ throw new DatabaseAccessError()
225
+ }
226
+ }
227
+
228
+ async deleteItem({ existing }) {
229
+ const count = await this.getMappingsCountForConcordance(existing)
230
+ if (count > 0) {
231
+ throw new MalformedRequestError(`Can't delete a concordance that still has mappings associated with it (${count} mappings).`)
232
+ }
233
+ super.deleteItem({ existing })
234
+ }
235
+
236
+ async getMappingsCountForConcordance(concordance) {
237
+ const uris = [concordance.uri].concat(concordance.identifier || [])
238
+ return await Mapping.countDocuments({ $or: uris.map(uri => ({ "partOf.uri": uri })) })
239
+ }
240
+
241
+ async postAdjustmentForConcordance(uriOrId) {
242
+ try {
243
+ const concordance = await this.getItem(uriOrId)
244
+ const count = await this.getMappingsCountForConcordance(concordance)
245
+ if (`${count}` !== concordance.extent) {
246
+ // Update extent with new count
247
+ await this.model.updateOne({ _id: concordance._id }, { extent: `${count}`, modified: (new Date()).toISOString() })
248
+ }
249
+ } catch (error) {
250
+ this.error(error)
251
+ }
252
+ }
253
+
254
+ async createIndexes() {
255
+ await this._createIndexes([
256
+ [{ uri: 1 }, {}],
257
+ [{ identifier: 1 }, {}],
258
+ [{ notation: 1 }, {}],
259
+ [{ "fromScheme.uri": 1 }, {}],
260
+ [{ "toScheme.uri": 1 }, {}],
261
+ ])
262
+ }
263
+
264
+ }
@@ -0,0 +1,30 @@
1
+ import { models } from "../models/index.js"
2
+ import { AbstractService } from "./abstract.js"
3
+ import { createAdjuster } from "../utils/adjust.js"
4
+
5
+ export class DataService extends AbstractService {
6
+ constructor(config) {
7
+ super(config)
8
+ this.adjust = createAdjuster(config)
9
+ }
10
+
11
+ async getData(req) {
12
+ const uris = req.query.uri?.split("|") ?? []
13
+ return [].concat(...await Promise.all(Object.keys(models).map(async type => {
14
+ // Don't return data the user is not authorized to read
15
+ if (!req.isAuthorizedFor({ type: `${type}s`, action: "read" })) {
16
+ return []
17
+ }
18
+ const model = models[type]
19
+ const prop = model.schema.paths.id ? "id" : "uri"
20
+ const results = await model.find({
21
+ $or: [
22
+ { [prop]: { $in: uris } },
23
+ { identifier: { $in: uris } },
24
+ ],
25
+ }).lean()
26
+ // Return adjusted data (needs to be done separately for each data type)
27
+ return this.adjust.data({ req, data: results, type: `${type}s` })
28
+ })))
29
+ }
30
+ }
@@ -0,0 +1,34 @@
1
+ import { AnnotationService } from "./annotations.js"
2
+ import { ConceptService } from "./concepts.js"
3
+ import { ConcordanceService } from "./concordances.js"
4
+ import { MappingService } from "./mappings.js"
5
+ import { SchemeService } from "./schemes.js"
6
+ import { RegistryService } from "./registries.js"
7
+
8
+ export function createServices(config) {
9
+ const annotationService = new AnnotationService(config)
10
+ const conceptService = new ConceptService(config)
11
+ const concordanceService = new ConcordanceService(config)
12
+ const mappingService = new MappingService(config)
13
+ const schemeService = new SchemeService(config)
14
+ const registryService = new RegistryService(config)
15
+
16
+ const services = {
17
+ scheme: schemeService,
18
+ concept: conceptService,
19
+ concordance: concordanceService,
20
+ mapping: mappingService,
21
+ annotation: annotationService,
22
+ registry: registryService,
23
+ }
24
+
25
+ for (let type of Object.keys(services)) {
26
+ const plural = type === "registry" ? "registries" : `${type}s`
27
+
28
+ Object.defineProperty(services, plural, {
29
+ get: () => services[type],
30
+ })
31
+ }
32
+
33
+ return services
34
+ }