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,107 @@
1
+ {
2
+ "title": "Testing",
3
+ "auth": {
4
+ "algorithm": "HS256",
5
+ "key": "test"
6
+ },
7
+ "schemes": {
8
+ "read": {
9
+ "auth": false
10
+ },
11
+ "create": {
12
+ "auth": false
13
+ },
14
+ "update": {
15
+ "auth": false,
16
+ "crossUser": true
17
+ },
18
+ "delete": {
19
+ "auth": false,
20
+ "crossUser": true
21
+ }
22
+ },
23
+ "concepts": {
24
+ "read": {
25
+ "auth": false
26
+ },
27
+ "create": {
28
+ "auth": false
29
+ },
30
+ "update": {
31
+ "auth": false,
32
+ "crossUser": true
33
+ },
34
+ "delete": {
35
+ "auth": true,
36
+ "identities": ["a:group"]
37
+ }
38
+ },
39
+ "concordances": {
40
+ "read": {
41
+ "auth": false
42
+ },
43
+ "create": {
44
+ "auth": true
45
+ },
46
+ "update": {
47
+ "auth": true,
48
+ "crossUser": false
49
+ },
50
+ "delete": {
51
+ "auth": true,
52
+ "crossUser": false
53
+ }
54
+ },
55
+ "mappings": {
56
+ "read": {
57
+ "auth": false
58
+ },
59
+ "create": {
60
+ "auth": true
61
+ },
62
+ "update": {
63
+ "auth": true,
64
+ "crossUser": false
65
+ },
66
+ "delete": {
67
+ "auth": true,
68
+ "crossUser": false
69
+ },
70
+ "fromSchemeWhitelist": null,
71
+ "toSchemeWhitelist": null,
72
+ "anonymous": false
73
+ },
74
+ "annotations": {
75
+ "read": {
76
+ "auth": false
77
+ },
78
+ "create": {
79
+ "auth": true
80
+ },
81
+ "update": {
82
+ "auth": true,
83
+ "crossUser": false
84
+ },
85
+ "delete": {
86
+ "auth": true,
87
+ "crossUser": false,
88
+ "identityProviders": null
89
+ },
90
+ "moderatingIdentities": ["http://test-moderating.user"],
91
+ "mismatchTagVocabulary": {
92
+ "uri": "https://uri.gbv.de/terminology/mismatch/"
93
+ }
94
+ },
95
+ "identities": ["http://test.user", "http://test-moderating.user"],
96
+ "identityProviders": ["test"],
97
+ "identityGroups": {
98
+ "a:group": {
99
+ "identities": [
100
+ "http://in-group.user"
101
+ ],
102
+ "claims": [
103
+ ]
104
+ }
105
+ },
106
+ "namespace": "f06ab008-a909-40a2-b8e7-643e955b522d"
107
+ }
@@ -0,0 +1,77 @@
1
+ import _ from "lodash"
2
+ import { validateConfig, setupConfig } from "../config/setup.js"
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+
6
+ import { v4 as uuid } from "uuid"
7
+
8
+ // Prepare environment
9
+ import * as dotenv from "dotenv"
10
+ dotenv.config()
11
+ const env = process.env.NODE_ENV
12
+ const configFile = process.env.CONFIG_FILE || "./config.json"
13
+
14
+ const __dirname = import.meta.dirname
15
+
16
+ // Adjust path if it's relative
17
+ let configFilePath
18
+ if (configFile.startsWith("/")) {
19
+ configFilePath = configFile
20
+ } else {
21
+ configFilePath = path.resolve(__dirname, configFile)
22
+ }
23
+
24
+ // If file doesn't exist, create it with an empty array
25
+ if (env !== "test" && !fs.existsSync(configFilePath)) {
26
+ fs.writeFileSync(configFilePath, "{}")
27
+ }
28
+
29
+ // Load environment config
30
+ let configEnv = {}
31
+ let configEnvFile = path.resolve(__dirname, `./config.${env}.json`)
32
+ if (fs.existsSync(configEnvFile)) {
33
+ configEnv = JSON.parse(fs.readFileSync(configEnvFile))
34
+ console.log(`Read configuration from ${configEnvFile}`)
35
+ }
36
+ // Load user config
37
+ let configUser = {}
38
+ if (env !== "test") {
39
+ configUser = JSON.parse(fs.readFileSync(configFilePath))
40
+ console.log(`Read configuration from ${configFilePath}`)
41
+ }
42
+
43
+ // Validate
44
+ Object.entries({ environment: configEnv, user: configUser }).forEach(([name, config]) => {
45
+ try {
46
+ validateConfig(config)
47
+ } catch(error) {
48
+ console.error(`Could not validate ${name} configuration: ${error}`)
49
+ process.exit(1)
50
+ }
51
+ })
52
+
53
+ // Validate
54
+ try {
55
+ validateConfig(configEnv)
56
+ } catch(error) {
57
+ console.error(`Could not validate environemnt configuration: ${error}`)
58
+ process.exit(1)
59
+ }
60
+ try {
61
+ validateConfig(configUser)
62
+ } catch(error) {
63
+ console.error(`Could not validate user configuration: ${error}`)
64
+ process.exit(1)
65
+ }
66
+
67
+ // Before merging, check whether `namespace` exists in the user config and if not, generate a namespace and save it to user config
68
+ if (!configUser.namespace && env != "test") {
69
+ configUser.namespace = uuid()
70
+ fs.writeFileSync(configFilePath, JSON.stringify(configUser, null, 2))
71
+ console.log(`Info/Config: Created a namespace and wrote it to ${configFilePath}.`)
72
+ }
73
+
74
+ const config = _.defaultsDeep({ env }, configEnv, configUser)
75
+ setupConfig(config)
76
+
77
+ export default config
@@ -0,0 +1,212 @@
1
+ import _ from "lodash"
2
+
3
+ import AJV from "ajv"
4
+ import addAjvFormats from "ajv-formats"
5
+
6
+ import configSchema from "./config.schema.json" with { type: "json" }
7
+ import statusSchema from "../status.schema.json" with { type: "json" }
8
+ import configDefault from "./config.default.json" with { type: "json" }
9
+ import info from "../package.json" with { type: "json" }
10
+
11
+ const ajv = new AJV({ allErrors: true })
12
+ addAjvFormats(ajv)
13
+ ajv.addSchema(configSchema)
14
+ ajv.addSchema(statusSchema)
15
+
16
+ function ajvErrorsToString(errors) {
17
+ let message = ""
18
+ for (let error of errors || []) {
19
+ switch (error.keyword) {
20
+ case "additionalProperties":
21
+ message += `${error.dataPath} ${error.message} (${error.params.additionalProperty})`
22
+ break
23
+ default:
24
+ message += `${error.dataPath} ${error.message} (${error.schemaPath})`
25
+ }
26
+ message += "\n "
27
+ }
28
+ return message.trimEnd()
29
+ }
30
+
31
+ export function validateConfig(data) {
32
+ if (!ajv.validate(configSchema, data)) {
33
+ throw new Error(ajvErrorsToString(ajv.errors))
34
+ }
35
+ for (let type in (data.registries?.types || {})) {
36
+ if (data.registries.types[type].mustExist && data.registries.types[type].uriRequired === false) {
37
+ throw new Error(`Registry member type ${type} mustExist so uriRequired must not be false`)
38
+ }
39
+ }
40
+ return data
41
+ }
42
+
43
+ export function validateStatus(data) {
44
+ if (!ajv.validate(statusSchema, data)) {
45
+ throw new Error(ajvErrorsToString(ajv.errors))
46
+ }
47
+ return data
48
+ }
49
+
50
+ export function setupConfig(config) {
51
+ config.env = config.env ?? "development"
52
+ const testing = config.env === "test"
53
+
54
+ // Merge in default values
55
+ config = _.defaultsDeep(config, configDefault)
56
+ const defaultChangesConfig = { retries: 20, interval: 5000 }
57
+ if (config.changes === true) {
58
+ config.changes = defaultChangesConfig
59
+ } else if (config.changes) {
60
+ config.changes = { ...defaultChangesConfig, ...config.changes }
61
+ }
62
+
63
+ // Add versions from package
64
+ config.version = info.apiVersion
65
+ config.serverVersion = info.version
66
+
67
+ // Logging functions
68
+ config.log = (...args) => {
69
+ if (!testing && (config.verbosity === true || config.verbosity === "log")) {
70
+ console.log(new Date(), ...args)
71
+ }
72
+ }
73
+ config.warn = (...args) => {
74
+ if (!testing && (config.verbosity === true || config.verbosity === "log" || config.verbosity === "warn")) {
75
+ console.warn(new Date(), ...args)
76
+ }
77
+ }
78
+ config.error = (...args) => {
79
+ if (!testing && config.verbosity !== false) {
80
+ console.error(new Date(), ...args)
81
+ }
82
+ }
83
+
84
+ // Set composed config variables
85
+ config.mongo.auth = config.mongo.user ? `${config.mongo.user}:${config.mongo.pass}@` : ""
86
+ config.mongo.url = `mongodb://${config.mongo.auth}${config.mongo.host}:${config.mongo.port}`
87
+ // Adjust database name during tests
88
+ if (testing) {
89
+ config.mongo.db += "-test-" + config.namespace
90
+ }
91
+
92
+ // Set baseUrl to localhost if not set
93
+ if (!config.baseUrl) {
94
+ Object.defineProperty(config, "baseUrl", {
95
+ get: function () {
96
+ return `http://localhost:${this.port}/`
97
+ },
98
+ })
99
+ }
100
+ if (!config.baseUrl.endsWith("/")) {
101
+ config.baseUrl += "/"
102
+ }
103
+
104
+ // Further expansion of config
105
+ const defaultActions = {
106
+ read: {
107
+ auth: false,
108
+ },
109
+ create: {
110
+ auth: true,
111
+ },
112
+ update: {
113
+ auth: true,
114
+ crossUser: false,
115
+ },
116
+ delete: {
117
+ auth: true,
118
+ crossUser: false,
119
+ },
120
+ }
121
+ const allTypes = ["schemes", "concepts", "mappings", "concordances", "annotations", "registries"]
122
+ for (let type of allTypes) {
123
+ if (config[type] === true) {
124
+ // Default is read-only without authentication
125
+ config[type] = {
126
+ read: {
127
+ auth: false,
128
+ },
129
+ }
130
+ }
131
+ if (config[type]) {
132
+ for (let action of ["read", "create", "update", "delete"]) {
133
+ if (config[type][action] === true) {
134
+ config[type][action] = defaultActions[action]
135
+ }
136
+ // Fill identities, identityProviders, and ips if necessary (not for read)
137
+ if (config[type][action] && action != "read") {
138
+ for (let prop of ["identities", "identityProviders", "ips"]) {
139
+ if (config[type][action][prop] === undefined) {
140
+ const value = config[type][prop] || config[prop]
141
+ if (value) {
142
+ config[type][action][prop] = value
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ // Fill in origin of URIs
149
+ if (config[type].create) {
150
+ const { uriBase, uriOrigin } = config[type].create
151
+ if (!uriOrigin) {
152
+ config[type].create.uriOrigin = "external"
153
+ } else if (uriOrigin !== "external" && uriBase === false) {
154
+ throw new Error(`uriBase of ${type}.create must not be false if uriOrigin is not external`)
155
+ }
156
+
157
+ //if (type
158
+ // TODO: hard-coded settings for mappings, concordances, and annotations
159
+ }
160
+ }
161
+ }
162
+
163
+ // mappings: if anonymous is given, assume crossUser for update and delete
164
+ if (config.mappings?.anonymous) {
165
+ if (config.mappings.update) {
166
+ config.mappings.update.crossUser = true
167
+ }
168
+ if (config.mappings.delete) {
169
+ config.mappings.delete.crossUser = true
170
+ }
171
+ }
172
+
173
+ // registries: assume types as enabled above
174
+ if (config.registries) {
175
+ // default types
176
+ if (!config.registries.types) {
177
+ config.registries.types = Object.fromEntries(allTypes.map(type => [type, !!config[type]]))
178
+ }
179
+ const { types } = config.registries
180
+ for (let type in types) {
181
+ // default values
182
+ if (types[type] === true) {
183
+ types[type] = {
184
+ mustExist: false,
185
+ skipInvalid: false,
186
+ }
187
+ }
188
+ if (types[type] && !("uriRequired" in types[type])) {
189
+ types[type].uriRequired = true
190
+ }
191
+ if (types[type]?.mustExist && !config[type]?.read) {
192
+ config.warn(`registry with member type ${type} require URI but config.${type}.read is not enabled!`)
193
+ }
194
+ }
195
+ }
196
+
197
+ // Adjust annotations.mismatchTagVocabulary to have the correct "API" field so that clients using cocoda-sdk can natively query it
198
+ if (config.annotations?.mismatchTagVocabulary) {
199
+ if (!config.annotations?.mismatchTagVocabulary?.API?.[0]) {
200
+ config.annotations.mismatchTagVocabulary.API = [
201
+ {
202
+ type: "http://bartoc.org/api-type/jskos",
203
+ url: config.baseUrl,
204
+ },
205
+ ]
206
+ } else {
207
+ config.warn("annotations.mismatchTagVocabulary currently does not support loading concepts from an external API. It will be attempted to load concepts from this instance instead.")
208
+ }
209
+ }
210
+
211
+ return config
212
+ }
Binary file
package/docker/.env ADDED
@@ -0,0 +1 @@
1
+ CONFIG_FILE=/config/config.json
@@ -0,0 +1,20 @@
1
+ FROM node:22-alpine
2
+
3
+ WORKDIR /usr/src/app
4
+
5
+ # Copy and install dependencies
6
+ COPY package*.json ./
7
+ RUN npm ci
8
+
9
+ # Bundle app source
10
+ COPY . .
11
+
12
+ EXPOSE 3000
13
+
14
+ RUN mkdir /config
15
+ COPY docker/.env .env
16
+
17
+ # Use pm2 to run app
18
+ RUN npm i -g pm2
19
+
20
+ CMD ["/usr/src/app/docker/docker-entrypoint.sh"]
@@ -0,0 +1,175 @@
1
+ # [JSKOS Server](https://github.com/gbv/jskos-server)
2
+
3
+ JSKOS Server implements the JSKOS API web service and storage for [JSKOS](https://gbv.github.io/jskos/jskos.html) data such as controlled vocabularies, concepts, and concept mappings. It is part of a larger infrastructure of [Project coli-conc](https://coli-conc.gbv.de).
4
+
5
+ - See [GitHub](https://github.com/gbv/jskos-server) for more information about the tool.
6
+
7
+ **Note:** The old Docker Hub image (`coliconc/jskos-server`) is deprecated as of March 2023 and will not be updated anymore. We are moving all our Docker images to GitHub's Container Registry. From now on, **all new Docker images** will be available under `ghcr.io/gbv/jskos-server` (https://github.com/gbv/jskos-server/pkgs/container/jskos-server). Old images will still be available through Docker Hub for the foreseeable future.
8
+
9
+ ## Supported Architectures
10
+ Currently, only `x86-64` is supported, but we are planning to add more architectures soon.
11
+
12
+ ## Available Tags
13
+ - The current release version is available under `latest`. However, new major versions might break compatibility of the previously used config file, therefore it is recommended to use a version tag instead.
14
+ - We follow SemVer for versioning the application. Therefore, `x` offers the latest image for the major version x, `x.y` offers the latest image for the minor version x.y, and `x.y.z` offers the image for a specific patch version x.y.z.
15
+ - Additionally, the latest development version is available under `dev`.
16
+
17
+
18
+
19
+ ## Usage
20
+
21
+ It is recommended to run the image using [Docker Compose](https://docs.docker.com/compose/) together with the required MongoDB database. Note that depending on your system, it might be necessary to use `sudo docker compose`. For older Docker versions, use `docker-compose` instead of `docker compose`.
22
+
23
+ JSKOS Server can run with **or without** MongoDB configured as a **replica set**, depending on whether you want to use the **Change Streams WebSocket API** (`/changes`).
24
+
25
+ If you **do not need** real-time change notifications via WebSocket, you can run MongoDB in standalone mode. This is the simplest and most common setup.
26
+
27
+
28
+ 1. Create `docker-compose.yml`
29
+
30
+ ##### Option 1: Without Replica Set (simpler setup)
31
+
32
+ In this setup, the Change API must remain **disabled** (default). The server will run normally without real-time WebSocket updates.
33
+
34
+ ```yaml
35
+ version: "3"
36
+
37
+ services:
38
+ jskos-server:
39
+ image: ghcr.io/gbv/jskos-server
40
+ # replace this with your UID/GID if necessary (id -u; id -g); remove on macOS/Windows
41
+ user: 1000:1000
42
+ depends_on:
43
+ - mongo
44
+ volumes:
45
+ - ./data/config:/config
46
+ # environment:
47
+ # - NODE_ENV=production # note that this requires the server to be run behind a HTTPS proxy
48
+ ports:
49
+ - 3000:3000
50
+ restart: unless-stopped
51
+
52
+ mongo:
53
+ image: mongo:7
54
+ # replace this with your UID/GID if necessary (id -u; id -g); remove on macOS/Windows
55
+ user: 1000:1000
56
+ volumes:
57
+ - ./data/db:/data/db
58
+ restart: unless-stopped
59
+ ```
60
+
61
+ ##### Option 2: With Replica Set (for Change Streams support)
62
+
63
+ If you want to use the WebSocket API for Change Streams, MongoDB must be configured as a **replica set**. You can do this with an additional setup container. In this setup, you **can enable** `changes` in your `config.json` to use the `/changes` WebSocket endpoint.
64
+
65
+ ```yaml
66
+ version: "3"
67
+
68
+ services:
69
+ jskos-server:
70
+ image: ghcr.io/gbv/jskos-server
71
+ # replace this with your UID/GID if necessary (id -u; id -g); remove on macOS/Windows
72
+ user: 1000:1000
73
+ depends_on:
74
+ - mongo
75
+ volumes:
76
+ - ./data/config:/config
77
+ # environment:
78
+ # - NODE_ENV=production # note that this requires the server to be run behind a HTTPS proxy
79
+ ports:
80
+ - 3000:3000
81
+ restart: unless-stopped
82
+
83
+ mongo:
84
+ image: mongo:7
85
+ # replace this with your UID/GID if necessary (id -u; id -g); remove on macOS/Windows
86
+ user: 1000:1000
87
+ entrypoint: [ "/usr/bin/mongod", # The MongoDB server binary
88
+ "--bind_ip_all", # Permit connections from any network interface
89
+ "--replSet", "rs0" # Start in replica‐set mode, naming the set “rs0”
90
+ ]
91
+ volumes:
92
+ - ./data/db:/data/db
93
+ restart: unless-stopped
94
+
95
+ mongo-setup-replica-set:
96
+ image: mongo:7
97
+ depends_on:
98
+ - mongo
99
+ volumes:
100
+ - ./mongo-initdb.d:/docker-entrypoint-initdb.d
101
+ restart: "no"
102
+ ```
103
+
104
+ You'll also need to add a `mongo_setup.sh` script in `./mongo-initdb.d/` to initialize the replica set.
105
+
106
+
107
+ 2. Create data folders:
108
+
109
+ ```bash
110
+ mkdir -p ./data/{config,db}
111
+ ```
112
+
113
+ 3. Start the application:
114
+
115
+ ```bash
116
+ docker compose up -d
117
+ ```
118
+
119
+ This will create and start a jskos-server container running under host port 3000 with data persistence under `./data`:
120
+
121
+ - `./data/config`: configuration files required for jskos-server (`config.json`), see below
122
+ - `./data/db`: data of the MongoDB container (note: make sure MongoDB data persistence works with your system, see section "Where to Store Data" [here](https://hub.docker.com/_/mongo))
123
+
124
+ Note that the `user` directives in the compose file make sure that the generated files are owned by your host user account. Use `id -u`/`id -g` to find out your UID/GID respectively, or remove the directives if you are using Docker on macOS/Windows.
125
+
126
+ You can now access the application under `http://localhost:3000`.
127
+
128
+ ## Application Setup
129
+ Note: After adjusting any configurations, it is required to restart or recreate the container:
130
+ - After changing configuration files, restart the container: `docker compose restart jskos-server`
131
+ - After changing `docker-compose.yml` (e.g. adjusting environment variables), recreate the container: `docker compose up -d`
132
+
133
+
134
+ ### Replica Set Initialization Script
135
+
136
+ Place the following `mongo_setup.sh` (or similar) in `./mongo-initdb.d/` on the host:
137
+
138
+ ```bash
139
+ #!/bin/bash
140
+
141
+ echo "🕒 Sleeping for 10 seconds to allow MongoDB to start..."
142
+ sleep 10
143
+
144
+ echo "⚙️ Initiating Replica Set at $(date)"
145
+
146
+ mongosh --host mongo:27017 <<EOF
147
+ rs.initiate({
148
+ _id: "rs0",
149
+ members: [ { _id: 0, host: "mongo:27017", priority: 1 } ]
150
+ });
151
+ EOF
152
+ ```
153
+
154
+ * **Sleep**: ensures the `mongo` service is accepting connections.
155
+ * **`mongosh`**: connects to the `mongo` container by its Docker network alias.
156
+ * **`rs.initiate()`**: configures the single-node replica set named `rs0`.
157
+
158
+ With this setup, **jskos-server** can safely use MongoDB change streams (`db.collection.watch()`) for real-time WebSocket notifications.
159
+
160
+ ### Configuration
161
+ The folder `/config` (mounted as `./data/config` if configured as above) contains the configuration file `config.json` where jskos-server is configured. Please refer to the [documentation on GitHub](https://github.com/gbv/jskos-server#configuration) to see how to configure jskos-server. Note that this image creates a configuration file if none is found on startup. By default, the MongoDB `mongo` will be used (as configured above).
162
+
163
+ ### Environment Variables
164
+
165
+ | Environment Variable | Description | Example Value |
166
+ |----------------------|-----------------------------------------------------------------------------------------------|---------------------|
167
+ | `NODE_ENV` | Should be set to `production` if used in production.* | production |
168
+ | `CONFIG_FILE` | Path to the configuration file **inside the container**. Usually does not need to be changed. | /config/config.json |
169
+
170
+ *If `NODE_ENV` is set to production (which is recommended for productive use), jskos-server expects to be run behind a proxy using HTTPS. This also means that the config option `baseUrl` is required (see [documentation on GitHub](https://github.com/gbv/jskos-server#configuration)).
171
+
172
+ ### Data Import
173
+ To get your data into jskos-server, please refer to [this section](https://github.com/gbv/jskos-server#data-import) in the documentation. For those commands to work inside the Docker container, skip the linking part and replace `./bin/import.js` with `docker compose exec jskos-server /usr/src/app/bin/import.js` for each command.
174
+
175
+ **Note:** If files are imported, these have to be mounted into the container first, and the path inside the container has to be given. For example, you could mount the host folder `./data/imports` to `/imports` inside the container and then use the path `/imports/myfile.ndjson` with the import command. **We are planning to improve the import process with the 1.2.0 release, so those commands will change in the near future.** It will also be possible to upload files via a web interface so that no commands will be necessary anymore.
@@ -0,0 +1,29 @@
1
+ version: "3"
2
+
3
+ services:
4
+ jskos-server:
5
+ build:
6
+ context: ..
7
+ dockerfile: docker/Dockerfile
8
+ depends_on:
9
+ - mongo
10
+ volumes:
11
+ - ./data/config:/config
12
+ ports:
13
+ - 3000:3000
14
+ restart: unless-stopped
15
+
16
+ mongo:
17
+ image: mongo:7
18
+ entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs0" ]
19
+ volumes:
20
+ - ./data/db:/data/db
21
+ restart: unless-stopped
22
+
23
+ mongo-setup-replica-set:
24
+ image: mongo:7
25
+ depends_on:
26
+ - mongo
27
+ volumes:
28
+ - ./mongo-initdb.d:/docker-entrypoint-initdb.d
29
+ restart: "no"
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+
3
+ # Add configuration file if necessary
4
+ if [ ! -f /config/config.json ]; then
5
+ echo '{"mongo":{"host":"mongo"}}' > /config/config.json
6
+ fi
7
+
8
+ pm2-runtime server.js
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+
3
+ echo "🕒 Sleeping for 10 seconds to allow MongoDB to start..."
4
+ sleep 10
5
+
6
+ echo "📅 mongo_setup.sh time now: $(date +"%T")"
7
+
8
+ echo "⚙️ Initiating Replica Set..."
9
+
10
+ mongosh --host mongo:27017 <<EOF
11
+ rs.initiate({
12
+ _id: "rs0",
13
+ version: 1,
14
+ members: [
15
+ {
16
+ _id: 0,
17
+ host: "mongo:27017",
18
+ priority: 1
19
+ }
20
+ ]
21
+ });
22
+ EOF
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "jskos-server",
3
+ "script": "./server.js",
4
+ "env": {
5
+ "NODE_ENV": "production"
6
+ }
7
+ }