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.
- package/.github/workflows/releases.yml +9 -24
- package/conf/config.schema.json +16 -0
- package/conf/deprecated-lang.json +4 -0
- package/conf/deprecated-logger.json +7 -0
- package/docs/configure-environment.md +78 -0
- package/docs/developer-workflow.md +649 -0
- package/docs/error-handling.md +49 -0
- package/docs/plugins/configuration.js +75 -0
- package/docs/plugins/configuration.md +14 -0
- package/docs/plugins/errors.js +22 -0
- package/docs/plugins/errorsref.md +9 -0
- package/errors/errors.json +87 -0
- package/errors/node-core.json +39 -0
- package/index.js +5 -0
- package/lib/AbstractModule.js +3 -13
- package/lib/AdaptError.js +57 -0
- package/lib/App.js +66 -51
- package/lib/Config.js +226 -0
- package/lib/DataCache.js +6 -0
- package/lib/DependencyLoader.js +46 -103
- package/lib/Errors.js +50 -0
- package/lib/Hook.js +2 -3
- package/lib/Lang.js +126 -0
- package/lib/Logger.js +149 -0
- package/lib/utils/getArgs.js +4 -6
- package/migrations/3.0.0-conf-migrate-lang-config.js +7 -0
- package/migrations/3.0.0-conf-migrate-logger-config.js +9 -0
- package/package.json +8 -25
- package/tests/AbstractModule.spec.js +4 -54
- package/tests/AdaptError.spec.js +62 -0
- package/tests/Config.spec.js +122 -0
- package/tests/DataCache.spec.js +84 -1
- package/tests/DependencyLoader.spec.js +61 -146
- package/tests/Errors.spec.js +91 -0
- package/tests/Lang.spec.js +116 -0
- package/tests/Logger.spec.js +187 -0
- package/tests/utils-getArgs.spec.js +2 -8
- package/tests/App.spec.js +0 -160
package/lib/Lang.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { globSync } from 'glob'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles loading and translation of language strings.
|
|
7
|
+
* @memberof core
|
|
8
|
+
*/
|
|
9
|
+
class Lang {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {Object} options.dependencies Key/value map of dependency configs (each with a rootDir)
|
|
13
|
+
* @param {String} options.defaultLang The default language for translations
|
|
14
|
+
* @param {String} options.rootDir The application root directory
|
|
15
|
+
* @param {Function} [options.log] Optional logging function (level, id, ...args)
|
|
16
|
+
*/
|
|
17
|
+
constructor ({ dependencies, defaultLang, rootDir, log } = {}) {
|
|
18
|
+
/**
|
|
19
|
+
* The loaded language phrases
|
|
20
|
+
* @type {Object}
|
|
21
|
+
*/
|
|
22
|
+
this.phrases = {}
|
|
23
|
+
/**
|
|
24
|
+
* The default language for translations
|
|
25
|
+
* @type {String}
|
|
26
|
+
*/
|
|
27
|
+
this.defaultLang = defaultLang
|
|
28
|
+
/**
|
|
29
|
+
* Optional logging function (level, id, ...args)
|
|
30
|
+
* @type {Function}
|
|
31
|
+
*/
|
|
32
|
+
this.log = log
|
|
33
|
+
this.loadPhrases(dependencies, rootDir, log)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the languages supported by the application
|
|
38
|
+
* @type {Array<String>}
|
|
39
|
+
*/
|
|
40
|
+
get supportedLanguages () {
|
|
41
|
+
return Object.keys(this.phrases)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Loads and merges all language phrases from dependencies
|
|
46
|
+
* @param {Object} dependencies Key/value map of dependency configs (each with a rootDir)
|
|
47
|
+
* @param {String} appRootDir The application root directory
|
|
48
|
+
* @param {Function} [log] Optional logging function (level, id, ...args)
|
|
49
|
+
*/
|
|
50
|
+
loadPhrases (dependencies = {}, appRootDir, log) {
|
|
51
|
+
const dirs = [
|
|
52
|
+
...(appRootDir ? [appRootDir] : []),
|
|
53
|
+
...Object.values(dependencies).map(d => d.rootDir)
|
|
54
|
+
]
|
|
55
|
+
for (const dir of dirs) {
|
|
56
|
+
const files = globSync('lang/**/*.json', { cwd: dir, absolute: true })
|
|
57
|
+
for (const f of files) {
|
|
58
|
+
try {
|
|
59
|
+
const relative = path.relative(path.join(dir, 'lang'), f)
|
|
60
|
+
const parts = relative.replace(/\.json$/, '').split(path.sep)
|
|
61
|
+
const lang = parts[0]
|
|
62
|
+
const prefix = parts.length > 1 ? parts.slice(1).join('.') + '.' : ''
|
|
63
|
+
if (!this.phrases[lang]) this.phrases[lang] = {}
|
|
64
|
+
const contents = JSON.parse(fs.readFileSync(f, 'utf8'))
|
|
65
|
+
Object.entries(contents).forEach(([k, v]) => { this.phrases[lang][`${prefix}${k}`] = v })
|
|
66
|
+
} catch (e) {
|
|
67
|
+
log?.('error', 'lang', e.message, f)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns translated language string. If key is an Error, translates using
|
|
75
|
+
* the error code as the key and error data for substitution. Non-Error,
|
|
76
|
+
* non-string values are returned unchanged.
|
|
77
|
+
* @param {String} lang The target language (falls back to defaultLang)
|
|
78
|
+
* @param {String|Error} key The unique string key, or an Error to translate
|
|
79
|
+
* @param {Object} data Dynamic data to be inserted into translated string
|
|
80
|
+
* @return {String}
|
|
81
|
+
*/
|
|
82
|
+
translate (lang, key, data) {
|
|
83
|
+
if (typeof lang !== 'string') {
|
|
84
|
+
lang = this.defaultLang
|
|
85
|
+
}
|
|
86
|
+
if (key instanceof Error) {
|
|
87
|
+
if (!key.code) return key.message || String(key)
|
|
88
|
+
return this.translate(lang, `error.${key.code}`, key.data ?? key)
|
|
89
|
+
}
|
|
90
|
+
if (typeof key !== 'string') {
|
|
91
|
+
return key
|
|
92
|
+
}
|
|
93
|
+
const s = this.phrases[lang]?.[key]
|
|
94
|
+
if (!s) {
|
|
95
|
+
this.log?.('warn', 'lang', `missing key '${lang}.${key}'`)
|
|
96
|
+
return key
|
|
97
|
+
}
|
|
98
|
+
if (!data) {
|
|
99
|
+
return s
|
|
100
|
+
}
|
|
101
|
+
return this.substituteData(s, lang, data)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Replaces placeholders in a translated string with data values.
|
|
106
|
+
* Supports ${key} for simple substitution, and $map{key:attrs:delim}
|
|
107
|
+
* for mapping over array values.
|
|
108
|
+
* @param {String} s The translated string
|
|
109
|
+
* @param {String} lang The target language
|
|
110
|
+
* @param {Object} data Key/value pairs to substitute
|
|
111
|
+
* @return {String}
|
|
112
|
+
*/
|
|
113
|
+
substituteData (s, lang, data) {
|
|
114
|
+
for (const [k, v] of Object.entries(data)) {
|
|
115
|
+
const items = [v].flat().map(item => item instanceof Error ? this.translate(lang, item) : item)
|
|
116
|
+
s = s.replaceAll(`\${${k}}`, items)
|
|
117
|
+
for (const [match, expr] of s.matchAll(new RegExp(String.raw`\$map{${k}:(.+)}`, 'g'))) {
|
|
118
|
+
const [attrs, delim] = expr.split(':')
|
|
119
|
+
s = s.replace(match, items.map(val => attrs.split(',').map(a => val?.[a] ?? a).join(delim)))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return s
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default Lang
|
package/lib/Logger.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import Hook from './Hook.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Provides console logging with configurable levels, colours, and module-specific overrides.
|
|
6
|
+
* @memberof core
|
|
7
|
+
*/
|
|
8
|
+
class Logger {
|
|
9
|
+
static levelColours = {
|
|
10
|
+
error: chalk.red,
|
|
11
|
+
warn: chalk.yellow,
|
|
12
|
+
success: chalk.green,
|
|
13
|
+
info: chalk.cyan,
|
|
14
|
+
debug: chalk.dim,
|
|
15
|
+
verbose: chalk.grey.italic
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a Logger instance from config values
|
|
20
|
+
* @param {Object} options
|
|
21
|
+
* @param {Array<String>} options.levels Log level config strings. An empty array mutes all output.
|
|
22
|
+
* @param {Boolean} options.showTimestamp Whether to show timestamps
|
|
23
|
+
*/
|
|
24
|
+
constructor ({ levels = Object.keys(Logger.levelColours), showTimestamp = true } = {}) {
|
|
25
|
+
/**
|
|
26
|
+
* Hook invoked on each message logged
|
|
27
|
+
* @type {Hook}
|
|
28
|
+
*/
|
|
29
|
+
this.logHook = new Hook()
|
|
30
|
+
/** @ignore */
|
|
31
|
+
this.config = {
|
|
32
|
+
levels: Object.entries(Logger.levelColours).reduce((m, [level, colour]) => {
|
|
33
|
+
m[level] = {
|
|
34
|
+
enable: Logger.isLevelEnabled(levels, level),
|
|
35
|
+
moduleOverrides: Logger.getModuleOverrides(levels, level),
|
|
36
|
+
lineOverrides: Logger.getLineOverrides(levels, level),
|
|
37
|
+
colour
|
|
38
|
+
}
|
|
39
|
+
return m
|
|
40
|
+
}, {}),
|
|
41
|
+
idOverrides: Logger.getIdOverrides(levels),
|
|
42
|
+
timestamp: showTimestamp,
|
|
43
|
+
mute: levels.length === 0
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Logs a message to the console. When `args[0]` is a string it's treated as
|
|
49
|
+
* a short id for line-level filtering (e.g. `'verbose.server.ADD_ROUTE'`).
|
|
50
|
+
* @param {String} level Severity of the message
|
|
51
|
+
* @param {String} id Identifier for the message (typically the module name)
|
|
52
|
+
* @param {...*} args Arguments to be logged
|
|
53
|
+
*/
|
|
54
|
+
log (level, id, ...args) {
|
|
55
|
+
const shortId = typeof args[0] === 'string' ? args[0] : undefined
|
|
56
|
+
if (this.config.mute || !Logger.isLoggingEnabled(this.config.levels, level, id, shortId, this.config.idOverrides)) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const colour = this.config.levels[level]?.colour
|
|
60
|
+
const logFunc = console[level] ?? console.log
|
|
61
|
+
const timestamp = this.config.timestamp ? chalk.dim(`${new Date().toISOString()} `) : ''
|
|
62
|
+
logFunc(`${timestamp}${colour ? colour(level) : level} ${chalk.magenta(id)}`, ...args)
|
|
63
|
+
this.logHook.invoke(new Date(), level, id, ...args).catch((error) => {
|
|
64
|
+
console.error('Logger logHook invocation failed:', error)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Determines whether a specific log level is enabled
|
|
70
|
+
* @param {Array<String>} levelsConfig Array of level configuration strings
|
|
71
|
+
* @param {String} level The log level to check
|
|
72
|
+
* @return {Boolean}
|
|
73
|
+
*/
|
|
74
|
+
static isLevelEnabled (levelsConfig, level) {
|
|
75
|
+
return !levelsConfig.includes(`!${level}`) && levelsConfig.includes(level)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns per-level module overrides (e.g. `debug.core` / `!debug.core`).
|
|
80
|
+
* @param {Array<String>} levelsConfig Array of level configuration strings
|
|
81
|
+
* @param {String} level The log level to find overrides for
|
|
82
|
+
* @return {Array<String>}
|
|
83
|
+
*/
|
|
84
|
+
static getModuleOverrides (levelsConfig, level) {
|
|
85
|
+
return levelsConfig.filter(l => Logger.matchesLevelPrefix(l, level) && Logger.entrySegmentCount(l) === 2)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns per-level line overrides (e.g. `debug.core.LOAD` / `!debug.core.LOAD`).
|
|
90
|
+
* @param {Array<String>} levelsConfig Array of level configuration strings
|
|
91
|
+
* @param {String} level The log level to find overrides for
|
|
92
|
+
* @return {Array<String>}
|
|
93
|
+
*/
|
|
94
|
+
static getLineOverrides (levelsConfig, level) {
|
|
95
|
+
return levelsConfig.filter(l => Logger.matchesLevelPrefix(l, level) && Logger.entrySegmentCount(l) >= 3)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns id-wide overrides — entries whose first segment isn't a known level,
|
|
100
|
+
* meaning they apply to that id at every level (e.g. `core` / `!core`).
|
|
101
|
+
* @param {Array<String>} levelsConfig Array of level configuration strings
|
|
102
|
+
* @return {Array<String>}
|
|
103
|
+
*/
|
|
104
|
+
static getIdOverrides (levelsConfig) {
|
|
105
|
+
const knownLevels = Object.keys(Logger.levelColours)
|
|
106
|
+
return levelsConfig.filter(entry => {
|
|
107
|
+
const body = entry.startsWith('!') ? entry.slice(1) : entry
|
|
108
|
+
const firstSegment = body.split('.')[0]
|
|
109
|
+
return body.length > 0 && !knownLevels.includes(firstSegment)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @ignore */
|
|
114
|
+
static matchesLevelPrefix (entry, level) {
|
|
115
|
+
return entry.startsWith(`${level}.`) || entry.startsWith(`!${level}.`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @ignore */
|
|
119
|
+
static entrySegmentCount (entry) {
|
|
120
|
+
const body = entry.startsWith('!') ? entry.slice(1) : entry
|
|
121
|
+
return body.split('.').length
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns whether a message should be logged. Resolution order, most-specific
|
|
126
|
+
* wins: line-level (`!level.id.shortId`) → per-level module (`!level.id`)
|
|
127
|
+
* → id-wide (`!id`) → global level.
|
|
128
|
+
* @param {Object} configLevels The resolved levels config object
|
|
129
|
+
* @param {String} level Logging level
|
|
130
|
+
* @param {String} id Id of log caller
|
|
131
|
+
* @param {String} [shortId] Optional line-level id (typically `args[0]`)
|
|
132
|
+
* @param {Array<String>} [idOverrides] Id-wide override entries
|
|
133
|
+
* @returns {Boolean}
|
|
134
|
+
*/
|
|
135
|
+
static isLoggingEnabled (configLevels, level, id, shortId, idOverrides = []) {
|
|
136
|
+
const { enable, moduleOverrides = [], lineOverrides = [] } = configLevels?.[level] || {}
|
|
137
|
+
if (typeof shortId === 'string') {
|
|
138
|
+
if (lineOverrides.includes(`!${level}.${id}.${shortId}`)) return false
|
|
139
|
+
if (lineOverrides.includes(`${level}.${id}.${shortId}`)) return true
|
|
140
|
+
}
|
|
141
|
+
if (moduleOverrides.includes(`!${level}.${id}`)) return false
|
|
142
|
+
if (moduleOverrides.includes(`${level}.${id}`)) return true
|
|
143
|
+
if (idOverrides.includes(`!${id}`)) return false
|
|
144
|
+
if (idOverrides.includes(id)) return true
|
|
145
|
+
return Boolean(enable)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default Logger
|
package/lib/utils/getArgs.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Returns the passed arguments, parsed
|
|
4
|
+
* Returns the passed arguments, parsed for easy access
|
|
5
5
|
* @return {Object} The parsed arguments
|
|
6
|
-
* @see {@link https://github.com/substack/minimist#readme}
|
|
7
6
|
*/
|
|
8
7
|
export function getArgs () {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
return args
|
|
8
|
+
const { values, positionals } = parseArgs({ strict: false, args: process.argv.slice(2) })
|
|
9
|
+
return { ...values, params: positionals }
|
|
12
10
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default function (migration) {
|
|
2
|
+
migration.describe('Move adapt-authoring-logger config keys to adapt-authoring-core')
|
|
3
|
+
|
|
4
|
+
migration
|
|
5
|
+
.where('adapt-authoring-logger')
|
|
6
|
+
.replace('levels', 'adapt-authoring-core', 'logLevels')
|
|
7
|
+
.replace('showTimestamp', 'adapt-authoring-core', 'showLogTimestamp')
|
|
8
|
+
.remove('mute', 'dateFormat')
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A bundle of reusable 'core' functionality",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-core",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -11,34 +11,17 @@
|
|
|
11
11
|
},
|
|
12
12
|
"repository": "github:adapt-security/adapt-authoring-core",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
14
|
+
"adapt-authoring-migrations": "^2.0.0",
|
|
15
|
+
"adapt-schemas": "^3.1.0",
|
|
16
|
+
"chalk": "^5.4.1",
|
|
17
|
+
"glob": "^13.0.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@semantic-release
|
|
21
|
-
"
|
|
22
|
-
"semantic-release": "^25.0.2",
|
|
20
|
+
"@adaptlearning/semantic-release-config": "^1.0.0",
|
|
21
|
+
"fs-extra": "^11.3.4",
|
|
23
22
|
"standard": "^17.1.0"
|
|
24
23
|
},
|
|
25
24
|
"release": {
|
|
26
|
-
"
|
|
27
|
-
[
|
|
28
|
-
"@semantic-release/commit-analyzer",
|
|
29
|
-
{
|
|
30
|
-
"preset": "eslint"
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
|
-
[
|
|
34
|
-
"@semantic-release/release-notes-generator",
|
|
35
|
-
{
|
|
36
|
-
"preset": "eslint"
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"@semantic-release/npm",
|
|
40
|
-
"@semantic-release/github",
|
|
41
|
-
"@semantic-release/git"
|
|
42
|
-
]
|
|
25
|
+
"extends": "@adaptlearning/semantic-release-config"
|
|
43
26
|
}
|
|
44
27
|
}
|
|
@@ -448,13 +448,8 @@ describe('AbstractModule', () => {
|
|
|
448
448
|
assert.equal(result, 'testValue')
|
|
449
449
|
})
|
|
450
450
|
|
|
451
|
-
it('should return undefined
|
|
451
|
+
it('should return undefined when config is not available', async () => {
|
|
452
452
|
const mockApp = {
|
|
453
|
-
config: {
|
|
454
|
-
get: () => {
|
|
455
|
-
throw new Error('config error')
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
453
|
dependencyloader: {
|
|
459
454
|
moduleLoadedHook: {
|
|
460
455
|
tap: () => {},
|
|
@@ -615,57 +610,12 @@ describe('AbstractModule', () => {
|
|
|
615
610
|
assert.deepEqual(loggedArgs, ['arg1', 'arg2', 'arg3'])
|
|
616
611
|
})
|
|
617
612
|
|
|
618
|
-
it('should
|
|
619
|
-
|
|
620
|
-
let tapCallback
|
|
621
|
-
const mockApp = {
|
|
622
|
-
dependencyloader: {
|
|
623
|
-
moduleLoadedHook: {
|
|
624
|
-
tap: (fn) => { tapCallback = fn },
|
|
625
|
-
untap: () => {}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
613
|
+
it('should silently skip when logger is not available', async () => {
|
|
614
|
+
const mockApp = {}
|
|
629
615
|
const module = new AbstractModule(mockApp, { name: 'test-mod' })
|
|
630
616
|
await module.onReady()
|
|
631
617
|
|
|
632
|
-
module.log('warn', '
|
|
633
|
-
assert.ok(tapCallback)
|
|
634
|
-
|
|
635
|
-
mockApp.logger = {
|
|
636
|
-
name: 'adapt-authoring-logger',
|
|
637
|
-
log: (level) => {
|
|
638
|
-
loggedLevel = level
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
tapCallback(null, { name: 'adapt-authoring-logger' })
|
|
643
|
-
assert.equal(loggedLevel, 'warn')
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
it('should not log when loaded module is not the logger', async () => {
|
|
647
|
-
const logCalled = false
|
|
648
|
-
let tapCallback
|
|
649
|
-
const mockApp = {
|
|
650
|
-
dependencyloader: {
|
|
651
|
-
moduleLoadedHook: {
|
|
652
|
-
tap: (fn) => { tapCallback = fn },
|
|
653
|
-
untap: () => {}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const module = new AbstractModule(mockApp, { name: 'test-mod' })
|
|
658
|
-
await module.onReady()
|
|
659
|
-
|
|
660
|
-
// No logger set yet, so log queues the callback
|
|
661
|
-
module.log('info', 'some message')
|
|
662
|
-
assert.ok(tapCallback)
|
|
663
|
-
|
|
664
|
-
// Now simulate a non-logger module loading - _log checks !this.app.logger
|
|
665
|
-
// which is true (no logger), so it returns false
|
|
666
|
-
const result = tapCallback(null, { name: 'adapt-authoring-other' })
|
|
667
|
-
assert.equal(result, false)
|
|
668
|
-
assert.equal(logCalled, false)
|
|
618
|
+
assert.doesNotThrow(() => module.log('warn', 'no logger'))
|
|
669
619
|
})
|
|
670
620
|
})
|
|
671
621
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import AdaptError from '../lib/AdaptError.js'
|
|
4
|
+
|
|
5
|
+
describe('AdaptError', () => {
|
|
6
|
+
describe('constructor', () => {
|
|
7
|
+
it('should set code and default statusCode', () => {
|
|
8
|
+
const error = new AdaptError('TEST_ERROR')
|
|
9
|
+
assert.equal(error.code, 'TEST_ERROR')
|
|
10
|
+
assert.equal(error.statusCode, 500)
|
|
11
|
+
assert.equal(error.isFatal, false)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should set custom statusCode', () => {
|
|
15
|
+
const error = new AdaptError('NOT_FOUND', 404)
|
|
16
|
+
assert.equal(error.statusCode, 404)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should set isFatal from metadata', () => {
|
|
20
|
+
const error = new AdaptError('FATAL_ERROR', 500, { isFatal: true })
|
|
21
|
+
assert.equal(error.isFatal, true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should default isFatal to false when not in metadata', () => {
|
|
25
|
+
const error = new AdaptError('ERROR', 500, { description: 'test' })
|
|
26
|
+
assert.equal(error.isFatal, false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should store metadata', () => {
|
|
30
|
+
const meta = { description: 'test error', data: { id: 'test' } }
|
|
31
|
+
const error = new AdaptError('ERROR', 500, meta)
|
|
32
|
+
assert.deepEqual(error.meta, meta)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should extend Error', () => {
|
|
36
|
+
const error = new AdaptError('TEST')
|
|
37
|
+
assert.ok(error instanceof Error)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('#setData()', () => {
|
|
42
|
+
it('should set data and return self for chaining', () => {
|
|
43
|
+
const error = new AdaptError('TEST')
|
|
44
|
+
const result = error.setData({ id: '123' })
|
|
45
|
+
assert.equal(result, error)
|
|
46
|
+
assert.deepEqual(error.data, { id: '123' })
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('#toString()', () => {
|
|
51
|
+
it('should include code without data', () => {
|
|
52
|
+
const error = new AdaptError('TEST')
|
|
53
|
+
assert.ok(error.toString().includes('TEST'))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should include stringified data when set', () => {
|
|
57
|
+
const error = new AdaptError('TEST')
|
|
58
|
+
error.setData({ id: '123' })
|
|
59
|
+
assert.ok(error.toString().includes('123'))
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import Config from '../lib/Config.js'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
describe('Config', () => {
|
|
7
|
+
describe('constructor', () => {
|
|
8
|
+
it('should initialise with empty config', () => {
|
|
9
|
+
const config = new Config()
|
|
10
|
+
assert.deepEqual(config.publicAttributes, [])
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('#has()', () => {
|
|
15
|
+
it('should return true for existing keys', () => {
|
|
16
|
+
const config = new Config()
|
|
17
|
+
config._config['test.key'] = 'value'
|
|
18
|
+
assert.equal(config.has('test.key'), true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should return false for missing keys', () => {
|
|
22
|
+
const config = new Config()
|
|
23
|
+
assert.equal(config.has('missing.key'), false)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('#get()', () => {
|
|
28
|
+
it('should return value for existing key', () => {
|
|
29
|
+
const config = new Config()
|
|
30
|
+
config._config['test.key'] = 'value'
|
|
31
|
+
assert.equal(config.get('test.key'), 'value')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should return undefined for missing key', () => {
|
|
35
|
+
const config = new Config()
|
|
36
|
+
assert.equal(config.get('missing'), undefined)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('#getPublicConfig()', () => {
|
|
41
|
+
it('should return only public attributes', () => {
|
|
42
|
+
const config = new Config()
|
|
43
|
+
config._config['mod.public'] = 'yes'
|
|
44
|
+
config._config['mod.private'] = 'no'
|
|
45
|
+
config.publicAttributes = ['mod.public']
|
|
46
|
+
const result = config.getPublicConfig()
|
|
47
|
+
assert.deepEqual(result, { 'mod.public': 'yes' })
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('.envVarToConfigKey()', () => {
|
|
52
|
+
it('should convert ADAPT_AUTHORING_ prefixed vars', () => {
|
|
53
|
+
const result = Config.envVarToConfigKey('ADAPT_AUTHORING_CORE__dataDir')
|
|
54
|
+
assert.equal(result, 'adapt-authoring-core.dataDir')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should prefix non-adapt vars with env.', () => {
|
|
58
|
+
const result = Config.envVarToConfigKey('NODE_ENV')
|
|
59
|
+
assert.equal(result, 'env.NODE_ENV')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('#storeEnvSettings()', () => {
|
|
64
|
+
it('should store env vars in config', () => {
|
|
65
|
+
const config = new Config()
|
|
66
|
+
process.env.TEST_CONFIG_VAR = 'test_value'
|
|
67
|
+
config.storeEnvSettings()
|
|
68
|
+
assert.equal(config.get('env.TEST_CONFIG_VAR'), 'test_value')
|
|
69
|
+
delete process.env.TEST_CONFIG_VAR
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should parse JSON env values', () => {
|
|
73
|
+
const config = new Config()
|
|
74
|
+
process.env.TEST_JSON_VAR = '42'
|
|
75
|
+
config.storeEnvSettings()
|
|
76
|
+
assert.equal(config.get('env.TEST_JSON_VAR'), 42)
|
|
77
|
+
delete process.env.TEST_JSON_VAR
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('#resolveDirectory()', () => {
|
|
82
|
+
it('should resolve $ROOT', () => {
|
|
83
|
+
const config = new Config()
|
|
84
|
+
config.rootDir = '/app'
|
|
85
|
+
assert.equal(config.resolveDirectory('$ROOT/APP_DATA/data'), path.resolve('/app', 'APP_DATA/data'))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should resolve $DATA', () => {
|
|
89
|
+
const config = new Config()
|
|
90
|
+
config.rootDir = '/app'
|
|
91
|
+
config._config['adapt-authoring-core.dataDir'] = '/app/APP_DATA/data'
|
|
92
|
+
assert.equal(config.resolveDirectory('$DATA/uploads'), path.resolve('/app/APP_DATA/data', 'uploads'))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should resolve $TEMP', () => {
|
|
96
|
+
const config = new Config()
|
|
97
|
+
config.rootDir = '/app'
|
|
98
|
+
config._config['adapt-authoring-core.tempDir'] = '/app/APP_DATA/temp'
|
|
99
|
+
assert.equal(config.resolveDirectory('$TEMP/cache'), path.resolve('/app/APP_DATA/temp', 'cache'))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should not resolve unresolved variables', () => {
|
|
103
|
+
const config = new Config()
|
|
104
|
+
config.rootDir = '/app'
|
|
105
|
+
assert.equal(config.resolveDirectory('$DATA/uploads'), '$DATA/uploads')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should return non-variable paths unchanged', () => {
|
|
109
|
+
const config = new Config()
|
|
110
|
+
config.rootDir = '/app'
|
|
111
|
+
assert.equal(config.resolveDirectory('/absolute/path'), '/absolute/path')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('#storeUserSettings()', () => {
|
|
116
|
+
it('should handle missing config file gracefully', async () => {
|
|
117
|
+
const config = new Config()
|
|
118
|
+
config.configFilePath = '/nonexistent/path/config.js'
|
|
119
|
+
await assert.doesNotReject(() => config.storeUserSettings())
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|