@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,409 @@
1
+ # HTTP session
2
+
3
+ HTTP is stateless, so the session keeps what your app knows about a user from one request to the next: who is signed
4
+ in, a cart, a status message to show once.
5
+
6
+ ## Introduction
7
+
8
+ Oriel stores sessions in your database by default, and can keep them in files, in a cookie or in Redis instead.
9
+
10
+ The `StartSession` middleware, in the `web` group, loads the session before your route runs and saves it once the
11
+ response is ready, so every route in `routes/web.ts` has one. The `api` group has no `StartSession`: a route in
12
+ `routes/api.ts` gets an empty session that is never saved.
13
+
14
+ ### Configuration
15
+
16
+ The session's settings are in `config/session.ts`. A new app uses the `database` driver:
17
+
18
+ ```ts title="config/session.ts"
19
+ import { dirname, join } from 'node:path'
20
+ import { env, Str } from '@orieljs/framework'
21
+
22
+ const basePath = dirname(import.meta.dirname)
23
+
24
+ export default {
25
+ driver: env('SESSION_DRIVER', 'database'),
26
+ lifetime: Number(env('SESSION_LIFETIME', 120)),
27
+ expire_on_close: false,
28
+ encrypt: false,
29
+ files: join(basePath, 'storage/framework/sessions'),
30
+ connection: env('SESSION_CONNECTION'),
31
+ table: 'sessions',
32
+ lottery: [2, 100],
33
+ cookie: env('SESSION_COOKIE', Str.slug(String(env('APP_NAME', 'oriel')), '_') + '_session'),
34
+ path: '/',
35
+ domain: env('SESSION_DOMAIN', null),
36
+ secure: env('SESSION_SECURE_COOKIE', null),
37
+ http_only: true,
38
+ same_site: 'lax',
39
+ }
40
+ ```
41
+
42
+ `driver` says where the sessions are stored:
43
+
44
+ - `database`: in the `sessions` table of your database.
45
+ - `file`: one file per session, in `storage/framework/sessions` (the `files` setting).
46
+ - `cookie`: the whole session in an encrypted cookie.
47
+ - `redis`: in Redis.
48
+ - `array`: in memory, in the app's process. Your tests use it.
49
+
50
+ `lifetime` is the number of minutes a session lasts without a request, 120 by default. With `expire_on_close`, the
51
+ cookie lasts until the browser closes. `encrypt` encrypts the session's data with your `APP_KEY` before it is stored.
52
+
53
+ `cookie` names the session cookie, after your app's name: `blog_session` for an app called Blog. `path`, `domain`,
54
+ `http_only` and `same_site` are the cookie's attributes. `secure` sends the cookie over HTTPS only; when it is `null`,
55
+ as in a new app, the cookie is secure when the request came over HTTPS.
56
+
57
+ `lottery` cleans up expired sessions: on average, 2 requests in 100 delete the sessions older than `lifetime` from
58
+ the database, the files or the memory. Redis and cookie sessions expire on their own.
59
+
60
+ > [!NOTE]
61
+ > Laravel's `array` driver keeps nothing between requests. Oriel's process outlives each request, so its `array`
62
+ > driver keeps every session in memory for as long as the process runs, and loses them all when it restarts: use it
63
+ > for tests, not in production.
64
+
65
+ ### Driver prerequisites
66
+
67
+ #### Database
68
+
69
+ The `database` driver needs the `sessions` table, which a new app's first migration creates, with the users table:
70
+
71
+ ```ts
72
+ await Schema.create('sessions', (t) => {
73
+ t.string('id').primary()
74
+ t.foreignId('user_id').nullable().index()
75
+ t.string('ip_address', 45).nullable()
76
+ t.text('user_agent').nullable()
77
+ t.longText('payload')
78
+ t.integer('last_activity').index()
79
+ })
80
+ ```
81
+
82
+ Each row also records the signed-in user's id, the IP address and the user agent of the session's last request.
83
+ `connection` names the database connection to use, the default one when it is `null`, and `table` the table.
84
+
85
+ #### Redis
86
+
87
+ The `redis` driver talks to the Redis server of `config/database.ts`: `connection` names an entry of
88
+ `database.redis` (`default` unless you set `SESSION_CONNECTION`). Its keys start with `oriel_session:` and expire after
89
+ `lifetime`. Oriel's Redis client, `ioredis`, comes with the framework, and the connection is closed when your app shuts
90
+ down.
91
+
92
+ ## Interacting with the session
93
+
94
+ ### Retrieving data
95
+
96
+ You reach the session through the request, or with the `session()` helper. Through the request:
97
+
98
+ ```ts title="app/Http/Controllers/UserController.ts"
99
+ import { request, view } from '@orieljs/framework'
100
+ import { User } from '../../Models/User'
101
+ import { Controller } from './Controller'
102
+
103
+ export class UserController extends Controller {
104
+ async show(id: string) {
105
+ const value = request().session().get('key')
106
+
107
+ // ...
108
+
109
+ return view('user.profile', { user: await User.findOrFail(id) })
110
+ }
111
+ }
112
+ ```
113
+
114
+ `get()` takes a default, returned when the key isn't in the session, and a type for the value, which is a cast, not
115
+ a check:
116
+
117
+ ```ts
118
+ const value = request().session().get('key', 'default')
119
+
120
+ const theme = request().session().get<string>('theme')
121
+ ```
122
+
123
+ > [!NOTE]
124
+ > Laravel calls a closure you give `get()` as its default. Oriel returns the default as it is, a function too, so
125
+ > compute the default before you call `get()`.
126
+
127
+ Keys take dots: `get('user.name')` reads the `name` of the object stored under `user`.
128
+
129
+ #### The global session helper
130
+
131
+ `session()` with a key returns its value, with a default if you give one. With an object, it stores the object's
132
+ values. With nothing, it returns the session itself:
133
+
134
+ ```ts
135
+ Route.get('/home', () => {
136
+ // Retrieve a piece of data from the session...
137
+ const value = session('key')
138
+
139
+ // Specifying a default value...
140
+ const withDefault = session('key', 'default')
141
+
142
+ // Store a piece of data in the session...
143
+ session({ key: 'value' })
144
+
145
+ return { value, withDefault, all: session().all() }
146
+ })
147
+ ```
148
+
149
+ The `Session` [facade](facades.md) does the same: `Session.get('key')`, `Session.put('key', 'value')`. The three reach
150
+ the same session, the current request's, and your tests check them all with `assertSessionHas()`.
151
+
152
+ #### Retrieving all session data
153
+
154
+ ```ts
155
+ const data = request().session().all()
156
+ ```
157
+
158
+ #### Retrieving a portion of the session data
159
+
160
+ `only()` and `except()` take a list of keys:
161
+
162
+ ```ts
163
+ const data = request().session().only(['username', 'email'])
164
+
165
+ const others = request().session().except(['username', 'email'])
166
+ ```
167
+
168
+ #### Determining if an item exists in the session
169
+
170
+ `has()` tells you whether a key is present and not `null`:
171
+
172
+ ```ts
173
+ if (request().session().has('users')) {
174
+ // ...
175
+ }
176
+ ```
177
+
178
+ `exists()` tells you whether a key is present, even with a `null` value, and `missing()` whether it isn't. Both take
179
+ several keys:
180
+
181
+ ```ts
182
+ if (request().session().exists('users')) {
183
+ // ...
184
+ }
185
+
186
+ if (request().session().missing('users')) {
187
+ // ...
188
+ }
189
+ ```
190
+
191
+ ### Storing data
192
+
193
+ Store a value with `put()`, or with the `session()` helper. `put()` also takes an object of values:
194
+
195
+ ```ts
196
+ // Via the request...
197
+ request().session().put('key', 'value')
198
+
199
+ // Via the global "session" helper...
200
+ session({ key: 'value' })
201
+ ```
202
+
203
+ The session is stored as JSON: a `Date` comes back as its ISO string on the next request, and a class instance, a
204
+ model for one, as a plain object. Store ids and plain values, and look models up again.
205
+
206
+ #### Pushing to array session values
207
+
208
+ `push()` adds a value to the list stored under a key, and starts the list when there is none:
209
+
210
+ ```ts
211
+ request().session().push('user.teams', 'developers')
212
+ ```
213
+
214
+ #### Retrieving and deleting an item
215
+
216
+ `pull()` returns a value and removes it from the session:
217
+
218
+ ```ts
219
+ const value = request().session().pull('key', 'default')
220
+ ```
221
+
222
+ #### Incrementing and decrementing session values
223
+
224
+ `increment()` and `decrement()` change the number stored under a key, by 1 or by the amount you give, and return the
225
+ new value. A missing key counts as 0:
226
+
227
+ ```ts
228
+ request().session().increment('count')
229
+
230
+ request().session().increment('count', 2)
231
+
232
+ request().session().decrement('count')
233
+
234
+ request().session().decrement('count', 2)
235
+ ```
236
+
237
+ ### Flash data
238
+
239
+ `flash()` stores a value for this request and the next one only, which suits a status message shown once after a
240
+ redirect:
241
+
242
+ ```ts
243
+ request().session().flash('status', 'Your post was published.')
244
+ ```
245
+
246
+ `reflash()` keeps all of the flash data for one more request, and `keep()` keeps only the keys you name:
247
+
248
+ ```ts
249
+ request().session().reflash()
250
+
251
+ request().session().keep('username', 'email')
252
+ ```
253
+
254
+ `now()` stores a value for the current request only:
255
+
256
+ ```ts
257
+ request().session().now('status', 'Your post was published.')
258
+ ```
259
+
260
+ A redirect flashes data too: `redirect('/dashboard').with('status', 'Profile updated.')`. The input a failed
261
+ validation sends back, which `old()` reads, and its errors, which `errors()` reads, are flash data as well (see
262
+ [Requests](requests.md#old-input) and [Validation](validation.md)).
263
+
264
+ ### Deleting data
265
+
266
+ `forget()` removes keys from the session, and `flush()` removes everything:
267
+
268
+ ```ts
269
+ // Forget a single key...
270
+ request().session().forget('name')
271
+
272
+ // Forget multiple keys...
273
+ request().session().forget('name', 'status')
274
+
275
+ request().session().flush()
276
+ ```
277
+
278
+ `remove()` removes a key, as `forget()` does, and returns its value, as `pull()` does:
279
+
280
+ ```ts
281
+ const name = request().session().remove<string>('name')
282
+ ```
283
+
284
+ `forget()` and `keep()` take the keys as separate arguments, where Laravel's take a list too.
285
+
286
+ ### Regenerating the session ID
287
+
288
+ Regenerating the session's ID stops a session fixation attack, in which an attacker gets a user to sign in with a
289
+ session ID the attacker already knows. The starter kits regenerate it when a user signs in. To do it yourself, call
290
+ `regenerate()`, which also gives the session a new [CSRF token](csrf.md):
291
+
292
+ ```ts
293
+ await request().session().regenerate()
294
+ ```
295
+
296
+ `regenerate(true)` also deletes the old session from the store. `invalidate()` removes all the data from the session
297
+ and gives it a new ID, deleting the old one. As it removes the CSRF token with the rest, call `regenerateToken()`
298
+ after it, as the starter kits do when a user signs out:
299
+
300
+ ```ts
301
+ await request().session().invalidate()
302
+
303
+ request().session().regenerateToken()
304
+ ```
305
+
306
+ ### The CSRF token
307
+
308
+ The session holds the CSRF token that protects your forms. `token()` returns it, as `csrfToken()` does, and
309
+ `regenerateToken()` replaces it:
310
+
311
+ ```ts
312
+ const token = request().session().token()
313
+ ```
314
+
315
+ [CSRF protection](csrf.md) explains how the token is sent and checked.
316
+
317
+ ## Adding custom session drivers
318
+
319
+ ### Implementing the driver
320
+
321
+ A custom driver is a class that implements the `SessionHandler` contract. A stubbed MongoDB driver looks like this:
322
+
323
+ ```ts title="app/Extensions/MongoSessionHandler.ts"
324
+ import type { SessionHandler } from '@orieljs/framework/contracts'
325
+
326
+ export class MongoSessionHandler implements SessionHandler {
327
+ async read(id: string): Promise<string> {
328
+ return ''
329
+ }
330
+
331
+ async write(id: string, data: string): Promise<void> {}
332
+
333
+ async destroy(id: string): Promise<void> {}
334
+
335
+ async gc(maxLifetimeSeconds: number): Promise<void> {}
336
+ }
337
+ ```
338
+
339
+ Oriel has no folder for such classes: put them where you like, as `app/Extensions` here. What each method does:
340
+
341
+ - `read()` returns the session's data, as the string `write()` stored, or `''` when there is none or it has expired.
342
+ Oriel turns the data into JSON and back itself.
343
+ - `write()` stores the data string under the session's ID.
344
+ - `destroy()` deletes a session.
345
+ - `gc()` deletes the sessions not written for more than `maxLifetimeSeconds` seconds. A store that expires its
346
+ entries on its own can leave it empty.
347
+
348
+ ### Registering the driver
349
+
350
+ Register the driver with `Session.extend()`, in a [service provider's](providers.md#the-boot-method) `boot()`. The
351
+ callback receives the request's container and the session's configuration, and returns a handler; it runs for each
352
+ request's session:
353
+
354
+ ```ts title="app/Providers/AppServiceProvider.ts"
355
+ // @filename: app/Extensions/MongoSessionHandler.ts
356
+ export declare const MongoSessionHandler: new () => import('@orieljs/framework/contracts').SessionHandler
357
+ // ---cut---
358
+ import { ServiceProvider, Session } from '@orieljs/framework'
359
+ import { MongoSessionHandler } from '../Extensions/MongoSessionHandler'
360
+
361
+ export class AppServiceProvider extends ServiceProvider {
362
+ override boot(): void {
363
+ Session.extend('mongo', () => new MongoSessionHandler())
364
+ }
365
+ }
366
+ ```
367
+
368
+ Then set `SESSION_DRIVER=mongo` in your `.env`, or `driver` in `config/session.ts`.
369
+
370
+ ## Testing
371
+
372
+ Your tests run with the `array` driver, and the session carries over from one request of a test to the next.
373
+ `withSession()` puts data in it before a request ([HTTP tests](http-tests.md) covers the rest):
374
+
375
+ ```ts title="tests/Feature/Cart.test.ts"
376
+ // @filename: tests/TestCase.ts
377
+ import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
378
+ export class TestCase extends BaseTestCase {}
379
+ // ---cut---
380
+ import { test } from 'vitest'
381
+ import { TestCase } from '../TestCase'
382
+
383
+ const t = TestCase.boot()
384
+
385
+ test('the cart remembers its items', async () => {
386
+ const response = await t.withSession({ cart: ['apple'] }).post('/cart', { item: 'pear' })
387
+
388
+ response.assertSessionHas('cart', ['apple', 'pear']).assertSessionMissing('coupon')
389
+ })
390
+ ```
391
+
392
+ `flushSession()` empties the session: the next request starts with none, so a user who signed in on an earlier request
393
+ is signed out. A user you gave `actingAs()` stays signed in. `withSession()` called after a request sets its keys over
394
+ the ones the stored session holds.
395
+
396
+ A response's session assertions check the session as the request left it, flash data included:
397
+
398
+ | Assertion | Passes when |
399
+ | --- | --- |
400
+ | `assertSessionHas(key)` | The key is in the session and not `null` |
401
+ | `assertSessionHas(key, value)` | The key holds that value; `value` may also be a function that returns `true` |
402
+ | `assertSessionHas([keys])` | Every key is in the session and not `null` |
403
+ | `assertSessionHasAll({ key: value })` | Every key holds its value |
404
+ | `assertSessionMissing(key)` | The key isn't in the session, or holds `null` |
405
+ | `assertSessionHasInput(key)` | The key is in the old input |
406
+ | `assertSessionHasErrors(keys)` | The session has validation errors for those keys |
407
+ | `assertSessionHasNoErrors()` | The session has no validation errors |
408
+
409
+ `response.session()` returns the session itself, for anything else.