New: Route loading utility for declarative routes.json support#58
New: Route loading utility for declarative routes.json support#58
Conversation
Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
|
@copilot please amend according to the latest instructions in the body of the issue |
Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
Done in 14f416d. Changes per the updated issue:
|
taylortom
left a comment
There was a problem hiding this comment.
Review
Critical: Custom validator doesn't support $merge/$ref
loadRouteConfig.js implements a hand-rolled validateSchema() that only understands basic JSON Schema keywords (type, required, properties, propertyNames). It has no support for $merge, $ref, or the project's schema composition system. This means:
- Consumer schemas (
apiroutes,authroutes) are never actually built/resolved — the$mergedeclarations are ignored - Route item schema overrides (
items.$ref) are never applied - The API and auth schemas are effectively decorative
The utility must use the project's existing schema system from _adapt-schemas to build and validate against composed schemas.
Route item validation ignores consumer schema
Lines 119-126 always validate route items against the base routeitem.schema.json from SCHEMA_DIR, regardless of what schema the consumer passed. Even if the consumer's top-level schema overrides routes.items to reference their own route item schema, this hard-coded validation bypasses it.
permissions should be on the base routeitem schema
Per updated issue: permissions is used by both API and auth routes, so it belongs on the base routeitem.schema.json, not duplicated on consumer schemas. Additionally, permission values should support null to indicate unsecured routes (replacing the need for a separate unsecured boolean on auth routes):
"permissions": {
"type": "object",
"propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] },
"additionalProperties": {
"oneOf": [
{ "type": "array", "items": { "type": "string" } },
{ "type": "null" }
]
}
}Minor: schemaDir option
The schemaDir option exists as a workaround for testing because the hand-rolled validator can't resolve $merge/$ref. With the project's schema system this wouldn't be needed — schemas are resolved by name, not file path.
Good
- Schema files (
routes.schema.json,routeitem.schema.json) are structurally correct - Handler resolution logic is clean — aliases, binding, error messages
ENOENTfallback returningnullis correct- Test coverage is solid for the handler resolution side
Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
…s to routeitem schema Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
Done in 0bf9727. Changes:
|
taylortom
left a comment
There was a problem hiding this comment.
Follow-up review (post-0bf9727)
The switch from the hand-rolled validator to adapt-schemas is a good step, but the approach should go one further — use the jsonschema module instead of importing adapt-schemas directly.
Use jsonschema module, not adapt-schemas
Every other module in the project accesses schema functionality via app.waitForModule('jsonschema'). No module imports adapt-schemas directly — it's an internal implementation detail of the jsonschema module. The current PR:
- Adds
adapt-schemasas a direct dependency inpackage.json(pinned to a branch) — this shouldn't be needed - Manually rebuilds a
Schemasregistry on every call viabuildSchemas()— expensive and redundant since the jsonschema module already auto-discovers and registers allschema/*.schema.jsonfiles from dependencies at startup - Misses custom keywords — things like
isObjectIdare added at runtime to the jsonschema module. A standaloneadapt-schemasimport won't have them - Misses caching/config — the jsonschema module handles schema compilation caching and app configuration
The utility should simplify to:
import { App } from 'adapt-authoring-core'
export async function loadRouteConfig (rootDir, target, options = {}) {
// ... file reading ...
const schemaName = options.schema || 'routes'
const jsonschema = await App.instance.waitForModule('jsonschema')
const schema = await jsonschema.getSchema(schemaName)
await schema.validate(config)
// ... handler resolution ...
}This eliminates buildSchemas(), the schemaFile option, and the adapt-schemas dependency entirely. The base schemas (routes, routeitem) and consumer schemas (apiroutes, authroutes) are registered automatically from each module's schema/ directory during app startup.
Other observations on the latest diff
- No test for
permissions: nullvalidation — therouteitemschema now supportsnullpermission values but there's no test verifying{ "permissions": { "post": null } }passes validation or that invalid values like{ "permissions": { "post": 42 } }are rejected - Schema registry rebuilt every call — even setting aside the
adapt-schemasvsjsonschemapoint,buildSchemas()is called on everyloadRouteConfiginvocation with no caching between calls
… import Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
Done in b4744be. Changes:
|
taylortom
left a comment
There was a problem hiding this comment.
Follow-up review (post-b4744be)
This is in good shape now. The core design issues are resolved — uses the jsonschema module, schemas are correct, handler resolution is clean.
1. adapt-schemas still in devDependencies
The adapt-schemas dependency was moved from production to devDependencies, which is correct since the utility now uses the jsonschema module. However, the tests import Schemas directly from adapt-schemas to build the mock. This is fine for now, but worth noting — if the test approach changes to use the real jsonschema module in integration tests, this devDependency could be dropped entirely.
2. Permissions tests duplicate the schema inline
The permissions test block defines a perm-routes consumer schema that re-declares the permissions property inline rather than referencing routeitem. Since routeitem.schema.json already defines permissions, a test that validates against a schema using "items": { "$ref": "routeitem" } would be more representative of real consumer usage and would catch regressions in the base schema itself.
3. schema.validate() — check if async
The code calls schema.validate(config) without await. Looking at the adapt-schemas source, validate() may be synchronous, but worth verifying — if it returns a Promise the error handling would silently miss validation failures.
4. Minor: process.exitCode = 0 in test setup
The before() hook resets process.exitCode after App.init() fails. This works but is fragile — if something else legitimately sets exitCode during the test run, it would be masked. A comment explaining why it's there is helpful (already partially there).
Good
loadRouteConfigis clean and focused — 60 lines, clear separation of file reading, validation, handler resolution- Schema files are correct and minimal
- Test coverage is comprehensive — missing file, valid config, handler binding, aliases, unresolvable handlers, schema validation failures, consumer schemas with
$merge, permissions with null - Tests properly clean up registered schemas via
deregisterSchemainfinally/afterblocks - No unnecessary dependencies in production
In real consumer schemas (apiroutes, authroutes), route items reference routeitem via $ref which is resolved by the jsonschema module at startup. In tests, AJV throws anchor conflicts when $ref targets an already-registered schema, so we inline the constraints instead.
Consumers can pass a defaults path to prepend resolved default routes from a template file, eliminating duplicate _getDefaultRoutes() methods across API and auth modules.
The test after() hook already cleans up the tmp directory. A leftover from an interrupted run is better surfaced by git status than hidden.
# [2.1.0](v2.0.0...v2.1.0) (2026-02-26) ### Chore * Add comments explaining inline test schemas vs $ref ([2620c74](2620c74)) * Import loadRouteConfig via barrel file in tests ([f7dc358](f7dc358)) * Remove tests/tmp/ from .gitignore (refs #58) ([47a0dd1](47a0dd1)), closes [#58](#58) * Use npm version for adapt-schemas instead of GitHub branch ref ([1027922](1027922)) * Use readJson from core barrel instead of readFile + JSON.parse ([8dfb2b4](8dfb2b4)) ### Fix * remove lockfile and peerDependenciesMeta, add .npmrc ([40bed7e](40bed7e)) * replace hand-rolled validator with adapt-schemas, add permissions to routeitem schema ([0bf9727](0bf9727)) * use app.waitForModule jsonschema instead of direct adapt-schemas import ([b4744be](b4744be)) ### New * Add defaults option to loadRouteConfig for default route templates ([ad081c9](ad081c9)) * add loadRouteConfig utility for declarative routes.json support ([c3db63a](c3db63a)) * add schema validation to loadRouteConfig, add base JSON schemas ([14f416d](14f416d)) * Route loading utility for declarative routes.json support (#58) ([c535b3e](c535b3e)), closes [#58](#58) ### WIP * planning adapt-schemas integration ([0ee0a80](0ee0a80))
|
🎉 This PR is included in version 2.1.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Route definitions are currently spread across imperative code in API/auth modules. This adds a
loadRouteConfigutility to the server module that reads a declarativeroutes.jsonfile, validates it against base JSON schemas via the app'sjsonschemamodule (with full$merge/$refcomposition support), and resolves handler strings to bound methods — givingAbstractApiModuleandAbstractAuthModulea shared foundation to build on.New
schema/routes.schema.json— base JSON Schema for the top-levelroutes.jsonstructure (requiresrootas a string,routesas an array); consumer modules extend this via$mergeto add their own fieldsschema/routeitem.schema.json— base JSON Schema for individual route entries (requiresrouteandhandlers, validates HTTP method names, validates handler values are strings); includespermissionsfield supporting scope arrays ornullfor unsecured routes; consumer modules extend this via$mergeloadRouteConfig(rootDir, target, options?)— readsroutes.jsonfrom a module root, validates it against a named schema viaapp.waitForModule('jsonschema')(inheriting custom keywords, caching, and auto-discovered consumer schemas), resolves handler strings againsttarget, returnsnullif no file exists, throws descriptive errors for schema violations or unresolvable handlersHandler strings are resolved as
target[str].bind(target);handlerAliaseslets callers map special strings (e.g."default","query") to pre-resolved values. Consumer-specific top-level fields (e.g.schemaName) are preserved in the returned config. Route item validation is handled by the schema itself (consumer schemas defineroutes.itemsconstraints via$merge). Consumer schemas (apiroutes,authroutes) are auto-discovered from each module'sschema/directory at startup by thejsonschemamodule — no manual registration needed.Testing
node --test tests/utils-loadRouteConfig.spec.js— 13 tests covering: missing file returnsnull, config fields preserved, consumer-specific fields preserved after validation, handler binding, alias override, clear error on unresolvable handler, schema validation rejects missing required fields and wrong types, route item validation via consumer$mergeschema, consumer-provided schema validation,permissions: nullaccepted for unsecured routes, and invalid HTTP method keys in permissions rejectedOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.