@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.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  5. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  6. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  7. package/dist/chunk-GVNLZZTN.js.map +1 -0
  8. package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
  9. package/dist/chunk-SPQUX3C7.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  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-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,444 @@
1
+ # HTTP responses
2
+
3
+ Whatever a route or a controller returns becomes the response: text, JSON, a view, a redirect, a download or a
4
+ `Response` you build yourself.
5
+
6
+ ## Creating responses
7
+
8
+ #### Strings and arrays
9
+
10
+ A route returns its response. A string is sent as HTML, with a `Content-Type` of `text/html; charset=utf-8`:
11
+
12
+ ```ts
13
+ Route.get('/', () => 'Hello World')
14
+ ```
15
+
16
+ A number or a boolean is sent the same way, as its text. An array or a plain object is sent as JSON, with a
17
+ `Content-Type` of `application/json`:
18
+
19
+ ```ts
20
+ Route.get('/', () => [1, 2, 3])
21
+ ```
22
+
23
+ `null` and `undefined` send an empty 204 response, so an action that returns nothing answers "No Content".
24
+
25
+ #### Response objects
26
+
27
+ `response()` with content builds a `Response` with the status and the headers you give it. The status is 200
28
+ unless you give one, and text is HTML unless you name another `Content-Type`:
29
+
30
+ ```ts
31
+ Route.get('/home', () => response('Hello World', 200, { 'Content-Type': 'text/plain' }))
32
+ ```
33
+
34
+ > [!NOTE]
35
+ > Laravel's `Response` extends Symfony's response class, with chainable methods such as `header()` and `cookie()`.
36
+ > Oriel uses the web-standard `Response` built into Node and Bun, and its server sends it as it is, so a route can
37
+ > also return one from a library or from `fetch()`. Its headers are set when you build it, or through its `headers`
38
+ > object, as below.
39
+
40
+ #### Eloquent models and collections
41
+
42
+ A model, a collection of models or a paginator is sent as JSON. A model leaves out its hidden attributes, such as
43
+ the `password` and `remember_token` of the `User` model:
44
+
45
+ ```ts
46
+ Route.get('/user/{user}', (user: User) => user)
47
+ ```
48
+
49
+ #### Views
50
+
51
+ A view returned from a route is rendered and sent as HTML:
52
+
53
+ ```ts
54
+ Route.get('/', () => view('welcome', { name: 'Taylor' }))
55
+ ```
56
+
57
+ [Views](views.md) covers writing them.
58
+
59
+ > [!WARNING]
60
+ > Return a view, not a TSX element. A route that returns `<Welcome />` sends the element's description as JSON. To
61
+ > send TSX that isn't a view file, render it first: `response(await View.renderToString(<Welcome />))`.
62
+
63
+ ### Attaching headers to responses
64
+
65
+ Give the headers as `response()`'s third argument, as an object or as a list of pairs:
66
+
67
+ ```ts
68
+ declare const content: string
69
+ declare const type: string
70
+ // ---cut---
71
+ const res = response(content, 200, {
72
+ 'Content-Type': type,
73
+ 'X-Header-One': 'Header Value',
74
+ 'X-Header-Two': 'Header Value',
75
+ })
76
+ ```
77
+
78
+ A `Response`'s `headers` object adds, replaces and removes headers after it is built:
79
+
80
+ ```ts
81
+ declare const content: string
82
+ // ---cut---
83
+ const res = response(content)
84
+
85
+ res.headers.set('X-Header-One', 'Header Value')
86
+ res.headers.append('Vary', 'Accept')
87
+ res.headers.delete('X-Debug')
88
+ ```
89
+
90
+ On Node, the headers of a response from `fetch()` or `Response.redirect()` can't be changed; Bun allows it. A
91
+ middleware, which may receive such a response, changes the headers with `withHeaders()`, which returns a copy whose
92
+ headers can be changed on both:
93
+
94
+ ```ts title="app/Http/Middleware/AddFrameOptions.ts"
95
+ import { withHeaders } from '@orieljs/framework'
96
+ import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
97
+
98
+ export class AddFrameOptions implements Middleware {
99
+ async handle(request: HttpRequest, next: Next): Promise<Response> {
100
+ const response = await next(request)
101
+
102
+ return withHeaders(response, (headers) => {
103
+ headers.set('X-Frame-Options', 'DENY')
104
+ headers.delete('X-Powered-By')
105
+ })
106
+ }
107
+ }
108
+ ```
109
+
110
+ Oriel has no `cache.headers` middleware: set `Cache-Control` as any other header.
111
+
112
+ ### Attaching cookies to responses
113
+
114
+ Queue a cookie with the `Cookie` facade, and it is added to the response when the response is ready. `cookie()`
115
+ makes the cookie, from its name, its value and the number of minutes it lasts:
116
+
117
+ ```ts
118
+ Route.get('/', () => {
119
+ Cookie.queue(cookie('name', 'value', 60))
120
+
121
+ return 'Hello World'
122
+ })
123
+ ```
124
+
125
+ Queued cookies are added by the `AddQueuedCookiesToResponse` middleware, in the `web` group. A route in
126
+ `routes/api.ts` doesn't send them.
127
+
128
+ A redirect takes a cookie of its own with `withCookie()`, in any group:
129
+
130
+ ```ts
131
+ const res = redirect('/dashboard').withCookie('name', 'value', 60)
132
+ ```
133
+
134
+ > [!NOTE]
135
+ > Laravel attaches a cookie to a response with its `cookie()` method, and `Cookie::queue()` takes the cookie's name,
136
+ > value and minutes. A web-standard `Response` has no such method, so Oriel queues a cookie that `cookie()` made:
137
+ > `Cookie.queue(cookie('name', 'value', 60))`.
138
+
139
+ #### Generating cookie instances
140
+
141
+ `cookie()` returns a plain cookie, which isn't sent until you queue it. Its fourth argument changes the cookie's
142
+ `path`, `domain`, `secure`, `httpOnly` and `sameSite`:
143
+
144
+ ```ts
145
+ const theme = cookie('theme', 'dark', 60 * 24 * 30, { httpOnly: false, sameSite: 'strict' })
146
+
147
+ Cookie.queue(theme)
148
+ ```
149
+
150
+ A cookie is `HttpOnly` unless you say otherwise, and takes its path, domain, `secure` and `sameSite` from
151
+ `config/session.ts`. With 0 minutes, the default, it lasts until the browser closes. `Cookie.forever('name', 'value')`
152
+ makes one that lasts 400 days, and `Cookie.make()` is the same as `cookie()`.
153
+
154
+ To write a cookie onto a `Response` yourself, give its `Set-Cookie` header the text `serializeCookie()` makes:
155
+
156
+ ```ts
157
+ const res = response('Hello World', 200, { 'Set-Cookie': serializeCookie(cookie('name', 'value', 60)) })
158
+ ```
159
+
160
+ #### Expiring cookies early
161
+
162
+ `Cookie.forget()` makes a cookie that has already expired, so the browser deletes it. Queue it:
163
+
164
+ ```ts
165
+ Cookie.queue(Cookie.forget('name'))
166
+ ```
167
+
168
+ Laravel's `withoutCookie()` and `Cookie::expire()` aren't in Oriel.
169
+
170
+ ### Cookies and encryption
171
+
172
+ The `EncryptCookies` middleware, in the `web` group, encrypts every cookie the response sets, however it was set:
173
+ queued, attached to a redirect or written as a `Set-Cookie` header. The browser can't read or change them, and
174
+ `request().cookie()` decrypts them on the next request (see
175
+ [Requests](requests.md#retrieving-cookies-from-requests)). A cookie that deletes one, as `Cookie.forget()` makes, is
176
+ sent as it is.
177
+
178
+ To send some cookies unencrypted, such as a cookie your scripts read, list their names with
179
+ `EncryptCookies.except()` in `bootstrap/app.ts`, as the starter kits do for the appearance cookie:
180
+
181
+ ```ts title="bootstrap/app.ts"
182
+ import { dirname } from 'node:path'
183
+ import { Application, EncryptCookies } from '@orieljs/framework'
184
+ import providers from './providers'
185
+
186
+ export function createApplication(): Promise<Application> {
187
+ return Application.configure({ basePath: dirname(import.meta.dirname) })
188
+ .withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })
189
+ .withMiddleware(() => {
190
+ EncryptCookies.except(['cookie_name'])
191
+ })
192
+ .withProviders(providers)
193
+ .create()
194
+ }
195
+ ```
196
+
197
+ Such a cookie is also read as the browser sent it. Laravel's `$middleware->encryptCookies(except: [...])` is
198
+ `EncryptCookies.except([...])` in Oriel.
199
+
200
+ The `api` group has no `EncryptCookies`: an API route's cookies are sent and read as they are.
201
+
202
+ ## Redirects
203
+
204
+ `redirect()` with a path returns a redirect response, a 302 to that path on the host the request came to. A second
205
+ argument changes the status:
206
+
207
+ ```ts
208
+ Route.get('/dashboard', () => redirect('/home/dashboard'))
209
+
210
+ Route.get('/old', () => redirect('/new', 301))
211
+ ```
212
+
213
+ `back()` redirects to the previous page, such as when a submitted form is invalid. It reads the request's `Referer`
214
+ header, then the last page the session recorded, and falls back to the path you give it, or `/`:
215
+
216
+ ```ts
217
+ Route.post('/user/profile', () => {
218
+ // Validate the request...
219
+
220
+ return back().withInput()
221
+ })
222
+ ```
223
+
224
+ The session records each `GET` page a user visits through the `web` group, leaving out AJAX requests and prefetches.
225
+
226
+ ### Redirecting to named routes
227
+
228
+ `redirect()` with no arguments returns the redirector, whose `route()` redirects to a named route:
229
+
230
+ ```ts
231
+ const res = redirect().route('login')
232
+ ```
233
+
234
+ A route's parameters are the second argument, and the status the third:
235
+
236
+ ```ts
237
+ // For a route with the following URI: /profile/{id}
238
+
239
+ const res = redirect().route('profile', { id: 1 })
240
+ ```
241
+
242
+ Once `route:types` has run, TypeScript checks the route's name and its parameters, as it does for `route()`: see
243
+ [typed route parameters](routing.md#typed-route-parameters). Laravel's `redirect()->action()`, which redirects to a
244
+ controller action, isn't in Oriel: name the route and redirect to its name.
245
+
246
+ #### Populating parameters via Eloquent models
247
+
248
+ A model fills a parameter with its route key, its `id` unless you change it:
249
+
250
+ ```ts
251
+ declare const user: User
252
+ // ---cut---
253
+ // For a route with the following URI: /profile/{id}
254
+
255
+ const res = redirect().route('profile', user)
256
+ ```
257
+
258
+ A `{id:slug}` parameter, or `getRouteKeyName()` on the model, fills it from another column: see
259
+ [customizing the key](routing.md#customizing-the-key).
260
+
261
+ ### Redirecting to external domains
262
+
263
+ `redirect()` sends a URL that starts with `http://`, `https://`, `//`, `#`, `mailto:`, `tel:` or `sms:` as it is,
264
+ and treats anything else as a path of your app. `away()` sends any target as it is:
265
+
266
+ ```ts
267
+ const res = redirect().away('https://www.google.com')
268
+ ```
269
+
270
+ ### Redirecting with flashed session data
271
+
272
+ A redirect usually comes with a message for the next page, [flashed to the session](session.md#flash-data). `with()`
273
+ flashes a value, or an object of values, as it redirects:
274
+
275
+ ```ts
276
+ Route.post('/user/profile', () => {
277
+ // ...
278
+
279
+ return redirect('/dashboard').with('status', 'Profile updated!')
280
+ })
281
+ ```
282
+
283
+ The next page reads it from the session. In a TSX view:
284
+
285
+ ```tsx title="resources/views/dashboard.tsx"
286
+ import { session } from '@orieljs/framework'
287
+
288
+ export default function Dashboard() {
289
+ const status = session<string | undefined>('status')
290
+
291
+ return <main>{status && <div class="alert alert-success">{status}</div>}</main>
292
+ }
293
+ ```
294
+
295
+ #### Redirecting with input
296
+
297
+ `withInput()` flashes the request's input, so the form can show it again after a failed validation. `old()` reads
298
+ it on the next request (see [Requests](requests.md#old-input)):
299
+
300
+ ```ts
301
+ const res = back().withInput()
302
+ ```
303
+
304
+ It takes the input to flash, too: `withInput(request().except('password'))`.
305
+
306
+ #### Redirecting with errors
307
+
308
+ `withErrors()` flashes error messages, by field, for `errors()` to read on the next page. A field takes one message
309
+ or a list:
310
+
311
+ ```ts
312
+ const res = back().withErrors({ email: 'These credentials do not match our records.' }).withInput()
313
+ ```
314
+
315
+ A second argument names an error bag, so two forms on one page keep their errors apart:
316
+ `withErrors({ email: '…' }, 'login')`, read with `errors('login')`. A failed [validation](validation.md) does all of
317
+ this for you.
318
+
319
+ A redirect takes headers too: `redirect('/home').header('X-Reason', 'moved')`. `redirect().refresh()` redirects to the
320
+ current URL. `redirect().guest(path)` remembers the page a guest was going to before it redirects, and
321
+ `redirect().intended(fallback, status)` later sends the user there; `redirect().setIntendedUrl(url)` sets that page
322
+ yourself. The Inertia starter kits' sign-in uses `intended()`, and
323
+ [Authentication](authentication.md#redirecting-unauthenticated-users) covers the flow.
324
+
325
+ ## Other response types
326
+
327
+ `response()` with no arguments returns the response factory, whose methods build the other kinds of response.
328
+
329
+ ### View responses
330
+
331
+ To send a view with your own status or headers, use the factory's `view()`. It renders the view and returns a
332
+ promise of the response:
333
+
334
+ ```ts
335
+ declare const data: Record<string, unknown>
336
+ declare const type: string
337
+ // ---cut---
338
+ Route.get('/hello', () => response().view('hello', data, 200, { 'Content-Type': type }))
339
+ ```
340
+
341
+ Without a status or headers of your own, return the `view()` itself.
342
+
343
+ ### JSON responses
344
+
345
+ `json()` turns its data into JSON, with a `Content-Type` of `application/json`. A status and headers follow:
346
+
347
+ ```ts
348
+ const res = response().json({ name: 'Abigail', state: 'CA' })
349
+
350
+ const created = response().json({ name: 'Abigail' }, 201)
351
+ ```
352
+
353
+ The data goes through `JSON.stringify()`, so models and collections become JSON as they do when a route returns
354
+ them, and dates become ISO strings. Laravel's JSONP `withCallback()` isn't in Oriel.
355
+
356
+ ### Empty responses
357
+
358
+ `noContent()` sends an empty 204 response, or another status you give it:
359
+
360
+ ```ts
361
+ const res = response().noContent()
362
+ ```
363
+
364
+ ### File downloads
365
+
366
+ `download()` sends a file for the browser to save. A second argument is the name the user sees, and a third the
367
+ headers:
368
+
369
+ ```ts
370
+ declare const pathToFile: string
371
+ declare const name: string
372
+ declare const headers: Record<string, string>
373
+ // ---cut---
374
+ Route.get('/report', () => response().download(pathToFile))
375
+
376
+ Route.get('/report/named', () => response().download(pathToFile, name, headers))
377
+ ```
378
+
379
+ The file is streamed from disk, with its size, and a `Content-Type` from its name's extension unless your headers
380
+ give one. A name that isn't ASCII is sent too, so `Résumé.pdf` arrives as it is. A file that doesn't exist is a 404.
381
+ `download()` returns a promise: return it from the route, or await it.
382
+
383
+ ### File responses
384
+
385
+ `file()` sends a file for the browser to show, such as an image or a PDF, rather than to download. It streams the
386
+ file as `download()` does, with a `Content-Type` from its extension unless your headers give one:
387
+
388
+ ```ts
389
+ declare const pathToFile: string
390
+ declare const headers: Record<string, string>
391
+ // ---cut---
392
+ Route.get('/invoice', () => response().file(pathToFile))
393
+
394
+ Route.get('/invoice/raw', () => response().file(pathToFile, headers))
395
+ ```
396
+
397
+ ## Streamed responses
398
+
399
+ `stream()` sends a web `ReadableStream`, chunk by chunk, as your code produces them. The client can start reading
400
+ before the response ends:
401
+
402
+ ```ts
403
+ Route.get('/stream', () => {
404
+ const encoder = new TextEncoder()
405
+
406
+ const body = new ReadableStream<Uint8Array>({
407
+ async start(controller) {
408
+ for (const text of ['developer', 'admin']) {
409
+ controller.enqueue(encoder.encode(text))
410
+ await new Promise((resolve) => setTimeout(resolve, 2000)) // Simulate delay between chunks...
411
+ }
412
+ controller.close()
413
+ },
414
+ })
415
+
416
+ return response().stream(body, 200, { 'X-Accel-Buffering': 'no' })
417
+ })
418
+ ```
419
+
420
+ > [!NOTE]
421
+ > Laravel's `stream()` takes a closure that echoes its output and flushes PHP's buffers. Node and Bun write a web
422
+ > stream's chunks as they come, with no output buffer, so Oriel's `stream()` takes the stream itself, from your code
423
+ > or from a library that returns one.
424
+
425
+ ### Streamed downloads
426
+
427
+ A stream becomes a download with a `Content-Disposition` header. `contentDisposition()` writes it, with the name
428
+ encoded as `download()` encodes it:
429
+
430
+ ```ts
431
+ declare const body: ReadableStream<Uint8Array>
432
+ // ---cut---
433
+ const res = response().stream(body, 200, {
434
+ 'Content-Type': 'text/markdown',
435
+ 'Content-Disposition': contentDisposition('attachment', 'readme.md'),
436
+ })
437
+ ```
438
+
439
+ `contentDisposition('inline', 'invoice.pdf')` asks the browser to show the file instead. Laravel's `streamJson()`
440
+ and `eventStream()` aren't in Oriel.
441
+
442
+ ## Response macros
443
+
444
+ Oriel has no response macros. A response you reuse is a function of your own that returns a `Response`.