Skip to content
Open
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
44 changes: 42 additions & 2 deletions assets/skills/koapp-router/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
name: koapp-router
description: Define routes, path parameters, validators, and nested routers with the @axiosleo/koapp Router class. Use when declaring HTTP/WebSocket/TCP routes in koapp, adding path params like /users/{:id}, composing nested routers, attaching per-route middlewares or after-handlers, validating params/query/body with validatorjs rules, or using shortcut helpers (get/post/put/patch/delete/any).
description: Define routes, path parameters, validators, and nested routers with the @axiosleo/koapp Router class. Use when declaring HTTP/WebSocket/TCP routes in koapp, adding path params like /users/{:id}, composing nested routers, attaching per-route middlewares or after-handlers, validating params/query/body with validatorjs rules, using shortcut helpers (get/post/put/patch/delete/any), or registering routes dynamically at runtime (e.g. routes loaded from a database) with registerRouters / app.registerRouters.
---

# @axiosleo/koapp Router

Source: [`src/router.js`](../../../src/router.js).
Source: [`src/router.js`](../../../src/router.js) (Router class) and
[`src/core.js`](../../../src/core.js) (`registerRouters` for runtime registration).

`Router` is the single route-definition primitive for all three application
types (`KoaApplication`, `SocketApplication`, `WebSocketApplication`). A
Expand Down Expand Up @@ -191,6 +192,45 @@ For a request `PUT /users/42`:
3. If nothing matches, follow the nearest `/***` fallback
4. If even that misses, the framework lets the next Koa middleware run (typically 404)

## Runtime route registration

Routers passed via `config.routers` are resolved once at startup. To inject
additional routes **while the app is running** (e.g. route info stored in a
database instead of declared with the `Router` class), use `registerRouters`:

```javascript
const { registerRouters } = require('@axiosleo/koapp');

// register a single route: pass one object
registerRouters(app.routes, {
path: '/dynamic/{:id}', // alias of prefix; must start with "/"
method: 'get', // normalized to uppercase; 'GET|POST' and 'ANY' work too
handlers: async (context) => { // a single function is wrapped into an array
// handle request
}
});

// register multiple routes: pass an array
registerRouters(app.routes, [routeA, routeB]);
```

Every application instance also exposes it as a chainable method:

```javascript
app.registerRouters({ path: '/dynamic', method: 'ANY', handlers: [handler] });
```

Notes:

- Each item may be a `Router` instance or a plain object with `path` (or
`prefix`), `method`, `handlers`, plus optional `middlewares`, `afters`,
`validators`, and nested `routers`.
- Routes are inserted into the live route tree, so they take effect for the
next request immediately - no restart needed.
- Registering the same path + method again appends a new entry; lookup
returns the **first** registered one, so existing routes are not overridden.
- A route without `method` never matches; use `'ANY'` to accept all methods.

## Common pitfalls

- Leaving `method: ''` (or unset) on a `router.push` creates a route that
Expand Down
46 changes: 46 additions & 0 deletions assets/skills/koapp-router/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,49 @@ const api = new Router('/api/v1');
registerCrud(api, 'users', usersController);
registerCrud(api, 'posts', postsController);
```

## Runtime registration from database

Routes stored in a database (not declared with the `Router` class) can be
injected while the app is running. Map each record to a plain route object
and register it - new routes serve the very next request, no restart needed:

```javascript
const { KoaApplication, success } = require('@axiosleo/koapp');

const app = new KoaApplication({
port: 8080,
routers: [staticRouter] // routes known at startup
});
await app.start();

// later, while the app is running:
// e.g. rows like { path: '/pages/about', method: 'GET', template: '...' }
const rows = await db.query('SELECT path, method, template FROM dynamic_routes');

app.registerRouters(rows.map((row) => ({
path: row.path, // must start with "/"
method: row.method, // 'get' works too, normalized to uppercase
handlers: async (context) => { // single function is auto-wrapped
success({ html: render(row.template, context.params) });
}
})));

// registering a single route works the same way, no array needed
app.registerRouters({
path: '/pages/{:slug}',
method: 'ANY',
handlers: async (context) => {
const page = await db.findPage(context.params.slug);
success(page);
}
});
```

The standalone function does the same against any route tree:

```javascript
const { registerRouters } = require('@axiosleo/koapp');

registerRouters(app.routes, { path: '/health', method: 'GET', handlers: [ping] });
```
77 changes: 77 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,38 @@ export interface RouterOptions<T extends AppContext<any, any, any> = KoaContext>
validators?: RouterValidator;
}

/**
* Options for registering routes dynamically at runtime
* (e.g. route info loaded from a database, not declared with the Router class)
* @template T Context type extending AppContext
*
* @example
* ```typescript
* const route: DynamicRouterOptions = {
* path: '/dynamic/{:id}', // alias of prefix, must start with "/"
* method: 'get', // normalized to uppercase; supports 'GET|POST' and 'ANY'
* handlers: async (context) => { // single function will be wrapped into an array
* // handle request
* }
* };
*
* registerRouters(app.routes, route); // register a single route
* registerRouters(app.routes, [route, other]); // register multiple routes
* ```
*/
export interface DynamicRouterOptions<
T extends AppContext<any, any, any> = KoaContext,
> extends Omit<RouterOptions<T>, "handlers" | "routers"> {
/** Route path like '/dynamic/{:id}', alias of prefix */
path?: string;
/** Route path prefix, used when path is not provided */
prefix?: string;
/** Route handlers: a single function or an array of functions */
handlers?: ContextHandler<T> | ContextHandler<T>[];
/** Nested sub-routers */
routers?: Array<Router<AppContext<any, any, any>> | DynamicRouterOptions<any>>;
}

/**
* Router class for defining API routes and middleware
* @template T Context type extending AppContext (can be KoaContext, SocketContext, etc.)
Expand Down Expand Up @@ -1250,6 +1282,19 @@ export declare abstract class Application extends EventEmitter {

constructor(config: AppConfiguration);

/**
* Register one or more routers at runtime.
* Takes effect immediately for subsequent requests.
* @param routers Single router or an array of routers (Router instances or plain route objects)
* @returns The application instance for chaining
*/
registerRouters(
routers:
| Router<any>
| DynamicRouterOptions<any>
| Array<Router<any> | DynamicRouterOptions<any>>,
): this;

/**
* Start the application
* @returns Promise that resolves when application is started
Expand Down Expand Up @@ -1489,6 +1534,38 @@ export declare class Model {
// Utility Functions
// ========================================

/**
* Register one or more routers into an existing route tree at runtime.
* Accepts Router instances or plain objects (e.g. route info loaded from a database).
* Registered routes take effect immediately for subsequent requests.
* @template T Context type extending AppContext
* @param routes Route tree resolved at startup (e.g. app.routes)
* @param routers Single router or an array of routers
* @returns The route tree
*
* @example
* ```typescript
* // register a single route (plain object, e.g. loaded from database)
* registerRouters(app.routes, {
* path: '/dynamic/{:id}',
* method: 'GET',
* handlers: async (context) => { ... }
* });
*
* // register multiple routes at once
* registerRouters(app.routes, [routeA, routeB]);
* ```
*/
export function registerRouters<
T extends AppContext<any, any, any> = KoaContext,
>(
routes: any,
routers:
| Router<any>
| DynamicRouterOptions<T>
| Array<Router<any> | DynamicRouterOptions<T>>,
): any;

/**
* Initialize application context
* @template T Application type
Expand Down
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { Router } = require('./src/router');
const response = require('./src/response');
const Model = require('./src/model');
const { KoaSSEMiddleware } = require('./src/middlewares/sse');
const { initContext } = require('./src/core');
const { initContext, registerRouters } = require('./src/core');
const session = require('koa-session');
const { SocketClient } = require('./src/utils');
const {
Expand Down Expand Up @@ -33,6 +33,7 @@ module.exports = {
// functions
...response,
initContext,
registerRouters,

SocketClient
};
12 changes: 11 additions & 1 deletion src/apps/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const EventEmitter = require('events');
const { v4 } = require('uuid');
const { Configuration } = require('@axiosleo/cli-tool');
const { resolveRouters } = require('../core');
const { resolveRouters, registerRouters } = require('../core');

class Application extends EventEmitter {
constructor(config) {
Expand All @@ -19,6 +19,16 @@ class Application extends EventEmitter {
this.emit('starting', this);
}

/**
* Register one or more routers at runtime.
* Takes effect immediately for subsequent requests.
* @param {object|object[]} routers single router or an array of routers
*/
registerRouters(routers) {
registerRouters(this.routes, routers);
return this;
}

async start() {
throw new Error('not implemented');
}
Expand Down
34 changes: 32 additions & 2 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,41 @@ const recur = (tree, prefix, router, middlewares = [], afters = []) => {
}
};

const resolveRouters = (routers = []) => {
const normalizeRouter = (router) => {
const item = Object.assign({}, router);
if (!item.prefix && item.path) {
item.prefix = item.path;
}
if (item.method) {
item.method = `${item.method}`.toUpperCase();
}
if (is.func(item.handlers)) {
item.handlers = [item.handlers];
}
if (is.array(item.routers) && item.routers.length) {
item.routers = item.routers.map(normalizeRouter);
}
return item;
};

/**
* Register one or more routers into an existing route tree.
* Accepts Router instances or plain objects (e.g. route info loaded from database),
* so routes can be injected dynamically while the app is running.
* @param {object} routes route tree resolved by resolveRouters (e.g. app.routes)
* @param {object|object[]} routers single router or an array of routers
*/
const registerRouters = (routes, routers = []) => {
if (!is.array(routers)) {
routers = [routers];
}
routers.forEach(item => recur(routes, '', normalizeRouter(item), []));
return routes;
};

const resolveRouters = (routers = []) => {
const tree = {};
routers.forEach(item => recur(tree, '', item, []));
registerRouters(tree, routers);
return tree;
};

Expand Down Expand Up @@ -212,4 +241,5 @@ module.exports = {
initContext,
getRouteInfo,
resolveRouters,
registerRouters,
};
Loading
Loading