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,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
|
+
}
|
package/services/data.js
ADDED
|
@@ -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
|
+
}
|