@orieljs/framework 0.1.0-alpha.5 → 0.1.0-alpha.7

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.
Files changed (87) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/chunk-7ZQZYDOC.js +3253 -0
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +535 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +2202 -5952
  16. package/dist/index.js +4900 -944
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +19 -5
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-A5XRLEWM.js.map +0 -1
  83. package/dist/chunk-BSQKOLSK.js.map +0 -1
  84. package/dist/chunk-PM5YTG57.js +0 -880
  85. package/dist/chunk-PM5YTG57.js.map +0 -1
  86. package/dist/chunk-WNJP46B7.js.map +0 -1
  87. package/dist/index.d-DOlTeG-8.d.ts +0 -99
@@ -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.