@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8
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/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
- package/dist/chunk-SPQUX3C7.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Middleware inspect and filter the requests that reach your app: they can check a request, turn it away, or change
|
|
4
|
+
the response on its way out.
|
|
5
|
+
|
|
6
|
+
Oriel's own middleware check that users are signed in, start the session and verify CSRF tokens. Yours live in
|
|
7
|
+
`app/Http/Middleware`.
|
|
8
|
+
|
|
9
|
+
## Defining middleware
|
|
10
|
+
|
|
11
|
+
`make:middleware` writes a new middleware class in `app/Http/Middleware`:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
node oriel make:middleware EnsureTokenIsValid
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A middleware has a `handle()` method that receives the request and `next`, the rest of the app. This one lets the
|
|
18
|
+
request through only when its `token` input matches, and sends everyone else to `/home`:
|
|
19
|
+
|
|
20
|
+
```ts title="app/Http/Middleware/EnsureTokenIsValid.ts"
|
|
21
|
+
import { redirect } from '@orieljs/framework'
|
|
22
|
+
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
|
|
23
|
+
|
|
24
|
+
export class EnsureTokenIsValid implements Middleware {
|
|
25
|
+
async handle(request: HttpRequest, next: Next): Promise<Response> {
|
|
26
|
+
if (request.input('token') !== 'my-secret-token') {
|
|
27
|
+
return redirect('/home').toResponse(request)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return next(request)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To let the request pass, call `next(request)` and return what it returns. Think of middleware as layers a request
|
|
36
|
+
passes through on its way to your route: each layer can examine the request and turn it away.
|
|
37
|
+
|
|
38
|
+
`handle()` returns a `Response`, so turn a redirect into one with its `toResponse(request)`, or throw: `abort(403)`
|
|
39
|
+
ends the request with a 403.
|
|
40
|
+
|
|
41
|
+
#### Middleware and responses
|
|
42
|
+
|
|
43
|
+
A middleware can do its work before the request goes on:
|
|
44
|
+
|
|
45
|
+
```ts title="app/Http/Middleware/BeforeMiddleware.ts"
|
|
46
|
+
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
|
|
47
|
+
|
|
48
|
+
export class BeforeMiddleware implements Middleware {
|
|
49
|
+
async handle(request: HttpRequest, next: Next): Promise<Response> {
|
|
50
|
+
// Perform action
|
|
51
|
+
|
|
52
|
+
return next(request)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
or after the app has answered, with the response in hand:
|
|
58
|
+
|
|
59
|
+
```ts title="app/Http/Middleware/AfterMiddleware.ts"
|
|
60
|
+
import { Log } from '@orieljs/framework'
|
|
61
|
+
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
|
|
62
|
+
|
|
63
|
+
export class AfterMiddleware implements Middleware {
|
|
64
|
+
async handle(request: HttpRequest, next: Next): Promise<Response> {
|
|
65
|
+
const response = await next(request)
|
|
66
|
+
|
|
67
|
+
Log.info('Answered', { path: request.path(), status: response.status })
|
|
68
|
+
|
|
69
|
+
return response
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Dependency injection
|
|
75
|
+
|
|
76
|
+
A new instance of your middleware handles each request, built by the [service container](container.md). Take the
|
|
77
|
+
services it needs with `inject()` in a field:
|
|
78
|
+
|
|
79
|
+
```ts title="app/Http/Middleware/EnsureTokenIsValid.ts"
|
|
80
|
+
import { inject, redirect } from '@orieljs/framework'
|
|
81
|
+
import { ConfigRepository, type HttpRequest, type Middleware, type Next } from '@orieljs/framework/contracts'
|
|
82
|
+
|
|
83
|
+
export class EnsureTokenIsValid implements Middleware {
|
|
84
|
+
readonly #config = inject(ConfigRepository)
|
|
85
|
+
|
|
86
|
+
async handle(request: HttpRequest, next: Next): Promise<Response> {
|
|
87
|
+
if (request.input('token') !== this.#config.get('services.webhooks.token')) {
|
|
88
|
+
return redirect('/home').toResponse(request)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return next(request)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> [!NOTE]
|
|
97
|
+
> Laravel injects a middleware's dependencies through its constructor's type hints. TypeScript types are gone when
|
|
98
|
+
> your app runs, so Oriel builds middleware with no constructor arguments, and `inject()` reads what they need from
|
|
99
|
+
> the container instead.
|
|
100
|
+
|
|
101
|
+
## Registering middleware
|
|
102
|
+
|
|
103
|
+
### Global middleware
|
|
104
|
+
|
|
105
|
+
A global middleware runs on every request. Add it in `bootstrap/app.ts`, with `withMiddleware()`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
declare const EnsureTokenIsValid: MiddlewareClass
|
|
109
|
+
// ---cut---
|
|
110
|
+
Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
|
|
111
|
+
middleware.append(EnsureTokenIsValid)
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`append()` adds it at the end of the global list, and `prepend()` at the start. The list starts with `TrimStrings`,
|
|
116
|
+
which trims every text input except `password`, `password_confirmation` and `current_password`, and
|
|
117
|
+
`ConvertEmptyStringsToNull`, which turns empty inputs into `null`.
|
|
118
|
+
|
|
119
|
+
Laravel's global list also checks trusted proxies and the size of the body. In Oriel these aren't middleware: the
|
|
120
|
+
request applies them from `config/http.ts`, as [Requests](requests.md) explains.
|
|
121
|
+
|
|
122
|
+
### Assigning middleware to routes
|
|
123
|
+
|
|
124
|
+
Give a route a middleware with `middleware()`, by class or by alias:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
declare const EnsureTokenIsValid: MiddlewareClass
|
|
128
|
+
// ---cut---
|
|
129
|
+
Route.get('/profile', () => 'Profile').middleware(EnsureTokenIsValid)
|
|
130
|
+
|
|
131
|
+
Route.get('/', () => 'Home').middleware(['auth', EnsureTokenIsValid])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A group gives its middleware to every route in it: see [route groups](routing.md#route-groups).
|
|
135
|
+
|
|
136
|
+
#### Excluding middleware
|
|
137
|
+
|
|
138
|
+
`withoutMiddleware()` takes a middleware off one route of a group, whatever parameters it was given there:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
declare const EnsureTokenIsValid: MiddlewareClass
|
|
142
|
+
// ---cut---
|
|
143
|
+
Route.middleware(EnsureTokenIsValid).group(() => {
|
|
144
|
+
Route.get('/', () => 'Home')
|
|
145
|
+
|
|
146
|
+
Route.get('/profile', () => 'Profile').withoutMiddleware(EnsureTokenIsValid)
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
It removes a middleware a route gets from its group or from the `web` group, but not a global one. There is no
|
|
151
|
+
`withoutMiddleware()` for a whole group: put those routes in a group of their own.
|
|
152
|
+
|
|
153
|
+
### Middleware groups
|
|
154
|
+
|
|
155
|
+
A group puts several middleware under one name, which routes use as they would an alias. Define one with `group()`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
declare const First: MiddlewareClass
|
|
159
|
+
declare const Second: MiddlewareClass
|
|
160
|
+
// ---cut---
|
|
161
|
+
Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
|
|
162
|
+
middleware.group('group-name', [First, Second])
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
Route.get('/', () => 'Home').middleware('group-name')
|
|
168
|
+
|
|
169
|
+
Route.middleware('group-name').group(() => {
|
|
170
|
+
// ...
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A group's list may name other groups, and aliases with their parameters: `['auth', 'throttle:60,1']`.
|
|
175
|
+
|
|
176
|
+
#### The default middleware groups
|
|
177
|
+
|
|
178
|
+
The routes in `routes/web.ts` are in the `web` group, and those in `routes/api.ts` in the `api` group:
|
|
179
|
+
|
|
180
|
+
| The `web` group | What it does |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `EncryptCookies` | Decrypts the request's cookies and encrypts the response's |
|
|
183
|
+
| `AddQueuedCookiesToResponse` | Adds the cookies queued on the cookie jar (`Cookie.queue()`) to the response |
|
|
184
|
+
| `StartSession` | Loads the session, and saves it once the response is ready |
|
|
185
|
+
| `VerifyCsrfToken` | Refuses a form without the CSRF token (see [CSRF protection](csrf.md)) |
|
|
186
|
+
|
|
187
|
+
The `api` group is empty: API routes have no session, no cookie encryption and no CSRF check.
|
|
188
|
+
|
|
189
|
+
> [!NOTE]
|
|
190
|
+
> Laravel's groups also hold `ShareErrorsFromSession` and `SubstituteBindings`. Oriel needs neither: `errors()` reads
|
|
191
|
+
> the validation errors from the session wherever you call it, and route model binding is a step of the router, which
|
|
192
|
+
> runs for every route.
|
|
193
|
+
|
|
194
|
+
`web()` and `api()` change the default groups: `append` adds middleware at the end, `prepend` at the start, and
|
|
195
|
+
`remove` takes them out. The Inertia starter kits add two middleware of their own to the `web` group, in
|
|
196
|
+
`bootstrap/app.ts`:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
declare const HandleAppearance: MiddlewareClass
|
|
200
|
+
declare const HandleInertiaRequests: MiddlewareClass
|
|
201
|
+
// ---cut---
|
|
202
|
+
Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
|
|
203
|
+
middleware.web({ append: [HandleAppearance, HandleInertiaRequests] })
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`group('web', [...])` replaces the `web` group with the list you give it.
|
|
208
|
+
|
|
209
|
+
### Middleware aliases
|
|
210
|
+
|
|
211
|
+
An alias is a short name for a middleware class, which routes can use with parameters. Define yours with `alias()`:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
declare const EnsureUserIsSubscribed: MiddlewareClass
|
|
215
|
+
// ---cut---
|
|
216
|
+
Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
|
|
217
|
+
middleware.alias({ subscribed: EnsureUserIsSubscribed })
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
Route.get('/profile', () => 'Profile').middleware('subscribed')
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A name that is neither an alias nor a group fails when a route that uses it is called, with a 500 that starts
|
|
226
|
+
"Middleware [subscribed] is not defined." These aliases are defined for you.
|
|
227
|
+
[Authentication](authentication.md#protecting-routes), [Authorization](authorization.md#via-middleware), [Email
|
|
228
|
+
verification](verification.md#protecting-routes) and [History encryption](inertia.md#history-encryption) cover the ones
|
|
229
|
+
they own.
|
|
230
|
+
|
|
231
|
+
| Alias | Middleware | What it does |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `auth` | `Authenticate` | Lets signed-in users through. A guest gets a 401 for JSON, or a redirect to the login page. `auth:token` names the guard; with several, the first that has a user wins |
|
|
234
|
+
| `can` | `Authorize` | Checks an ability with the gate, as `can:update,post`; a parameter that names a route parameter passes the bound model. A denial is a 403 |
|
|
235
|
+
| `guest` | `RedirectIfAuthenticated` | Lets guests through, and redirects signed-in users |
|
|
236
|
+
| `inertia.encrypt` | `EncryptHistory` | Encrypts the browser history state of the Inertia pages behind it |
|
|
237
|
+
| `precognitive` | `HandlePrecognitiveRequests` | Answers a precognitive request once its data is validated (see [precognitive requests](#precognitive-requests)) |
|
|
238
|
+
| `signed` | `ValidateSignature` | Wants a valid, unexpired signed URL, else a 403; `signed:relative` checks a URL signed without its host |
|
|
239
|
+
| `throttle` | `ThrottleRequests` | Limits a client's requests in a window of minutes, as `throttle:60,1` for 60 a minute (see [rate limiting](routing.md#rate-limiting)) |
|
|
240
|
+
| `verified` | `EnsureEmailIsVerified` | Wants a verified email address: JSON gets a 403, a page a redirect to the `verification.notice` route, or the route you name, as `verified:profile.edit` |
|
|
241
|
+
|
|
242
|
+
#### Redirecting guests and signed-in users
|
|
243
|
+
|
|
244
|
+
`redirectGuestsTo()` sets where `auth` sends a guest, and `redirectUsersTo()` where `guest` sends a signed-in user.
|
|
245
|
+
Each takes a URL, or a function that receives the request:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
|
|
249
|
+
middleware.redirectGuestsTo('/login')
|
|
250
|
+
middleware.redirectUsersTo((request) => (request.is('admin/*') ? '/admin' : '/dashboard'))
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
They set `http.redirect_guests_to` and `http.redirect_users_to`, which you can also set in `config/http.ts`; a new
|
|
255
|
+
app's sets `redirect_guests_to` to `/login`. Without a value, a guest goes to the `login` route, else `/login`, and a
|
|
256
|
+
signed-in user to the `dashboard` route, else the `home` route, else `/`.
|
|
257
|
+
|
|
258
|
+
### Sorting middleware
|
|
259
|
+
|
|
260
|
+
A request runs through the global middleware first, in their order, then the route's: its groups' middleware, outer
|
|
261
|
+
group first, then its own. Among a route's middleware, the same middleware with the same parameters runs once, where
|
|
262
|
+
it first appears.
|
|
263
|
+
|
|
264
|
+
Within a route's middleware, the `web` and `api` groups' middleware and `auth` and `guest` run first, keeping their
|
|
265
|
+
order. Then route model binding runs, then everything else. So `can:update,post` sees the bound `Post`, and a guest
|
|
266
|
+
is redirected before any database lookup. `throttle` is among everything else, so it runs after binding: a throttled
|
|
267
|
+
route with a model parameter looks the model up before it counts the request, and a missing model is a 404 that isn't
|
|
268
|
+
counted.
|
|
269
|
+
|
|
270
|
+
> [!NOTE]
|
|
271
|
+
> Laravel sorts route middleware by a priority list, which `priority()` changes. Oriel keeps only the part of it that
|
|
272
|
+
> route model binding needs, the session and the signed-in user before the lookup, and has no `priority()`: when two
|
|
273
|
+
> of your middleware depend on each other, list them in that order.
|
|
274
|
+
|
|
275
|
+
## Middleware parameters
|
|
276
|
+
|
|
277
|
+
A middleware can take parameters. They arrive as strings after `next`. This one wants the user to have one of the
|
|
278
|
+
roles it is given:
|
|
279
|
+
|
|
280
|
+
```ts title="app/Http/Middleware/EnsureUserHasRole.ts"
|
|
281
|
+
declare module '../../Models/User' {
|
|
282
|
+
interface User {
|
|
283
|
+
role: string
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ---cut---
|
|
287
|
+
import { abort } from '@orieljs/framework'
|
|
288
|
+
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
|
|
289
|
+
import type { User } from '../../Models/User'
|
|
290
|
+
|
|
291
|
+
export class EnsureUserHasRole implements Middleware {
|
|
292
|
+
async handle(request: HttpRequest, next: Next, ...roles: string[]): Promise<Response> {
|
|
293
|
+
const user = await request.user<User>()
|
|
294
|
+
|
|
295
|
+
if (user === undefined || !roles.includes(user.role)) {
|
|
296
|
+
abort(403)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return next(request)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Give it an alias, then name the parameters after a `:`, separated by commas:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
declare const EnsureUserHasRole: MiddlewareClass
|
|
308
|
+
// ---cut---
|
|
309
|
+
Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
|
|
310
|
+
middleware.alias({ role: EnsureUserHasRole })
|
|
311
|
+
})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
Route.put('/posts/{id}', (id: string) => id).middleware('role:editor')
|
|
316
|
+
|
|
317
|
+
Route.put('/posts/{id}', (id: string) => id).middleware('role:editor,publisher')
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
> [!NOTE]
|
|
321
|
+
> Laravel can write the class and its parameters together, as `EnsureUserHasRole::class.':editor'`. A class in
|
|
322
|
+
> TypeScript is a value, not a string, so Oriel takes parameters only after an alias, as `'role:editor'`. A group's
|
|
323
|
+
> name takes none.
|
|
324
|
+
|
|
325
|
+
## Terminable middleware
|
|
326
|
+
|
|
327
|
+
A middleware can do some work after the response has been sent to the browser. Give it a `terminate()` method, which
|
|
328
|
+
receives the request and the response:
|
|
329
|
+
|
|
330
|
+
```ts title="app/Http/Middleware/TerminatingMiddleware.ts"
|
|
331
|
+
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
|
|
332
|
+
|
|
333
|
+
export class TerminatingMiddleware implements Middleware {
|
|
334
|
+
async handle(request: HttpRequest, next: Next): Promise<Response> {
|
|
335
|
+
return next(request)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async terminate(request: HttpRequest, response: Response): Promise<void> {
|
|
339
|
+
// ...
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Register it as a global or a route middleware, as any other. On Node, `terminate()` runs once the response is
|
|
345
|
+
written; on Bun, right after it is handed to the server. An error in `terminate()` is reported, and doesn't reach the
|
|
346
|
+
user.
|
|
347
|
+
|
|
348
|
+
> [!NOTE]
|
|
349
|
+
> Laravel calls `terminate()` on a fresh instance unless you register the middleware as a singleton. Oriel calls it
|
|
350
|
+
> on the instance that handled the request, so `handle()` can leave it what it needs in a field.
|
|
351
|
+
|
|
352
|
+
## Precognitive requests
|
|
353
|
+
|
|
354
|
+
The `precognitive` middleware lets a form check its data on the server before the user submits it, as Laravel's
|
|
355
|
+
Precognition does. A request with the `Precognition: true` header runs the route's action only up to its validation. The
|
|
356
|
+
React, Vue and Svelte [Inertia forms](inertia.md#precognition) send such requests when they validate; the Solid and
|
|
357
|
+
Angular forms don't offer it.
|
|
358
|
+
|
|
359
|
+
- When the data passes, the request ends there: a 204 with a `Precognition-Success: true` header.
|
|
360
|
+
- When it fails, the usual 422 with the errors.
|
|
361
|
+
|
|
362
|
+
A `Precognition-Validate-Only: name,email` header limits the check to those fields. Every response of the route varies
|
|
363
|
+
on `Precognition`, and a precognitive one carries a `Precognition: true` header.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
Route.post('/users', async () => {
|
|
367
|
+
const data = await request().validate({
|
|
368
|
+
name: 'required|string|max:255',
|
|
369
|
+
email: 'required|email|unique:users,email',
|
|
370
|
+
password: 'required|min:8',
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const user = await User.create(data)
|
|
374
|
+
|
|
375
|
+
return redirect().route('users.show', user)
|
|
376
|
+
}).middleware('precognitive')
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
> [!WARNING]
|
|
380
|
+
> Validate first. Oriel validates inside the action, where Laravel resolves the form request before calling it, so
|
|
381
|
+
> what the action does before its validation runs on every precognitive request. A precognitive `POST`, `PUT`, `PATCH`
|
|
382
|
+
> or `DELETE` that reaches the end of its action without validating is a 500 that says so, but a redirect or an error
|
|
383
|
+
> returned before the validation goes back to the form as it is: an action that creates something and then redirects,
|
|
384
|
+
> without validating, creates it on every check.
|