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
package/test/chai.js ADDED
@@ -0,0 +1,14 @@
1
+ // Setup chai and plugins
2
+
3
+ import * as chaiModule from "chai"
4
+
5
+ import chaiAsPromised from "chai-as-promised"
6
+ chaiModule.use(chaiAsPromised)
7
+
8
+ import chaiHttp from "chai-http"
9
+ const chai = chaiModule.use(chaiHttp)
10
+
11
+ // eslint-disable-next-line no-unused-vars
12
+ const should = chai.should()
13
+
14
+ export default chai
@@ -0,0 +1,179 @@
1
+ import { assertMongoDB, dropDatabaseBeforeAndAfter, setupInMemoryMongo, teardownInMemoryMongo, createCollectionsAndIndexes } from "./test-utils.js"
2
+ import WebSocket from "ws"
3
+ import assert from "assert"
4
+ import config from "../config/index.js"
5
+ import { app } from "../server.js"
6
+ import { setupChangesApi } from "../utils/changes.js"
7
+ import { objectTypes } from "jskos-tools"
8
+ import mongoose from "mongoose"
9
+
10
+ // Map each route to its collection name and expected JSKOS type
11
+ const routes = {
12
+ voc: { coll: "terminologies", type: "ConceptScheme" },
13
+ concepts: { coll: "concepts", type: "Concept" },
14
+ mappings: { coll: "mappings", type: "ConceptMapping" },
15
+ concordances: { coll: "concordances", type: "Concordance" },
16
+ annotations: { coll: "annotations", type: "Annotation" },
17
+ registries: { coll: "registries", type: "Registry" },
18
+ }
19
+
20
+ describe("Change‐Streams API setup", () => {
21
+ // Capture console.log output
22
+ let loggedMessages = []
23
+ const originalLog = console.log
24
+
25
+ before(async () => {
26
+ console.log = (msg) => loggedMessages.push(msg)
27
+ })
28
+
29
+ after(async () => {
30
+ console.log = originalLog
31
+ })
32
+
33
+ it("should skip registering when changes is false", async () => {
34
+ // ensure flag is off
35
+ config.changes = false
36
+
37
+ // call the exported setup function
38
+ await setupChangesApi(app, config)
39
+
40
+ // assert our early‐return message was logged
41
+ loggedMessages.includes("Change API is disabled by configuration.")
42
+ })
43
+
44
+ })
45
+
46
+ describe("WebSocket Change‐Streams (integration)", function () {
47
+
48
+ before(async () => {
49
+ await setupInMemoryMongo({ replSet: true })
50
+ config.changes = true
51
+ await setupChangesApi(app, config)
52
+ await createCollectionsAndIndexes()
53
+ // optionally spin up your HTTP+WS server here
54
+ })
55
+
56
+ after(async () => {
57
+ // close server if you started one
58
+ await teardownInMemoryMongo()
59
+ })
60
+
61
+ // 🗑 Drop DB before *and* after every single `it()` in this file
62
+ dropDatabaseBeforeAndAfter()
63
+
64
+ // 🔌 Sanity‐check that mongoose really is connected
65
+ assertMongoDB()
66
+
67
+ // generate tests for each route
68
+ for (const [route, info] of Object.entries(routes)) {
69
+ lifecycleTests(route, info)
70
+ }
71
+
72
+ })
73
+
74
+ function lifecycleTests(route, { coll, type }) {
75
+ const uriBase = `urn:test:${route}`
76
+ const typeUri = objectTypes[type].type?.[0]
77
+
78
+ describe(`${route}/changes`, () => {
79
+ it("emits create", done => {
80
+ const ws = new WebSocket(`ws://localhost:${config.port}/${route}/changes`)
81
+ ws.on("open", async () => {
82
+ await mongoose.connection.db.collection(coll).insertOne({
83
+ uri: `${uriBase}:1`,
84
+ type: typeUri,
85
+ prefLabel: { en: ["A"] },
86
+ })
87
+ })
88
+ ws.on("message", raw => {
89
+ const evt = JSON.parse(raw)
90
+ if (evt.type !== "create") {
91
+ return
92
+ }
93
+ try {
94
+ assert.strictEqual(evt.type, "create")
95
+ assert.strictEqual(evt.objectType, type)
96
+ assert.deepStrictEqual(evt.document.prefLabel.en, ["A"])
97
+ assertOptionalIsoTimestamp(evt.timestamp)
98
+ ws.close()
99
+ done()
100
+ } catch (err) {
101
+ done(err)
102
+ }
103
+ })
104
+ ws.on("error", err => done(err))
105
+ })
106
+
107
+ it("emits update", done => {
108
+ const ws = new WebSocket(`ws://localhost:${config.port}/${route}/changes`)
109
+ ws.on("open", async () => {
110
+ const { insertedId } = await mongoose.connection.db.collection(coll).insertOne({
111
+ uri: `${uriBase}:2`,
112
+ type: typeUri,
113
+ prefLabel: { en: ["B"] },
114
+ })
115
+ await mongoose.connection.db.collection(coll).updateOne(
116
+ { _id: insertedId },
117
+ { $set: { prefLabel: { en: ["BB"] } } },
118
+ )
119
+ })
120
+ ws.on("message", raw => {
121
+ const evt = JSON.parse(raw)
122
+ if (evt.type !== "update") {
123
+ return
124
+ }
125
+ try {
126
+ assert.strictEqual(evt.objectType, type)
127
+ assert.deepStrictEqual(evt.document.prefLabel.en, ["BB"])
128
+ assertOptionalIsoTimestamp(evt.timestamp)
129
+ ws.close()
130
+ done()
131
+ } catch (err) {
132
+ done(err)
133
+ }
134
+ })
135
+ ws.on("error", err => done(err))
136
+ })
137
+
138
+ it("emits delete", done => {
139
+ const ws = new WebSocket(`ws://localhost:${config.port}/${route}/changes`)
140
+ ws.on("open", async () => {
141
+ const { insertedId } = await mongoose.connection.db.collection(coll).insertOne({
142
+ uri: `${uriBase}:3`,
143
+ type: typeUri,
144
+ prefLabel: { en: ["C"] },
145
+ })
146
+ await mongoose.connection.db.collection(coll).deleteOne({ _id: insertedId })
147
+ })
148
+ ws.on("message", raw => {
149
+ const evt = JSON.parse(raw)
150
+ if (evt.type !== "delete") {
151
+ return
152
+ }
153
+ try {
154
+ // on delete fullDocument is omitted
155
+ assert.strictEqual(evt.document, undefined)
156
+ ws.close()
157
+ done()
158
+ } catch (err) {
159
+ done(err)
160
+ }
161
+ })
162
+ ws.on("error", err => done(err))
163
+ })
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Validates that the provided timestamp, if present, is a string matching the strict ISO 8601 format `YYYY-MM-DDTHH:mm:ss.SSSZ`.
169
+ *
170
+ * @param {?string} ts - The timestamp to validate.
171
+ * @throws {AssertionError} If the timestamp is not a string or does not match the ISO 8601 format.
172
+ */
173
+ function assertOptionalIsoTimestamp(ts) {
174
+ if (ts == null) {
175
+ return
176
+ }
177
+ assert.strictEqual(typeof ts, "string")
178
+ assert.match(ts, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
179
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "uri": "uri:conceptNoFileEnding",
3
+ "inScheme":[{"uri":"http://dewey.info/scheme/edition/e23/"}]
4
+ }
@@ -0,0 +1,123 @@
1
+ [
2
+ {
3
+ "@context": "https://gbv.github.io/jskos/context.json",
4
+ "created": "2000-02-02",
5
+ "identifier": [
6
+ "856c92e9-8b1f-3131-bfbe-f2d2266527d3"
7
+ ],
8
+ "modified": "2005-11-02",
9
+ "notation": [
10
+ "6"
11
+ ],
12
+ "prefLabel": {
13
+ "de": "Technik, Medizin, angewandte Wissenschaften"
14
+ },
15
+ "topConceptOf": [
16
+ {
17
+ "uri": "http://dewey.info/scheme/edition/e23/"
18
+ }
19
+ ],
20
+ "type": [
21
+ "http://www.w3.org/2004/02/skos/core#Concept"
22
+ ],
23
+ "uri": "http://dewey.info/class/6/e23/",
24
+ "narrower": [
25
+ null
26
+ ]
27
+ },
28
+ {
29
+ "@context": "https://gbv.github.io/jskos/context.json",
30
+ "broader": [
31
+ {
32
+ "uri": "http://dewey.info/class/6/e23/"
33
+ }
34
+ ],
35
+ "created": "2000-02-02",
36
+ "identifier": [
37
+ "028d7bfc-77bf-3062-a6a8-857f66153fb9"
38
+ ],
39
+ "inScheme": [
40
+ {
41
+ "uri": "http://dewey.info/scheme/edition/e23/"
42
+ }
43
+ ],
44
+ "modified": "2005-11-02",
45
+ "notation": [
46
+ "60"
47
+ ],
48
+ "prefLabel": {
49
+ "de": "Technik",
50
+ "en": "Technology"
51
+ },
52
+ "type": [
53
+ "http://www.w3.org/2004/02/skos/core#Concept"
54
+ ],
55
+ "uri": "http://dewey.info/class/60/e23/",
56
+ "narrower": [
57
+ null
58
+ ]
59
+ },
60
+ {
61
+ "@context": "https://gbv.github.io/jskos/context.json",
62
+ "broader": [
63
+ {
64
+ "uri": "http://dewey.info/class/6/e23/"
65
+ }
66
+ ],
67
+ "created": "2000-02-02",
68
+ "identifier": [
69
+ "c14b599b-42cc-327e-abe2-fc1bb609e99e"
70
+ ],
71
+ "inScheme": [
72
+ {
73
+ "uri": "http://dewey.info/scheme/edition/e23/"
74
+ }
75
+ ],
76
+ "modified": "2005-11-02",
77
+ "notation": [
78
+ "61"
79
+ ],
80
+ "prefLabel": {
81
+ "de": "Medizin & Gesundheit"
82
+ },
83
+ "type": [
84
+ "http://www.w3.org/2004/02/skos/core#Concept"
85
+ ],
86
+ "uri": "http://dewey.info/class/61/e23/",
87
+ "narrower": [
88
+ null
89
+ ]
90
+ },
91
+ {
92
+ "@context": "https://gbv.github.io/jskos/context.json",
93
+ "broader": [
94
+ {
95
+ "uri": "http://dewey.info/class/6/e23/"
96
+ }
97
+ ],
98
+ "created": "2000-02-02",
99
+ "identifier": [
100
+ "ac872f11-eb09-3f7d-a307-613f3042791e"
101
+ ],
102
+ "inScheme": [
103
+ {
104
+ "uri": "http://dewey.info/scheme/edition/e23/"
105
+ }
106
+ ],
107
+ "modified": "2005-11-02",
108
+ "notation": [
109
+ "62"
110
+ ],
111
+ "prefLabel": {
112
+ "de": "Ingenieurwissenschaften"
113
+ },
114
+ "type": [
115
+ "http://www.w3.org/2004/02/skos/core#Concept"
116
+ ],
117
+ "uri": "http://dewey.info/class/62/e23/",
118
+ "narrower": [
119
+ null
120
+ ]
121
+ }
122
+ ]
123
+
@@ -0,0 +1,2 @@
1
+ {"@context":"https://gbv.github.io/jskos/context.json","creator":[{"prefLabel":{"de":"VZG"}}],"extent":"2267","fromScheme":{"notation":["DDC"],"uri":"http://bartoc.org/en/node/241"},"notation":["ddc_rvk_recht"],"scopeNote":{"de":["Recht"]},"toScheme":{"notation":["RVK"],"uri":"http://bartoc.org/en/node/533"},"type":["http://rdfs.org/ns/void#Linkset"],"uri":"http://coli-conc.gbv.de/concordances/ddc_rvk_recht"}
2
+ {"@context":"https://gbv.github.io/jskos/context.json","creator":[{"prefLabel":{"de":"VZG"}}],"extent":"2813","fromScheme":{"notation":["DDC"],"uri":"http://bartoc.org/en/node/241"},"notation":["ddc_rvk_medizin"],"scopeNote":{"de":["Medizin und Gesundheit"]},"toScheme":{"notation":["RVK"],"uri":"http://bartoc.org/en/node/533"},"type":["http://rdfs.org/ns/void#Linkset"],"uri":"http://coli-conc.gbv.de/concordances/ddc_rvk_medizin"}
package/test/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import fs from "node:fs"
2
+ import assert from "node:assert"
3
+ import { validateConfig, setupConfig } from "../config/setup.js"
4
+
5
+ describe("Configuration", () => {
6
+ for (let file of [
7
+ "config/config.default.json",
8
+ "config/config.test.json",
9
+ ].concat(fs.readdirSync("./test/configs").map(f => `test/configs/${f}`))) {
10
+ const shouldFail = file.includes("fail-")
11
+ it(`should ${shouldFail ? "not validate" : "validate and setup"} ${file}`, () => {
12
+ let config
13
+ try {
14
+ config = JSON.parse(fs.readFileSync(file))
15
+ } catch { /* ignore */ }
16
+ if (shouldFail) {
17
+ assert.throws(() => validateConfig(config))
18
+ } else {
19
+ validateConfig(config)
20
+ setupConfig(config)
21
+ assert.ok(config)
22
+ assert.ok(config.log)
23
+ }
24
+ })
25
+ }
26
+ })
@@ -0,0 +1,90 @@
1
+ {
2
+ "verbosity": "log",
3
+ "baseUrl": "https://example.com/",
4
+ "title": "JSKOS Server Complex Example Config",
5
+ "port": 12345,
6
+ "proxies": [
7
+ "127.0.0.1/8"
8
+ ],
9
+ "mongo": {
10
+ "user": "a",
11
+ "pass": "b",
12
+ "host": "127.0.0.1",
13
+ "port": 27018,
14
+ "db": "jskos-server-db"
15
+ },
16
+ "auth": {
17
+ "algorithm": "RS256",
18
+ "key": "public key here"
19
+ },
20
+ "schemes": true,
21
+ "concepts": {
22
+ "read": {
23
+ "auth": false
24
+ },
25
+ "create": {
26
+ "auth": true,
27
+ "ips": [
28
+ "5.6.7.8"
29
+ ]
30
+ },
31
+ "update": {
32
+ "identityProviders": [
33
+ "github"
34
+ ]
35
+ },
36
+ "delete": {
37
+ "auth": false,
38
+ "identities": [
39
+ "http://some.uri"
40
+ ]
41
+ }
42
+ },
43
+ "mappings": {
44
+ "read": {
45
+ "auth": false
46
+ },
47
+ "create": {
48
+ "auth": true
49
+ },
50
+ "update": {
51
+ "auth": true,
52
+ "crossUser": true
53
+ },
54
+ "delete": {
55
+ "auth": true,
56
+ "crossUser": false
57
+ },
58
+ "fromSchemeWhitelist": [
59
+ {
60
+ "uri": "some:scheme"
61
+ }
62
+ ],
63
+ "anonymous": true,
64
+ "cardinality": "1-to-1"
65
+ },
66
+ "concordances": false,
67
+ "annotations": {
68
+ "read": {
69
+ "auth": false
70
+ },
71
+ "create": {
72
+ "auth": true,
73
+ "identities": [
74
+ "http://some.uri"
75
+ ]
76
+ },
77
+ "update": {
78
+ "auth": true,
79
+ "crossUser": false
80
+ },
81
+ "delete": {
82
+ "auth": true,
83
+ "crossUser": false,
84
+ "ips": [
85
+ "134.5.6.7/24"
86
+ ]
87
+ },
88
+ "moderatingIdentities": []
89
+ }
90
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ []
File without changes
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemes": {
3
+ "cardinality": "1-to-1"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "concepts": {
3
+ "cardinality": "1-to-1"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "concordances": {
3
+ "fromSchemeWhitelist": []
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "annotations": {
3
+ "toSchemeWhitelist": null
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "some": "property"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "port": "5000"
3
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "registries": {
3
+ "mixedTypes": true,
4
+ "update": {
5
+ "crossUser": true
6
+ },
7
+ "types": {
8
+ "schemes": true,
9
+ "concepts": {
10
+ "uriRequired": false,
11
+ "mustExist": true,
12
+ "skipInvalid": true
13
+ }
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "registries": {
3
+ "mixedTypes": true,
4
+ "update": {
5
+ "crossUser": true
6
+ },
7
+ "types": {
8
+ "schemes": true,
9
+ "concepts": {
10
+ "uriRequired": false,
11
+ "mustExist": false,
12
+ "skipInvalid": true
13
+ }
14
+ }
15
+ }
16
+ }