jskos-server 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +20 -0
- package/.editorconfig +9 -0
- package/.github/workflows/docker.yml +59 -0
- package/.github/workflows/gh-pages.yml +23 -0
- package/.github/workflows/gh-release.yml +19 -0
- package/.github/workflows/test.yml +39 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +2710 -0
- package/bin/extra.js +81 -0
- package/bin/import.js +438 -0
- package/bin/mongodb.js +21 -0
- package/bin/reset.js +257 -0
- package/bin/upgrade.js +34 -0
- package/config/config.default.json +88 -0
- package/config/config.schema.json +877 -0
- package/config/config.test.json +107 -0
- package/config/index.js +77 -0
- package/config/setup.js +212 -0
- package/depdendencies.png +0 -0
- package/docker/.env +1 -0
- package/docker/Dockerfile +20 -0
- package/docker/README.md +175 -0
- package/docker/docker-compose.yml +29 -0
- package/docker/docker-entrypoint.sh +8 -0
- package/docker/mongo-initdb.d/mongo_setup.sh +22 -0
- package/ecosystem.example.json +7 -0
- package/errors/index.js +94 -0
- package/eslint.config.js +17 -0
- package/index.js +10 -0
- package/models/annotations.js +13 -0
- package/models/concepts.js +12 -0
- package/models/concordances.js +12 -0
- package/models/index.js +33 -0
- package/models/mappings.js +20 -0
- package/models/meta.js +21 -0
- package/models/registries.js +12 -0
- package/models/schemes.js +12 -0
- package/package.json +91 -0
- package/routes/annotations.js +83 -0
- package/routes/common.js +86 -0
- package/routes/concepts.js +64 -0
- package/routes/concordances.js +86 -0
- package/routes/data.js +19 -0
- package/routes/mappings.js +108 -0
- package/routes/registries.js +24 -0
- package/routes/schemes.js +72 -0
- package/routes/validate.js +37 -0
- package/server.js +190 -0
- package/services/abstract.js +328 -0
- package/services/annotations.js +237 -0
- package/services/concepts.js +459 -0
- package/services/concordances.js +264 -0
- package/services/data.js +30 -0
- package/services/index.js +34 -0
- package/services/mappings.js +978 -0
- package/services/registries.js +319 -0
- package/services/schemes.js +318 -0
- package/services/validate.js +39 -0
- package/status.schema.json +145 -0
- package/test/abstract-service.js +36 -0
- package/test/annotations/annotation.json +13 -0
- package/test/api.js +2481 -0
- package/test/chai.js +14 -0
- package/test/changes.js +179 -0
- package/test/concepts/conceptNoFileEnding +4 -0
- package/test/concepts/concepts-ddc-6-60-61-62.json +123 -0
- package/test/concordances/concordances.ndjson +2 -0
- package/test/config.js +26 -0
- package/test/configs/complex-config.json +90 -0
- package/test/configs/empty-object.json +1 -0
- package/test/configs/fail-array.json +1 -0
- package/test/configs/fail-empty.json +0 -0
- package/test/configs/fail-mapping-only-props1.json +5 -0
- package/test/configs/fail-mapping-only-props2.json +5 -0
- package/test/configs/fail-mapping-only-props3.json +5 -0
- package/test/configs/fail-mapping-only-props4.json +5 -0
- package/test/configs/fail-nonexisting-prop.json +3 -0
- package/test/configs/fail-port-string.json +3 -0
- package/test/configs/fail-registry-types.json +16 -0
- package/test/configs/registry-types.json +16 -0
- package/test/data-write.js +784 -0
- package/test/eslint.js +22 -0
- package/test/import-reset.js +287 -0
- package/test/infer-mappings.js +340 -0
- package/test/ipcheck.js +287 -0
- package/test/mappings/README.md +1 -0
- package/test/mappings/ddc-gnd-1.mapping.json +33 -0
- package/test/mappings/ddc-gnd-2.mapping.json +67 -0
- package/test/mappings/mapping-ddc-gnd-noScheme.json +145 -0
- package/test/mappings/mapping-ddc-gnd.json +175 -0
- package/test/mappings/mappings-ddc.json +214 -0
- package/test/registries/registries.ndjson +2 -0
- package/test/services.js +557 -0
- package/test/terminologies/terminologies.json +94 -0
- package/test/test-utils.js +182 -0
- package/test/utils.js +425 -0
- package/test/validate.js +226 -0
- package/utils/adjust.js +206 -0
- package/utils/auth.js +154 -0
- package/utils/changes.js +88 -0
- package/utils/db.js +106 -0
- package/utils/ipcheck.js +76 -0
- package/utils/middleware.js +636 -0
- package/utils/searchHelper.js +153 -0
- package/utils/status.js +77 -0
- package/utils/users.js +7 -0
- package/utils/utils.js +114 -0
- package/utils/uuid.js +6 -0
- package/utils/version.js +324 -0
- package/views/base.ejs +172 -0
|
@@ -0,0 +1,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
|
+
}
|
package/config/index.js
ADDED
|
@@ -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
|
package/config/setup.js
ADDED
|
@@ -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"]
|
package/docker/README.md
ADDED
|
@@ -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,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
|