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/routes/data.js ADDED
@@ -0,0 +1,19 @@
1
+ import express from "express"
2
+ import { DataService } from "../services/data.js"
3
+ import { wrapAsync, supportDownloadFormats, returnJSON } from "../utils/middleware.js"
4
+ import { useAuth } from "../utils/auth.js"
5
+
6
+ export default config => {
7
+ const router = express.Router()
8
+ const dataService = new DataService(config)
9
+
10
+ router.get(
11
+ "/",
12
+ useAuth(false),
13
+ supportDownloadFormats([]),
14
+ wrapAsync(async req => dataService.getData(req)),
15
+ returnJSON,
16
+ )
17
+
18
+ return router
19
+ }
@@ -0,0 +1,108 @@
1
+ import express from "express"
2
+ import { MappingService } from "../services/mappings.js"
3
+ import * as utils from "../utils/middleware.js"
4
+ import { wrapAsync, wrapDownload } from "../utils/middleware.js"
5
+ import { useAuth } from "../utils/auth.js"
6
+ import { readRoute, createRoute } from "./common.js"
7
+
8
+ export default config => {
9
+ const router = express.Router()
10
+ const service = new MappingService(config)
11
+
12
+ // /mappings/suggest and /mappings/voc need to come before /mappings/:_id!
13
+ router.get(
14
+ "/suggest",
15
+ useAuth(config.concepts?.read?.auth),
16
+ wrapAsync(async (req) => {
17
+ return await service.getNotationSuggestions(req.query)
18
+ }),
19
+ utils.addPaginationHeaders,
20
+ utils.returnJSON,
21
+ )
22
+ router.get(
23
+ "/voc",
24
+ useAuth(config.schemes?.read?.auth),
25
+ wrapAsync(async (req) => {
26
+ return await service.getMappingSchemes(req.query)
27
+ }),
28
+ utils.addPaginationHeaders,
29
+ utils.adjust,
30
+ utils.returnJSON,
31
+ )
32
+
33
+ readRoute(router, "/", config.mappings.read, service, "mappings", ["json", "ndjson", "csv", "tsv"])
34
+ createRoute(router, "/", config.mappings.create, service)
35
+
36
+ if (config.mappings.read) {
37
+ router.get(
38
+ "/infer",
39
+ useAuth(config.mappings.read.auth),
40
+ wrapAsync(async req => service.inferMappings(req.query)),
41
+ utils.addPaginationHeaders,
42
+ utils.adjust,
43
+ utils.returnJSON,
44
+ )
45
+
46
+ router.get(
47
+ "/:_id",
48
+ useAuth(config.mappings.read.auth),
49
+ utils.supportDownloadFormats(["json", "ndjson", "csv", "tsv"]),
50
+ wrapAsync(async req => service.getMapping(req.params._id)),
51
+ wrapDownload(utils.adjust, false),
52
+ wrapDownload(utils.returnJSON, false),
53
+ wrapDownload(utils.handleDownload("mapping"), true),
54
+ )
55
+ }
56
+
57
+ if (config.mappings.update) {
58
+ router.put(
59
+ "/:_id",
60
+ useAuth(config.mappings.update.auth),
61
+ utils.bodyParser,
62
+ wrapAsync(async (req) => {
63
+ return await service.putMapping({
64
+ _id: req.params._id,
65
+ body: req.body,
66
+ user: req.user,
67
+ existing: req.existing,
68
+ })
69
+ }),
70
+ utils.adjust,
71
+ utils.returnJSON,
72
+ )
73
+
74
+ router.patch(
75
+ "/:_id",
76
+ useAuth(config.mappings.update.auth),
77
+ utils.bodyParser,
78
+ wrapAsync(async (req) => {
79
+ return await service.patchMapping({
80
+ _id: req.params._id,
81
+ body: req.body,
82
+ user: req.user,
83
+ existing: req.existing,
84
+ })
85
+ }),
86
+ utils.adjust,
87
+ utils.returnJSON,
88
+ )
89
+ }
90
+
91
+ if (config.mappings.delete) {
92
+ router.delete(
93
+ "/:_id",
94
+ useAuth(config.mappings.delete.auth),
95
+ utils.bodyParser,
96
+ wrapAsync(async (req) => {
97
+ return await service.deleteItem({
98
+ _id: req.params._id,
99
+ user: req.user,
100
+ existing: req.existing,
101
+ })
102
+ }),
103
+ (req, res) => res.sendStatus(204),
104
+ )
105
+ }
106
+
107
+ return router
108
+ }
@@ -0,0 +1,24 @@
1
+ import express from "express"
2
+ import { RegistryService } from "../services/registries.js"
3
+ import { createRoute, readRoute, updateRoute, deleteRoute, suggestRoute } from "./common.js"
4
+
5
+ export default config => {
6
+ const router = express.Router()
7
+ const { registries } = config
8
+ if (!registries) {
9
+ return router
10
+ }
11
+
12
+ const service = new RegistryService(config)
13
+
14
+ readRoute(router, "/", registries.read, service, "registries")
15
+ createRoute(router, "/", registries.create, service)
16
+ updateRoute(router, "/", registries.update, service)
17
+ deleteRoute(router, "/", registries.delete, service)
18
+
19
+ // TODO: patchRoute
20
+
21
+ suggestRoute(router, "/suggest", registries.read, service)
22
+
23
+ return router
24
+ }
@@ -0,0 +1,72 @@
1
+ import express from "express"
2
+ import { SchemeService } from "../services/schemes.js"
3
+ import { ConceptService } from "../services/concepts.js"
4
+ import * as utils from "../utils/middleware.js"
5
+ import { wrapAsync, wrapDownload } from "../utils/middleware.js"
6
+ import { useAuth } from "../utils/auth.js"
7
+ import { MalformedRequestError } from "../errors/index.js"
8
+ import { readRoute, createRoute, updateRoute, deleteRoute, suggestRoute } from "./common.js"
9
+
10
+ export default config => {
11
+ const router = express.Router()
12
+ const service = new SchemeService(config)
13
+ const { schemes, concepts } = config
14
+
15
+ readRoute(router, "/", schemes.read, service, "schemes")
16
+ createRoute(router, "/", schemes.create, service)
17
+ updateRoute(router, "/", schemes.update, service)
18
+ deleteRoute(router, "/", schemes.delete, service)
19
+
20
+ suggestRoute(router, "/suggest", schemes.read, service)
21
+
22
+ if (concepts) {
23
+ const conceptService = new ConceptService(config)
24
+
25
+ router.get(
26
+ "/top",
27
+ useAuth(concepts.read.auth),
28
+ utils.supportDownloadFormats([]),
29
+ wrapAsync(async (req) => {
30
+ return await conceptService.getTop(req.query)
31
+ }),
32
+ utils.addPaginationHeaders,
33
+ utils.adjust,
34
+ utils.returnJSON,
35
+ )
36
+
37
+ router.get(
38
+ "/concepts",
39
+ useAuth(concepts.read.auth),
40
+ utils.supportDownloadFormats(["json", "ndjson"]),
41
+ wrapAsync(async (req) => {
42
+ if (!req.query.uri) {
43
+ throw new MalformedRequestError("Parameter `uri` (URI of a vocabulary) is required for endpoint /voc/concepts")
44
+ }
45
+ const query = { ...req.query, voc: req.query.uri }
46
+ delete query.uri
47
+ return await conceptService.queryItems(query)
48
+ }),
49
+ wrapDownload(utils.addPaginationHeaders, false),
50
+ wrapDownload(utils.adjust, false),
51
+ wrapDownload(utils.returnJSON, false),
52
+ wrapDownload(utils.handleDownload("concepts"), true),
53
+ )
54
+
55
+ if (concepts.delete) {
56
+ router.delete(
57
+ "/concepts",
58
+ useAuth(concepts.delete.auth),
59
+ utils.bodyParser,
60
+ wrapAsync(async (req) => {
61
+ return await conceptService.deleteConceptsFromScheme({
62
+ scheme: req.existing,
63
+ setApi: req.query.setApi,
64
+ })
65
+ }),
66
+ (req, res) => res.sendStatus(204),
67
+ )
68
+ }
69
+ }
70
+
71
+ return router
72
+ }
@@ -0,0 +1,37 @@
1
+ import express from "express"
2
+ import { ValidateService } from "../services/validate.js"
3
+ import { wrapAsync, returnJSON } from "../utils/middleware.js"
4
+ import axios from "axios"
5
+ import { MalformedRequestError } from "../errors/index.js"
6
+
7
+ export default config => {
8
+ const router = express.Router()
9
+ const service = new ValidateService(config)
10
+
11
+ router.get(
12
+ "/",
13
+ wrapAsync(async req => {
14
+ const url = req.query.url
15
+ if (!url) {
16
+ throw new MalformedRequestError("Please use HTTP POST or provide an URL to load data from!")
17
+ }
18
+ try {
19
+ const data = (await axios.get(url)).data
20
+ return await service.validate(data, req.query)
21
+ } catch (error) {
22
+ console.log(error)
23
+ throw new MalformedRequestError(`Error loading data from URL ${url}.`)
24
+ }
25
+ }),
26
+ returnJSON,
27
+ )
28
+
29
+ router.post(
30
+ "/",
31
+ express.json(),
32
+ wrapAsync(async req => service.validate(req.body, req.query)),
33
+ returnJSON,
34
+ )
35
+
36
+ return router
37
+ }
package/server.js ADDED
@@ -0,0 +1,190 @@
1
+ import config from "./config/index.js"
2
+ import * as utils from "./utils/middleware.js"
3
+ import express from "express"
4
+ import * as db from "./utils/db.js"
5
+ import morgan from "morgan"
6
+ import nocache from "nocache"
7
+
8
+ import createAnnotationRouter from "./routes/annotations.js"
9
+ import createConceptRouter from "./routes/concepts.js"
10
+ import createConcordanceRouter from "./routes/concordances.js"
11
+ import createMappingRouter from "./routes/mappings.js"
12
+ import createSchemeRouter from "./routes/schemes.js"
13
+ import createRegistryRouter from "./routes/registries.js"
14
+ import createDataRouter from "./routes/data.js"
15
+ import createValidateRouter from "./routes/validate.js"
16
+
17
+ import { serverStatus } from "./utils/status.js"
18
+
19
+ import { ipcheck } from "./utils/ipcheck.js"
20
+ import { useAuth } from "./utils/auth.js"
21
+ import * as errors from "./errors/index.js"
22
+ import portfinder from "portfinder"
23
+ import expressWs from "express-ws"
24
+ import { setupChangesApi, isChangesApiAvailable } from "./utils/changes.js"
25
+
26
+ const __dirname = import.meta.dirname
27
+ const connection = db.connection
28
+
29
+ config.log(`Running in ${config.env} mode.`)
30
+
31
+ if (!config.baseUrl) {
32
+ config.warn("Warning: If you're using jskos-server behind a reverse proxy, it is necessary to add `baseUrl` to the configuration file!")
33
+ }
34
+
35
+ // Initialize express with settings
36
+ const app = express()
37
+
38
+ // Initialize WebSocket support
39
+ expressWs(app)
40
+
41
+
42
+ app.set("json spaces", 2)
43
+ if (config.proxies && config.proxies.length) {
44
+ app.set("trust proxy", config.proxies)
45
+ }
46
+
47
+ // Configure view engine to render EJS templates.
48
+ app.set("views", __dirname + "/views")
49
+ app.set("view engine", "ejs")
50
+
51
+ // Database connection
52
+ const connect = async () => {
53
+ try {
54
+ await db.connect(true)
55
+ // TODO: `indexExists` causes a deprecation warning. Find a different solution.
56
+ if (config.schemes && !(await connection.collection("terminologies").indexExists("text"))) {
57
+ config.warn("Text index on terminologies collection missing. /voc/search and /voc/suggest are disabled. Run `npm run import -- --indexes` or `npm run import -- -i schemes` to created indexes.")
58
+ config.status["voc-search"] = null
59
+ config.status["voc-suggest"] = null
60
+ }
61
+ if (config.concepts && !(await connection.collection("concepts").indexExists("text"))) {
62
+ config.warn("Text index on concepts collection missing. /concepts/search and /concepts/suggest are disabled. Run `npm run import -- --indexes` or `npm run import -- -i concepts` to created indexes.")
63
+ config.status.search = null
64
+ config.status.suggest = null
65
+ }
66
+ if (config.annotations?.mismatchTagVocabulary?.uri) {
67
+ const model = await import("./models/concepts.js")
68
+ const concepts = await model.Concept.find({ "inScheme.uri": config.annotations.mismatchTagVocabulary.uri })
69
+ if (concepts.length === 0) {
70
+ config.warn("annotations.mismatchTagVocabulary is configured, but no data for that vocabulary could be found in the database. Import the vocabulary data into this instance for the setting to work.")
71
+ }
72
+ }
73
+ } catch (error) {
74
+ config.warn("Error connecting to database, reconnect in a few seconds...")
75
+ }
76
+ }
77
+ // Connect immediately on startup
78
+ connect()
79
+
80
+ // Logging for access logs
81
+ if (config.verbosity === true || config.verbosity === "log") {
82
+ app.use(morgan(":date[iso] \":method :url HTTP/:http-version\" :status :res[content-length] \":referrer\" \":user-agent\""))
83
+ }
84
+
85
+ // Add default headers
86
+ app.use(utils.addDefaultHeaders)
87
+
88
+ // Disable client side caching
89
+ app.use(nocache())
90
+
91
+ // Disable ETags
92
+ app.set("etag", false)
93
+ app.use(express.urlencoded({ extended: false }))
94
+
95
+ // Set some properties on req that will be used by other middleware
96
+ app.use(utils.addMiddlewareProperties)
97
+
98
+ // Add routes
99
+
100
+ // Root path for static page
101
+ app.get("/", (req, res) => {
102
+ res.setHeader("Content-Type", "text/html")
103
+ res.render("base", {
104
+ config,
105
+ isChangesApiAvailable,
106
+ })
107
+ })
108
+ // JSON Schema for /status
109
+ app.use("/status.schema.json", express.static(__dirname + "/status.schema.json"))
110
+
111
+ // Status page /status
112
+ app.get("/status",
113
+ (req, res) => {
114
+ res.status(200).json(serverStatus(config, connection.readyState === 1))
115
+ })
116
+
117
+ // Database check middleware
118
+ app.use((req, res, next) => {
119
+ if (connection.readyState === 1) {
120
+ next()
121
+ } else {
122
+ // No connection to database, return error
123
+ next(new errors.DatabaseAccessError())
124
+ }
125
+ })
126
+ // IP check middleware
127
+ app.use(ipcheck(config))
128
+
129
+ // /checkAuth
130
+ app.get("/checkAuth", useAuth(true), (req, res) => {
131
+ res.sendStatus(204)
132
+ })
133
+
134
+ // Add conditional routes
135
+ if (config.schemes) {
136
+ app.use("/voc", createSchemeRouter(config))
137
+ }
138
+ if (config.mappings) {
139
+ app.use("/mappings", createMappingRouter(config))
140
+ }
141
+ if (config.concordances) {
142
+ app.use("/concordances", createConcordanceRouter(config))
143
+ }
144
+ if (config.annotations) {
145
+ app.use("/annotations", createAnnotationRouter(config))
146
+ }
147
+ if (config.concepts) {
148
+ app.use(createConceptRouter(config))
149
+ }
150
+ if (config.registries) {
151
+ app.use("/registries", createRegistryRouter(config))
152
+ }
153
+
154
+ // These routes are always enabled
155
+ app.use("/data", createDataRouter(config))
156
+ app.use("/validate", createValidateRouter(config))
157
+
158
+ // Error handling
159
+ app.use((error, req, res, next) => {
160
+ // Check if error is defined in errors
161
+ if (Object.values(errors).includes(error.constructor)) {
162
+ res.status(error.statusCode).send({
163
+ error: error.constructor.name,
164
+ status: error.statusCode,
165
+ message: error.message,
166
+ })
167
+ } else {
168
+ next(error)
169
+ }
170
+ })
171
+
172
+ // Changes API
173
+ await setupChangesApi(app, config)
174
+
175
+ const start = async () => {
176
+ if (config.env == "test") {
177
+ portfinder.basePort = config.port
178
+ config.port = await portfinder.getPortPromise()
179
+ }
180
+ app.listen(config.port, () => {
181
+ config.log(`Now listening on port ${config.port}`)
182
+ })
183
+ }
184
+ // Start express server immediately even if database is not yet connected
185
+ start()
186
+
187
+ export {
188
+ app,
189
+ connection as db,
190
+ }