adapt-authoring-core 2.5.0 → 3.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.
@@ -0,0 +1,75 @@
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
+
15
+ loadSchemas () {
16
+ const schemas = {}
17
+ Object.values(this.app.dependencies).forEach(c => {
18
+ const confDir = path.join(c.rootDir, 'conf')
19
+ try {
20
+ schemas[c.name] = JSON.parse(fs.readFileSync(path.join(confDir, 'config.schema.json')))
21
+ } catch (e) {}
22
+ })
23
+ return schemas
24
+ }
25
+
26
+ generateCodeExample (schemas) {
27
+ let output = '```javascript\nexport default {\n'
28
+ this.contents.forEach((name) => {
29
+ const schema = schemas[name]
30
+ output += ` '${name}': {\n`
31
+ Object.entries(schema.properties).forEach(([attr, config]) => {
32
+ const required = schema.required && schema.required.includes(attr)
33
+ if (config.description) output += ` // ${config.description}\n`
34
+ output += ` ${attr}: ${this.defaultToMd(config)}, // ${config.type}, ${required ? 'required' : 'optional'}\n`
35
+ })
36
+ output += ' },\n'
37
+ })
38
+ output += '};\n```'
39
+ return output
40
+ }
41
+
42
+ generateList (schemas) {
43
+ let output = ''
44
+
45
+ this.contents.forEach(dep => {
46
+ const schema = schemas[dep]
47
+ output += `<h3 id="${dep}" class="dep">${dep}</h3>\n\n`
48
+ output += '<div class="options">\n'
49
+ Object.entries(schema.properties).forEach(([attr, config]) => {
50
+ const required = schema.required && schema.required.includes(attr)
51
+ output += '<div class="attribute">\n'
52
+ output += `<div class="title"><span class="main">${attr}</span> (${config.type || ''}, ${required ? 'required' : 'optional'})</div>\n`
53
+ output += '<div class="inner">\n'
54
+ output += `<div class="description">${config.description}</div>\n`
55
+ if (!required) {
56
+ output += `<div class="default"><span class="label">Default</span>: <pre class="no-bg">${this.defaultToMd(config)}</pre></div>\n`
57
+ }
58
+ output += '</div>\n'
59
+ output += '</div>\n'
60
+ })
61
+ output += '</div>'
62
+ output += '\n\n'
63
+ })
64
+
65
+ return output
66
+ }
67
+
68
+ /**
69
+ * Returns a string formatted nicely for markdown
70
+ */
71
+ defaultToMd (config) {
72
+ const s = JSON.stringify(config.default, null, 2)
73
+ return s?.length < 75 ? s : s?.replaceAll('\n', '\n ')
74
+ }
75
+ }
@@ -0,0 +1,14 @@
1
+ # Configuration reference
2
+ This page lists all configuration options supported by the [core bundle](coremodules) of Adapt authoring modules. For details on how to set up your configuration, including using environment variables, see [Configuring your environment](configure-environment).
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,22 @@
1
+ export default class Errors {
2
+ async run () {
3
+ this.manualFile = 'errorsref.md'
4
+ this.contents = Object.keys(this.app.errors)
5
+ this.replace = { ERRORS: this.generateMd() }
6
+ }
7
+
8
+ generateMd () {
9
+ return Object.keys(this.app.errors).reduce((md, k) => {
10
+ const e = this.app.errors[k]
11
+ return `${md}\n| \`${e.code}\` | ${e.meta.description} | ${e.statusCode} | <ul>${this.dataToMd(e.meta.data)}</ul> |`
12
+ }, '| Error code | Description | HTTP status code | Supplemental data |\n| - | - | :-: | - |')
13
+ }
14
+
15
+ dataToMd (data) {
16
+ if (!data) return ''
17
+ return Object.entries(data).reduce((acc, [k, v]) => {
18
+ const nested = typeof v === 'object' ? this.dataToMd(v) : v
19
+ return `${acc}<li>\`${k}\`: ${nested}</li>`
20
+ }, '')
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ # Errors Reference
2
+
3
+ This page documents all errors which are likely to be thrown in the system, along with the appropriate HTTP status code and any supplemental data which is stored with the error.
4
+
5
+ Supplemental data can be used at the point that errors are translated to provide more context to a specific error. All data stored with an error can be assumed to be a primitive type for easy printing.
6
+
7
+ {{{TABLE_OF_CONTENTS}}}
8
+
9
+ {{{ERRORS}}}
@@ -1,4 +1,84 @@
1
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
+ "DEP_ALREADY_LOADED": {
11
+ "data": { "module": "The module name" },
12
+ "description": "Module has already been loaded",
13
+ "statusCode": 500
14
+ },
15
+ "DEP_FAILED": {
16
+ "data": { "module": "The module name" },
17
+ "description": "Required dependency failed to load",
18
+ "statusCode": 500,
19
+ "isFatal": true
20
+ },
21
+ "DEP_INVALID_EXPORT": {
22
+ "data": { "module": "The module name" },
23
+ "description": "Module must export a class as its default export",
24
+ "statusCode": 500
25
+ },
26
+ "DEP_MISSING": {
27
+ "data": { "module": "The module name" },
28
+ "description": "Required module is not installed",
29
+ "statusCode": 500,
30
+ "isFatal": true
31
+ },
32
+ "DEP_NO_ONREADY": {
33
+ "data": { "module": "The module name" },
34
+ "description": "Module must define an onReady function",
35
+ "statusCode": 500
36
+ },
37
+ "DEP_TIMEOUT": {
38
+ "data": { "module": "The module name", "timeout": "The timeout in ms" },
39
+ "description": "Module load exceeded timeout",
40
+ "statusCode": 500
41
+ },
42
+ "FUNC_DISABLED": {
43
+ "data": {
44
+ "name": "The name of the function"
45
+ },
46
+ "description": "Function has been disabled",
47
+ "statusCode": 500
48
+ },
49
+ "FUNC_NOT_OVERRIDDEN": {
50
+ "data": {
51
+ "name": "The name of the function"
52
+ },
53
+ "description": "Function must be overridden in child class",
54
+ "statusCode": 500
55
+ },
56
+ "INVALID_PARAMS": {
57
+ "data": {
58
+ "params": "The invalid params"
59
+ },
60
+ "description": "Invalid parameters have been provided",
61
+ "statusCode": 400
62
+ },
63
+ "LOAD_ERROR": {
64
+ "description": "Config failed to load",
65
+ "statusCode": 500
66
+ },
67
+ "NOT_FOUND": {
68
+ "data": {
69
+ "id": "An identifier for the missing item",
70
+ "type": "Type of the missing item"
71
+ },
72
+ "description": "Requested item could not be found",
73
+ "statusCode": 404
74
+ },
75
+ "SERVER_ERROR": {
76
+ "description": "Generic server error",
77
+ "statusCode": 500,
78
+ "data": {
79
+ "error": "The original error"
80
+ }
81
+ },
2
82
  "SPAWN": {
3
83
  "data": {
4
84
  "cmd": "The command",
@@ -7,5 +87,12 @@
7
87
  },
8
88
  "description": "Error occurred spawning command",
9
89
  "statusCode": 500
90
+ },
91
+ "UNKNOWN_LANG": {
92
+ "data": {
93
+ "lang": "language"
94
+ },
95
+ "description": "unknown language",
96
+ "statusCode": 400
10
97
  }
11
98
  }
@@ -0,0 +1,39 @@
1
+ {
2
+ "EACCES": {
3
+ "description": "An attempt was made to access a file in a way forbidden by its file access permissions",
4
+ "statusCode": 500
5
+ },
6
+ "EADDRINUSE": {
7
+ "description": "An attempt to bind a server to a local address failed due to another server on the local system already occupying that address",
8
+ "statusCode": 500
9
+ },
10
+ "ECONNREFUSED": {
11
+ "description": "No connection could be made because the target machine actively refused it",
12
+ "statusCode": 500
13
+ },
14
+ "EEXIST": {
15
+ "description": "An existing file was the target of an operation that required that the target not exist",
16
+ "statusCode": 500,
17
+ "data": {
18
+ "path": "Path to target file or directory"
19
+ }
20
+ },
21
+ "ENOENT": {
22
+ "description": "No entity (file or directory) could be found by the given path",
23
+ "statusCode": 500,
24
+ "data": {
25
+ "path": "Path to target file or directory"
26
+ }
27
+ },
28
+ "ENOTEMPTY": {
29
+ "description": "A directory with entries was the target of an operation that requires an empty directory",
30
+ "statusCode": 500,
31
+ "data": {
32
+ "path": "Path to target file or directory"
33
+ }
34
+ },
35
+ "MODULE_NOT_FOUND": {
36
+ "description": "A module file could not be resolved while attempting a require() or import operation",
37
+ "statusCode": 500
38
+ }
39
+ }
package/index.js CHANGED
@@ -1,6 +1,11 @@
1
1
  export { default as AbstractModule } from './lib/AbstractModule.js'
2
+ export { default as AdaptError } from './lib/AdaptError.js'
2
3
  export { default as App } from './lib/App.js'
4
+ export { default as Config } from './lib/Config.js'
3
5
  export { default as DataCache } from './lib/DataCache.js'
4
6
  export { default as DependencyLoader } from './lib/DependencyLoader.js'
7
+ export { default as Errors } from './lib/Errors.js'
5
8
  export { default as Hook } from './lib/Hook.js'
9
+ export { default as Lang } from './lib/Lang.js'
10
+ export { default as Logger } from './lib/Logger.js'
6
11
  export { metadataFileName, packageFileName, isObject, getArgs, spawn, readJson, writeJson, toBoolean, ensureDir, escapeRegExp, stringifyValues, loadDependencyFiles } from './lib/Utils.js'
@@ -105,26 +105,16 @@ class AbstractModule {
105
105
  * @return {*}
106
106
  */
107
107
  getConfig (key) {
108
- try {
109
- return this.app.config.get(`${this.name}.${key}`)
110
- } catch (e) {
111
- return undefined
112
- }
108
+ return this.app.config?.get(`${this.name}.${key}`)
113
109
  }
114
110
 
115
111
  /**
116
- * Log a message using the Logger module
112
+ * Log a message using the Logger
117
113
  * @param {String} level Log level of message
118
114
  * @param {...*} rest Arguments to log
119
115
  */
120
116
  log (level, ...rest) {
121
- const _log = (e, instance) => {
122
- if (!this.app.logger || (instance && instance.name !== this.app.logger.name)) return false
123
- this.app.dependencyloader.moduleLoadedHook.untap(_log)
124
- this.app.logger.log(level, this.name.replace(/^adapt-authoring-/, ''), ...rest)
125
- return true
126
- }
127
- if (!_log()) this.app.dependencyloader.moduleLoadedHook.tap(_log)
117
+ this.app.logger?.log(level, this.name.replace(/^adapt-authoring-/, ''), ...rest)
128
118
  }
129
119
  }
130
120
 
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A generic error class for use in Adapt applications
3
+ * @memberof core
4
+ */
5
+ class AdaptError extends Error {
6
+ /**
7
+ * @constructor
8
+ * @param {string} code The human-readable error code
9
+ * @param {number} statusCode The HTTP status code
10
+ * @param {object} metadata Metadata describing the error
11
+ */
12
+ constructor (code, statusCode = 500, metadata = {}) {
13
+ super(code)
14
+ /**
15
+ * The error code
16
+ * @type {String}
17
+ */
18
+ this.code = code
19
+ /**
20
+ * The HTTP status code
21
+ * @type {String}
22
+ */
23
+ this.statusCode = statusCode
24
+ /**
25
+ * Whether this error should halt the application
26
+ * @type {Boolean}
27
+ */
28
+ this.isFatal = metadata.isFatal ?? false
29
+ /**
30
+ * Metadata describing the error
31
+ * @type {Object}
32
+ */
33
+ this.meta = metadata
34
+ }
35
+
36
+ /**
37
+ * Chainable function to allow setting of data for use in user-friendly error messages later on.
38
+ * @param {object} data
39
+ * @returns {AdaptError}
40
+ * @example
41
+ * // note calling this function will also return
42
+ * // the error itself to allow for easy error throwing
43
+ * throw this.app.errors.MY_ERROR
44
+ * .setData({ hello: 'world' })
45
+ */
46
+ setData (data) {
47
+ this.data = data
48
+ return this
49
+ }
50
+
51
+ /** @override */
52
+ toString () {
53
+ return `${this.constructor.name}: ${this.code} ${this.data ? JSON.stringify(this.data) : ''}`
54
+ }
55
+ }
56
+
57
+ export default AdaptError
package/lib/App.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import AbstractModule from './AbstractModule.js'
2
+ import Config from './Config.js'
2
3
  import DependencyLoader from './DependencyLoader.js'
4
+ import Errors from './Errors.js'
5
+ import Lang from './Lang.js'
6
+ import Logger from './Logger.js'
3
7
  import fs from 'fs'
4
8
  import path from 'path'
9
+ import { runMigrations } from 'adapt-authoring-migrations'
5
10
  import { metadataFileName, packageFileName, getArgs } from './Utils.js'
6
11
 
7
12
  let instance
@@ -26,54 +31,85 @@ class App extends AbstractModule {
26
31
 
27
32
  /** @override */
28
33
  constructor () {
34
+ process.env.NODE_ENV ??= 'production'
29
35
  const rootDir = process.env.ROOT_DIR ?? process.cwd()
30
36
  const adaptJson = JSON.parse(fs.readFileSync(path.join(rootDir, metadataFileName)))
31
37
  const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, packageFileName)))
32
38
  super(null, { ...packageJson, ...adaptJson, name: 'adapt-authoring-core', rootDir })
33
- this.git = this.getGitInfo()
34
39
  }
35
40
 
36
41
  /** @override */
37
42
  async init () {
38
- /**
39
- * Reference to the passed arguments (parsed for easy reference)
40
- * @type {Object}
41
- */
42
- this.args = getArgs()
43
- /**
44
- * Instance of App instance (required by all AbstractModules)
45
- * @type {App}
46
- */
47
- this.app = this
48
- /**
49
- * Reference to the DependencyLoader instance
50
- * @type {DependencyLoader}
51
- */
52
- this.dependencyloader = new DependencyLoader(this)
43
+ try {
44
+ /**
45
+ * Instance of App instance (required by all AbstractModules)
46
+ * @type {App}
47
+ */
48
+ this.app = this
49
+ /**
50
+ * Reference to the passed arguments (parsed for easy reference)
51
+ * @type {Object}
52
+ */
53
+ this.args = getArgs()
54
+ /**
55
+ * Reference to the Config instance
56
+ * @type {Config}
57
+ */
58
+ this.config = undefined
59
+ /**
60
+ * Reference to the error registry
61
+ * @type {Errors}
62
+ */
63
+ this.errors = undefined
64
+ /**
65
+ * Reference to the Lang instance
66
+ * @type {Lang}
67
+ */
68
+ this.lang = undefined
69
+ /**
70
+ * Reference to the Logger instance
71
+ * @type {Logger}
72
+ */
73
+ this.logger = new Logger()
74
+ /**
75
+ * Reference to the DependencyLoader instance
76
+ * @type {DependencyLoader}
77
+ */
78
+ this.dependencyloader = new DependencyLoader(this)
79
+ /**
80
+ * Git metadata for the application (branch and commit hash)
81
+ * @type {Object}
82
+ */
83
+ this.git = this.getGitInfo()
53
84
 
54
- /** @ignore */ this._isStarting = false
85
+ await this.dependencyloader.loadConfigs()
55
86
 
56
- const configRootDir = this.getConfig('rootDir')
57
- if (configRootDir) /** @ignore */this.rootDir = configRootDir
87
+ const options = {
88
+ dependencies: this.dependencies,
89
+ configFilePath: path.join(this.rootDir, 'conf', `${process.env.NODE_ENV}.config.js`),
90
+ rootDir: this.rootDir,
91
+ log: (...args) => this.logger.log(...args)
92
+ }
93
+
94
+ await runMigrations({ ...options, dryRun: this.args['dry-run'] === true })
95
+
96
+ this.config = await new Config({ ...options, appName: this.name }).load()
97
+ this.logger = new Logger({ levels: this.getConfig('logLevels'), showTimestamp: this.getConfig('showLogTimestamp') })
98
+ this.errors = new Errors(options)
99
+ this.lang = new Lang({ ...options, defaultLang: this.getConfig('defaultLang') })
100
+
101
+ await this.dependencyloader.loadModules()
58
102
 
59
- let startError
60
- try {
61
- await this.start()
62
103
  this.log('verbose', 'GIT', 'INFO', this.git)
63
104
  this.log('verbose', 'DIR', 'rootDir', this.rootDir)
64
105
  this.log('verbose', 'DIR', 'dataDir', this.getConfig('dataDir'))
65
106
  this.log('verbose', 'DIR', 'tempDir', this.getConfig('tempDir'))
66
- } catch (e) {
67
- startError = e
107
+ } catch (cause) {
108
+ await this.setReady(new Error('Failed to start App', { cause }))
109
+ process.exit(1)
68
110
  }
69
111
  const failedMods = this.dependencyloader.failedModules
70
112
  if (failedMods.length) this.log('warn', `${failedMods.length} module${failedMods.length === 1 ? '' : 's'} failed to load: ${failedMods}. See above for details`)
71
- if (startError) {
72
- process.exitCode = 1
73
- const e = new Error('Failed to start App')
74
- e.cause = startError
75
- throw e
76
- }
77
113
  }
78
114
 
79
115
  /**
@@ -101,21 +137,6 @@ class App extends AbstractModule {
101
137
  }
102
138
  }
103
139
 
104
- /**
105
- * Starts the app
106
- * @return {Promise} Resolves when the app has started
107
- */
108
- async start () {
109
- if (this._isReady) throw new Error('warn', 'cannot start app, already started')
110
- if (this._isStarting) throw new Error('warn', 'cannot start app, already initialising')
111
-
112
- this._isStarting = true
113
-
114
- await this.dependencyloader.load()
115
-
116
- this._isStarting = false
117
- }
118
-
119
140
  /**
120
141
  * Enables waiting for other modules to load
121
142
  * @param {...String} modNames Names of modules to wait for
@@ -125,12 +146,6 @@ class App extends AbstractModule {
125
146
  const results = await Promise.all(modNames.map(m => this.dependencyloader.waitForModule(m)))
126
147
  return results.length > 1 ? results : results[0]
127
148
  }
128
-
129
- /** @override */
130
- setReady (error) {
131
- this._isStarting = false
132
- super.setReady(error)
133
- }
134
149
  }
135
150
 
136
151
  export default App