adapt-authoring-config 0.0.1
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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +19 -0
- package/README.md +1 -0
- package/adapt-authoring.json +12 -0
- package/artillery.yml +4 -0
- package/bin/confgen.js +132 -0
- package/docs/configure-environment.md +53 -0
- package/docs/defining-config.md +63 -0
- package/docs/plugins/configuration.js +70 -0
- package/docs/plugins/configuration.md +14 -0
- package/errors/errors.json +14 -0
- package/index.js +5 -0
- package/lib/ConfigModule.js +236 -0
- package/package.json +25 -0
- package/tests/configModule.spec.js +61 -0
- package/tests/configUtils.spec.js +34 -0
- package/tests/data/conf/config.schema.js +10 -0
- package/tests/data/incorrecttype/conf/config.schema.js +7 -0
- package/tests/data/invalid/conf/config.schema.js +8 -0
- package/tests/data/required/conf/config.schema.js +8 -0
- package/tests/data/testfile.json +3 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/.eslintrc
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: File a bug report
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to fill out this bug report!
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: What happened?
|
|
13
|
+
description: Please describe the issue
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: expected
|
|
18
|
+
attributes:
|
|
19
|
+
label: Expected behaviour
|
|
20
|
+
description: Tell us what should have happened
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: repro-steps
|
|
23
|
+
attributes:
|
|
24
|
+
label: Steps to reproduce
|
|
25
|
+
description: Tell us how to reproduce the issue
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: input
|
|
29
|
+
id: aat-version
|
|
30
|
+
attributes:
|
|
31
|
+
label: Authoring tool version
|
|
32
|
+
description: What version of the Adapt authoring tool are you running?
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: input
|
|
36
|
+
id: fw-version
|
|
37
|
+
attributes:
|
|
38
|
+
label: Framework version
|
|
39
|
+
description: What version of the Adapt framework are you running?
|
|
40
|
+
- type: dropdown
|
|
41
|
+
id: browsers
|
|
42
|
+
attributes:
|
|
43
|
+
label: What browsers are you seeing the problem on?
|
|
44
|
+
multiple: true
|
|
45
|
+
options:
|
|
46
|
+
- Firefox
|
|
47
|
+
- Chrome
|
|
48
|
+
- Safari
|
|
49
|
+
- Microsoft Edge
|
|
50
|
+
- type: textarea
|
|
51
|
+
id: logs
|
|
52
|
+
attributes:
|
|
53
|
+
label: Relevant log output
|
|
54
|
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
55
|
+
render: sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Request a new feature
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to request a new feature in the Adapt authoring tool! The Adapt team will consider all new feature requests, but unfortunately cannot commit to every one.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: Feature description
|
|
13
|
+
description: Please describe your feature request
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: checkboxes
|
|
17
|
+
id: contribute
|
|
18
|
+
attributes:
|
|
19
|
+
label: Can you work on this feature?
|
|
20
|
+
description: If you are able to commit your own time to work on this feature, it will greatly increase the liklihood of it being implemented by the core dev team. Otherwise, it will be triaged and prioritised alongside the core team's current priorities.
|
|
21
|
+
options:
|
|
22
|
+
- label: I can contribute
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[//]: # (Please title your PR according to eslint commit conventions)
|
|
2
|
+
[//]: # (See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-eslint#eslint-convention for details)
|
|
3
|
+
|
|
4
|
+
[//]: # (Add a link to the original issue)
|
|
5
|
+
|
|
6
|
+
[//]: # (Delete as appropriate)
|
|
7
|
+
### Fix
|
|
8
|
+
* A sentence describing each fix
|
|
9
|
+
|
|
10
|
+
### Update
|
|
11
|
+
* A sentence describing each udpate
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
* A sentence describing each new feature
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
* A sentence describing each breaking change
|
|
18
|
+
|
|
19
|
+
[//]: # (List appropriate steps for testing if needed)
|
|
20
|
+
### Testing
|
|
21
|
+
1. Steps for testing
|
|
22
|
+
|
|
23
|
+
[//]: # (Mention any other dependencies)
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Add labelled PRs to project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [ labeled ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
add-to-project:
|
|
9
|
+
if: ${{ github.event.label.name == 'dependencies' }}
|
|
10
|
+
name: Add to main project
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/add-to-project@v0.1.0
|
|
14
|
+
with:
|
|
15
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
16
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Add to main project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
pull_request:
|
|
8
|
+
types:
|
|
9
|
+
- opened
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
add-to-project:
|
|
13
|
+
name: Add to main project
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/add-to-project@v0.1.0
|
|
17
|
+
with:
|
|
18
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
19
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adapt-authoring-config
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"essentialType": "config",
|
|
3
|
+
"documentation": {
|
|
4
|
+
"enable": true,
|
|
5
|
+
"manualPlugins": ["docs/plugins/configuration.js"],
|
|
6
|
+
"manualPages": {
|
|
7
|
+
"configuration.md": "reference",
|
|
8
|
+
"configure-environment.md": "getting-started",
|
|
9
|
+
"defining-config.md": "basics"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
package/artillery.yml
ADDED
package/bin/confgen.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generates a template config file which can be populated with required values.
|
|
4
|
+
* @param {String} [environment] The enviroment to write the config for
|
|
5
|
+
* @param {String} --defaults Will include default values
|
|
6
|
+
* @param {String} --replace Will override any existing values
|
|
7
|
+
* @param {String} --update Will update existing configuration with any missing values
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs/promises'
|
|
10
|
+
import { globSync } from 'glob'
|
|
11
|
+
import path from 'path'
|
|
12
|
+
import { pathToFileURL } from 'url'
|
|
13
|
+
import { Utils } from 'adapt-authoring-core'
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
defaults: useDefaults,
|
|
17
|
+
params: [env],
|
|
18
|
+
replace: replaceExisting,
|
|
19
|
+
update: updateExisting
|
|
20
|
+
} = Utils.getArgs()
|
|
21
|
+
const NODE_ENV = env || process.env.NODE_ENV
|
|
22
|
+
const confDir = path.resolve(path.join(process.cwd(), 'conf'))
|
|
23
|
+
const outpath = path.join(confDir, `${NODE_ENV}.config.js`)
|
|
24
|
+
const configJson = {}
|
|
25
|
+
|
|
26
|
+
async function init () {
|
|
27
|
+
if (!NODE_ENV) {
|
|
28
|
+
return console.log('ERROR: NODE_ENV must be specified\n')
|
|
29
|
+
}
|
|
30
|
+
if (replaceExisting && updateExisting) {
|
|
31
|
+
return console.log('ERROR: --update and --replace cannot both be specified, please choose one and run the utility again')
|
|
32
|
+
}
|
|
33
|
+
if (useDefaults) {
|
|
34
|
+
console.log('Default values will be included')
|
|
35
|
+
}
|
|
36
|
+
let isExistingConfig = false
|
|
37
|
+
let existingConfig
|
|
38
|
+
try {
|
|
39
|
+
existingConfig = (await import(pathToFileURL(outpath))).default
|
|
40
|
+
isExistingConfig = true
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(`No config found for NODE_ENV '${NODE_ENV}'. File will be written to ${outpath}\n`)
|
|
43
|
+
}
|
|
44
|
+
if (isExistingConfig) {
|
|
45
|
+
const msg = `Config already exists for NODE_ENV '${NODE_ENV}'. `
|
|
46
|
+
if (replaceExisting) {
|
|
47
|
+
console.log(`${msg}All existing values will be replaced.`)
|
|
48
|
+
} else if (updateExisting) {
|
|
49
|
+
console.log(`${msg}Any missing values will be added.`)
|
|
50
|
+
Object.assign(configJson, existingConfig)
|
|
51
|
+
} else {
|
|
52
|
+
return console.log(`${msg}Must specifiy --replace or --update to make changes.`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await generateConfig()
|
|
57
|
+
try {
|
|
58
|
+
await fs.mkdir(confDir, { recursive: true })
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e.code !== 'EEXIST') throw e
|
|
61
|
+
}
|
|
62
|
+
await fs.writeFile(outpath, `export default ${JSON.stringify(configJson, null, 2)};`)
|
|
63
|
+
|
|
64
|
+
console.log(`Config file written successfully to ${outpath}.\n`)
|
|
65
|
+
|
|
66
|
+
logRequired()
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.log(`ERROR: Failed to write ${outpath}\n${e}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function logRequired () {
|
|
73
|
+
const requiredAttrs = []
|
|
74
|
+
Object.entries(configJson).forEach(([name, config]) => {
|
|
75
|
+
Object.entries(config).forEach(([key, value]) => value === null && requiredAttrs.push(`${name}.${key}`))
|
|
76
|
+
})
|
|
77
|
+
if (requiredAttrs.length) {
|
|
78
|
+
console.log('Note: the following required attributes have been given a value of null and must be set for the application to run:\n')
|
|
79
|
+
console.log(requiredAttrs.join('\n'))
|
|
80
|
+
console.log('')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function getDeps () {
|
|
85
|
+
try {
|
|
86
|
+
const depRoot = `${process.cwd()}/node_modules/`.replaceAll(path.sep, path.posix.sep)
|
|
87
|
+
return globSync(`${depRoot}**/adapt-authoring.json`).map(f => {
|
|
88
|
+
const dirname = path.dirname(f)
|
|
89
|
+
return [dirname.replace(depRoot, ''), dirname]
|
|
90
|
+
})
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.log('Failed to load package', e)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function generateConfig () {
|
|
97
|
+
await Promise.all((await getDeps()).map(async ([name, dir]) => {
|
|
98
|
+
let schema
|
|
99
|
+
try {
|
|
100
|
+
schema = schema = JSON.parse(await fs.readFile(path.resolve(dir, 'conf/config.schema.json')))
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if (!configJson[name]) {
|
|
105
|
+
configJson[name] = {}
|
|
106
|
+
}
|
|
107
|
+
storeDefaults(schema, configJson[name])
|
|
108
|
+
// remove any empty objects
|
|
109
|
+
Object.entries(configJson).forEach(([key, config]) => !Object.keys(config).length && delete configJson[key])
|
|
110
|
+
}))
|
|
111
|
+
}
|
|
112
|
+
function storeDefaults (schema, defaults = {}) {
|
|
113
|
+
return Object.entries(schema.properties).reduce((memo, [attr, config]) => {
|
|
114
|
+
if (config.type === 'object' && config.properties) {
|
|
115
|
+
return { ...memo, [attr]: storeDefaults(config, memo) }
|
|
116
|
+
}
|
|
117
|
+
config.required = schema?.required?.includes(attr) ?? false
|
|
118
|
+
const shouldUpdate = replaceExisting || !Object.prototype.hasOwnProperty.call(memo, attr)
|
|
119
|
+
const useDefault = useDefaults && Object.prototype.hasOwnProperty.call(config, 'default')
|
|
120
|
+
if (shouldUpdate && (useDefault || config.required)) {
|
|
121
|
+
memo[attr] = getValueForAttr(config)
|
|
122
|
+
}
|
|
123
|
+
return memo
|
|
124
|
+
}, defaults)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getValueForAttr (config) {
|
|
128
|
+
if (config.required) return null
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(config, 'default')) return config.default
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
init()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Configuring your environment
|
|
2
|
+
> For a list of all supported configuration options, see [this page](configuration).
|
|
3
|
+
|
|
4
|
+
The authoring tool has been built to allow for multiple configurations for different system environments (e.g. testing, production, development).
|
|
5
|
+
|
|
6
|
+
## Set up your environment
|
|
7
|
+
|
|
8
|
+
To configure your tool for a specific environment, you must create a config file in `/conf` named according to the env value your system will be using (e.g. `dev.config.js`, `production.config.js`, `helloworld.config.js`). We recommend sticking to something short like `dev`, or `test`, but it's up to you what you name these; just make sure to set the environment variable to the same.
|
|
9
|
+
|
|
10
|
+
> The `NODE_ENV` environment variable is used to determine the current environment, so make sure that this is set appropriately when running the application:
|
|
11
|
+
|
|
12
|
+
Express.js has a number of [performance enhancing features](https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production) which are only enabled when the NODE_ENV is set to `production`, so we strongly recommend you use this for your production env name.
|
|
13
|
+
|
|
14
|
+
### Creating your config
|
|
15
|
+
|
|
16
|
+
Each config file is a JavaScript file which exports a single object. Within this file, settings are grouped by module:
|
|
17
|
+
|
|
18
|
+
```Javascript
|
|
19
|
+
export default {
|
|
20
|
+
'modulename': {
|
|
21
|
+
// settings
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
See [this page](configuration) for a complete list of all configuration options.
|
|
27
|
+
|
|
28
|
+
####
|
|
29
|
+
For convenience, we've bundled a script which will generate a new config file for you automatically.
|
|
30
|
+
|
|
31
|
+
You can do this by running the following:
|
|
32
|
+
```bash
|
|
33
|
+
npx at-confgen [NODE_ENV]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> If you choose to include the default settings in your configuration, please be aware that once set, these values will not be updated if the defaults change in the future. It is advised therefore that you leave out any settings that you don't wish to change.
|
|
37
|
+
|
|
38
|
+
See the [Bin scripts](binscripts#at-confgen) page for more information, included supported flags.
|
|
39
|
+
|
|
40
|
+
### Setting your 'env'
|
|
41
|
+
|
|
42
|
+
You can do this temporarily using the following:
|
|
43
|
+
|
|
44
|
+
**Bash/Mac OS Terminal**:
|
|
45
|
+
```bash
|
|
46
|
+
$ NODE_ENV=dev npm start
|
|
47
|
+
```
|
|
48
|
+
**Windows Powershell/Command Prompt**:
|
|
49
|
+
```bash
|
|
50
|
+
> set NODE_ENV=dev | npm start
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Please see the documentation for your own operating system for instructions on how to save environment variables in a more permanent way.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Defining module configuration
|
|
2
|
+
|
|
3
|
+
As a module developer, you will likely have a number of user-configurable attributes for adjusting the functionality of your module. This is great from a user perspective as it allows customisation, but can introduce various bugs as a result of bad user input (missing or unexpected values etc.)
|
|
4
|
+
|
|
5
|
+
The Adapt authoring tool's configuration module aims to pre-empt as many of these issues as possible through the use of configuration **schemas**, which can define the following:
|
|
6
|
+
- Required attributes
|
|
7
|
+
- Default values for optional attributes
|
|
8
|
+
- Expected type for values (e.g. number, string, array)
|
|
9
|
+
- Validation constraints (see JSON schema spec)
|
|
10
|
+
|
|
11
|
+
## Defining module configuration
|
|
12
|
+
_**Note**: it is not mandatory to include a config schema for your module, but it may help your general wellbeing/code neatness..._
|
|
13
|
+
|
|
14
|
+
All that's needed to enable this feature is to include a `config.schema.json` to a directory named `conf` in the root of your module.
|
|
15
|
+
|
|
16
|
+
This file must export a valid JSON object. See the JSON schema docs for more information.
|
|
17
|
+
|
|
18
|
+
### Example configuration schema
|
|
19
|
+
The below example shows a few common configuration use-cases:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": {
|
|
26
|
+
"requiredAttribute": {
|
|
27
|
+
"type": "Number",
|
|
28
|
+
"description": "This option is required"
|
|
29
|
+
},
|
|
30
|
+
"optionalAttribute": {
|
|
31
|
+
"type": "String",
|
|
32
|
+
"default": "This will be the default value",
|
|
33
|
+
"description": "An optional attribute with a default value"
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"required": ["requiredAttribute"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Directory paths
|
|
41
|
+
|
|
42
|
+
It may be necessary to define directory paths in your config schema (e.g. the assets module defines an `uploadDir` attribute). To make this simpler from a developer _and_ user perspective, you can make use of the special `isDirectory` JSON schema keyword which will automatically populate the value with a number of global directory paths.
|
|
43
|
+
|
|
44
|
+
If `isDirectory` is set in a config schema, the configuration module will look for the following keys and replace them with the corresponding `adapt-authoring-core` config values:
|
|
45
|
+
|
|
46
|
+
- `$ROOT` -> `adapt-authoring-core.rootDir`
|
|
47
|
+
- `$DATA` -> `adapt-authoring-core.dataDir`
|
|
48
|
+
- `$TEMP` -> `adapt-authoring-core.tempDir`
|
|
49
|
+
|
|
50
|
+
This allows the user to specify just the `dataDir` and `tempDir` attributes, and you can be safe in the knowledge that your config schema value will resolve to the correct location.
|
|
51
|
+
|
|
52
|
+
See the following JSON schema snippet as an example:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"uploadDir": {
|
|
57
|
+
"type": "String",
|
|
58
|
+
"description": "Temporary file upload directory for myplugin",
|
|
59
|
+
"isDirectory": true,
|
|
60
|
+
"default": "$TEMP/myfiles"
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default class Configuration {
|
|
5
|
+
async run() {
|
|
6
|
+
const schemas = this.loadSchemas();
|
|
7
|
+
this.contents = Object.keys(schemas).sort();
|
|
8
|
+
this.manualFile = 'configuration.md';
|
|
9
|
+
this.replace = {
|
|
10
|
+
'CODE_EXAMPLE': this.generateCodeExample(schemas),
|
|
11
|
+
'LIST': this.generateList(schemas)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
loadSchemas() {
|
|
15
|
+
const schemas = {};
|
|
16
|
+
Object.values(this.app.dependencies).forEach(c => {
|
|
17
|
+
const confDir = path.join(c.rootDir, 'conf');
|
|
18
|
+
try {
|
|
19
|
+
schemas[c.name] = JSON.parse(fs.readFileSync(path.join(confDir, 'config.schema.json')));
|
|
20
|
+
} catch(e) {}
|
|
21
|
+
});
|
|
22
|
+
return schemas;
|
|
23
|
+
}
|
|
24
|
+
generateCodeExample(schemas) {
|
|
25
|
+
let output = '\`\`\`javascript\nexport default {\n';
|
|
26
|
+
this.contents.forEach((name) => {
|
|
27
|
+
const schema = schemas[name];
|
|
28
|
+
output += ` '${name}': {\n`;
|
|
29
|
+
Object.entries(schema.properties).forEach(([attr, config]) => {
|
|
30
|
+
const required = schema.required && schema.required.includes(attr);
|
|
31
|
+
if(config.description) output += ` // ${config.description}\n`;
|
|
32
|
+
output += ` ${attr}: ${this.defaultToMd(config)}, // ${config.type}, ${required ? 'required' : 'optional'}\n`;
|
|
33
|
+
});
|
|
34
|
+
output += ` },\n`;
|
|
35
|
+
});
|
|
36
|
+
output += `};\n\`\`\``;
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
generateList(schemas) {
|
|
40
|
+
let output = '';
|
|
41
|
+
|
|
42
|
+
this.contents.forEach(dep => {
|
|
43
|
+
const schema = schemas[dep];
|
|
44
|
+
output += `<h3 id="${dep}" class="dep">${dep}</h3>\n\n`;
|
|
45
|
+
output += `<div class="options">\n`;
|
|
46
|
+
Object.entries(schema.properties).forEach(([attr, config]) => {
|
|
47
|
+
const required = schema.required && schema.required.includes(attr);
|
|
48
|
+
output += `<div class="attribute">\n`;
|
|
49
|
+
output += `<div class="title"><span class="main">${attr}</span> (${config.type || ''}, ${required ? 'required' : 'optional'})</div>\n`;
|
|
50
|
+
output += `<div class="inner">\n`;
|
|
51
|
+
output += `<div class="description">${config.description}</div>\n`;
|
|
52
|
+
if(!required) {
|
|
53
|
+
output += `<div class="default"><span class="label">Default</span>: <pre>${this.defaultToMd(config)}</pre></div>\n`;
|
|
54
|
+
}
|
|
55
|
+
output += `</div>\n`;
|
|
56
|
+
output += `</div>\n`;
|
|
57
|
+
});
|
|
58
|
+
output += `</div>`;
|
|
59
|
+
output += `\n\n`;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return output;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Returns a string formatted nicely for markdown
|
|
66
|
+
*/
|
|
67
|
+
defaultToMd(config) {
|
|
68
|
+
return JSON.stringify(config.default);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
This page lists all configuration options supported by the [core bundle](coremodules) of Adapt authoring modules.
|
|
3
|
+
|
|
4
|
+
{{{TABLE_OF_CONTENTS}}}
|
|
5
|
+
|
|
6
|
+
## Quick reference
|
|
7
|
+
See below for an overview of all available configuration options.
|
|
8
|
+
|
|
9
|
+
{{{CODE_EXAMPLE}}}
|
|
10
|
+
|
|
11
|
+
## Complete reference
|
|
12
|
+
See below for a full list of available configuration options.
|
|
13
|
+
|
|
14
|
+
{{{LIST}}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"FILE_SYNTAX_ERROR": {
|
|
3
|
+
"data": {
|
|
4
|
+
"path": "Path to the invalid file",
|
|
5
|
+
"message": "The error message"
|
|
6
|
+
},
|
|
7
|
+
"description": "File contains a syntax error",
|
|
8
|
+
"statusCode": 500
|
|
9
|
+
},
|
|
10
|
+
"LOAD_ERROR": {
|
|
11
|
+
"description": "Config failed to load",
|
|
12
|
+
"statusCode": 500
|
|
13
|
+
}
|
|
14
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/* eslint no-console: 0 */
|
|
2
|
+
import { AbstractModule } from 'adapt-authoring-core'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { pathToFileURL } from 'url'
|
|
6
|
+
/**
|
|
7
|
+
* Module to expose config API
|
|
8
|
+
* @memberof config
|
|
9
|
+
* @extends {AbstractModule}
|
|
10
|
+
*/
|
|
11
|
+
class ConfigModule extends AbstractModule {
|
|
12
|
+
/** @override */
|
|
13
|
+
async init () {
|
|
14
|
+
// set references to module on main App instance
|
|
15
|
+
this.app.config = this
|
|
16
|
+
|
|
17
|
+
/** @ignore */
|
|
18
|
+
this._config = {}
|
|
19
|
+
/**
|
|
20
|
+
* Path to the user configuration file
|
|
21
|
+
* @type {String}
|
|
22
|
+
*/
|
|
23
|
+
this.configFilePath = path.join(this.app.rootDir, 'conf', `${process.env.NODE_ENV}.config.js`)
|
|
24
|
+
/**
|
|
25
|
+
* The keys for all attributes which can be modified during runtime
|
|
26
|
+
* @type {Array<String>}
|
|
27
|
+
*/
|
|
28
|
+
this.mutableAttributes = []
|
|
29
|
+
/**
|
|
30
|
+
* The keys for all attributes marked as public
|
|
31
|
+
* @type {Array<String>}
|
|
32
|
+
*/
|
|
33
|
+
this.publicAttributes = []
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// need to wait for errors to ensure correct logging
|
|
37
|
+
await this.app.waitForModule('errors')
|
|
38
|
+
|
|
39
|
+
await this.storeUserSettings()
|
|
40
|
+
await this.storeEnvSettings()
|
|
41
|
+
await this.storeSchemaSettings()
|
|
42
|
+
|
|
43
|
+
this.log('info', `using config at ${this.configFilePath}`)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(e)
|
|
46
|
+
console.log(`\n${chalk.red(`Config failed to initialise for environment '${process.env.NODE_ENV}'. See above for details.`)}\n`)
|
|
47
|
+
throw this.app.errors.LOAD_ERROR
|
|
48
|
+
}
|
|
49
|
+
/*
|
|
50
|
+
* Note: we wait until after the ready signal before initialising router because ConfigModule needs to be
|
|
51
|
+
* available straight away (and not wait for server etc.)
|
|
52
|
+
*/
|
|
53
|
+
this.onReady().then(() => this.initRouter())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Adds routing functionality
|
|
58
|
+
* @return {Promise}
|
|
59
|
+
*/
|
|
60
|
+
async initRouter () {
|
|
61
|
+
const [auth, server] = await this.app.waitForModule('auth', 'server')
|
|
62
|
+
const router = server.api.createChildRouter('config')
|
|
63
|
+
router.addRoute({
|
|
64
|
+
route: '/',
|
|
65
|
+
handlers: { get: (req, res) => res.json(this.getPublicConfig(req.params.mutable)) },
|
|
66
|
+
meta: {
|
|
67
|
+
get: {
|
|
68
|
+
summary: 'Retrieve public config data',
|
|
69
|
+
responses: {
|
|
70
|
+
200: {
|
|
71
|
+
description: 'The public config item data',
|
|
72
|
+
content: { 'application/json': { schema: { type: 'object' } } }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
auth.unsecureRoute(router.path, 'get')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Copy env values to config
|
|
83
|
+
* @return {Promise}
|
|
84
|
+
*/
|
|
85
|
+
async storeEnvSettings () {
|
|
86
|
+
Object.entries(process.env).forEach(([key, val]) => {
|
|
87
|
+
try { // try to parse to allow for non-string values
|
|
88
|
+
val = JSON.parse(val)
|
|
89
|
+
} catch {} // ignore errors
|
|
90
|
+
this.set(this.envVarToConfigKey(key), val)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parses an environment variable key into a format expected by this module
|
|
96
|
+
* @param {String} envVar
|
|
97
|
+
* @return {String} The formatted key
|
|
98
|
+
*/
|
|
99
|
+
envVarToConfigKey (envVar) {
|
|
100
|
+
if (envVar.startsWith('ADAPT_AUTHORING_')) {
|
|
101
|
+
const [modPrefix, key] = envVar.split('__')
|
|
102
|
+
return `${modPrefix.replace(/_/g, '-').toLowerCase()}.${key}`
|
|
103
|
+
}
|
|
104
|
+
return `env.${envVar}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Loads the relevant config file into memory
|
|
109
|
+
* @return {Promise}
|
|
110
|
+
*/
|
|
111
|
+
async storeUserSettings () {
|
|
112
|
+
let c
|
|
113
|
+
try {
|
|
114
|
+
c = (await import(pathToFileURL(this.configFilePath))).default
|
|
115
|
+
} catch (e) {
|
|
116
|
+
if (e.code !== 'ENOENT' && e.code !== 'ERR_MODULE_NOT_FOUND') {
|
|
117
|
+
console.trace(e)
|
|
118
|
+
throw e
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!c) {
|
|
122
|
+
console.log(chalk.yellow(`No config file found at '${this.configFilePath}', attempting to run with defaults\n`))
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
Object.entries(c).forEach(([name, config]) => {
|
|
126
|
+
Object.entries(config).forEach(([key, val]) => {
|
|
127
|
+
this.set(`${name}.${key}`, val)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Processes all module config schema files
|
|
134
|
+
* @return {Promise}
|
|
135
|
+
*/
|
|
136
|
+
async storeSchemaSettings () {
|
|
137
|
+
const jsonschema = await this.app.waitForModule('jsonschema')
|
|
138
|
+
const deps = Object.values(this.app.dependencies)
|
|
139
|
+
// run core first as other modules may use its config values
|
|
140
|
+
await this.processModuleSchema(deps.find(d => d.name === this.app.name), jsonschema)
|
|
141
|
+
const promises = deps.map(d => this.processModuleSchema(d, jsonschema))
|
|
142
|
+
let hasErrored = false;
|
|
143
|
+
|
|
144
|
+
(await Promise.allSettled(promises)).forEach(r => {
|
|
145
|
+
if (r.status === 'rejected') {
|
|
146
|
+
hasErrored = true
|
|
147
|
+
if (r.reason?.data?.errors) {
|
|
148
|
+
console.log(`${r.reason.modName}: ${r.reason.data.errors}`)
|
|
149
|
+
} else {
|
|
150
|
+
console.log(r.reason)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
if (hasErrored) throw new Error()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Processes and validates a single module config schema (checks the user config specifies any required fields, and that they are the expected type)
|
|
159
|
+
* @param {Object} pkg Package.json data
|
|
160
|
+
* @param {JsonSchemaModule} jsonschema Module instance for validation
|
|
161
|
+
* @return {Promise}
|
|
162
|
+
*/
|
|
163
|
+
async processModuleSchema (pkg, jsonschema) {
|
|
164
|
+
if (!pkg.name || !pkg.rootDir) return
|
|
165
|
+
|
|
166
|
+
const schemaPath = path.resolve(pkg.rootDir, 'conf/config.schema.json')
|
|
167
|
+
let schema
|
|
168
|
+
try {
|
|
169
|
+
schema = await (await jsonschema.createSchema(schemaPath)).build()
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
// validate user config data
|
|
174
|
+
let data = Object.entries(schema.raw.properties).reduce((m, [k, v]) => {
|
|
175
|
+
if (v?._adapt?.isMutable) this.mutableAttributes.push(`${pkg.name}.${k}`)
|
|
176
|
+
if (v?._adapt?.isPublic) this.publicAttributes.push(`${pkg.name}.${k}`)
|
|
177
|
+
return { ...m, [k]: this.get(`${pkg.name}.${k}`) }
|
|
178
|
+
}, {})
|
|
179
|
+
try {
|
|
180
|
+
data = await schema.validate(data)
|
|
181
|
+
} catch (e) {
|
|
182
|
+
e.modName = pkg.name
|
|
183
|
+
throw e
|
|
184
|
+
}
|
|
185
|
+
// apply validated config settings
|
|
186
|
+
Object.entries(data).forEach(([key, val]) => this.set(`${pkg.name}.${key}`, val, { force: true }))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Determines whether an attribute has a set value
|
|
191
|
+
* @param {String} attr Attribute key name
|
|
192
|
+
* @return {Boolean} Whether the value exists
|
|
193
|
+
*/
|
|
194
|
+
has (attr) {
|
|
195
|
+
return Object.hasOwn(this._config, attr)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns a value for a given attribute
|
|
200
|
+
* @param {String} attr Attribute key name
|
|
201
|
+
* @return {*} The attribute's value
|
|
202
|
+
*/
|
|
203
|
+
get (attr) {
|
|
204
|
+
return this._config[attr]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Stores a value for the passed attribute
|
|
209
|
+
* @param {String} attr Attribute key name
|
|
210
|
+
* @param {*} val Value to set
|
|
211
|
+
* @param {objeect} options Custom options
|
|
212
|
+
* @param {objeect} options.force Whether to force an update
|
|
213
|
+
*/
|
|
214
|
+
set (attr, val, options = {}) {
|
|
215
|
+
if (this.has(attr) && !this.mutableAttributes.includes(attr) && options.force !== true) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
this._config[attr] = val
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Retrieves all config options marked as 'public'
|
|
223
|
+
* @param {Boolean} isMutable Whether options should also be mutable
|
|
224
|
+
* @return {Object}
|
|
225
|
+
*/
|
|
226
|
+
getPublicConfig (isMutable) {
|
|
227
|
+
return this.publicAttributes.reduce((m, a) => {
|
|
228
|
+
if (!isMutable || (isMutable && this.mutableAttributes.includes(a))) {
|
|
229
|
+
m[a] = this.get(a)
|
|
230
|
+
}
|
|
231
|
+
return m
|
|
232
|
+
}, {})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default ConfigModule
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adapt-authoring-config",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A configuration module for the Adapt authoring tool.",
|
|
5
|
+
"homepage": "https://github.com/adapt-security/adapt-authoring-config",
|
|
6
|
+
"license": "GPL-3.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"at-confgen": "./bin/confgen.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": "github:adapt-security/adapt-authoring-config",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"adapt-authoring-core": "github:adapt-security/adapt-authoring-core",
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"glob": "^11.0.0"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"adapt-authoring-core": "github:adapt-security/adapt-authoring-core"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"eslint": "^9.12.0",
|
|
23
|
+
"standard": "^17.1.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const Config = require('../lib/configUtils');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const should = require('should');
|
|
4
|
+
|
|
5
|
+
describe('Config module', function() {
|
|
6
|
+
before(function() {
|
|
7
|
+
this.config = new Config(global.ADAPT.app, {});
|
|
8
|
+
this.configJson = require(path.join(process.cwd(), 'conf', 'testing.config.js'));
|
|
9
|
+
});
|
|
10
|
+
describe('#initialise()', function() {
|
|
11
|
+
it('should error on missing required attribute', runConfigInitialise('required'));
|
|
12
|
+
it('should error on incorrect attribute type', runConfigInitialise('incorrecttype'));
|
|
13
|
+
it('should error on validator fail', runConfigInitialise('invalid'));
|
|
14
|
+
});
|
|
15
|
+
describe('#has()', function() {
|
|
16
|
+
it('should be able to verify a value exists', function() {
|
|
17
|
+
const exists = this.config.has('adapt-authoring-testing.test');
|
|
18
|
+
exists.should.be.true();
|
|
19
|
+
});
|
|
20
|
+
it('should be able to verify a value doesn\'t exist', function() {
|
|
21
|
+
const exists = this.config.has('adapt-authoring-testing.nonono');
|
|
22
|
+
exists.should.not.be.true();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('#get()', function() {
|
|
26
|
+
it('should be able to retrieve a value', function() {
|
|
27
|
+
const actualValue = this.config.get('adapt-authoring-testing.test');
|
|
28
|
+
const expectedValue = this.configJson['adapt-authoring-testing'].test;
|
|
29
|
+
actualValue.should.equal(expectedValue);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('#set()', function() {
|
|
33
|
+
it('should be able to set a value', function() {
|
|
34
|
+
const newValue = 'newtestvalue';
|
|
35
|
+
this.config.set('adapt-authoring-testing.test', newValue);
|
|
36
|
+
const actualValue = this.config.get('adapt-authoring-testing.test');
|
|
37
|
+
actualValue.should.equal(newValue);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('#getPublicConfig()', function() {
|
|
41
|
+
it('should be able to retrieve values marked as public', function() {
|
|
42
|
+
this.config.app.dependencies = [{ name: 'adapt-authoring-testing', dir: path.join(__dirname, 'data') }];
|
|
43
|
+
this.config.initialise();
|
|
44
|
+
const config = this.config.getPublicConfig();
|
|
45
|
+
const value = config['adapt-authoring-testing.one'];
|
|
46
|
+
config.should.be.an.Object();
|
|
47
|
+
value.should.equal('default');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Checks ConfigUtility#initialise
|
|
53
|
+
* Loads the testing data in tests/data/dirname
|
|
54
|
+
*/
|
|
55
|
+
function runConfigInitialise(dirname) {
|
|
56
|
+
return function() {
|
|
57
|
+
this.config.app.dependencies = [{ name: 'adapt-authoring-testing', dir: path.join(__dirname, 'data', dirname) }];
|
|
58
|
+
this.config.initialise();
|
|
59
|
+
this.config.errors.length.should.be.exactly(1);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const utils = require('../lib/ConfigUtils');
|
|
3
|
+
const should = require('should');
|
|
4
|
+
|
|
5
|
+
describe('Config utils', function() {
|
|
6
|
+
describe('#loadFile()', function() {
|
|
7
|
+
it('should be able to load a valid file', function() {
|
|
8
|
+
const filepath = path.join(__dirname, 'data', 'testfile.json');
|
|
9
|
+
const actualContents = utils.loadFile(filepath);
|
|
10
|
+
const expectedContents = require(filepath);
|
|
11
|
+
actualContents.should.deepEqual(expectedContents);
|
|
12
|
+
});
|
|
13
|
+
it('should not error on a missing file', function() {
|
|
14
|
+
should.doesNotThrow(function() {
|
|
15
|
+
const contents = utils.loadFile(path.join('this', 'path', 'does', 'not', 'exist.xyz'));
|
|
16
|
+
should(contents).be.undefined();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('#loadConfigSchema()', function() {
|
|
21
|
+
it('should be able to load a valid schema file', function() {
|
|
22
|
+
const dir = path.join(__dirname, 'data');
|
|
23
|
+
const actualContents = utils.loadConfigSchema(dir);
|
|
24
|
+
const expectedContents = require(path.join(dir, 'conf', 'config.schema.js'));
|
|
25
|
+
actualContents.should.deepEqual(expectedContents);
|
|
26
|
+
});
|
|
27
|
+
it('should not error on a missing schema file', function() {
|
|
28
|
+
should.doesNotThrow(function() {
|
|
29
|
+
const contents = utils.loadConfigSchema(path.join(__dirname, 'doesntexist'));
|
|
30
|
+
should(contents).be.undefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|