adapt-authoring-docs 1.3.2 → 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/standardjs.yml +2 -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
- package/docs/architecture.md +0 -175
- package/jsdoc3/.jsdocConfig.json +0 -57
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
|
}
|