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,557 @@
1
+ import jskos from "jskos-tools"
2
+ import _ from "lodash"
3
+ import assert from "node:assert"
4
+
5
+ import { teardownInMemoryMongo, setupInMemoryMongo, createCollectionsAndIndexes, assertIndexes, assertMongoDB, dropDatabaseBeforeAndAfter, arrayToStream } from "./test-utils.js"
6
+
7
+ import { InvalidBodyError } from "../errors/index.js"
8
+
9
+ import { createServices } from "../index.js"
10
+ import config from "../config/config.test.json" with { type: "json" }
11
+ const services = createServices(config)
12
+
13
+ describe("Services Features", () => {
14
+ before(async () => {
15
+ const mongoUri = await setupInMemoryMongo({ replSet: false })
16
+ process.env.MONGO_URI = mongoUri
17
+ await createCollectionsAndIndexes()
18
+ await assertIndexes()
19
+ })
20
+
21
+ after(async () => {
22
+ // close server if you started one
23
+ await teardownInMemoryMongo()
24
+ })
25
+
26
+ // 🗑 Drop DB before *and* after every single `it()` in this file
27
+ dropDatabaseBeforeAndAfter()
28
+
29
+ // 🔌 Sanity‐check that mongoose really is connected
30
+ assertMongoDB()
31
+
32
+ Object.keys(services).forEach(type => {
33
+ it(`should return an empty array for ${type} queryItems`, async () => {
34
+ const entities = await services[type].queryItems({ limit: 1, offset: 0 })
35
+ assert.strictEqual(entities.length, 0)
36
+ })
37
+ })
38
+
39
+ describe("Concordance Service", () => {
40
+
41
+ const fromScheme = { uri: "test:fromScheme" }
42
+ const toScheme = { uri: "test:toScheme" }
43
+
44
+ it("should post schemes for testing concordances", async () => {
45
+ await services.scheme.createItem({ bodyStream: await arrayToStream([fromScheme, toScheme]) })
46
+ })
47
+
48
+ it("should post a concordance with a named contributor, then remove the contributor name, then remove the contributor", async () => {
49
+ const concordance = {
50
+ fromScheme,
51
+ toScheme,
52
+ contributor: [{
53
+ uri: "test:user",
54
+ prefLabel: { de: "test" },
55
+ }],
56
+ }
57
+ const [postedConcordance] = await services.concordance.createItem({ bodyStream: await arrayToStream([concordance]) })
58
+ const patch = {
59
+ contributor: [{
60
+ uri: "test:user",
61
+ }],
62
+ }
63
+ const patchedConcordance = await services.concordance.patchConcordance({ body: patch, existing: postedConcordance })
64
+ assert.ok(!patchedConcordance.contributor[0].prefLabel, "PATCH requests should merge objects only on the top level.")
65
+ const patch2 = {
66
+ contributor: null,
67
+ }
68
+ const patchedConcordance2 = await services.concordance.patchConcordance({ body: patch2, existing: patchedConcordance })
69
+ assert.ok(patchedConcordance2.contributor === undefined, "A field should be removed when set to `null`.")
70
+ })
71
+
72
+ for (const scheme of [fromScheme, toScheme]) {
73
+ it("should delete scheme after testing concordances", async () => {
74
+ scheme._id = scheme.uri
75
+ scheme.concepts = []
76
+ await services.scheme.deleteItem({ uri: scheme.uri, existing: scheme })
77
+ })
78
+ }
79
+
80
+ })
81
+
82
+ describe("Mapping Service", () => {
83
+
84
+ describe("filter mappings by annotations", () => {
85
+ const mappings = [
86
+ {
87
+ uri: "mapping:1",
88
+ },
89
+ {
90
+ uri: "mapping:2",
91
+ },
92
+ {
93
+ uri: "mapping:3",
94
+ },
95
+ {
96
+ to: { memberSet: [{ uri: "urn:test:concept" }] },
97
+ uri: "mapping:4",
98
+ },
99
+ {
100
+ uri: "mapping:5",
101
+ },
102
+ ].map(mapping => {
103
+ // Add fromScheme and toScheme
104
+ mapping.fromScheme = { uri: "urn:test:fromScheme" }
105
+ mapping.toScheme = { uri: "urn:test:toScheme" }
106
+ // Add from if necessary
107
+ if (!mapping.from) {
108
+ mapping.from = { memberSet: [{ uri: "urn:test:fromConcept" }] }
109
+ }
110
+ // Add empty to if necessary
111
+ if (!mapping.to) {
112
+ mapping.to = { memberSet: [] }
113
+ }
114
+ return mapping
115
+ })
116
+ const annotations = [
117
+ {
118
+ target: "mapping:1",
119
+ motivation: "assessing",
120
+ bodyValue: "+1",
121
+ },
122
+ {
123
+ target: "mapping:1",
124
+ motivation: "assessing",
125
+ bodyValue: "-1",
126
+ },
127
+ {
128
+ target: "mapping:2",
129
+ motivation: "moderating",
130
+ creator: {
131
+ id: "urn:test:creator",
132
+ },
133
+ },
134
+ {
135
+ target: "mapping:3",
136
+ motivation: "moderating",
137
+ },
138
+ {
139
+ target: "mapping:4",
140
+ motivation: "assessing",
141
+ bodyValue: "+1",
142
+ },
143
+ ]
144
+
145
+ it("should post mappings and annotations used for tests", async () => {
146
+ let result
147
+ // Mappings
148
+ result = await services.mapping.createItem({ bodyStream: await arrayToStream(mappings) })
149
+ assert.strictEqual(result.length, mappings.length)
150
+ mappings.forEach(mapping => {
151
+ // Find corresponding mapping and set identifier/uri
152
+ const corresponding = result.find(m => jskos.compare(m, mapping))
153
+ assert.ok(!!corresponding)
154
+ mapping.identifier = corresponding.identifier
155
+ mapping.uri = corresponding.uri
156
+ })
157
+ // Annotations
158
+ // First adjust targets with actual URIs
159
+ annotations.forEach(annotation => {
160
+ const mapping = mappings.find(m => jskos.compare(m, { uri: annotation.target }))
161
+ assert.ok(!!mapping)
162
+ annotation.target = mapping.uri
163
+ })
164
+ result = await services.annotation.createItem({ bodyStream: await arrayToStream(annotations), admin: true })
165
+ assert.strictEqual(result.length, annotations.length)
166
+ })
167
+
168
+ it("should get correct number of mappings when using annotatedWith param", async () => {
169
+ const annotatedWith = "-1"
170
+ const result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedWith })
171
+ const expected = _.uniq(annotations.filter(a => a.bodyValue === annotatedWith).map(a => a.target)).sort()
172
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
173
+ })
174
+
175
+ it("should get the correct mappings when using annotatedWith param with comparison operator", async () => {
176
+ for (let { from, to, annotatedWith } of [
177
+ { to: "urn:test:concept", annotatedWith: ">=1" },
178
+ { from: "urn:test:fromConcept", annotatedWith: "=0" },
179
+ ]) {
180
+ const result = await services.mapping.queryItems({ limit: 100, offset: 0, from, to, annotatedWith })
181
+ // Replace = with == for later evaluation
182
+ if (annotatedWith.startsWith("=")) {
183
+ annotatedWith = `=${annotatedWith}`
184
+ }
185
+ const expected = mappings.filter(m => {
186
+ if (from && from !== _.get(m, "from.memberSet[0].uri") || to && to !== _.get(m, "to.memberSet[0].uri")) {
187
+ return false
188
+ }
189
+ const annotationSum = annotations.filter(a => a.target === m.uri).reduce((prev, cur) => {
190
+ if (cur.motivation !== "assessing") {
191
+ return prev
192
+ }
193
+ return prev + parseInt(cur.bodyValue)
194
+ }, 0)
195
+ return Function(`"use strict";return (${annotationSum}${annotatedWith})`)()
196
+ })
197
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected.map(e => e.uri).sort())
198
+ }
199
+ })
200
+
201
+ it("should get correct number of mappings when using annotatedFor param", async () => {
202
+ const annotatedFor = "assessing"
203
+ const result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedFor })
204
+ const expected = _.uniq(annotations.filter(a => a.motivation === annotatedFor).map(a => a.target)).sort()
205
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
206
+ })
207
+
208
+ it("should get correct number of mappings when using annotatedFor param with value `any`", async () => {
209
+ const annotatedFor = "any"
210
+ const result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedFor })
211
+ const expected = _.uniq(annotations.map(a => a.target)).sort()
212
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
213
+ })
214
+
215
+ it("should get correct number of mappings when using annotatedFor param with value `none`", async () => {
216
+ const annotatedFor = "none"
217
+ const result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedFor })
218
+ const expected = mappings.map(m => m.uri).filter(uri => !annotations.find(a => a.target === uri)).sort()
219
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
220
+ })
221
+
222
+ it("should get correct number of mappings when using annotatedFor param with negative value", async () => {
223
+ const annotatedFor = "!assessing"
224
+ const result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedFor })
225
+ const expected = mappings.map(m => m.uri).filter(uri => !annotations.find(a => a.target === uri && a.motivation === annotatedFor.slice(1))).sort()
226
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
227
+ })
228
+
229
+ it("should get correct number of mappings when using annotatedBy param", async () => {
230
+ const annotatedBy = "urn:test:creator|urn:other:uri"
231
+ const result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedBy })
232
+ const expected = _.uniq(annotations.filter(a => annotatedBy.split("|").includes(_.get(a, "creator.id"))).map(a => a.target)).sort()
233
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
234
+ })
235
+
236
+ it("should get correct number of mappings when using annotatedFor together with annotatedBy", async () => {
237
+ let result, expected
238
+ const annotatedFor = "moderating"
239
+ // First only annotatedFor
240
+ result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedFor })
241
+ expected = _.uniq(annotations.filter(a => a.motivation === annotatedFor).map(a => a.target)).sort()
242
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
243
+ // Then with annotatedBy
244
+ const annotatedBy = "urn:test:creator|urn:other:uri"
245
+ result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedFor, annotatedBy })
246
+ expected = _.uniq(annotations.filter(a => annotatedBy.split("|").includes(_.get(a, "creator.id")) && a.motivation === annotatedFor).map(a => a.target)).sort()
247
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
248
+ })
249
+
250
+ it("should get correct number of mappings when using annotatedWith together with to", async () => {
251
+ let result, expected
252
+ const annotatedWith = "+1"
253
+ // First only annotatedWith
254
+ result = await services.mapping.queryItems({ limit: 10, offset: 0, annotatedWith })
255
+ expected = _.uniq(annotations.filter(a => a.bodyValue === annotatedWith).map(a => a.target)).sort()
256
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected)
257
+ // Then with to
258
+ const to = "urn:test:concept"
259
+ result = await services.mapping.queryItems({ limit: 10, offset: 0, to, annotatedWith })
260
+ expected = mappings.filter(m => jskos.isContainedIn({ uri: to }, jskos.conceptsOfMapping(m, "to")) && annotations.find(a => a.target === m.uri && a.bodyValue === annotatedWith))
261
+ assert.deepStrictEqual(result.map(r => r.uri).sort(), expected.map(r => r.uri).sort())
262
+ })
263
+
264
+ it("should reject mappings without fromScheme/toScheme", async () => {
265
+ for (const mapping of [
266
+ {
267
+ from: {
268
+ memberSet: [{
269
+ uri: "urn:test:concept",
270
+ }],
271
+ },
272
+ to: {
273
+ memberSet: [{
274
+ uri: "urn:test:concept",
275
+ }],
276
+ },
277
+ toScheme: { uri: "urn:test:toScheme" },
278
+ },
279
+ {
280
+ from: {
281
+ memberSet: [{
282
+ uri: "urn:test:concept",
283
+ }],
284
+ },
285
+ fromScheme: { uri: "urn:test:fromScheme" },
286
+ to: {
287
+ memberSet: [{
288
+ uri: "urn:test:concept",
289
+ }],
290
+ },
291
+ },
292
+ ]) {
293
+ try {
294
+ await services.mapping.createItem({ bodyStream: await arrayToStream([mapping]) })
295
+ assert.fail("Expected createItem to fail")
296
+ } catch (error) {
297
+ assert.ok(error instanceof InvalidBodyError)
298
+ }
299
+ }
300
+ })
301
+
302
+ it("should add fromScheme and toScheme fields from concepts' inScheme property", async () => {
303
+ const mapping = {
304
+ from: {
305
+ memberSet: [{
306
+ uri: "urn:test:concept",
307
+ inScheme: [{ uri: "urn:test:fromScheme" }],
308
+ }],
309
+ },
310
+ to: {
311
+ memberSet: [{
312
+ uri: "urn:test:concept",
313
+ inScheme: [{ uri: "urn:test:toScheme" }],
314
+ }],
315
+ },
316
+ }
317
+ const postedMapping = (await services.mapping.createItem({ bodyStream: await arrayToStream([mapping]) }))?.[0]
318
+ assert.deepStrictEqual(postedMapping.fromScheme?.uri, mapping.from.memberSet[0].inScheme[0].uri)
319
+ assert.deepStrictEqual(postedMapping.toScheme?.uri, mapping.to.memberSet[0].inScheme[0].uri)
320
+ })
321
+ })
322
+
323
+ })
324
+
325
+ describe("Annotation Service", () => {
326
+
327
+ const mismatchTagScheme = {
328
+ uri: "https://uri.gbv.de/terminology/mismatch/",
329
+ }
330
+ const mismatchTagConcept = {
331
+ uri: "https://uri.gbv.de/terminology/mismatch/test",
332
+ inScheme: [{uri: "https://uri.gbv.de/terminology/mismatch/"}],
333
+ }
334
+
335
+ it("should post tag mismatch scheme and concepts", async () => {
336
+ await services.scheme.createItem({ bodyStream: await arrayToStream([mismatchTagScheme]) })
337
+ await services.concept.createItem({ bodyStream: await arrayToStream([mismatchTagConcept]) })
338
+ const concept = await services.concept.getItem(mismatchTagConcept.uri)
339
+ assert.strictEqual(concept?.uri, mismatchTagConcept.uri)
340
+ })
341
+
342
+ it("should post negative assessment annotation that is correctly tagged", async () => {
343
+ const annotation = {
344
+ target: "abc:def",
345
+ bodyValue: "-1",
346
+ body: [
347
+ {
348
+ type: "SpecificResource",
349
+ value: mismatchTagConcept.uri,
350
+ purpose: "tagging",
351
+ },
352
+ ],
353
+ }
354
+ const results = await services.annotation.createItem({ bodyStream: await arrayToStream([annotation]) })
355
+ assert.ok(results?.[0]?.id)
356
+ })
357
+
358
+ it("should not post negative assessment annotation that is correctly tagged with a URI that is not explicitly allowed", async () => {
359
+ const annotation = {
360
+ target: "abc:def",
361
+ bodyValue: "-1",
362
+ body: [
363
+ {
364
+ type: "SpecificResource",
365
+ value: mismatchTagConcept.uri + "2",
366
+ purpose: "tagging",
367
+ },
368
+ ],
369
+ }
370
+ try {
371
+ await services.annotation.createItem({ bodyStream: await arrayToStream([annotation]) })
372
+ assert.fail("No error was thrown even though it was expected.")
373
+ } catch (error) {
374
+ assert.ok(error instanceof InvalidBodyError)
375
+ }
376
+ })
377
+
378
+ it("should not post positive assessment annotation that is tagged", async () => {
379
+ const annotation = {
380
+ target: "abc:def",
381
+ bodyValue: "+1",
382
+ body: [
383
+ {
384
+ type: "SpecificResource",
385
+ value: mismatchTagConcept.uri,
386
+ purpose: "tagging",
387
+ },
388
+ ],
389
+ }
390
+ try {
391
+ await services.annotation.createItem({ bodyStream: await arrayToStream([annotation]) })
392
+ assert.fail("No error was thrown even though it was expected.")
393
+ } catch (error) {
394
+ assert.ok(error instanceof InvalidBodyError)
395
+ }
396
+ })
397
+
398
+ it("should require `body` to be an array", async () => {
399
+ const annotation = {
400
+ target: "abc:def",
401
+ bodyValue: "-1",
402
+ body: {
403
+ type: "SpecificResource",
404
+ value: mismatchTagConcept.uri,
405
+ purpose: "tagging",
406
+ },
407
+ }
408
+ try {
409
+ await services.annotation.createItem({ bodyStream: await arrayToStream([annotation]) })
410
+ assert.fail("No error was thrown even though it was expected.")
411
+ } catch (error) {
412
+ assert.ok(error instanceof InvalidBodyError)
413
+ }
414
+ })
415
+
416
+ it("should not negative assessment annotation that is tagged incorrectly (1)", async () => {
417
+ const annotation = {
418
+ target: "abc:def",
419
+ bodyValue: "-1",
420
+ body: [
421
+ {
422
+ type: "SpecificResources",
423
+ value: mismatchTagConcept.uri,
424
+ purpose: "tagging",
425
+ },
426
+ ],
427
+ }
428
+ try {
429
+ await services.annotation.createItem({ bodyStream: await arrayToStream([annotation]) })
430
+ assert.fail("No error was thrown even though it was expected.")
431
+ } catch (error) {
432
+ assert.ok(error instanceof InvalidBodyError)
433
+ }
434
+ })
435
+
436
+ it("should not negative assessment annotation that is tagged incorrectly (2)", async () => {
437
+ const annotation = {
438
+ target: "abc:def",
439
+ bodyValue: "-1",
440
+ body: [
441
+ {
442
+ type: "SpecificResource",
443
+ value: mismatchTagConcept.uri,
444
+ purpose: "tag",
445
+ },
446
+ ],
447
+ }
448
+ try {
449
+ await services.annotation.createItem({ bodyStream: await arrayToStream([annotation]) })
450
+ assert.fail("No error was thrown even though it was expected.")
451
+ } catch (error) {
452
+ assert.ok(error instanceof InvalidBodyError)
453
+ }
454
+ })
455
+
456
+ })
457
+
458
+ describe("Registry Service", () => {
459
+
460
+ const registryExample = {
461
+ uri: "urn:test:registry:1",
462
+ notation: ["ERMS"],
463
+ prefLabel: { en: "Example Registry" },
464
+ definition: { en: ["Example definition mentioning ERMS."] },
465
+ url: "https://example.org/registry/1",
466
+ }
467
+
468
+ it("should post a registry and return with uri", async () => {
469
+ const result = await services.registry.createItem({
470
+ bodyStream: await arrayToStream([registryExample]),
471
+ })
472
+ assert.ok(result.length === 1)
473
+ assert.ok(result[0].uri)
474
+ })
475
+
476
+ it("should get a registry by id/uri after posting", async () => {
477
+ const doc = await services.registry.getItem(registryExample.uri)
478
+ assert.strictEqual(doc?.uri, registryExample.uri)
479
+ })
480
+
481
+ it("should patch a registry and remove a field when set to null", async () => {
482
+ const existing = await services.registry.getItem(registryExample.uri)
483
+
484
+ // Add a field and verify it exists
485
+ const patched1 = await services.registry.patchRegistry({
486
+ existing,
487
+ body: { publisher: [{ uri: "urn:test:publisher", prefLabel: { en: "Pub" } }] },
488
+ })
489
+ assert.ok(patched1.publisher?.length)
490
+
491
+ // Now remove field and verify it is removed
492
+ const patched2 = await services.registry.patchRegistry({
493
+ existing: patched1,
494
+ body: { publisher: null },
495
+ })
496
+ assert.ok(patched2.publisher === undefined, "A field should be removed when set to `null`.")
497
+ })
498
+
499
+ it("should put a registry and preserve immutable fields while updating", async () => {
500
+ const existing = await services.registry.getItem(registryExample.uri)
501
+
502
+ const body = {
503
+ // intentionally omit _id/id/created; service should preserve/override them
504
+ uri: registryExample.uri,
505
+ prefLabel: { en: "Example Registry Updated" },
506
+ notation: ["ERMS"],
507
+ }
508
+
509
+ const updated = await services.registry.updateItem({ body, existing })
510
+ assert.strictEqual(updated.id, existing.id)
511
+ assert.strictEqual(updated.created, existing.created)
512
+ assert.ok(updated.modified, "modified should be set")
513
+ assert.strictEqual(updated.prefLabel?.en, "Example Registry Updated")
514
+ })
515
+
516
+ it("should delete a registry", async () => {
517
+ const existing = await services.registry.getItem(registryExample.uri)
518
+ await services.registry.deleteItem({ existing })
519
+ try {
520
+ await services.registry.getItem(registryExample.uri)
521
+ assert.fail("Expected getItem to fail after delete")
522
+ } catch (error) {
523
+ assert.ok(error)
524
+ }
525
+ })
526
+
527
+ it("should reject invalid registry bodies", async () => {
528
+ try {
529
+ await services.registry.createItem({
530
+ bodyStream: await arrayToStream([{uri:42}]),
531
+ bulk: false,
532
+ })
533
+ assert.fail("Expected createItem to fail")
534
+ } catch (error) {
535
+ assert.ok(error instanceof InvalidBodyError)
536
+ }
537
+ })
538
+
539
+ it("should filter out invalid registry bodies in bulk mode", async () => {
540
+ const items = [
541
+ { uri: "registry:1" },
542
+ { uri: 42 },
543
+ ]
544
+ try {
545
+ const res = await services.registry.createItem({
546
+ bodyStream: await arrayToStream(items),
547
+ bulk: true,
548
+ })
549
+ assert.deepStrictEqual(res, [ { uri: "registry:1" } ])
550
+ } catch (error) {
551
+ assert.fail("Should not fail")
552
+ }
553
+ })
554
+
555
+ })
556
+
557
+ })
@@ -0,0 +1,94 @@
1
+ [
2
+ {
3
+ "type": [
4
+ "http://www.w3.org/2004/02/skos/core#ConceptScheme",
5
+ "http://w3id.org/nkos/nkostype#thesaurus"
6
+ ],
7
+ "languages": [
8
+ "en",
9
+ "de"
10
+ ],
11
+ "uri": "http://bartoc.org/en/node/313",
12
+ "license": [
13
+ {
14
+ "uri": "http://opendatacommons.org/licenses/odbl/1.0/"
15
+ }
16
+ ],
17
+ "prefLabel": {
18
+ "de": "Standard Thesaurus Wirtschaft",
19
+ "en": "STW Thesaurus for Economics"
20
+ },
21
+ "subject": [
22
+ {
23
+ "uri": "http://dewey.info/class/3/e23/"
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "uri": "http://dewey.info/scheme/edition/e23/",
29
+ "prefLabel": {
30
+ "de": "Dewey-Dezimalklassifikation",
31
+ "en": "Dewey Decimal Classification"
32
+ },
33
+ "notation": [
34
+ "DDC"
35
+ ],
36
+ "identifier": [
37
+ "http://bartoc.org/en/node/241"
38
+ ],
39
+ "license": [
40
+ {
41
+ "uri": "http://creativecommons.org/licenses/by-nc-nd/3.0/"
42
+ }
43
+ ],
44
+ "publisher": [
45
+ {
46
+ "uri": "http://d-nb.info/gnd/1086052218",
47
+ "prefLabel": {
48
+ "de": "OCLC"
49
+ },
50
+ "altLabel": {
51
+ "de": [
52
+ "OCLC Online Computer Library Center"
53
+ ]
54
+ },
55
+ "url": "https://www.oclc.org/"
56
+ }
57
+ ],
58
+ "type": [
59
+ "http://www.w3.org/2004/02/skos/core#ConceptScheme",
60
+ "http://w3id.org/nkos/nkostype#classification_schema"
61
+ ],
62
+ "subject": [
63
+ {
64
+ "uri": "http://dewey.info/class/0/e23/"
65
+ }
66
+ ],
67
+ "languages": [
68
+ "en",
69
+ "fr",
70
+ "de"
71
+ ]
72
+ }
73
+ ,
74
+ {
75
+ "uri" : "http://bartoc.org/en/node/732",
76
+ "identifier": [
77
+ "http://www.wikidata.org/entity/Q1571087"
78
+ ],
79
+ "languages": [
80
+ "de"
81
+ ],
82
+ "notation": [
83
+ "ASB"
84
+ ],
85
+ "prefLabel":{
86
+ "de": "Allgemeine Systematik für Öffentliche Bibliotheken",
87
+ "en": "General Systematic for Public Libraries"
88
+ },
89
+ "type": [
90
+ "http://www.w3.org/2004/02/skos/core#ConceptScheme",
91
+ "http://w3id.org/nkos/nkostype#classification_schema"
92
+ ]
93
+ }
94
+ ]