@avelonjs/next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/package.json +60 -0
- package/src/adapter.ts +194 -0
- package/src/codegen.ts +159 -0
- package/src/cookies.ts +137 -0
- package/src/dispatch.ts +74 -0
- package/src/index.ts +25 -0
- package/src/middleware.ts +75 -0
- package/src/page.ts +77 -0
- package/src/request.ts +139 -0
- package/src/response.ts +88 -0
- package/src/routing.ts +275 -0
package/src/routing.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Invalid,
|
|
3
|
+
NotFound,
|
|
4
|
+
type ControllerClass,
|
|
5
|
+
type HttpMethod,
|
|
6
|
+
type RouteDefinition,
|
|
7
|
+
type RouteManifest,
|
|
8
|
+
} from '@avelonjs/core'
|
|
9
|
+
|
|
10
|
+
const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'] as const
|
|
11
|
+
type ResourceAction = (typeof RESOURCE_ACTIONS)[number]
|
|
12
|
+
|
|
13
|
+
interface MutableRoute extends RouteDefinition<ControllerClass> {
|
|
14
|
+
middleware: string[]
|
|
15
|
+
bindings: Record<string, string>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function singular(name: string): string {
|
|
19
|
+
return name.endsWith('s') ? name.slice(0, -1) : name
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function joinPath(prefix: string, path: string): string {
|
|
23
|
+
if (prefix.length === 0) return path
|
|
24
|
+
const base = prefix.startsWith('/') ? prefix : `/${prefix}`
|
|
25
|
+
return `${base.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Fluent builder for one route entry. */
|
|
29
|
+
export class RouteBuilder {
|
|
30
|
+
constructor(private readonly definition: MutableRoute) {}
|
|
31
|
+
|
|
32
|
+
/** Sets the stable route name used by `route()`. */
|
|
33
|
+
name(name: string): this {
|
|
34
|
+
this.definition.name = name
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Appends middleware aliases. */
|
|
39
|
+
middleware(...names: string[]): this {
|
|
40
|
+
this.definition.middleware.push(...names)
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Marks a path parameter for route-model binding. */
|
|
45
|
+
bind(param: string, model: string | { readonly name: string }): this {
|
|
46
|
+
this.definition.bindings[param] = typeof model === 'string' ? model : model.name
|
|
47
|
+
return this
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Additionally generates a JSON route handler for this route. */
|
|
51
|
+
api(): this {
|
|
52
|
+
this.definition.api = true
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Fluent resource expander returned by {@link Registrar.resource}. */
|
|
58
|
+
export class ResourceBuilder {
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly registrar: Registrar,
|
|
61
|
+
private readonly members: MutableRoute[],
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
/** Marks member parameters for route-model binding. */
|
|
65
|
+
bind(param: string, model: string | { readonly name: string }): this {
|
|
66
|
+
const modelName = typeof model === 'string' ? model : model.name
|
|
67
|
+
for (const definition of this.members) {
|
|
68
|
+
if (definition.path.includes(`{${param}}`)) definition.bindings[param] = modelName
|
|
69
|
+
}
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Keeps only the named resource actions. */
|
|
74
|
+
only(...actions: ResourceAction[]): this {
|
|
75
|
+
const keep = new Set(actions)
|
|
76
|
+
this.#filter((action) => keep.has(action))
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Drops the named resource actions. */
|
|
81
|
+
except(...actions: ResourceAction[]): this {
|
|
82
|
+
const drop = new Set(actions)
|
|
83
|
+
this.#filter((action) => !drop.has(action))
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Additionally generates JSON handlers for every remaining resource route. */
|
|
88
|
+
api(): this {
|
|
89
|
+
for (const definition of this.members) definition.api = true
|
|
90
|
+
return this
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#filter(keep: (action: ResourceAction) => boolean): void {
|
|
94
|
+
const removed = new Set(
|
|
95
|
+
this.members.filter((definition) => !keep(definition.action as ResourceAction)),
|
|
96
|
+
)
|
|
97
|
+
for (const definition of removed) this.registrar.drop(definition)
|
|
98
|
+
const remaining = this.members.filter((definition) => !removed.has(definition))
|
|
99
|
+
this.members.length = 0
|
|
100
|
+
this.members.push(...remaining)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class Registrar {
|
|
105
|
+
readonly #definitions: MutableRoute[] = []
|
|
106
|
+
#middleware: string[] = []
|
|
107
|
+
#prefix = ''
|
|
108
|
+
|
|
109
|
+
#push(
|
|
110
|
+
method: HttpMethod,
|
|
111
|
+
path: string,
|
|
112
|
+
controller: ControllerClass,
|
|
113
|
+
action: string,
|
|
114
|
+
name?: string,
|
|
115
|
+
): MutableRoute {
|
|
116
|
+
const definition: MutableRoute = {
|
|
117
|
+
name: name ?? `${controller.name}.${action}`,
|
|
118
|
+
method,
|
|
119
|
+
path: joinPath(this.#prefix, path.startsWith('/') ? path : `/${path}`),
|
|
120
|
+
controller,
|
|
121
|
+
action,
|
|
122
|
+
middleware: [...this.#middleware],
|
|
123
|
+
bindings: {},
|
|
124
|
+
api: false,
|
|
125
|
+
}
|
|
126
|
+
this.#definitions.push(definition)
|
|
127
|
+
return definition
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Removes a definition. Used by resource `only` / `except`. */
|
|
131
|
+
drop(definition: MutableRoute): void {
|
|
132
|
+
const index = this.#definitions.indexOf(definition)
|
|
133
|
+
if (index >= 0) this.#definitions.splice(index, 1)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Registers a GET route. */
|
|
137
|
+
get(path: string, controller: ControllerClass, action = 'index'): RouteBuilder {
|
|
138
|
+
return new RouteBuilder(this.#push('GET', path, controller, action))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Registers a POST route. */
|
|
142
|
+
post(path: string, controller: ControllerClass, action = 'store'): RouteBuilder {
|
|
143
|
+
return new RouteBuilder(this.#push('POST', path, controller, action))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Registers a PUT route. */
|
|
147
|
+
put(path: string, controller: ControllerClass, action = 'update'): RouteBuilder {
|
|
148
|
+
return new RouteBuilder(this.#push('PUT', path, controller, action))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Registers a PATCH route. */
|
|
152
|
+
patch(path: string, controller: ControllerClass, action = 'update'): RouteBuilder {
|
|
153
|
+
return new RouteBuilder(this.#push('PATCH', path, controller, action))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Registers a DELETE route. */
|
|
157
|
+
delete(path: string, controller: ControllerClass, action = 'destroy'): RouteBuilder {
|
|
158
|
+
return new RouteBuilder(this.#push('DELETE', path, controller, action))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Expands to the seven resource routes: index, create, store, show, edit, update, destroy.
|
|
163
|
+
*/
|
|
164
|
+
resource(name: string, controller: ControllerClass): ResourceBuilder {
|
|
165
|
+
const param = singular(name)
|
|
166
|
+
const base = `/${name}`
|
|
167
|
+
const member = `${base}/{${param}}`
|
|
168
|
+
const members = [
|
|
169
|
+
this.#push('GET', base, controller, 'index', `${name}.index`),
|
|
170
|
+
this.#push('GET', `${base}/create`, controller, 'create', `${name}.create`),
|
|
171
|
+
this.#push('POST', base, controller, 'store', `${name}.store`),
|
|
172
|
+
this.#push('GET', member, controller, 'show', `${name}.show`),
|
|
173
|
+
this.#push('GET', `${member}/edit`, controller, 'edit', `${name}.edit`),
|
|
174
|
+
this.#push('PUT', member, controller, 'update', `${name}.update`),
|
|
175
|
+
this.#push('DELETE', member, controller, 'destroy', `${name}.destroy`),
|
|
176
|
+
]
|
|
177
|
+
return new ResourceBuilder(this, members)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Applies middleware to every route registered inside `group`. */
|
|
181
|
+
middleware(...names: string[]): { group: (callback: () => void) => void } {
|
|
182
|
+
return {
|
|
183
|
+
group: (callback) => {
|
|
184
|
+
const previous = this.#middleware
|
|
185
|
+
this.#middleware = [...previous, ...names]
|
|
186
|
+
callback()
|
|
187
|
+
this.#middleware = previous
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Prefixes every route registered inside `group`. */
|
|
193
|
+
prefix(prefix: string): { group: (callback: () => void) => void } {
|
|
194
|
+
return {
|
|
195
|
+
group: (callback) => {
|
|
196
|
+
const previous = this.#prefix
|
|
197
|
+
this.#prefix = joinPath(previous, prefix.startsWith('/') ? prefix : `/${prefix}`)
|
|
198
|
+
callback()
|
|
199
|
+
this.#prefix = previous
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Returns registered routes in declaration order. */
|
|
205
|
+
all(): readonly RouteDefinition<ControllerClass>[] {
|
|
206
|
+
return this.#definitions
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Builds a mountable manifest from the current registrations. */
|
|
210
|
+
manifest(version = '1'): RouteManifest<ControllerClass> {
|
|
211
|
+
return { version, routes: this.all() }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Finds a named route or throws. */
|
|
215
|
+
find(name: string): RouteDefinition<ControllerClass> {
|
|
216
|
+
const match = this.#definitions.find((definition) => definition.name === name)
|
|
217
|
+
if (match === undefined) {
|
|
218
|
+
throw new NotFound(`Route ${name} was not found.`, {
|
|
219
|
+
metadata: { resource: 'route', identifier: name },
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
return match
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Clears registrations. Intended for tests. */
|
|
226
|
+
reset(): void {
|
|
227
|
+
this.#definitions.length = 0
|
|
228
|
+
this.#middleware = []
|
|
229
|
+
this.#prefix = ''
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Process-wide route registrar used by `routes/web.ts`. */
|
|
234
|
+
export const Route = new Registrar()
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolves a named route to a path, filling `{param}` placeholders.
|
|
238
|
+
*
|
|
239
|
+
* `route('posts.show', id)` fills the first parameter. Pass an object when a route has several.
|
|
240
|
+
*/
|
|
241
|
+
export function route(
|
|
242
|
+
name: string,
|
|
243
|
+
params?: string | number | Readonly<Record<string, string | number>>,
|
|
244
|
+
): string {
|
|
245
|
+
const definition = Route.find(name)
|
|
246
|
+
const values =
|
|
247
|
+
params === undefined ? [] : typeof params === 'object' ? Object.values(params) : [params]
|
|
248
|
+
let index = 0
|
|
249
|
+
const path = definition.path.replace(/\{(\w+)\}/g, () => {
|
|
250
|
+
const value = values[index++]
|
|
251
|
+
if (value === undefined) {
|
|
252
|
+
throw new Invalid(`Missing parameter for route ${name}.`, {
|
|
253
|
+
metadata: { fields: { route: [`${name} is missing a path parameter.`] } },
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
return String(value)
|
|
257
|
+
})
|
|
258
|
+
return path
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Stable generated export name for a write route. */
|
|
262
|
+
export function exportName(definition: RouteDefinition): string {
|
|
263
|
+
return definition.name.replace(/[^A-Za-z0-9]+/g, '_')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** `/posts/{post}/edit` -> `posts/[post]/edit`. */
|
|
267
|
+
export function uriToSegments(uri: string): string {
|
|
268
|
+
return uri
|
|
269
|
+
.split('/')
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
.map((segment) =>
|
|
272
|
+
segment.startsWith('{') && segment.endsWith('}') ? `[${segment.slice(1, -1)}]` : segment,
|
|
273
|
+
)
|
|
274
|
+
.join('/')
|
|
275
|
+
}
|