adapt-authoring-docs 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/releases.yml +1 -1
- package/.github/workflows/standardjs.yml +3 -4
- package/.github/workflows/tests.yml +1 -2
- package/bin/docgen.js +38 -22
- package/bin/docserve.js +26 -38
- package/docsify/docsify.js +3 -3
- package/jsdoc3/jsdoc3.js +5 -5
- package/lib/docsData.js +298 -0
- package/package.json +3 -9
- package/swagger/swagger.js +8 -12
- package/tests/docsData.spec.js +462 -0
- package/tests/swagger.spec.js +15 -33
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Lint
|
|
2
2
|
on: push
|
|
3
3
|
jobs:
|
|
4
4
|
default:
|
|
@@ -8,6 +8,5 @@ jobs:
|
|
|
8
8
|
- uses: actions/setup-node@master
|
|
9
9
|
with:
|
|
10
10
|
node-version: 'lts/*'
|
|
11
|
-
|
|
12
|
-
- run:
|
|
13
|
-
- run: npx standard
|
|
11
|
+
- run: npm install
|
|
12
|
+
- run: npx standard
|
package/bin/docgen.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Generates documentation for the installed modules.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { readJson } from 'adapt-authoring-core'
|
|
6
|
+
import { loadDependencies, loadConfigDefaults, loadSchemas, loadErrors, buildRouterTree, buildPermissions } from '../lib/docsData.js'
|
|
6
7
|
import docsify from '../docsify/docsify.js'
|
|
7
8
|
import fs from 'fs/promises'
|
|
8
9
|
import jsdoc3 from '../jsdoc3/jsdoc3.js'
|
|
@@ -11,10 +12,13 @@ import swagger from '../swagger/swagger.js'
|
|
|
11
12
|
|
|
12
13
|
const DEBUG = process.argv.includes('--verbose')
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
process.
|
|
15
|
+
function getArg (name) {
|
|
16
|
+
const idx = process.argv.indexOf(name)
|
|
17
|
+
return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rootDir = path.resolve(getArg('--rootDir') || process.cwd())
|
|
16
21
|
|
|
17
|
-
const app = App.instance
|
|
18
22
|
let outputdir
|
|
19
23
|
|
|
20
24
|
const defaultPages = { // populated in cacheConfigs
|
|
@@ -27,10 +31,10 @@ const defaultPages = { // populated in cacheConfigs
|
|
|
27
31
|
* Documentation for a module can be enabled in:
|
|
28
32
|
* package.json > adapt_authoring.documentation.enable
|
|
29
33
|
*/
|
|
30
|
-
function cacheConfigs () {
|
|
34
|
+
function cacheConfigs (appData) {
|
|
31
35
|
const cache = []
|
|
32
|
-
const excludes =
|
|
33
|
-
Object.values(
|
|
36
|
+
const excludes = appData.pkg.documentation.excludes ?? []
|
|
37
|
+
Object.values(appData.dependencies).forEach(dep => {
|
|
34
38
|
const c = dep.documentation
|
|
35
39
|
|
|
36
40
|
let omitMsg
|
|
@@ -49,16 +53,16 @@ function cacheConfigs () {
|
|
|
49
53
|
...c,
|
|
50
54
|
name: dep.name,
|
|
51
55
|
version: dep.version,
|
|
52
|
-
module:
|
|
56
|
+
module: dep.module !== false,
|
|
53
57
|
rootDir: dep.rootDir,
|
|
54
58
|
includes: c.includes || {}
|
|
55
59
|
})
|
|
56
60
|
})
|
|
57
61
|
cache.push({
|
|
58
|
-
...
|
|
62
|
+
...appData.pkg.documentation,
|
|
59
63
|
enable: true,
|
|
60
64
|
name: 'adapt-authoring',
|
|
61
|
-
rootDir:
|
|
65
|
+
rootDir: appData.rootDir,
|
|
62
66
|
includes: {}
|
|
63
67
|
})
|
|
64
68
|
return cache
|
|
@@ -71,19 +75,31 @@ async function copyRootFiles () {
|
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
async function docs () {
|
|
74
|
-
|
|
78
|
+
const dependencies = await loadDependencies(rootDir)
|
|
79
|
+
const pkg = { ...await readJson(path.join(rootDir, 'package.json')), ...await readJson(path.join(rootDir, 'adapt-authoring.json')) }
|
|
80
|
+
const config = await loadConfigDefaults(dependencies)
|
|
81
|
+
const schemas = await loadSchemas(dependencies)
|
|
82
|
+
const errors = await loadErrors(dependencies)
|
|
83
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
84
|
+
const permissions = buildPermissions(routerTree)
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
const appData = {
|
|
87
|
+
rootDir,
|
|
88
|
+
pkg,
|
|
89
|
+
dependencies,
|
|
90
|
+
config,
|
|
91
|
+
errors,
|
|
92
|
+
schemas,
|
|
93
|
+
routerTree,
|
|
94
|
+
permissions
|
|
81
95
|
}
|
|
82
|
-
|
|
96
|
+
|
|
97
|
+
console.log(`Generating documentation for ${appData.pkg.name}@${appData.pkg.version} ${DEBUG ? ' :: DEBUG' : ''}`)
|
|
98
|
+
|
|
83
99
|
const { name } = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url)))
|
|
84
|
-
outputdir = path.resolve(process.cwd(), config.get(`${name}.outputDir`))
|
|
100
|
+
outputdir = path.resolve(process.cwd(), getArg('--outputDir') || appData.config.get(`${name}.outputDir`))
|
|
85
101
|
|
|
86
|
-
const cachedConfigs = cacheConfigs()
|
|
102
|
+
const cachedConfigs = cacheConfigs(appData)
|
|
87
103
|
|
|
88
104
|
console.log('\nThis might take a minute or two...\n')
|
|
89
105
|
|
|
@@ -91,9 +107,9 @@ async function docs () {
|
|
|
91
107
|
await fs.rm(outputdir, { recursive: true, force: true })
|
|
92
108
|
await fs.mkdir(outputdir)
|
|
93
109
|
await copyRootFiles()
|
|
94
|
-
await jsdoc3(
|
|
95
|
-
await docsify(
|
|
96
|
-
await swagger(
|
|
110
|
+
await jsdoc3(appData, cachedConfigs, outputdir, defaultPages)
|
|
111
|
+
await docsify(appData, cachedConfigs, outputdir, defaultPages)
|
|
112
|
+
await swagger(appData, cachedConfigs, outputdir)
|
|
97
113
|
} catch (e) {
|
|
98
114
|
console.log(e)
|
|
99
115
|
process.exit(1)
|
package/bin/docserve.js
CHANGED
|
@@ -1,51 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Generates an HTTP server for viewing the local copy of the documentation (note these must be
|
|
3
|
+
* Generates an HTTP server for viewing the local copy of the documentation (note these must be built first with `at-docgen`)
|
|
4
4
|
*/
|
|
5
|
-
import { App } from 'adapt-authoring-core'
|
|
6
5
|
import { spawn } from 'child_process'
|
|
7
6
|
import http from 'http-server'
|
|
8
7
|
import path from 'path'
|
|
9
|
-
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
return
|
|
13
|
-
'.ico': 'image/x-icon',
|
|
14
|
-
'.html': 'text/html',
|
|
15
|
-
'.js': 'text/javascript',
|
|
16
|
-
'.json': 'application/json',
|
|
17
|
-
'.css': 'text/css',
|
|
18
|
-
'.png': 'image/png',
|
|
19
|
-
'.jpg': 'image/jpeg',
|
|
20
|
-
'.svg': 'image/svg+xml',
|
|
21
|
-
'.pdf': 'application/pdf',
|
|
22
|
-
'.doc': 'application/msword'
|
|
23
|
-
}[ext] || 'text/plain'
|
|
8
|
+
|
|
9
|
+
function getArg (name) {
|
|
10
|
+
const idx = process.argv.indexOf(name)
|
|
11
|
+
return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : undefined
|
|
24
12
|
}
|
|
25
|
-
*/
|
|
26
|
-
process.env.NODE_ENV ??= 'production'
|
|
27
|
-
process.env.ADAPT_AUTHORING_LOGGER__mute = true
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
async function getOutputDir () {
|
|
15
|
+
const arg = getArg('--outputDir')
|
|
16
|
+
if (arg) return path.resolve(arg)
|
|
17
|
+
const { loadDependencies, loadConfigDefaults } = await import('../lib/docsData.js')
|
|
18
|
+
const rootDir = path.resolve(getArg('--rootDir') || process.cwd())
|
|
19
|
+
const dependencies = await loadDependencies(rootDir)
|
|
20
|
+
const config = await loadConfigDefaults(dependencies)
|
|
21
|
+
return path.resolve(config.get('adapt-authoring-docs.outputDir'))
|
|
22
|
+
}
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
const ROOT = await getOutputDir()
|
|
25
|
+
const PORT = Number(getArg('--port')) || 9000
|
|
26
|
+
const OPEN = process.argv.some(a => a === '--open')
|
|
38
27
|
|
|
39
|
-
|
|
28
|
+
const server = http.createServer({ root: ROOT, cache: -1 })
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
server.listen(PORT, () => {
|
|
31
|
+
const url = `http://localhost:${PORT}`
|
|
32
|
+
console.log(`Docs hosted at ${url}`)
|
|
44
33
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
34
|
+
if (OPEN) {
|
|
35
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
|
|
36
|
+
spawn(`${command} ${url}`, { shell: true })
|
|
37
|
+
.on('error', e => console.log('spawn error', e))
|
|
38
|
+
}
|
|
51
39
|
})
|
package/docsify/docsify.js
CHANGED
|
@@ -20,9 +20,9 @@ function generateSectionTitle (sectionName) {
|
|
|
20
20
|
/**
|
|
21
21
|
* Copies all doc files ready for the generator
|
|
22
22
|
*/
|
|
23
|
-
export default async function docsify (
|
|
23
|
+
export default async function docsify (appData, configs, outputdir, defaultPages) {
|
|
24
24
|
const dir = path.resolve(outputdir, 'manual')
|
|
25
|
-
const sectionsConf =
|
|
25
|
+
const sectionsConf = appData.config.get('adapt-authoring-docs.manualSections')
|
|
26
26
|
const defaultSection = Object.entries(sectionsConf).find(([, data]) => data.default)?.[0]
|
|
27
27
|
/**
|
|
28
28
|
* init docsify folder
|
|
@@ -39,7 +39,7 @@ export default async function docsify (app, configs, outputdir, defaultPages) {
|
|
|
39
39
|
try {
|
|
40
40
|
const wrapper = await new DocsifyPluginWrapper({
|
|
41
41
|
...c,
|
|
42
|
-
app,
|
|
42
|
+
app: appData,
|
|
43
43
|
docsRootDir: outputdir,
|
|
44
44
|
pluginEntry: path.resolve(c.rootDir, p),
|
|
45
45
|
outputDir: dir
|
package/jsdoc3/jsdoc3.js
CHANGED
|
@@ -14,7 +14,7 @@ function resolvePath (relativePath) {
|
|
|
14
14
|
return fileURLToPath(new URL(relativePath, import.meta.url))
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async function writeConfig (
|
|
17
|
+
async function writeConfig (appData, outputdir, indexFile) {
|
|
18
18
|
return fs.writeFile(configPath, JSON.stringify({
|
|
19
19
|
source: {
|
|
20
20
|
include: getSourceIncludes(indexFile)
|
|
@@ -25,7 +25,7 @@ async function writeConfig (app, outputdir, indexFile) {
|
|
|
25
25
|
search: true,
|
|
26
26
|
static: true,
|
|
27
27
|
menu: {
|
|
28
|
-
[`<img class="logo" src="assets/logo-outline-colour.png" />Adapt authoring tool back-end API documentation<br><span class="version">v${
|
|
28
|
+
[`<img class="logo" src="assets/logo-outline-colour.png" />Adapt authoring tool back-end API documentation<br><span class="version">v${appData.pkg.version}</span>`]: {
|
|
29
29
|
class: 'menu-title'
|
|
30
30
|
},
|
|
31
31
|
'Documentation home': {
|
|
@@ -60,7 +60,7 @@ async function writeConfig (app, outputdir, indexFile) {
|
|
|
60
60
|
meta: {
|
|
61
61
|
title: 'Adapt authoring tool UI documentation',
|
|
62
62
|
description: 'Adapt authoring tool UI documentation',
|
|
63
|
-
keyword: `v${
|
|
63
|
+
keyword: `v${appData.pkg.version}`
|
|
64
64
|
},
|
|
65
65
|
scripts: [
|
|
66
66
|
'styles/adapt.css',
|
|
@@ -88,10 +88,10 @@ function getSourceIncludes (indexFile) {
|
|
|
88
88
|
return includes
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export default async function jsdoc3 (
|
|
91
|
+
export default async function jsdoc3 (appData, configs, outputdir, defaultPages) {
|
|
92
92
|
cachedConfigs = configs
|
|
93
93
|
const dir = `${outputdir}/backend`
|
|
94
|
-
await writeConfig(
|
|
94
|
+
await writeConfig(appData, dir, defaultPages.sourceIndex)
|
|
95
95
|
try {
|
|
96
96
|
await execPromise(`npx jsdoc -c ${configPath}`)
|
|
97
97
|
} catch (e) {
|
package/lib/docsData.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { loadDependencyFiles, readJson } from 'adapt-authoring-core'
|
|
2
|
+
import { glob } from 'glob'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { pathToRegexp } from 'path-to-regexp'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Globs node_modules for adapt-authoring.json files, merges each with its
|
|
9
|
+
* package.json and returns a { [name]: config } map.
|
|
10
|
+
* @param {string} rootDir Absolute path to the app root
|
|
11
|
+
* @returns {Promise<Object>}
|
|
12
|
+
*/
|
|
13
|
+
export async function loadDependencies (rootDir) {
|
|
14
|
+
const files = await glob('node_modules/**/adapt-authoring.json', {
|
|
15
|
+
cwd: rootDir,
|
|
16
|
+
absolute: true
|
|
17
|
+
})
|
|
18
|
+
const seen = new Set()
|
|
19
|
+
const deps = {}
|
|
20
|
+
const sorted = files.sort((a, b) => a.length - b.length)
|
|
21
|
+
for (const file of sorted) {
|
|
22
|
+
const dir = path.dirname(file)
|
|
23
|
+
try {
|
|
24
|
+
const pkg = await readJson(path.join(dir, 'package.json'))
|
|
25
|
+
if (seen.has(pkg.name)) continue
|
|
26
|
+
seen.add(pkg.name)
|
|
27
|
+
const meta = await readJson(file)
|
|
28
|
+
deps[pkg.name] = { ...pkg, ...meta, rootDir: dir }
|
|
29
|
+
} catch {
|
|
30
|
+
// skip modules with unreadable config
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return deps
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads conf/config.schema.json from each dependency, extracts default values,
|
|
38
|
+
* and returns a { get(key) } object. Resolves $TEMP to os.tmpdir().
|
|
39
|
+
* @param {Object} dependencies Dependencies map from loadDependencies
|
|
40
|
+
* @returns {Promise<Object>}
|
|
41
|
+
*/
|
|
42
|
+
export async function loadConfigDefaults (dependencies) {
|
|
43
|
+
const allConfigs = await loadDependencyFiles('conf/config.schema.json', {
|
|
44
|
+
parse: true,
|
|
45
|
+
dependencies
|
|
46
|
+
})
|
|
47
|
+
const defaults = {}
|
|
48
|
+
for (const [depName, [schema]] of Object.entries(allConfigs)) {
|
|
49
|
+
const props = schema.properties || {}
|
|
50
|
+
const modDefaults = {}
|
|
51
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
52
|
+
if (prop.default !== undefined) modDefaults[key] = prop.default
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(modDefaults).length) defaults[depName] = modDefaults
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
get: (key) => {
|
|
58
|
+
const dotIdx = key.indexOf('.')
|
|
59
|
+
if (dotIdx === -1) return defaults[key]
|
|
60
|
+
const modName = key.slice(0, dotIdx)
|
|
61
|
+
const propKey = key.slice(dotIdx + 1)
|
|
62
|
+
const val = defaults[modName]?.[propKey]
|
|
63
|
+
if (typeof val === 'string') return val.replace('$TEMP', os.tmpdir())
|
|
64
|
+
return val
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Loads and merges all schema/*.schema.json files across dependencies.
|
|
71
|
+
* @param {Object} dependencies Dependencies map from loadDependencies
|
|
72
|
+
* @returns {Promise<Object>} { schemas, raw, getSchema }
|
|
73
|
+
*/
|
|
74
|
+
export async function loadSchemas (dependencies) {
|
|
75
|
+
const allSchemas = await loadDependencyFiles('schema/*.schema.json', {
|
|
76
|
+
parse: true,
|
|
77
|
+
dependencies
|
|
78
|
+
})
|
|
79
|
+
const schemaMap = {}
|
|
80
|
+
const rawMap = {}
|
|
81
|
+
for (const [, arr] of Object.entries(allSchemas)) {
|
|
82
|
+
for (const schema of arr) {
|
|
83
|
+
const name = schema.$anchor || schema.$id || 'unknown'
|
|
84
|
+
schemaMap[name] = true
|
|
85
|
+
rawMap[name] = schema
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
schemas: schemaMap,
|
|
90
|
+
raw: rawMap,
|
|
91
|
+
getSchema: async (name) => ({ built: rawMap[name] || {} })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Loads and merges all errors/*.json files across dependencies.
|
|
97
|
+
* @param {Object} dependencies Dependencies map from loadDependencies
|
|
98
|
+
* @returns {Promise<Object>} Merged error map
|
|
99
|
+
*/
|
|
100
|
+
export async function loadErrors (dependencies) {
|
|
101
|
+
const allErrors = await loadDependencyFiles('errors/*.json', {
|
|
102
|
+
parse: true,
|
|
103
|
+
dependencies
|
|
104
|
+
})
|
|
105
|
+
const merged = {}
|
|
106
|
+
for (const arr of Object.values(allErrors)) {
|
|
107
|
+
for (const obj of arr) Object.assign(merged, obj)
|
|
108
|
+
}
|
|
109
|
+
// Match the AdaptError shape that plugins expect:
|
|
110
|
+
// { code, statusCode, meta: { description, data } }
|
|
111
|
+
return Object.entries(merged)
|
|
112
|
+
.sort()
|
|
113
|
+
.reduce((m, [code, { description, statusCode, data }]) => {
|
|
114
|
+
const meta = { description }
|
|
115
|
+
if (data) meta.data = data
|
|
116
|
+
m[code] = { code, statusCode, meta }
|
|
117
|
+
return m
|
|
118
|
+
}, {})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Reads routes.json files from dependencies and assembles an Express-like
|
|
123
|
+
* { path, routes, childRouters } tree rooted at /api.
|
|
124
|
+
* @param {Object} dependencies Dependencies map from loadDependencies
|
|
125
|
+
* @returns {Promise<Object>} Router tree
|
|
126
|
+
*/
|
|
127
|
+
export async function buildRouterTree (dependencies) {
|
|
128
|
+
const apiRouter = { path: '/api', routes: [], childRouters: [] }
|
|
129
|
+
let authRouter = null
|
|
130
|
+
let apiDefaultRoutes = null
|
|
131
|
+
let authDefaultRoutes = null
|
|
132
|
+
|
|
133
|
+
const authDep = dependencies['adapt-authoring-auth']
|
|
134
|
+
const apiDep = dependencies['adapt-authoring-api']
|
|
135
|
+
|
|
136
|
+
if (apiDep) {
|
|
137
|
+
try {
|
|
138
|
+
apiDefaultRoutes = await readJson(path.join(apiDep.rootDir, 'lib', 'default-routes.json'))
|
|
139
|
+
} catch { /* no defaults */ }
|
|
140
|
+
}
|
|
141
|
+
if (authDep) {
|
|
142
|
+
try {
|
|
143
|
+
authDefaultRoutes = await readJson(path.join(authDep.rootDir, 'lib', 'default-routes.json'))
|
|
144
|
+
} catch { /* no defaults */ }
|
|
145
|
+
try {
|
|
146
|
+
const coreAuthConfig = await readJson(path.join(authDep.rootDir, 'lib', 'routes.json'))
|
|
147
|
+
authRouter = {
|
|
148
|
+
path: '/api/auth',
|
|
149
|
+
routes: (coreAuthConfig.routes || []).map(r => staticRouteEntry(r)),
|
|
150
|
+
childRouters: []
|
|
151
|
+
}
|
|
152
|
+
apiRouter.childRouters.push(authRouter)
|
|
153
|
+
} catch { /* no auth routes */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const allRoutes = await loadDependencyFiles('routes.json', {
|
|
157
|
+
parse: true,
|
|
158
|
+
dependencies
|
|
159
|
+
})
|
|
160
|
+
for (const [depName, [config]] of Object.entries(allRoutes)) {
|
|
161
|
+
if (dependencies[depName].module === false) continue
|
|
162
|
+
if (config.type !== undefined) {
|
|
163
|
+
addAuthTypeRouter(config, authRouter, authDefaultRoutes)
|
|
164
|
+
} else if (config.root) {
|
|
165
|
+
addApiRouter(config, apiRouter, apiDefaultRoutes)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return apiRouter
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Walks the router tree and builds a permission store with
|
|
173
|
+
* { get: [], post: [], ... } arrays of [regexp, scopes] tuples.
|
|
174
|
+
* @param {Object} routerTree Router tree from buildRouterTree
|
|
175
|
+
* @returns {Object} Permission store
|
|
176
|
+
*/
|
|
177
|
+
export function buildPermissions (routerTree) {
|
|
178
|
+
const store = { get: [], post: [], put: [], patch: [], delete: [] }
|
|
179
|
+
walkRouterPermissions(routerTree, store)
|
|
180
|
+
return store
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ------- internal helpers -------
|
|
184
|
+
|
|
185
|
+
function addApiRouter (config, parentRouter, defaultRoutes) {
|
|
186
|
+
const root = config.root
|
|
187
|
+
const scope = config.permissionsScope || root
|
|
188
|
+
const schemaName = config.schemaName ?? root
|
|
189
|
+
const collectionName = config.collectionName ?? root
|
|
190
|
+
|
|
191
|
+
let routes = config.routes || []
|
|
192
|
+
|
|
193
|
+
if (config.useDefaultRoutes !== false && defaultRoutes) {
|
|
194
|
+
routes = mergeWithDefaults(routes, defaultRoutes.routes || [])
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* eslint-disable no-template-curly-in-string */
|
|
198
|
+
const replacements = {
|
|
199
|
+
'${scope}': scope,
|
|
200
|
+
'${schemaName}': schemaName,
|
|
201
|
+
'${collectionName}': collectionName
|
|
202
|
+
}
|
|
203
|
+
/* eslint-enable no-template-curly-in-string */
|
|
204
|
+
|
|
205
|
+
const childRouter = {
|
|
206
|
+
path: `/api/${root}`,
|
|
207
|
+
routes: routes.map(r => staticRouteEntry(replacePlaceholders(r, replacements))),
|
|
208
|
+
childRouters: []
|
|
209
|
+
}
|
|
210
|
+
parentRouter.childRouters.push(childRouter)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function addAuthTypeRouter (config, authRouter, defaultRoutes) {
|
|
214
|
+
if (!authRouter) return
|
|
215
|
+
const type = config.type
|
|
216
|
+
let routes = config.routes || []
|
|
217
|
+
|
|
218
|
+
if (defaultRoutes) {
|
|
219
|
+
routes = mergeWithDefaults(routes, defaultRoutes.routes || [])
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const childRouter = {
|
|
223
|
+
path: `/api/auth/${type}`,
|
|
224
|
+
routes: routes.map(r => staticRouteEntry(r)),
|
|
225
|
+
childRouters: []
|
|
226
|
+
}
|
|
227
|
+
authRouter.childRouters.push(childRouter)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function mergeWithDefaults (customRoutes, defaultRoutes) {
|
|
231
|
+
const overrides = new Map(
|
|
232
|
+
customRoutes.filter(r => r.override).map(r => [r.route, r])
|
|
233
|
+
)
|
|
234
|
+
const matched = new Set()
|
|
235
|
+
const mergedDefaults = defaultRoutes.map(d => {
|
|
236
|
+
const o = overrides.get(d.route)
|
|
237
|
+
if (!o) return d
|
|
238
|
+
matched.add(d.route)
|
|
239
|
+
const { override, ...rest } = o
|
|
240
|
+
return { ...d, ...rest, handlers: { ...d.handlers, ...rest.handlers } }
|
|
241
|
+
})
|
|
242
|
+
const remaining = customRoutes.filter(r => !r.override || !matched.has(r.route))
|
|
243
|
+
return [...mergedDefaults, ...remaining]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function staticRouteEntry (routeDef) {
|
|
247
|
+
const handlers = {}
|
|
248
|
+
if (routeDef.handlers) {
|
|
249
|
+
for (const method of Object.keys(routeDef.handlers)) {
|
|
250
|
+
handlers[method] = () => {}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
route: routeDef.route,
|
|
255
|
+
handlers,
|
|
256
|
+
meta: routeDef.meta || {},
|
|
257
|
+
internal: routeDef.internal || false,
|
|
258
|
+
permissions: routeDef.permissions || {}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function walkRouterPermissions (router, store) {
|
|
263
|
+
for (const routeDef of router.routes) {
|
|
264
|
+
const fullRoute = `${router.path}${routeDef.route !== '/' ? routeDef.route : ''}`
|
|
265
|
+
for (const [method, scopes] of Object.entries(routeDef.permissions || {})) {
|
|
266
|
+
if (scopes === null) continue
|
|
267
|
+
const m = method.toLowerCase()
|
|
268
|
+
if (!store[m]) continue
|
|
269
|
+
try {
|
|
270
|
+
const { regexp } = pathToRegexp(fullRoute)
|
|
271
|
+
store[m].push([regexp, scopes])
|
|
272
|
+
} catch {
|
|
273
|
+
// skip routes that can't be compiled (unusual patterns)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const child of router.childRouters) {
|
|
278
|
+
walkRouterPermissions(child, store)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function replacePlaceholders (obj, replacements) {
|
|
283
|
+
if (typeof obj === 'string') {
|
|
284
|
+
return Object.entries(replacements).reduce(
|
|
285
|
+
(s, [k, v]) => v != null ? s.replaceAll(k, v) : s,
|
|
286
|
+
obj
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
if (Array.isArray(obj)) {
|
|
290
|
+
return obj.map(item => replacePlaceholders(item, replacements))
|
|
291
|
+
}
|
|
292
|
+
if (obj && typeof obj === 'object' && obj.constructor === Object) {
|
|
293
|
+
return Object.fromEntries(
|
|
294
|
+
Object.entries(obj).map(([k, v]) => [k, replacePlaceholders(v, replacements)])
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
return obj
|
|
298
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-docs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Tools for auto-generating documentation for the Adapt authoring tool",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-docs",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"test": "node --test 'tests/**/*.spec.js'"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"adapt-authoring-core": "^2.0.0",
|
|
17
18
|
"comment-parser": "^1.4.1",
|
|
18
19
|
"docdash": "^2.0.2",
|
|
19
20
|
"docsify-cli": "^4.4.4",
|
|
@@ -21,16 +22,9 @@
|
|
|
21
22
|
"glob": "^13.0.0",
|
|
22
23
|
"http-server": "^14.1.1",
|
|
23
24
|
"jsdoc": "^4.0.3",
|
|
25
|
+
"path-to-regexp": "^8.0.0",
|
|
24
26
|
"swagger-ui": "^5.17.14"
|
|
25
27
|
},
|
|
26
|
-
"peerDependencies": {
|
|
27
|
-
"adapt-authoring-core": "^1.7.0"
|
|
28
|
-
},
|
|
29
|
-
"peerDependenciesMeta": {
|
|
30
|
-
"adapt-authoring-core": {
|
|
31
|
-
"optional": true
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
28
|
"devDependencies": {
|
|
35
29
|
"@semantic-release/git": "^10.0.1",
|
|
36
30
|
"conventional-changelog-eslint": "^6.0.0",
|
package/swagger/swagger.js
CHANGED
|
@@ -8,14 +8,12 @@ function resolvePath (relativePath) {
|
|
|
8
8
|
/**
|
|
9
9
|
*
|
|
10
10
|
*/
|
|
11
|
-
export default async function swagger (
|
|
12
|
-
const server = await app.waitForModule('server')
|
|
13
|
-
await app.onReady()
|
|
11
|
+
export default async function swagger (appData, configs, outputdir) {
|
|
14
12
|
const spec = {
|
|
15
13
|
openapi: '3.0.3',
|
|
16
|
-
info: { version:
|
|
17
|
-
components: { schemas: await generateSchemaSpec(
|
|
18
|
-
paths: generatePathSpec(
|
|
14
|
+
info: { version: appData.pkg.version },
|
|
15
|
+
components: { schemas: await generateSchemaSpec(appData.schemas) },
|
|
16
|
+
paths: generatePathSpec(appData.permissions, appData.routerTree)
|
|
19
17
|
}
|
|
20
18
|
// generate UI
|
|
21
19
|
const dir = path.resolve(outputdir, 'rest')
|
|
@@ -38,8 +36,7 @@ export default async function swagger (app, configs, outputdir) {
|
|
|
38
36
|
])
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
async function generateSchemaSpec (
|
|
42
|
-
const jsonschema = await app.waitForModule('jsonschema')
|
|
39
|
+
async function generateSchemaSpec (jsonschema) {
|
|
43
40
|
const schemas = {}
|
|
44
41
|
await Promise.all(Object.keys(jsonschema.schemas).map(async s => {
|
|
45
42
|
schemas[s] = sanitiseSchema((await jsonschema.getSchema(s)).built)
|
|
@@ -57,8 +54,7 @@ function sanitiseSchema (schema) {
|
|
|
57
54
|
return schema
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
function generatePathSpec (
|
|
61
|
-
const perms = app.dependencyloader.instances['adapt-authoring-auth'].permissions.routes
|
|
57
|
+
function generatePathSpec (permissions, router, paths = {}) {
|
|
62
58
|
router.routes.forEach(r => {
|
|
63
59
|
const parameters = r.route.split('/').filter(r => r.startsWith(':')).map(r => {
|
|
64
60
|
return {
|
|
@@ -70,7 +66,7 @@ function generatePathSpec (app, router, paths = {}) {
|
|
|
70
66
|
const route = `${router.path}${r.route !== '/' ? r.route : ''}`
|
|
71
67
|
paths[route] = Object.keys(r.handlers).reduce((memo, method) => {
|
|
72
68
|
const meta = r.meta?.[method] || {}
|
|
73
|
-
const scopes =
|
|
69
|
+
const scopes = permissions[method].find(p => route.match(p[0]))?.[1] || []
|
|
74
70
|
let description = r.internal ? 'ROUTE IS ONLY ACCESSIBLE FROM LOCALHOST.<br/><br/>' : ''
|
|
75
71
|
description += scopes.length
|
|
76
72
|
? `Required scopes: ${scopes.map(s => `<span>${s}</span>`).join(' ')}`
|
|
@@ -90,7 +86,7 @@ function generatePathSpec (app, router, paths = {}) {
|
|
|
90
86
|
}, {})
|
|
91
87
|
})
|
|
92
88
|
if (router.childRouters.length) {
|
|
93
|
-
router.childRouters.forEach(childRouter => generatePathSpec(
|
|
89
|
+
router.childRouters.forEach(childRouter => generatePathSpec(permissions, childRouter, paths))
|
|
94
90
|
}
|
|
95
91
|
return Object.keys(paths).sort().reduce((m, k) => Object.assign(m, { [k]: paths[k] }), {})
|
|
96
92
|
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it, before, after } from 'node:test'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
/* eslint-disable no-template-curly-in-string */
|
|
8
|
+
/**
|
|
9
|
+
* Creates a temporary fixture directory that simulates a minimal
|
|
10
|
+
* adapt-authoring app so docsData functions can be tested in isolation.
|
|
11
|
+
*/
|
|
12
|
+
async function createFixture () {
|
|
13
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'docs-data-'))
|
|
14
|
+
|
|
15
|
+
// root package.json + adapt-authoring.json
|
|
16
|
+
await fs.writeFile(path.join(root, 'package.json'), JSON.stringify({
|
|
17
|
+
name: 'adapt-authoring',
|
|
18
|
+
version: '1.0.0'
|
|
19
|
+
}))
|
|
20
|
+
await fs.writeFile(path.join(root, 'adapt-authoring.json'), JSON.stringify({
|
|
21
|
+
module: false,
|
|
22
|
+
documentation: { enable: true },
|
|
23
|
+
essentialApis: []
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
// --- fake api module (library, not a loaded module) ---
|
|
27
|
+
const apiDir = path.join(root, 'node_modules', 'adapt-authoring-api')
|
|
28
|
+
await fs.mkdir(path.join(apiDir, 'lib'), { recursive: true })
|
|
29
|
+
await fs.writeFile(path.join(apiDir, 'package.json'), JSON.stringify({
|
|
30
|
+
name: 'adapt-authoring-api',
|
|
31
|
+
version: '1.0.0'
|
|
32
|
+
}))
|
|
33
|
+
await fs.writeFile(path.join(apiDir, 'adapt-authoring.json'), JSON.stringify({
|
|
34
|
+
module: false,
|
|
35
|
+
documentation: { enable: true }
|
|
36
|
+
}))
|
|
37
|
+
await fs.writeFile(path.join(apiDir, 'lib', 'default-routes.json'), JSON.stringify({
|
|
38
|
+
routes: [
|
|
39
|
+
{
|
|
40
|
+
route: '/',
|
|
41
|
+
handlers: { post: 'requestHandler', get: 'queryHandler' },
|
|
42
|
+
permissions: { post: ['write:${scope}'], get: ['read:${scope}'] }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
route: '/:_id',
|
|
46
|
+
handlers: { get: 'requestHandler', delete: 'requestHandler' },
|
|
47
|
+
permissions: { get: ['read:${scope}'], delete: ['write:${scope}'] }
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
// --- fake auth module ---
|
|
53
|
+
const authDir = path.join(root, 'node_modules', 'adapt-authoring-auth')
|
|
54
|
+
await fs.mkdir(path.join(authDir, 'lib'), { recursive: true })
|
|
55
|
+
await fs.writeFile(path.join(authDir, 'package.json'), JSON.stringify({
|
|
56
|
+
name: 'adapt-authoring-auth',
|
|
57
|
+
version: '1.0.0'
|
|
58
|
+
}))
|
|
59
|
+
await fs.writeFile(path.join(authDir, 'adapt-authoring.json'), JSON.stringify({
|
|
60
|
+
documentation: { enable: true }
|
|
61
|
+
}))
|
|
62
|
+
await fs.writeFile(path.join(authDir, 'lib', 'routes.json'), JSON.stringify({
|
|
63
|
+
routes: [
|
|
64
|
+
{
|
|
65
|
+
route: '/check',
|
|
66
|
+
handlers: { get: 'checkHandler' },
|
|
67
|
+
permissions: { get: null }
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}))
|
|
71
|
+
await fs.writeFile(path.join(authDir, 'lib', 'default-routes.json'), JSON.stringify({
|
|
72
|
+
routes: [
|
|
73
|
+
{
|
|
74
|
+
route: '/',
|
|
75
|
+
handlers: { post: 'authenticateHandler' },
|
|
76
|
+
permissions: { post: null }
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
route: '/register',
|
|
80
|
+
handlers: { post: 'registerHandler' },
|
|
81
|
+
permissions: { post: ['register:users'] }
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
// --- fake auth-local module (auth type) ---
|
|
87
|
+
const authLocalDir = path.join(root, 'node_modules', 'adapt-authoring-auth-local')
|
|
88
|
+
await fs.mkdir(authLocalDir, { recursive: true })
|
|
89
|
+
await fs.writeFile(path.join(authLocalDir, 'package.json'), JSON.stringify({
|
|
90
|
+
name: 'adapt-authoring-auth-local',
|
|
91
|
+
version: '1.0.0'
|
|
92
|
+
}))
|
|
93
|
+
await fs.writeFile(path.join(authLocalDir, 'adapt-authoring.json'), JSON.stringify({
|
|
94
|
+
documentation: { enable: true }
|
|
95
|
+
}))
|
|
96
|
+
await fs.writeFile(path.join(authLocalDir, 'routes.json'), JSON.stringify({
|
|
97
|
+
type: 'local',
|
|
98
|
+
routes: [
|
|
99
|
+
{
|
|
100
|
+
route: '/',
|
|
101
|
+
override: true,
|
|
102
|
+
handlers: { post: 'authenticateHandler' },
|
|
103
|
+
meta: { post: { summary: 'Local auth' } }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
route: '/changepass',
|
|
107
|
+
handlers: { post: 'changePasswordHandler' },
|
|
108
|
+
permissions: { post: null }
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}))
|
|
112
|
+
|
|
113
|
+
// --- fake content module (API module) ---
|
|
114
|
+
const contentDir = path.join(root, 'node_modules', 'adapt-authoring-content')
|
|
115
|
+
await fs.mkdir(path.join(contentDir, 'schema'), { recursive: true })
|
|
116
|
+
await fs.mkdir(path.join(contentDir, 'errors'), { recursive: true })
|
|
117
|
+
await fs.mkdir(path.join(contentDir, 'conf'), { recursive: true })
|
|
118
|
+
await fs.writeFile(path.join(contentDir, 'package.json'), JSON.stringify({
|
|
119
|
+
name: 'adapt-authoring-content',
|
|
120
|
+
version: '1.0.0'
|
|
121
|
+
}))
|
|
122
|
+
await fs.writeFile(path.join(contentDir, 'adapt-authoring.json'), JSON.stringify({
|
|
123
|
+
documentation: { enable: true }
|
|
124
|
+
}))
|
|
125
|
+
await fs.writeFile(path.join(contentDir, 'routes.json'), JSON.stringify({
|
|
126
|
+
root: 'content',
|
|
127
|
+
routes: [
|
|
128
|
+
{
|
|
129
|
+
route: '/clone',
|
|
130
|
+
handlers: { post: 'handleClone' },
|
|
131
|
+
permissions: { post: ['write:${scope}'] }
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}))
|
|
135
|
+
await fs.writeFile(path.join(contentDir, 'schema', 'content.schema.json'), JSON.stringify({
|
|
136
|
+
$anchor: 'content',
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
title: { type: 'string' },
|
|
140
|
+
body: { type: 'string' }
|
|
141
|
+
}
|
|
142
|
+
}))
|
|
143
|
+
await fs.writeFile(path.join(contentDir, 'errors', 'errors.json'), JSON.stringify({
|
|
144
|
+
CONTENT_NOT_FOUND: { statusCode: 404, description: 'Content not found' }
|
|
145
|
+
}))
|
|
146
|
+
await fs.writeFile(path.join(contentDir, 'conf', 'config.schema.json'), JSON.stringify({
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
cacheDir: { type: 'string', default: '$TEMP/content-cache' }
|
|
150
|
+
}
|
|
151
|
+
}))
|
|
152
|
+
|
|
153
|
+
// --- fake docs module with config ---
|
|
154
|
+
const docsDir = path.join(root, 'node_modules', 'adapt-authoring-docs')
|
|
155
|
+
await fs.mkdir(path.join(docsDir, 'conf'), { recursive: true })
|
|
156
|
+
await fs.writeFile(path.join(docsDir, 'package.json'), JSON.stringify({
|
|
157
|
+
name: 'adapt-authoring-docs',
|
|
158
|
+
version: '1.0.0'
|
|
159
|
+
}))
|
|
160
|
+
await fs.writeFile(path.join(docsDir, 'adapt-authoring.json'), JSON.stringify({
|
|
161
|
+
module: false,
|
|
162
|
+
documentation: { enable: true }
|
|
163
|
+
}))
|
|
164
|
+
await fs.writeFile(path.join(docsDir, 'conf', 'config.schema.json'), JSON.stringify({
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
outputDir: { type: 'string', default: '$TEMP/docs-build' },
|
|
168
|
+
manualSections: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
default: { 'getting-started': {}, 'other-guides': { default: true } }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}))
|
|
174
|
+
|
|
175
|
+
// --- module with useDefaultRoutes: false ---
|
|
176
|
+
const courseassetDir = path.join(root, 'node_modules', 'adapt-authoring-courseassets')
|
|
177
|
+
await fs.mkdir(courseassetDir, { recursive: true })
|
|
178
|
+
await fs.writeFile(path.join(courseassetDir, 'package.json'), JSON.stringify({
|
|
179
|
+
name: 'adapt-authoring-courseassets',
|
|
180
|
+
version: '1.0.0'
|
|
181
|
+
}))
|
|
182
|
+
await fs.writeFile(path.join(courseassetDir, 'adapt-authoring.json'), JSON.stringify({
|
|
183
|
+
documentation: { enable: true }
|
|
184
|
+
}))
|
|
185
|
+
await fs.writeFile(path.join(courseassetDir, 'routes.json'), JSON.stringify({
|
|
186
|
+
root: 'courseassets',
|
|
187
|
+
useDefaultRoutes: false,
|
|
188
|
+
routes: [
|
|
189
|
+
{
|
|
190
|
+
route: '/query',
|
|
191
|
+
handlers: { post: 'queryHandler' },
|
|
192
|
+
permissions: { post: ['read:${scope}'] }
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}))
|
|
196
|
+
|
|
197
|
+
// --- module with custom permissionsScope ---
|
|
198
|
+
const themeDir = path.join(root, 'node_modules', 'adapt-authoring-coursetheme')
|
|
199
|
+
await fs.mkdir(themeDir, { recursive: true })
|
|
200
|
+
await fs.writeFile(path.join(themeDir, 'package.json'), JSON.stringify({
|
|
201
|
+
name: 'adapt-authoring-coursetheme',
|
|
202
|
+
version: '1.0.0'
|
|
203
|
+
}))
|
|
204
|
+
await fs.writeFile(path.join(themeDir, 'adapt-authoring.json'), JSON.stringify({
|
|
205
|
+
documentation: { enable: true }
|
|
206
|
+
}))
|
|
207
|
+
await fs.writeFile(path.join(themeDir, 'routes.json'), JSON.stringify({
|
|
208
|
+
root: 'coursethemepresets',
|
|
209
|
+
permissionsScope: 'content',
|
|
210
|
+
routes: [
|
|
211
|
+
{
|
|
212
|
+
route: '/:_id/apply',
|
|
213
|
+
handlers: { post: 'applyHandler' },
|
|
214
|
+
permissions: { post: ['write:${scope}'] }
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}))
|
|
218
|
+
|
|
219
|
+
return root
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
describe('docsData', () => {
|
|
223
|
+
let fixtureDir
|
|
224
|
+
let dependencies
|
|
225
|
+
|
|
226
|
+
before(async () => {
|
|
227
|
+
fixtureDir = await createFixture()
|
|
228
|
+
const { loadDependencies } = await import('../lib/docsData.js')
|
|
229
|
+
dependencies = await loadDependencies(fixtureDir)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
after(async () => {
|
|
233
|
+
if (fixtureDir) await fs.rm(fixtureDir, { recursive: true, force: true }).catch(() => {})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('loadDependencies', () => {
|
|
237
|
+
it('should discover all modules with adapt-authoring.json', () => {
|
|
238
|
+
assert.ok(dependencies['adapt-authoring-content'])
|
|
239
|
+
assert.ok(dependencies['adapt-authoring-auth'])
|
|
240
|
+
assert.ok(dependencies['adapt-authoring-auth-local'])
|
|
241
|
+
assert.ok(dependencies['adapt-authoring-docs'])
|
|
242
|
+
assert.ok(dependencies['adapt-authoring-api'])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should merge package.json and adapt-authoring.json for each dep', () => {
|
|
246
|
+
const content = dependencies['adapt-authoring-content']
|
|
247
|
+
assert.equal(content.name, 'adapt-authoring-content')
|
|
248
|
+
assert.equal(content.version, '1.0.0')
|
|
249
|
+
assert.ok(content.documentation)
|
|
250
|
+
assert.ok(content.rootDir)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should set rootDir on each dependency', () => {
|
|
254
|
+
for (const dep of Object.values(dependencies)) {
|
|
255
|
+
assert.ok(dep.rootDir, `${dep.name} should have rootDir`)
|
|
256
|
+
assert.ok(dep.rootDir.includes('node_modules'), `${dep.name} rootDir should be in node_modules`)
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('loadConfigDefaults', () => {
|
|
262
|
+
it('should resolve $TEMP to os.tmpdir()', async () => {
|
|
263
|
+
const { loadConfigDefaults } = await import('../lib/docsData.js')
|
|
264
|
+
const config = await loadConfigDefaults(dependencies)
|
|
265
|
+
const outputDir = config.get('adapt-authoring-docs.outputDir')
|
|
266
|
+
assert.ok(outputDir.startsWith(os.tmpdir()), `expected ${outputDir} to start with ${os.tmpdir()}`)
|
|
267
|
+
assert.ok(outputDir.endsWith('/docs-build'))
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should return object defaults', async () => {
|
|
271
|
+
const { loadConfigDefaults } = await import('../lib/docsData.js')
|
|
272
|
+
const config = await loadConfigDefaults(dependencies)
|
|
273
|
+
const sections = config.get('adapt-authoring-docs.manualSections')
|
|
274
|
+
assert.ok(sections)
|
|
275
|
+
assert.ok(sections['getting-started'] !== undefined)
|
|
276
|
+
assert.ok(sections['other-guides']?.default === true)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('should return undefined for unknown config keys', async () => {
|
|
280
|
+
const { loadConfigDefaults } = await import('../lib/docsData.js')
|
|
281
|
+
const config = await loadConfigDefaults(dependencies)
|
|
282
|
+
assert.equal(config.get('nonexistent.key'), undefined)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should resolve $TEMP in non-docs modules', async () => {
|
|
286
|
+
const { loadConfigDefaults } = await import('../lib/docsData.js')
|
|
287
|
+
const config = await loadConfigDefaults(dependencies)
|
|
288
|
+
const cacheDir = config.get('adapt-authoring-content.cacheDir')
|
|
289
|
+
assert.ok(cacheDir.startsWith(os.tmpdir()))
|
|
290
|
+
assert.ok(cacheDir.endsWith('/content-cache'))
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe('buildRouterTree', () => {
|
|
295
|
+
it('should return api root router', async () => {
|
|
296
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
297
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
298
|
+
assert.equal(routerTree.path, '/api')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should create child routers for API modules', async () => {
|
|
302
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
303
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
304
|
+
const paths = routerTree.childRouters.map(r => r.path)
|
|
305
|
+
assert.ok(paths.includes('/api/content'))
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should merge default routes for API modules', async () => {
|
|
309
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
310
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
311
|
+
const contentRouter = routerTree.childRouters.find(r => r.path === '/api/content')
|
|
312
|
+
assert.ok(contentRouter)
|
|
313
|
+
const routePaths = contentRouter.routes.map(r => r.route)
|
|
314
|
+
// default routes: / and /:_id, plus custom: /clone
|
|
315
|
+
assert.ok(routePaths.includes('/'), 'should have default root route')
|
|
316
|
+
assert.ok(routePaths.includes('/:_id'), 'should have default :_id route')
|
|
317
|
+
assert.ok(routePaths.includes('/clone'), 'should have custom clone route')
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should skip default routes when useDefaultRoutes is false', async () => {
|
|
321
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
322
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
323
|
+
const router = routerTree.childRouters.find(r => r.path === '/api/courseassets')
|
|
324
|
+
assert.ok(router)
|
|
325
|
+
assert.equal(router.routes.length, 1)
|
|
326
|
+
assert.equal(router.routes[0].route, '/query')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('should replace placeholders in route config', async () => {
|
|
330
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
331
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
332
|
+
const contentRouter = routerTree.childRouters.find(r => r.path === '/api/content')
|
|
333
|
+
const rootRoute = contentRouter.routes.find(r => r.route === '/')
|
|
334
|
+
assert.deepEqual(rootRoute.permissions.post, ['write:content'])
|
|
335
|
+
assert.deepEqual(rootRoute.permissions.get, ['read:content'])
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should use permissionsScope for placeholder when defined', async () => {
|
|
339
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
340
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
341
|
+
const themeRouter = routerTree.childRouters.find(r => r.path === '/api/coursethemepresets')
|
|
342
|
+
assert.ok(themeRouter)
|
|
343
|
+
const applyRoute = themeRouter.routes.find(r => r.route === '/:_id/apply')
|
|
344
|
+
assert.deepEqual(applyRoute.permissions.post, ['write:content'])
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('should create auth router at /api/auth', async () => {
|
|
348
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
349
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
350
|
+
const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
|
|
351
|
+
assert.ok(authRouter)
|
|
352
|
+
assert.ok(authRouter.routes.some(r => r.route === '/check'))
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should create auth type child routers', async () => {
|
|
356
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
357
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
358
|
+
const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
|
|
359
|
+
const localRouter = authRouter.childRouters.find(r => r.path === '/api/auth/local')
|
|
360
|
+
assert.ok(localRouter)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('should merge auth type routes with auth defaults', async () => {
|
|
364
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
365
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
366
|
+
const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
|
|
367
|
+
const localRouter = authRouter.childRouters.find(r => r.path === '/api/auth/local')
|
|
368
|
+
const routePaths = localRouter.routes.map(r => r.route)
|
|
369
|
+
// override route: /, default: /register, custom: /changepass
|
|
370
|
+
assert.ok(routePaths.includes('/'), 'should have root (overridden) route')
|
|
371
|
+
assert.ok(routePaths.includes('/register'), 'should have default register route')
|
|
372
|
+
assert.ok(routePaths.includes('/changepass'), 'should have custom changepass route')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('should apply override merging from auth type routes', async () => {
|
|
376
|
+
const { buildRouterTree } = await import('../lib/docsData.js')
|
|
377
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
378
|
+
const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
|
|
379
|
+
const localRouter = authRouter.childRouters.find(r => r.path === '/api/auth/local')
|
|
380
|
+
const rootRoute = localRouter.routes.find(r => r.route === '/')
|
|
381
|
+
// override merged meta from auth-local onto default
|
|
382
|
+
assert.ok(rootRoute.meta?.post?.summary === 'Local auth')
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
describe('loadSchemas', () => {
|
|
387
|
+
it('should load schemas from schema/*.schema.json', async () => {
|
|
388
|
+
const { loadSchemas } = await import('../lib/docsData.js')
|
|
389
|
+
const schemas = await loadSchemas(dependencies)
|
|
390
|
+
assert.ok(schemas.schemas.content)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('should provide getSchema returning built schema', async () => {
|
|
394
|
+
const { loadSchemas } = await import('../lib/docsData.js')
|
|
395
|
+
const schemas = await loadSchemas(dependencies)
|
|
396
|
+
const result = await schemas.getSchema('content')
|
|
397
|
+
assert.ok(result.built)
|
|
398
|
+
assert.equal(result.built.type, 'object')
|
|
399
|
+
assert.ok(result.built.properties.title)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('should return empty object for unknown schemas', async () => {
|
|
403
|
+
const { loadSchemas } = await import('../lib/docsData.js')
|
|
404
|
+
const schemas = await loadSchemas(dependencies)
|
|
405
|
+
const result = await schemas.getSchema('nonexistent')
|
|
406
|
+
assert.deepEqual(result.built, {})
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
describe('loadErrors', () => {
|
|
411
|
+
it('should load and merge errors from errors/*.json', async () => {
|
|
412
|
+
const { loadErrors } = await import('../lib/docsData.js')
|
|
413
|
+
const errors = await loadErrors(dependencies)
|
|
414
|
+
assert.ok(errors.CONTENT_NOT_FOUND)
|
|
415
|
+
assert.equal(errors.CONTENT_NOT_FOUND.statusCode, 404)
|
|
416
|
+
assert.equal(errors.CONTENT_NOT_FOUND.code, 'CONTENT_NOT_FOUND')
|
|
417
|
+
assert.equal(errors.CONTENT_NOT_FOUND.meta.description, 'Content not found')
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
describe('buildPermissions', () => {
|
|
422
|
+
it('should build permission store with HTTP methods', async () => {
|
|
423
|
+
const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
|
|
424
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
425
|
+
const perms = buildPermissions(routerTree)
|
|
426
|
+
assert.ok(Array.isArray(perms.get))
|
|
427
|
+
assert.ok(Array.isArray(perms.post))
|
|
428
|
+
assert.ok(Array.isArray(perms.put))
|
|
429
|
+
assert.ok(Array.isArray(perms.patch))
|
|
430
|
+
assert.ok(Array.isArray(perms.delete))
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('should contain permission entries for secured routes', async () => {
|
|
434
|
+
const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
|
|
435
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
436
|
+
const perms = buildPermissions(routerTree)
|
|
437
|
+
assert.ok(perms.post.length > 0, 'should have POST permission entries')
|
|
438
|
+
assert.ok(perms.get.length > 0, 'should have GET permission entries')
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('should skip routes with null permissions (unsecured)', async () => {
|
|
442
|
+
const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
|
|
443
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
444
|
+
const perms = buildPermissions(routerTree)
|
|
445
|
+
// /api/auth/check has permissions: { get: null } — should not appear
|
|
446
|
+
const checkMatch = perms.get.find(([re]) => re.test('/api/auth/check'))
|
|
447
|
+
assert.equal(checkMatch, undefined, '/api/auth/check should not be in secured routes')
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should store scopes as arrays in permission entries', async () => {
|
|
451
|
+
const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
|
|
452
|
+
const routerTree = await buildRouterTree(dependencies)
|
|
453
|
+
const perms = buildPermissions(routerTree)
|
|
454
|
+
for (const entries of Object.values(perms)) {
|
|
455
|
+
for (const [re, scopes] of entries) {
|
|
456
|
+
assert.ok(re instanceof RegExp, 'first element should be RegExp')
|
|
457
|
+
assert.ok(Array.isArray(scopes), 'second element should be array')
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
})
|
package/tests/swagger.spec.js
CHANGED
|
@@ -331,39 +331,21 @@ function createMockApp (options = {}) {
|
|
|
331
331
|
|
|
332
332
|
return {
|
|
333
333
|
pkg: { version: '1.0.0' },
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
permissions: {
|
|
338
|
-
routes: {
|
|
339
|
-
get: [],
|
|
340
|
-
post: [],
|
|
341
|
-
put: [],
|
|
342
|
-
patch: [],
|
|
343
|
-
delete: []
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
334
|
+
schemas: {
|
|
335
|
+
schemas: mockSchemas,
|
|
336
|
+
getSchema: mock.fn(async (s) => schemas[s])
|
|
348
337
|
},
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
childRouters
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return {}
|
|
366
|
-
}),
|
|
367
|
-
onReady: mock.fn(async () => {})
|
|
338
|
+
routerTree: {
|
|
339
|
+
path: routerPath,
|
|
340
|
+
routes,
|
|
341
|
+
childRouters
|
|
342
|
+
},
|
|
343
|
+
permissions: {
|
|
344
|
+
get: [],
|
|
345
|
+
post: [],
|
|
346
|
+
put: [],
|
|
347
|
+
patch: [],
|
|
348
|
+
delete: []
|
|
349
|
+
}
|
|
368
350
|
}
|
|
369
351
|
}
|