Skip to content

Commit 6d815d0

Browse files
committed
feat: add url_builder client and redoc
1 parent 257a264 commit 6d815d0

22 files changed

+855
-424
lines changed

factories/url_builder_factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { EncryptionFactory } from '@adonisjs/encryption/factories'
1313
import { RouterFactory } from './router.ts'
1414
import type { Router } from '../src/router/main.ts'
1515
import { type LookupList } from '../src/types/url_builder.ts'
16-
import { createUrlBuilder } from '../src/router/url_builder.ts'
16+
import { createUrlBuilder } from '../src/client/url_builder.ts'
1717
import { createSignedUrlBuilder } from '../src/router/signed_url_builder.ts'
1818

1919
type FactoryParameters = {
@@ -61,7 +61,7 @@ export class URLBuilderFactory<Routes extends LookupList> {
6161
const router = this.#createRouter()
6262

6363
return {
64-
urlFor: createUrlBuilder<Routes>(router, router.qs.stringify),
64+
urlFor: createUrlBuilder<Routes>(() => router.toJSON(), router.qs.stringify),
6565
signedUrlFor: createSignedUrlBuilder<Routes>(
6666
router,
6767
this.#createEncryption(),

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
".": "./build/index.js",
1414
"./helpers": "./build/src/helpers.js",
1515
"./types": "./build/src/types/main.js",
16+
"./client/url_builder": "./build/src/client/url_builder.js",
1617
"./factories": "./build/factories/main.js"
1718
},
1819
"engines": {
@@ -181,6 +182,7 @@
181182
"./index.ts",
182183
"./src/helpers.ts",
183184
"./src/types/main.ts",
185+
"./src/client/url_builder.ts",
184186
"./factories/main.ts"
185187
],
186188
"outDir": "./build",

src/client/helpers.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* @adonisjs/http-server
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { type ClientRouteMatchItTokens, type ClientRouteJSON, type URLOptions } from './types.ts'
11+
12+
/**
13+
* Finds a route by its identifier across domains.
14+
*
15+
* Searches for routes by name, pattern, or controller reference. When no domain
16+
* is specified, searches across all domains. Supports legacy lookup strategies
17+
* for backwards compatibility.
18+
*
19+
* @param domainsRoutes - Object mapping domain names to route arrays
20+
* @param routeIdentifier - Route name, pattern, or controller reference to find
21+
* @param domain - Optional domain to limit search scope
22+
* @param method - Optional HTTP method to filter routes
23+
* @param disableLegacyLookup - Whether to disable pattern and controller lookup
24+
*
25+
* @example
26+
* ```ts
27+
* const route = findRoute(routes, 'users.show', 'api', 'GET')
28+
* const route2 = findRoute(routes, '/users/:id', undefined, 'GET')
29+
* ```
30+
*/
31+
export function findRoute<Route extends ClientRouteJSON>(
32+
domainsRoutes: { [domain: string]: Route[] },
33+
routeIdentifier: string,
34+
domain?: string,
35+
method?: string,
36+
disableLegacyLookup?: boolean
37+
): null | Route {
38+
/**
39+
* Search for route in all the domains when no domain name is
40+
* mentioned.
41+
*/
42+
if (!domain) {
43+
let route: Route | null = null
44+
for (const routeDomain of Object.keys(domainsRoutes)) {
45+
route = findRoute(domainsRoutes, routeIdentifier, routeDomain, method, disableLegacyLookup)
46+
if (route) {
47+
break
48+
}
49+
}
50+
return route
51+
}
52+
53+
const routes = domainsRoutes[domain]
54+
if (!routes) {
55+
return null
56+
}
57+
58+
const lookupByName = true
59+
60+
/**
61+
* Pattern and controller are supported for legacy reasons. However
62+
* the URL builder only works with names
63+
*/
64+
const lookupByPattern = !disableLegacyLookup
65+
const lookupByController = !disableLegacyLookup
66+
67+
return (
68+
routes.find((route) => {
69+
if (method && !route.methods.includes(method)) {
70+
return false
71+
}
72+
73+
if (
74+
(lookupByName && route.name === routeIdentifier) ||
75+
(lookupByPattern && route.pattern === routeIdentifier)
76+
) {
77+
return true
78+
}
79+
80+
if (lookupByController && route.handler && typeof route.handler === 'object') {
81+
return 'reference' in route.handler && route.handler.reference === routeIdentifier
82+
}
83+
84+
return false
85+
}) || null
86+
)
87+
}
88+
89+
/**
90+
* Makes URL for a given route pattern using its parsed tokens. The
91+
* tokens could be generated using the "parseRoute" method.
92+
*
93+
* @param pattern - The route pattern
94+
* @param tokens - Array of parsed route tokens
95+
* @param searchParamsStringifier - Function to stringify query parameters
96+
* @param params - Route parameters as array or object
97+
* @param options - URL options
98+
* @returns {string} The generated URL
99+
*/
100+
export function createURL(
101+
pattern: string,
102+
tokens: Pick<ClientRouteMatchItTokens, 'val' | 'type' | 'end'>[],
103+
searchParamsStringifier: (qs: Record<string, any>) => string,
104+
params?: any[] | { [param: string]: any },
105+
options?: URLOptions
106+
): string {
107+
const uriSegments: string[] = []
108+
const paramsArray = Array.isArray(params) ? params : null
109+
const paramsObject = !Array.isArray(params) ? (params ?? {}) : {}
110+
111+
let paramsIndex = 0
112+
for (const token of tokens) {
113+
/**
114+
* Static param
115+
*/
116+
if (token.type === 0) {
117+
uriSegments.push(token.val === '/' ? '' : `${token.val}${token.end}`)
118+
continue
119+
}
120+
121+
/**
122+
* Wildcard param. It will always be the last param, hence we will provide
123+
* it all the remaining values
124+
*/
125+
if (token.type === 2) {
126+
const values = paramsArray ? paramsArray.slice(paramsIndex) : paramsObject['*']
127+
if (!Array.isArray(values) || !values.length) {
128+
throw new Error(
129+
`Cannot make URL for "${pattern}". Invalid value provided for the wildcard param`
130+
)
131+
}
132+
133+
uriSegments.push(`${values.join('/')}${token.end}`)
134+
break
135+
}
136+
137+
const paramName = token.val
138+
const value = paramsArray ? paramsArray[paramsIndex] : paramsObject[paramName]
139+
const isDefined = value !== undefined && value !== null
140+
141+
/**
142+
* Required param
143+
*/
144+
if (token.type === 1 && !isDefined) {
145+
throw new Error(
146+
`Cannot make URL for "${pattern}". Missing value for the "${paramName}" param`
147+
)
148+
}
149+
150+
if (isDefined) {
151+
uriSegments.push(`${value}${token.end}`)
152+
}
153+
154+
paramsIndex++
155+
}
156+
157+
let URI = `/${uriSegments.join('/')}`
158+
159+
/**
160+
* Prefix base URL
161+
*/
162+
if (options?.prefixUrl) {
163+
URI = `${options?.prefixUrl.replace(/\/$/, '')}${URI}`
164+
}
165+
166+
/**
167+
* Append query string
168+
*/
169+
if (options?.qs) {
170+
const queryString = searchParamsStringifier(options?.qs)
171+
URI = queryString ? `${URI}?${queryString}` : URI
172+
}
173+
174+
return URI
175+
}

0 commit comments

Comments
 (0)