diff --git a/src/client/types.ts b/src/client/types.ts index 45901fe..39935d3 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -218,6 +218,23 @@ export type UrlFor > ): { method: 'DELETE'; url: string; form: { action: string; method: 'DELETE' } } + /** + * Make URL for a QUERY route. An error will be raised if the route doesn't + * exist. + * + * ```ts + * urlFor.query('users.search') // { method: 'QUERY', url: '/users/search' } + * urlFor.query('users.show', [1]) // Error: Route not found QUERY@users.show + * ``` + */ + query( + ...[identifier, params, options]: RouteBuilderArguments< + RouteIdentifier, + Routes['QUERY'][RouteIdentifier], + Options + > + ): { method: 'QUERY'; url: string; form: { action: string; method: 'QUERY' } } + /** * Make URL for a custom route method. An error will be raised if the route doesn't * exist for the same method. diff --git a/src/client/url_builder.ts b/src/client/url_builder.ts index 5224bc8..d33b8ed 100644 --- a/src/client/url_builder.ts +++ b/src/client/url_builder.ts @@ -160,6 +160,23 @@ export function createUrlBuilder( } } + urlFor.query = function urlForMethodQuery(...[identifier, params, options]) { + const method = 'QUERY' + const url = createUrlForRoute(identifier, params, options, method) + + return { + url, + method, + toString() { + return url + }, + form: { + action: url, + method, + }, + } + } + urlFor.method = function urlForCustomMethod(method, ...[identifier, params, options]) { const url = createUrlForRoute(identifier, params, options, method) return { diff --git a/src/router/main.ts b/src/router/main.ts index 612b485..cc3cd07 100644 --- a/src/router/main.ts +++ b/src/router/main.ts @@ -330,6 +330,19 @@ export class Router extends Macroable { return this.route(pattern, ['DELETE'], handler) } + /** + * Define `QUERY` route + * @param pattern - The route pattern + * @param handler - Route handler (function, string, or controller tuple) + * @returns The created route instance + */ + query>( + pattern: string, + handler: string | RouteFn | [LazyImport | T, GetControllerHandlers?] + ) { + return this.route(pattern, ['QUERY'], handler) + } + /** * Creates a group of routes. A route group can apply transforms * to routes in bulk diff --git a/src/router/signed_url_builder.ts b/src/router/signed_url_builder.ts index d59f7ea..c01f315 100644 --- a/src/router/signed_url_builder.ts +++ b/src/router/signed_url_builder.ts @@ -162,6 +162,23 @@ export function createSignedUrlBuilder( } } + signedRoute.query = function routeQuery(...[identifier, params, options]) { + const method = 'QUERY' + const url = createSignedUrlForRoute(identifier, params, options, method) + + return { + url, + method, + toString() { + return url + }, + form: { + action: url, + method, + }, + } + } + signedRoute.method = function routeGet(method, ...[identifier, params, options]) { const url = createSignedUrlForRoute(identifier, params, options, method) diff --git a/tests/router/router.spec.ts b/tests/router/router.spec.ts index 3f577d8..5f88163 100644 --- a/tests/router/router.spec.ts +++ b/tests/router/router.spec.ts @@ -23,6 +23,7 @@ test.group('Router | add', () => { const putRoute = router.put('/', '#controllers/home.update') const patchRoute = router.patch('/', '#controllers/home.updatePatch') const deleteRoute = router.delete('/', '#controllers/home.destroy') + const queryRoute = router.query('/', '#controllers/home.search') const anyRoute = router.any('/', '#controllers/home.handle') assert.containSubset(getRoute.toJSON(), { @@ -70,6 +71,15 @@ test.group('Router | add', () => { name: 'home.destroy', }) + assert.containSubset(queryRoute.toJSON(), { + pattern: '/', + methods: ['QUERY'], + meta: {}, + matchers: {}, + domain: 'root', + name: 'home.search', + }) + assert.containSubset(anyRoute.toJSON(), { pattern: '/', methods: ['HEAD', 'OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'], @@ -1126,6 +1136,7 @@ test.group('Router | handler', () => { async update() {} async updatePatch() {} async destroy() {} + async search() {} async handle() {} } @@ -1155,6 +1166,10 @@ test.group('Router | handler', () => { assert.isObject(deleteRoute.toJSON().handler) assert.property(deleteRoute.toJSON().handler, 'handle') + const queryRoute = router.query('/', [HomeController, 'search']) + assert.isObject(queryRoute.toJSON().handler) + assert.property(queryRoute.toJSON().handler, 'handle') + const anyRoute = router.any('/', [HomeController, 'handle']) assert.isObject(anyRoute.toJSON().handler) assert.property(anyRoute.toJSON().handler, 'handle') @@ -1168,6 +1183,7 @@ test.group('Router | handler', () => { async update() {} async updatePatch() {} async destroy() {} + async search() {} async handle() {} } @@ -1191,6 +1207,10 @@ test.group('Router | handler', () => { assert.isObject(deleteRoute.toJSON().handler) assert.property(deleteRoute.toJSON().handler, 'handle') + const queryRoute = router.query('/', [HomeController, 'search']) + assert.isObject(queryRoute.toJSON().handler) + assert.property(queryRoute.toJSON().handler, 'handle') + const anyRoute = router.any('/', [HomeController, 'handle']) assert.isObject(anyRoute.toJSON().handler) assert.property(anyRoute.toJSON().handler, 'handle') diff --git a/tests/router/url_builder.spec.ts b/tests/router/url_builder.spec.ts index 63f8059..7e3897a 100644 --- a/tests/router/url_builder.spec.ts +++ b/tests/router/url_builder.spec.ts @@ -103,12 +103,20 @@ test.group('URLBuilder', () => { paramsTuple: [string] } } + QUERY: { + 'users.show': { + params: { id: string } + paramsTuple: [string] + } + } }>() .merge({ router }) .create() router.route('/users', ['POST'], () => {}).as('users.index') - router.route('/users/:id', ['GET', 'PUT', 'PATCH', 'DELETE'], () => {}).as('users.show') + router + .route('/users/:id', ['GET', 'PUT', 'PATCH', 'DELETE', 'QUERY'], () => {}) + .as('users.show') router.commit() assert.containSubset(urlFor.get('users.show', { id: '1' }), { method: 'GET', url: '/users/1' }) @@ -158,6 +166,16 @@ test.group('URLBuilder', () => { }) assert.equal(`${urlFor.delete('users.show', { id: '1' })}`, '/users/1') + assert.containSubset(urlFor.query('users.show', { id: '1' }), { + method: 'QUERY', + url: '/users/1', + }) + assert.deepEqual(urlFor.query('users.show', { id: '1' }).form, { + method: 'QUERY', + action: '/users/1', + }) + assert.equal(`${urlFor.query('users.show', { id: '1' })}`, '/users/1') + assert.containSubset(urlFor.method('GET', 'users.show', { id: '1' }), { method: 'GET', url: '/users/1', @@ -251,6 +269,12 @@ test.group('URLBuilder', () => { paramsTuple: [string] } } + QUERY: { + 'users.search': { + params?: {} + paramsTuple: [string] + } + } }>() .merge({ router, encryption }) .create() @@ -259,6 +283,7 @@ test.group('URLBuilder', () => { router.route('/users/:id', ['GET'], () => {}).as('users.show') router.route('/users/:id', ['PUT', 'PATCH'], () => {}).as('users.update') router.route('/users/:id', ['DELETE'], () => {}).as('users.delete') + router.route('/users/search', ['QUERY'], () => {}).as('users.search') router.commit() function verifySignature(uri: string) { @@ -276,6 +301,7 @@ test.group('URLBuilder', () => { verifySignature(`${signedUrlFor.put('users.update', { id: '1' })}`) verifySignature(`${signedUrlFor.patch('users.update', { id: '1' })}`) verifySignature(`${signedUrlFor.delete('users.delete', { id: '1' })}`) + verifySignature(`${signedUrlFor.query('users.search')}`) verifySignature(`${signedUrlFor.method('GET', 'users.show', { id: '1' })}`) }) diff --git a/tests/server.spec.ts b/tests/server.spec.ts index 43be740..8582e45 100644 --- a/tests/server.spec.ts +++ b/tests/server.spec.ts @@ -17,6 +17,7 @@ import { AppFactory } from '@adonisjs/application/factories' import { createServer, IncomingMessage, ServerResponse } from 'node:http' import { Router } from '../src/router/main.ts' +import { httpServer as httpServerFactory } from '../factories/http_server.ts' import { HttpContext } from '../src/http_context/main.ts' import { ServerFactory } from '../factories/server_factory.ts' import { defineNamedMiddleware } from '../src/define_middleware.ts' @@ -101,6 +102,26 @@ test.group('Server | Response handling', () => { assert.equal(text, 'handled') }) + test('invoke router handler for QUERY method requests', async ({ assert }) => { + const app = new AppFactory().create(BASE_URL, () => {}) + const server = new ServerFactory().merge({ app }).create() + + await app.init() + + server.use([]) + server.getRouter().query('/users/search', async ({ request }) => request.method()) + await server.boot() + + const { url } = await httpServerFactory.create(server.handle.bind(server)) + const response = await fetch(`${url}/users/search`, { + method: 'QUERY', + body: 'name = "virk"', + }) + + assert.equal(response.status, 200) + assert.equal(await response.text(), 'QUERY') + }) + test('use route handler return value when response.send is not called', async ({ assert }) => { const app = new AppFactory().create(BASE_URL, () => {}) const server = new ServerFactory().merge({ app }).create()