-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Context
Route definitions are currently spread across imperative code in API modules (setValues(), addRoutes()) and auth modules (init()), plus separate apidefs.js files for OpenAPI metadata. This means the API surface of each module can only be determined by running the application.
This change introduces a routes.json convention — a single declarative file per module — and a route loading utility in the server module to read and process these files. Server already owns the Router class and all route registration mechanics, so it's the natural home for the shared loading logic.
Both AbstractApiModule (api) and AbstractAuthModule (auth) will consume this utility and layer on their own semantics (CRUD defaults, permissions, secure/unsecure).
Part of the broader static docs refactor: adapt-authoring-docs#47.
Related: adapt-authoring-api#74, adapt-authoring-auth#67.
What server owns
1. The routes.json base format
A single JSON file in each module's root (alongside adapt-authoring.json). Server defines the base format — only the fields that are generic to all route definitions. Consumer-specific fields (API's schemaName, auth's parentRouter, etc.) are defined by their respective modules.
{
"root": "content",
"routes": [
{
"route": "/insertrecursive",
"handlers": { "post": "insertRecursive" },
"permissions": { "post": ["write:content"] },
"internal": false,
"meta": {
"post": {
"summary": "Insert hierarchical content data",
"requestBody": { "..." : "..." },
"responses": { "201": { "..." : "..." } }
}
}
}
]
}2. Schemas
Server owns two base schemas. Consumer modules (API, auth) extend these via $merge to add their own fields.
schema/routes.schema.json — top-level route config
Defines the top-level structure of routes.json. The routes array is typed but has no items constraint — each consumer overrides this property with a $ref to their own route item schema.
{
"$anchor": "routes",
"type": "object",
"properties": {
"root": {
"type": "string",
"description": "Router root path"
},
"routes": {
"type": "array",
"description": "Route definitions"
}
},
"required": ["root"]
}schema/routeitem.schema.json — base route item
Defines the base fields for a single route entry. Consumer modules $merge from this to add their own per-route fields. Includes permissions as a base field since both API and auth modules use it.
A permission value of null for a given method means the route is explicitly unsecured for that method. This replaces the need for a separate unsecured boolean.
{
"$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" }
},
"permissions": {
"type": "object",
"description": "Keys are HTTP methods. Values are arrays of permission scope strings, or null to mark the method as unsecured.",
"propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] },
"additionalProperties": {
"oneOf": [
{ "type": "array", "items": { "type": "string" } },
{ "type": "null" }
]
}
},
"internal": {
"type": "boolean",
"description": "Restrict route to localhost",
"default": false
},
"meta": {
"type": "object",
"description": "Keys are HTTP methods, values are OpenAPI operation objects",
"propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] }
}
},
"required": ["route", "handlers"]
}3. Route loading utility — lib/utils/loadRouteConfig.js
A utility function that:
- Reads
routes.jsonfrom a module's root directory - Validates against the appropriate schema using the project's schema system (
_adapt-schemas), which supports$mergeand$reffor schema composition - Resolves handler strings against a target object — e.g.
"insertRecursive"becomestarget.insertRecursive.bind(target) - Returns structured route config objects ready for
router.addRoute()
The utility does NOT know about:
- CRUD default routes (that's API's concern)
- Permission interpretation /
secureRoute/unsecureRoute(that's auth's concern) - API middleware (that's API's concern)
It purely handles: file reading + schema validation + handler string resolution.
Proposed API
import { loadRouteConfig } from 'adapt-authoring-server'
// In AbstractApiModule.addRoutes():
const config = await loadRouteConfig(this.rootDir, this, {
schema: 'apiroutes',
handlerAliases: {
'default': this.requestHandler(),
'query': this.queryHandler()
}
})
// In AbstractAuthModule.init():
const config = await loadRouteConfig(this.rootDir, this, {
schema: 'authroutes'
})Parameters:
rootDir— path to the module's root (whereroutes.jsonlives)target— the object to resolve handler strings against (the module instance)options.schema— schema name to validate against (defaults to'routes'base schema)options.handlerAliases— map of special handler strings to pre-resolved functions
Returns null if no routes.json exists (caller falls back to current behaviour).
Validation behaviour
Validation must use the project's existing schema system from _adapt-schemas, which supports $merge and $ref for schema composition. This ensures consumer schemas (e.g. apiroutes which $merges from routes) are properly built and validated at runtime.
Validation happens in two stages with distinct error classes:
- Schema validation — validates the parsed JSON against the specified schema. Throws with the file path and validation errors if it fails (structural problem).
- Handler resolution — resolves handler strings against the target object. Throws with the handler name and target type if a handler string cannot be resolved and is not in
handlerAliases(runtime problem).
Handler resolution rules
| Handler string | Resolved to |
|---|---|
"insertRecursive" |
target.insertRecursive.bind(target) |
String in handlerAliases |
The pre-resolved value from the aliases map |
| Unresolvable string | Throws error |
Note: "default" and "query" are API-specific conventions. Server's utility does not know about them — the API module passes them via handlerAliases. This keeps server generic.
Key files
| File | Change |
|---|---|
schema/routes.schema.json |
New base top-level route config schema |
schema/routeitem.schema.json |
New base route item schema (includes permissions with null support) |
lib/utils/loadRouteConfig.js |
New route loading utility (must use project schema system for validation) |
lib/utils.js |
Export loadRouteConfig |
Testing
- Test with a fixture
routes.json— verify file reading, schema validation, and handler resolution - Test with missing
routes.json— returnsnull - Test handler resolution for various string values including aliases
- Test that unresolvable handler strings throw clear errors
- Test that schema validation rejects invalid
routes.json(missing required fields, wrong types, invalid HTTP methods) - Test that a consumer-provided schema name is used for validation