diff --git a/package-lock.json b/package-lock.json index d57fd634..2525e231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -734,18 +734,18 @@ } }, "node_modules/@ebec/core": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@ebec/core/-/core-1.0.1.tgz", - "integrity": "sha512-YONiuIHxImAHIBr3EHwmOfMycqSLjlb5RmKUhOPLNC9aoKL4B0JbfpFOqfpIQlexfELQvNXSWdnEYs/uRp6ytA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ebec/core/-/core-1.1.0.tgz", + "integrity": "sha512-00+4zWS0df0RBHNuQUixzBhYLzyqSDvBr7VsQiReT0fv2r52/mqNHcPNp4cts6O/bDIvz5O6Zw0S4jlNug9Kkw==", "license": "MIT" }, "node_modules/@ebec/http": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@ebec/http/-/http-4.0.0.tgz", - "integrity": "sha512-3nzhuWuTWXBAVe+nd3WCqU33IZdNU3aRqFvY4/G96T10DSx/AE63yXnFxpERxz9Ib20JhWXWwhAEAK9zQME59g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ebec/http/-/http-4.1.0.tgz", + "integrity": "sha512-4yTeOJtcsLIiF0vBPPT6N45hmDgLekXZkdMvRBAonaJ6Idu5lM8+YLdU3SCAjKIEYJGXgJDXKFgp/oty+6P5Ew==", "license": "MIT", "dependencies": { - "@ebec/core": "^1.0.1" + "@ebec/core": "^1.1.0" } }, "node_modules/@emnapi/core": { @@ -9528,15 +9528,16 @@ } }, "node_modules/routup": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/routup/-/routup-5.0.1.tgz", - "integrity": "sha512-s5LEKYf0ItLnUZspjOCJzfwMoka+19D+DYl5fuzYYmkzPFWhmzy1MJ3GvSvdoEPOxIhJYqBY6dlO7vsi4L+NeA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/routup/-/routup-5.2.0.tgz", + "integrity": "sha512-TjG5WsZE2PMtkaYWTQwYLG+pl4fLUUtyX5pz/V5tyaLMbVvRAuCiH3nDfABNNoZ/3nXovuNVsw63hczf3Y36aQ==", "license": "MIT", "workspaces": [ "docs" ], "dependencies": { - "@ebec/http": "^4.0.0", + "@ebec/core": "^1.1.0", + "@ebec/http": "^4.1.0", "mime-explorer": "^1.1.0", "negotiator": "^1.0.0", "path-to-regexp": "^8.4.2", @@ -11796,10 +11797,10 @@ "version": "4.1.0", "license": "MIT", "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/basic": { @@ -11812,10 +11813,10 @@ "@routup/query": "^3.1.0" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/body": { @@ -11823,10 +11824,10 @@ "version": "3.1.0", "license": "MIT", "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/cookie": { @@ -11837,10 +11838,10 @@ "cookie-es": "^3.1.1" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/cors": { @@ -11848,10 +11849,10 @@ "version": "1.0.1", "license": "MIT", "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/decorators": { @@ -11868,11 +11869,11 @@ "@trapi/metadata": "^2.0.0-beta.3", "@trapi/swagger": "^2.0.0-beta.3", "jsonata": "^2.1.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { "@trapi/core": ">=2.0.0-beta.3 <3.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependenciesMeta": { "@trapi/core": { @@ -11887,6 +11888,9 @@ "dependencies": { "vitepress": "^1.6.4", "vue": "^3.5.34" + }, + "peerDependencies": { + "routup": "^5.2.0" } }, "packages/i18n": { @@ -11895,11 +11899,11 @@ "license": "MIT", "devDependencies": { "ilingo": "^5.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { "ilingo": "^5.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/logger": { @@ -11907,10 +11911,10 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "routup": "^5.0.0" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.0" + "routup": "^5.2.0" } }, "packages/prometheus": { @@ -11919,11 +11923,11 @@ "license": "MIT", "devDependencies": { "prom-client": "^15.1.3", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { "prom-client": ">=15.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/query": { @@ -11935,10 +11939,10 @@ "qs": "^6.15.1" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/rate-limit": { @@ -11946,10 +11950,10 @@ "version": "3.1.0", "license": "MIT", "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" } }, "packages/rate-limit-redis": { @@ -11959,12 +11963,12 @@ "devDependencies": { "@routup/rate-limit": "^3.1.0", "redis-extension": "^2.0.4", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { "@routup/rate-limit": "^3.1.0", "redis-extension": "^2.0.4", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependenciesMeta": { "redis-extension": { @@ -12031,11 +12035,11 @@ }, "devDependencies": { "@types/swagger-ui-dist": "^3.30.5", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependencies": { "@types/swagger-ui-dist": "^3.30.5", - "routup": "^5.0.1" + "routup": "^5.2.0" } } } diff --git a/packages/assets/package.json b/packages/assets/package.json index 6cd898ae..971faa38 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -52,10 +52,10 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/basic/package.json b/packages/basic/package.json index e042fd0c..e07f0547 100644 --- a/packages/basic/package.json +++ b/packages/basic/package.json @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "dependencies": { "@routup/body": "^3.1.0", @@ -71,7 +71,7 @@ "@routup/query": "^3.1.0" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "gitHead": "94d729e309c1eec0401afb4d8083f65ce3aa8e0b", "publishConfig": { diff --git a/packages/body/package.json b/packages/body/package.json index 489768ac..634dcdc1 100644 --- a/packages/body/package.json +++ b/packages/body/package.json @@ -49,10 +49,10 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/cookie/package.json b/packages/cookie/package.json index 77c84a2e..41406e4a 100644 --- a/packages/cookie/package.json +++ b/packages/cookie/package.json @@ -51,13 +51,13 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "dependencies": { "cookie-es": "^3.1.1" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "gitHead": "94d729e309c1eec0401afb4d8083f65ce3aa8e0b", "publishConfig": { diff --git a/packages/cors/package.json b/packages/cors/package.json index 8b021945..04e92b18 100644 --- a/packages/cors/package.json +++ b/packages/cors/package.json @@ -53,10 +53,10 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/decorators/README.md b/packages/decorators/README.md index d27b908c..d54497b9 100644 --- a/packages/decorators/README.md +++ b/packages/decorators/README.md @@ -67,6 +67,9 @@ export class UserController { return 'Hello, World!'; } + // `@DController` also accepts `string[]` to mount the controller under + // multiple paths, e.g. `@DController(['/users', '/members'])`. + @DGet('/:id') async getOne( @DPath('id') id: string, diff --git a/packages/decorators/package.json b/packages/decorators/package.json index 9559ea92..b21dff18 100644 --- a/packages/decorators/package.json +++ b/packages/decorators/package.json @@ -56,7 +56,7 @@ "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { "@trapi/core": ">=2.0.0-beta.3 <3.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependenciesMeta": { "@trapi/core": { @@ -73,7 +73,7 @@ "@trapi/metadata": "^2.0.0-beta.3", "@trapi/swagger": "^2.0.0-beta.3", "jsonata": "^2.1.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/decorators/src/class/module.ts b/packages/decorators/src/class/module.ts index 9f5d533a..9dc79649 100644 --- a/packages/decorators/src/class/module.ts +++ b/packages/decorators/src/class/module.ts @@ -2,7 +2,7 @@ import type { ClassType } from '../type'; import { useDecoratorMeta } from '../utils'; export function createClassDecorator( - url: string, + url: string | string[], middlewares?: ClassType[], ) : ClassDecorator { return (target: any) : void => { @@ -21,7 +21,7 @@ export function createClassDecorator( } export function DController( - url: string, + url: string | string[], middlewares?: ClassType[], ) : ClassDecorator { return createClassDecorator(url, middlewares); diff --git a/packages/decorators/src/mount.ts b/packages/decorators/src/mount.ts index 22f3e757..3806f67e 100644 --- a/packages/decorators/src/mount.ts +++ b/packages/decorators/src/mount.ts @@ -1,23 +1,13 @@ import type { IRouter, MethodName } from 'routup'; import { Router, defineCoreHandler } from 'routup'; import { buildDecoratorMethodArguments } from './method'; -import type { ClassType } from './type'; +import type { ClassType, DecoratorMeta } from './type'; import { createHandlerForClassType, isObject, useDecoratorMeta } from './utils'; -export function mountController( - router: IRouter, - input: (ClassType | Record), -) { - let controller : Record; - - if (isObject(input)) { - controller = input; - } else { - controller = new (input as ClassType)(); - } - - const meta = useDecoratorMeta(controller); - +function buildControllerRouter( + controller: Record, + meta: DecoratorMeta, +): Router { const childRouter = new Router(); for (let i = 0; i < meta.middlewares.length; i++) { @@ -61,7 +51,31 @@ export function mountController( childRouter.use(handler); } - router.use(meta.url, childRouter); + return childRouter; +} + +export function mountController( + router: IRouter, + input: (ClassType | Record), +) { + let controller : Record; + + if (isObject(input)) { + controller = input; + } else { + controller = new (input as ClassType)(); + } + + const meta = useDecoratorMeta(controller); + + if (Array.isArray(meta.url)) { + const childRouter = buildControllerRouter(controller, meta); + for (const url of meta.url) { + router.use(url, childRouter.clone()); + } + } else { + router.use(meta.url, buildControllerRouter(controller, meta)); + } } export function mountControllers( diff --git a/packages/decorators/src/type.ts b/packages/decorators/src/type.ts index ef669f16..8063a8a1 100644 --- a/packages/decorators/src/type.ts +++ b/packages/decorators/src/type.ts @@ -8,7 +8,7 @@ export interface ClassType extends Function { } export type DecoratorMeta = { - url: string, + url: string | string[], methods: { [key: string]: DecoratorMethodOptions diff --git a/packages/decorators/test/unit/multi-path.spec.ts b/packages/decorators/test/unit/multi-path.spec.ts new file mode 100644 index 00000000..42115638 --- /dev/null +++ b/packages/decorators/test/unit/multi-path.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { Router } from 'routup'; +import { DController, DGet, decorators } from '../../src'; + +function createTestRequest(url: string, options?: RequestInit): Request { + const fullUrl = url.startsWith('http') ? url : `http://localhost${url}`; + return new Request(fullUrl, options); +} + +@DController(['/users', '/members']) +class MultiPathController { + @DGet('') + async list() { + return 'list'; + } + + @DGet('/:id') + async show() { + return 'show'; + } +} + +describe('multi-path controller', () => { + it('mounts the controller under each path', async () => { + const router = new Router(); + router.use(decorators({ controllers: [MultiPathController] })); + + const users = await router.fetch(createTestRequest('/users')); + expect(users.status).toEqual(200); + expect(await users.text()).toEqual('list'); + + const members = await router.fetch(createTestRequest('/members')); + expect(members.status).toEqual(200); + expect(await members.text()).toEqual('list'); + + const usersId = await router.fetch(createTestRequest('/users/1')); + expect(usersId.status).toEqual(200); + expect(await usersId.text()).toEqual('show'); + + const membersId = await router.fetch(createTestRequest('/members/2')); + expect(membersId.status).toEqual(200); + expect(await membersId.text()).toEqual('show'); + }); +}); diff --git a/packages/docs/package.json b/packages/docs/package.json index c63725c4..f0e10784 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -18,5 +18,8 @@ "dependencies": { "vitepress": "^1.6.4", "vue": "^3.5.34" + }, + "peerDependencies": { + "routup": "^5.2.0" } } diff --git a/packages/docs/src/decorators/controllers.md b/packages/docs/src/decorators/controllers.md index 2755674c..f86ea073 100644 --- a/packages/docs/src/decorators/controllers.md +++ b/packages/docs/src/decorators/controllers.md @@ -33,6 +33,20 @@ export class UserController { The full path is `controllerPath + methodPath`, so the `show` handler above resolves to `GET /users/:id`. +## Multiple mount paths + +`@DController` also accepts an array of paths. The controller is mounted under each path, so every method is reachable through any of them: + +```typescript +@DController(['/users', '/members']) +export class UserController { + @DGet('') async list() {} + @DGet('/:id') async show(@DPath('id') id: string) {} +} +``` + +`GET /users` and `GET /members` both resolve to `list`; `GET /users/42` and `GET /members/42` both resolve to `show`. This mirrors `@trapi/core`'s controller `paths` field, so the OpenAPI generator picks up every mount point. + ## Returning values In routup v5, handlers return values directly — no `send(res, data)`. The return value is converted to a `Response` by the core (see [routup's response model](https://routup.dev/guide/response)): diff --git a/packages/i18n/package.json b/packages/i18n/package.json index aca83750..6c99b8f0 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -48,11 +48,11 @@ "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { "ilingo": "^5.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "devDependencies": { "ilingo": "^5.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "gitHead": "94d729e309c1eec0401afb4d8083f65ce3aa8e0b", "publishConfig": { diff --git a/packages/logger/package.json b/packages/logger/package.json index 1b01a2a5..7ce1ab1a 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -54,10 +54,10 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.0" + "routup": "^5.2.0" }, "devDependencies": { - "routup": "^5.0.0" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/prometheus/package.json b/packages/prometheus/package.json index 21962c14..3bfe8fa2 100644 --- a/packages/prometheus/package.json +++ b/packages/prometheus/package.json @@ -45,11 +45,11 @@ "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { "prom-client": ">=15.0.0", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "devDependencies": { "prom-client": "^15.1.3", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/query/package.json b/packages/query/package.json index 5af1df8a..ab1663f1 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -52,14 +52,14 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "dependencies": { "@types/qs": "^6.14.0", "qs": "^6.15.1" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "gitHead": "94d729e309c1eec0401afb4d8083f65ce3aa8e0b", "publishConfig": { diff --git a/packages/rate-limit-redis/package.json b/packages/rate-limit-redis/package.json index 0831f97a..c8d5b6c9 100644 --- a/packages/rate-limit-redis/package.json +++ b/packages/rate-limit-redis/package.json @@ -49,7 +49,7 @@ "peerDependencies": { "@routup/rate-limit": "^3.1.0", "redis-extension": "^2.0.4", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "peerDependenciesMeta": { "redis-extension": { @@ -59,7 +59,7 @@ "devDependencies": { "@routup/rate-limit": "^3.1.0", "redis-extension": "^2.0.4", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/rate-limit/package.json b/packages/rate-limit/package.json index 6ab908e1..7cf9218a 100644 --- a/packages/rate-limit/package.json +++ b/packages/rate-limit/package.json @@ -46,10 +46,10 @@ }, "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "devDependencies": { - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/swagger-ui/package.json b/packages/swagger-ui/package.json index 9a755240..2eae061c 100644 --- a/packages/swagger-ui/package.json +++ b/packages/swagger-ui/package.json @@ -53,7 +53,7 @@ "homepage": "https://github.com/routup/plugins#readme", "peerDependencies": { "@types/swagger-ui-dist": "^3.30.5", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "dependencies": { "@routup/assets": "^4.1.0", @@ -61,7 +61,7 @@ }, "devDependencies": { "@types/swagger-ui-dist": "^3.30.5", - "routup": "^5.0.1" + "routup": "^5.2.0" }, "publishConfig": { "access": "public"