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 ADDED
@@ -0,0 +1 @@
1
+ node_modules
package/.eslintrc ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "env": {
3
+ "browser": false,
4
+ "node": true,
5
+ "commonjs": false,
6
+ "es2020": true
7
+ },
8
+ "extends": [
9
+ "standard"
10
+ ],
11
+ "parserOptions": {
12
+ "ecmaVersion": 2020
13
+ }
14
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ scenarios:
2
+ - flow:
3
+ - get:
4
+ url: "/config"
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,5 @@
1
+ /**
2
+ * Application configuration storage
3
+ * @namespace config
4
+ */
5
+ export { default } from './lib/ConfigModule.js'
@@ -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
+ });
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ definition: {
3
+ one: {
4
+ type: 'String',
5
+ default: 'default',
6
+ description: 'Test option',
7
+ public: true
8
+ }
9
+ }
10
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ definition: {
3
+ typecheck: {
4
+ type: 'Number'
5
+ }
6
+ }
7
+ };
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ definition: {
3
+ validator: {
4
+ type: 'Number',
5
+ validator: val => val === 4
6
+ }
7
+ }
8
+ };
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ definition: {
3
+ required: {
4
+ type: 'Number',
5
+ required: true
6
+ }
7
+ }
8
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "test": true
3
+ }