Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
package-lock.json
node_modules/
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
* HTTP server functionality using Express.js
* @namespace server
*/
export { addExistenceProps, cacheRouteConfig, generateRouterMap, getAllRoutes, mapHandler } from './lib/utils.js'
export { addExistenceProps, cacheRouteConfig, generateRouterMap, getAllRoutes, loadRouteConfig, mapHandler } from './lib/utils.js'
export { default } from './lib/ServerModule.js'
1 change: 1 addition & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { addExistenceProps } from './utils/addExistenceProps.js'
export { cacheRouteConfig } from './utils/cacheRouteConfig.js'
export { generateRouterMap } from './utils/generateRouterMap.js'
export { getAllRoutes } from './utils/getAllRoutes.js'
export { loadRouteConfig } from './utils/loadRouteConfig.js'
export { mapHandler } from './utils/mapHandler.js'
76 changes: 76 additions & 0 deletions lib/utils/loadRouteConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import path from 'node:path'
import { App, readJson } from 'adapt-authoring-core'

/**
* Resolves handler strings in route definitions against a target object and handler aliases.
* @param {Array} routes Array of route definition objects
* @param {Object} target The object to resolve handler strings against
* @param {Object} aliases Map of handler string aliases to pre-resolved functions
* @return {Array} Routes with handler strings replaced by bound functions
*/
function resolveHandlers (routes, target, aliases) {
return routes.map(routeDef => {
const resolved = { ...routeDef }
if (routeDef.handlers) {
resolved.handlers = Object.fromEntries(
Object.entries(routeDef.handlers).map(([method, handlerStr]) => {
if (Object.hasOwn(aliases, handlerStr)) {
return [method, aliases[handlerStr]]
}
if (typeof target[handlerStr] !== 'function') {
throw new Error(`Cannot resolve handler '${handlerStr}': no such method on target`)
}
return [method, target[handlerStr].bind(target)]
})
)
}
return resolved
})
}

/**
* Reads and processes a routes.json file from a module's root directory,
* validating against the app's jsonschema module and resolving handler strings against a target object.
* @param {String} rootDir Path to the module root (where routes.json lives)
* @param {Object} target The object to resolve handler strings against
* @param {Object} [options] Optional configuration
* @param {String} [options.schema] Schema name to validate against (defaults to 'routes')
* @param {Object} [options.handlerAliases] Map of handler string aliases to pre-resolved functions
* @param {String} [options.defaults] Path to a default routes template JSON file. When provided and
* routes.json is found, the template's routes are resolved and prepended to config.routes.
* @return {Promise<Object|null>} Parsed config with resolved handlers, or null if no routes.json
* @memberof server
*/
export async function loadRouteConfig (rootDir, target, options = {}) {
const filePath = path.join(rootDir, 'routes.json')
let config
try {
config = await readJson(filePath)
} catch (e) {
if (e.code === 'ENOENT') return null
throw e
}
const jsonschema = await App.instance.waitForModule('jsonschema')
const schema = await jsonschema.getSchema(options.schema || 'routes')
try {
schema.validate(config)
} catch (e) {
throw new Error(`Invalid routes.json at ${filePath}: ${e.data?.errors || e.message}`)
}
const aliases = options.handlerAliases || {}

// Resolve handler strings in routes.json routes
const customRoutes = Array.isArray(config.routes)
? resolveHandlers(config.routes, target, aliases)
: []

// Prepend default routes from template if provided
if (options.defaults) {
const template = await readJson(options.defaults)
const defaultRoutes = resolveHandlers(template.routes || [], target, aliases)
config.routes = [...defaultRoutes, ...customRoutes]
} else {
config.routes = customRoutes
}
return config
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"devDependencies": {
"@semantic-release/git": "^10.0.1",
"adapt-schemas": "^1.1.0",
"conventional-changelog-eslint": "^6.0.0",
"semantic-release": "^25.0.2",
"standard": "^17.1.0"
Expand Down
39 changes: 39 additions & 0 deletions schema/routeitem.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$anchor": "routeitem",
"type": "object",
"properties": {
"route": {
"type": "string",
"description": "Express-style route path"
},
"handlers": {
"type": "object",
"description": "Keys are HTTP methods, values are handler name strings",
"propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] },
"additionalProperties": { "type": "string" }
},
"internal": {
"type": "boolean",
"description": "Restrict route to localhost",
"default": false
},
"permissions": {
"type": "object",
"description": "Keys are HTTP methods, values are permission scope arrays or null for unsecured",
"propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] },
"additionalProperties": {
"oneOf": [
{ "type": "array", "items": { "type": "string" } },
{ "type": "null" }
]
}
},
"meta": {
"type": "object",
"description": "Keys are HTTP methods, values are OpenAPI operation objects",
"propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] }
}
},
"required": ["route", "handlers"]
}
16 changes: 16 additions & 0 deletions schema/routes.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$anchor": "routes",
"type": "object",
"properties": {
"root": {
"type": "string",
"description": "Router root path"
},
"routes": {
"type": "array",
"description": "Route definitions"
}
},
"required": ["root"]
}
19 changes: 19 additions & 0 deletions tests/data/routes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"root": "content",
"routes": [
{
"route": "/insertrecursive",
"handlers": { "post": "insertRecursive" },
"internal": false,
"meta": {
"post": {
"summary": "Insert hierarchical content data"
}
}
},
{
"route": "/list",
"handlers": { "get": "listItems" }
}
]
}
Loading