@orieljs/framework 0.1.0-alpha.6 → 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 (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.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,406 @@
1
+ # Controllers
2
+
3
+ A controller groups the actions that handle related requests in one class: a `UserController` can show, store and
4
+ update your users.
5
+
6
+ ## Writing controllers
7
+
8
+ ### Basic controllers
9
+
10
+ `make:controller` writes a new controller in `app/Http/Controllers`:
11
+
12
+ ```sh
13
+ node oriel make:controller UserController
14
+ ```
15
+
16
+ A controller has any number of methods, each of which can answer a route. This one shows a user's profile:
17
+
18
+ ```ts title="app/Http/Controllers/UserController.ts"
19
+ import { view } from '@orieljs/framework'
20
+ import { User } from '../../Models/User'
21
+ import { Controller } from './Controller'
22
+
23
+ export class UserController extends Controller {
24
+ async show(id: string) {
25
+ return view('user.profile', { user: await User.findOrFail(id) })
26
+ }
27
+ }
28
+ ```
29
+
30
+ Point a route at a method by naming the controller and the method together. TypeScript checks that the method
31
+ exists:
32
+
33
+ ```ts
34
+ Route.get('/user/{id}', [UserController, 'show'])
35
+ ```
36
+
37
+ When a request matches the route, a new `UserController` is made and its `show()` called with the route's
38
+ parameters, in the order they appear in the URI. What the method returns becomes the response, as a closure's does.
39
+
40
+ Every controller `make:controller` writes extends `Controller` from `app/Http/Controllers/Controller.ts`, which a new
41
+ app has:
42
+
43
+ ```ts title="app/Http/Controllers/Controller.ts"
44
+ import { Controller as BaseController } from '@orieljs/framework'
45
+
46
+ /** The base controller every generated controller extends: `await this.authorize('update', post)` comes from it. */
47
+ export abstract class Controller extends BaseController {}
48
+ ```
49
+
50
+ The framework's `Controller` gives your controllers `authorize()` (see [authorizing actions](#authorizing-actions)). A
51
+ controller doesn't have to extend it, but methods all your controllers share can go in your base class.
52
+
53
+ ### Single action controllers
54
+
55
+ A controller for one complex action can be a class of its own, with a single `__invoke()` method:
56
+
57
+ ```ts title="app/Http/Controllers/ProvisionServerController.ts"
58
+ import { Controller } from './Controller'
59
+
60
+ export class ProvisionServerController extends Controller {
61
+ async __invoke() {
62
+ // ...
63
+ }
64
+ }
65
+ ```
66
+
67
+ Its route names only the class:
68
+
69
+ ```ts
70
+ declare class ProvisionServerController {
71
+ __invoke(): Promise<void>
72
+ }
73
+ // ---cut---
74
+ Route.post('/server', ProvisionServerController)
75
+ ```
76
+
77
+ `--invokable` (or `-i`) writes one:
78
+
79
+ ```sh
80
+ node oriel make:controller ProvisionServer --invokable
81
+ ```
82
+
83
+ `make:controller` adds the `Controller` suffix to a name that lacks it, so this writes
84
+ `ProvisionServerController.ts` in `app/Http/Controllers`.
85
+
86
+ ## Controller middleware
87
+
88
+ Give a controller's routes their middleware in your route files, on the route or on a group:
89
+
90
+ ```ts
91
+ Route.get('/profile', [UserController, 'show']).middleware('auth')
92
+ ```
93
+
94
+ > [!NOTE]
95
+ > Laravel's controllers can also list their own middleware, with a static `middleware()` method. Oriel's can't: the
96
+ > router reads every route's middleware from the route files, so they stay in one place.
97
+
98
+ ## Resource controllers
99
+
100
+ A resource is something your users create, read, update and delete, such as a photo. `--resource` (or `-r`) writes a
101
+ controller with a method for each of those actions:
102
+
103
+ ```sh
104
+ node oriel make:controller PhotoController --resource
105
+ ```
106
+
107
+ ```ts title="app/Http/Controllers/PhotoController.ts"
108
+ import { Controller } from './Controller'
109
+
110
+ export class PhotoController extends Controller {
111
+ async index() {
112
+ //
113
+ }
114
+
115
+ async create() {
116
+ //
117
+ }
118
+
119
+ async store() {
120
+ //
121
+ }
122
+
123
+ async show(photo: unknown) {
124
+ //
125
+ }
126
+
127
+ async edit(photo: unknown) {
128
+ //
129
+ }
130
+
131
+ async update(photo: unknown) {
132
+ //
133
+ }
134
+
135
+ async destroy(photo: unknown) {
136
+ //
137
+ }
138
+ }
139
+ ```
140
+
141
+ `Route.resource()` then registers a route for each method:
142
+
143
+ ```ts
144
+ declare class PhotoController {}
145
+ // ---cut---
146
+ Route.resource('photos', PhotoController)
147
+ ```
148
+
149
+ `node oriel route:list` shows the routes it made.
150
+
151
+ #### Actions handled by resource controllers
152
+
153
+ | Verb | URI | Action | Route name |
154
+ | --- | --- | --- | --- |
155
+ | `GET` | `/photos` | `index` | `photos.index` |
156
+ | `GET` | `/photos/create` | `create` | `photos.create` |
157
+ | `POST` | `/photos` | `store` | `photos.store` |
158
+ | `GET` | `/photos/{photo}` | `show` | `photos.show` |
159
+ | `GET` | `/photos/{photo}/edit` | `edit` | `photos.edit` |
160
+ | `PUT`, `PATCH` | `/photos/{photo}` | `update` | `photos.update` |
161
+ | `DELETE` | `/photos/{photo}` | `destroy` | `photos.destroy` |
162
+
163
+ Each `GET` route answers `HEAD` too. The parameter is the resource's name in the singular, so once your app has a
164
+ `Photo` model, `{photo}` is [bound to it](routing.md#route-model-binding) and a missing photo is a 404.
165
+
166
+ #### Specifying the resource model
167
+
168
+ `--model` (or `-m`) types the methods with your model, and writes the resource controller even without `--resource`:
169
+
170
+ ```sh
171
+ node oriel make:controller PhotoController --model=Photo
172
+ ```
173
+
174
+ `show()`, `edit()`, `update()` and `destroy()` then take `photo: Photo`, and each method has a comment saying what it
175
+ is for. When `app/Models` has no `Photo` yet, `make:model Photo` runs first.
176
+
177
+ #### Generating form requests
178
+
179
+ `--requests` (or `-R`), with `--model`, also writes a form request for `store()` and one for `update()`, and
180
+ validates with them:
181
+
182
+ ```sh
183
+ node oriel make:controller PhotoController --model=Photo --requests
184
+ ```
185
+
186
+ This writes `app/Http/Requests/StorePhotoRequest.ts` and `app/Http/Requests/UpdatePhotoRequest.ts`, and `store()`
187
+ starts with `const data = await StorePhotoRequest.validate()`. [Validation](validation.md) covers form requests.
188
+
189
+ ### Partial resource routes
190
+
191
+ A resource route can register only some of its actions. List them in `only`, or the ones to leave out in `except`:
192
+
193
+ ```ts
194
+ declare class PhotoController {}
195
+ // ---cut---
196
+ Route.resource('photos', PhotoController, { only: ['index', 'show'] })
197
+
198
+ Route.resource('photos', PhotoController, { except: ['create', 'store', 'update', 'destroy'] })
199
+ ```
200
+
201
+ > [!NOTE]
202
+ > Laravel chains these settings onto the resource, as `->only([...])`. Oriel's `Route.resource()` registers its
203
+ > routes as soon as you call it and returns nothing to chain onto, so it takes them in an options object, the third
204
+ > argument: `only`, `except`, `names` and `parameters`.
205
+
206
+ #### API resource routes
207
+
208
+ An API has no pages with forms, so `Route.apiResource()` leaves out `create` and `edit`:
209
+
210
+ ```ts
211
+ declare class PhotoController {}
212
+ // ---cut---
213
+ Route.apiResource('photos', PhotoController)
214
+ ```
215
+
216
+ `--api` writes a controller without those two methods, and with `--model`, typed with the model:
217
+
218
+ ```sh
219
+ node oriel make:controller PhotoController --api
220
+ node oriel make:controller PhotoController --api --model=Photo
221
+ ```
222
+
223
+ ### Nested resources
224
+
225
+ A resource that belongs to another, such as a photo's comments, is named with a dot:
226
+
227
+ ```ts
228
+ declare class PhotoCommentController {}
229
+ // ---cut---
230
+ Route.resource('photos.comments', PhotoCommentController)
231
+ ```
232
+
233
+ Its URIs hold both parameters, `/photos/{photo}/comments/{comment}`, and its routes are named `photos.comments.index`,
234
+ `photos.comments.show` and so on. The actions receive the parameters in that order:
235
+
236
+ ```ts title="app/Http/Controllers/PhotoCommentController.ts"
237
+ import { Controller } from './Controller'
238
+
239
+ export class PhotoCommentController extends Controller {
240
+ async show(photo: string, comment: string) {
241
+ return { photo, comment }
242
+ }
243
+ }
244
+ ```
245
+
246
+ Each parameter is bound to its model by its name, on its own, once your app has `Photo` and `Comment` models.
247
+
248
+ > [!NOTE]
249
+ > Laravel's `scoped()` resources check that the comment belongs to the photo, and `shallow()` resources drop the
250
+ > parent from the child's URIs. Oriel has neither: check the comment in the action, or find it through the photo's
251
+ > relation.
252
+
253
+ ### Naming resource routes
254
+
255
+ `names` replaces the names of some actions' routes:
256
+
257
+ ```ts
258
+ declare class PhotoController {}
259
+ // ---cut---
260
+ Route.resource('photos', PhotoController, { names: { create: 'photos.build' } })
261
+ ```
262
+
263
+ ### Naming resource route parameters
264
+
265
+ A resource's parameter is its name in the singular, with dashes turned into underscores: `blog-posts` gives
266
+ `{blog_post}`. `parameters` names it yourself:
267
+
268
+ ```ts
269
+ declare class AdminUserController {}
270
+ // ---cut---
271
+ Route.resource('users', AdminUserController, { parameters: { users: 'admin_user' } })
272
+ ```
273
+
274
+ The `show` route's URI is then `/users/{admin_user}`.
275
+
276
+ ### Supplementing resource controllers
277
+
278
+ Routes of your own can sit beside a resource's. Oriel matches a fixed segment before a parameter, so
279
+ `/photos/popular` reaches `popular()` rather than `show()`, wherever you define it:
280
+
281
+ ```ts
282
+ declare class PhotoController {
283
+ popular(): Promise<void>
284
+ }
285
+ // ---cut---
286
+ Route.get('/photos/popular', [PhotoController, 'popular'])
287
+ Route.resource('photos', PhotoController)
288
+ ```
289
+
290
+ A resource can also sit in a group, which gives it a prefix, a name prefix or middleware:
291
+ `Route.middleware('auth').resource('photos', PhotoController)`.
292
+
293
+ ## Dependency injection and controllers
294
+
295
+ #### Constructor injection
296
+
297
+ The [service container](container.md) makes a new controller for each request, with no constructor arguments. Take the
298
+ services a controller needs with `inject()`, in a field:
299
+
300
+ ```ts title="app/Http/Controllers/PodcastController.ts"
301
+ import { inject } from '@orieljs/framework'
302
+ import { ConfigRepository } from '@orieljs/framework/contracts'
303
+ import { Podcast } from '../../Models/Podcast'
304
+ import { Controller } from './Controller'
305
+
306
+ export class PodcastController extends Controller {
307
+ readonly #config = inject(ConfigRepository)
308
+
309
+ async index() {
310
+ return Podcast.latest().limit(this.#config.get('podcasts.per_page', 20)).get()
311
+ }
312
+ }
313
+ ```
314
+
315
+ > [!NOTE]
316
+ > Laravel reads a controller's constructor type hints and injects what they name. TypeScript types are gone when your
317
+ > app runs, so Oriel can't see them: it calls the constructor with no arguments, and `inject()` reads each service
318
+ > from the container. A constructor with parameters receives `undefined` for them.
319
+
320
+ #### Method injection
321
+
322
+ An action receives the route's parameters and nothing else. Read the request with `request()`:
323
+
324
+ ```ts title="app/Http/Controllers/UserController.ts"
325
+ import { redirect, request } from '@orieljs/framework'
326
+ import { Controller } from './Controller'
327
+
328
+ export class UserController extends Controller {
329
+ async store() {
330
+ const name = request().string('name').toString()
331
+
332
+ // Store the user...
333
+
334
+ return redirect('/users')
335
+ }
336
+ }
337
+ ```
338
+
339
+ A route parameter still arrives as the method's argument:
340
+
341
+ ```ts
342
+ Route.put('/user/{id}', [UserController, 'update'])
343
+ ```
344
+
345
+ ```ts title="app/Http/Controllers/UserController.ts"
346
+ import { redirect, request } from '@orieljs/framework'
347
+ import { User } from '../../Models/User'
348
+ import { Controller } from './Controller'
349
+
350
+ export class UserController extends Controller {
351
+ async update(id: string) {
352
+ const user = await User.findOrFail(id)
353
+
354
+ await user.update({ name: request().string('name').toString() })
355
+
356
+ return redirect('/users')
357
+ }
358
+ }
359
+ ```
360
+
361
+ > [!NOTE]
362
+ > Laravel injects the request into an action that type-hints it, before the route parameters. TypeScript types are
363
+ > gone when your app runs, so Oriel passes only the route parameters: they are always the action's arguments, in URI
364
+ > order, and the request comes from `request()`.
365
+
366
+ ## Authorizing actions
367
+
368
+ `this.authorize()` checks an ability for the signed-in user, through your gates and policies. When the user may not,
369
+ it throws, and the request ends with a 403, or the status the policy chose:
370
+
371
+ ```ts title="app/Http/Controllers/PostController.ts"
372
+ import { redirect, request } from '@orieljs/framework'
373
+ import type { Post } from '../../Models/Post'
374
+ import { Controller } from './Controller'
375
+
376
+ export class PostController extends Controller {
377
+ async update(post: Post) {
378
+ await this.authorize('update', post)
379
+
380
+ await post.update({ title: request().string('title').toString() })
381
+
382
+ return redirect().route('posts.show', post)
383
+ }
384
+ }
385
+ ```
386
+
387
+ `this.authorizeForUser(user, 'update', post)` checks the ability for another user. Gates and policies are covered on the
388
+ [Authorization](authorization.md) page.
389
+
390
+ ## Generating controllers
391
+
392
+ `make:controller` takes these options. Given several, `--model` wins over `--invokable`, and `--invokable` over
393
+ `--resource`:
394
+
395
+ | Option | What it writes |
396
+ | --- | --- |
397
+ | (none) | An empty controller |
398
+ | `-i`, `--invokable` | A controller with an `__invoke()` method |
399
+ | `-r`, `--resource` | The seven resource methods; the parameter is named after the controller, without its `Controller` suffix |
400
+ | `--api` | The five API resource methods, without `create` and `edit` |
401
+ | `-m`, `--model=Photo` | The resource methods typed with the model (with `--api`, the five); writes the model when it doesn't exist |
402
+ | `-R`, `--requests` | With `--model`: the `Store` and `Update` form requests, and their `validate()` calls |
403
+ | `--force` | Overwrites a controller that exists |
404
+
405
+ A name with slashes writes into a subfolder: `make:controller Settings/ProfileController` writes
406
+ `ProfileController.ts` in `app/Http/Controllers/Settings`, importing `../Controller`.
package/docs/csrf.md ADDED
@@ -0,0 +1,126 @@
1
+ # CSRF protection
2
+
3
+ Oriel protects your forms from cross-site request forgery: every form that changes something must send the
4
+ session's CSRF token, or the request is refused.
5
+
6
+ ## Introduction
7
+
8
+ Cross-site request forgery is an attack in which another site makes your users' browsers send requests to your app.
9
+ Picture a `/user/email` route that accepts a `POST` and changes the signed-in user's email address. Without CSRF
10
+ protection, any site the user visits could hold a form that posts its own address to that route, and submit it with
11
+ a little JavaScript. The browser would send the user's session cookie along, and your app would change the address.
12
+
13
+ To stop this, your app checks every `POST`, `PUT`, `PATCH` and `DELETE` request to a route in the `web` group for a
14
+ secret value, the CSRF token, which only your own pages know.
15
+
16
+ ## Preventing CSRF requests
17
+
18
+ Every session has a CSRF token. It is kept in the session, so another site can't read it, and it changes whenever the
19
+ session is regenerated, as the starter kits do when a user signs in. Read it from the request's session, or with
20
+ `csrfToken()`:
21
+
22
+ ```ts
23
+ Route.get('/token', () => {
24
+ const token = request().session().token()
25
+
26
+ return csrfToken()
27
+ })
28
+ ```
29
+
30
+ A form you write yourself sends the token in a hidden `_token` field, which `csrfField()` writes:
31
+
32
+ ```tsx title="resources/views/profile.tsx"
33
+ import { csrfField } from '@orieljs/framework'
34
+
35
+ export default function Profile() {
36
+ return (
37
+ <form method="post" action="/profile">
38
+ {csrfField()}
39
+
40
+ {/* the same as <input type="hidden" name="_token" value={csrfToken()} autocomplete="off" /> */}
41
+ <button type="submit">Save</button>
42
+ </form>
43
+ )
44
+ }
45
+ ```
46
+
47
+ The `VerifyCsrfToken` middleware, in the `web` [middleware group](middleware.md#the-default-middleware-groups), checks
48
+ that the token a request sends matches its session's. `GET`, `HEAD` and `OPTIONS` requests pass without one. A
49
+ request whose token is missing or wrong gets a 419: the "Page Expired" page, or, when the request wants JSON,
50
+ `{"message": "CSRF token mismatch."}`.
51
+
52
+ [Inertia](inertia.md) pages and [Live](live.md) components send the token for you, from the `XSRF-TOKEN` cookie (see
53
+ [X-XSRF-TOKEN](#x-xsrf-token)), so the starter kits' forms have no `_token` field.
54
+
55
+ A form that should reach a `PUT`, `PATCH` or `DELETE` route posts with a hidden `_method` field as well, from
56
+ `methodField()`: see [form method spoofing](routing.md#form-method-spoofing).
57
+
58
+ ### CSRF tokens and SPAs
59
+
60
+ A single-page app served from your app's own domain sends the token in the `X-XSRF-TOKEN` header, as Inertia does. A
61
+ frontend on another domain, or a mobile app, uses your API routes instead, which have no session and no CSRF check, and
62
+ signs in with [personal access tokens](authentication.md#api-tokens). Oriel sends no CORS headers, so a browser frontend
63
+ on another domain needs a middleware of your own that adds them.
64
+
65
+ ### Excluding URIs from CSRF protection
66
+
67
+ Some routes can't get the token: a payment provider's webhook, for one, doesn't know it. Such routes are best kept out
68
+ of the `web` group, in `routes/api.ts`. Otherwise, list their URIs in `validateCsrfTokens()` in `bootstrap/app.ts`:
69
+
70
+ ```ts
71
+ Application.configure({ basePath: '.' }).withMiddleware((middleware) => {
72
+ middleware.validateCsrfTokens({
73
+ except: ['stripe/*', 'http://example.com/foo/bar', 'http://example.com/foo/*'],
74
+ })
75
+ })
76
+ ```
77
+
78
+ An entry is a path, with or without its leading `/`, or a full URL, and `*` matches anything. The same list can live
79
+ in `csrf_except` in `config/http.ts`; `validateCsrfTokens()` adds to it. A single route can also leave the check out:
80
+ `Route.post('/hooks/github', handler).withoutMiddleware(VerifyCsrfToken)`.
81
+
82
+ > [!NOTE]
83
+ > The check is skipped while your tests run: when the app runs in the console with `APP_ENV` set to `testing`, as the
84
+ > `TestCase` of a new app sets it up (see [Testing](testing.md)). Your feature tests can post forms without a token.
85
+
86
+ ## X-CSRF-TOKEN
87
+
88
+ `VerifyCsrfToken` also reads the token from an `X-CSRF-TOKEN` header. You can put the token in a `meta` tag in your
89
+ layout:
90
+
91
+ ```tsx title="resources/views/partials/head.tsx"
92
+ import { csrfToken } from '@orieljs/framework'
93
+
94
+ export default function Head({ title }: { title: string }) {
95
+ return (
96
+ <head>
97
+ <title>{title}</title>
98
+ <meta name="csrf-token" content={csrfToken()} />
99
+ </head>
100
+ )
101
+ }
102
+ ```
103
+
104
+ and send it from your scripts:
105
+
106
+ ```ts title="resources/js/comments.ts"
107
+ export function postComment(body: string): Promise<Response> {
108
+ const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content ?? ''
109
+
110
+ return fetch('/comments', {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': token },
113
+ body: JSON.stringify({ body }),
114
+ })
115
+ }
116
+ ```
117
+
118
+ ## X-XSRF-TOKEN
119
+
120
+ Every response that passes `VerifyCsrfToken` carries the token in an `XSRF-TOKEN` cookie, encrypted as your other
121
+ cookies are. The cookie isn't `HttpOnly`, so your scripts can read it, and a request may send its value back in an
122
+ `X-XSRF-TOKEN` header, which `VerifyCsrfToken` decrypts. It takes its lifetime, path, domain, `secure` and `same_site`
123
+ settings from `config/session.ts`.
124
+
125
+ The cookie is there for the clients that send it on their own: Inertia's HTTP client and Live's client do, as
126
+ Axios and Angular's `HttpClient` do.
@@ -0,0 +1,144 @@
1
+ # Database testing
2
+
3
+ Your tests can migrate a database for each test, fill it with factories and seeders, and check the rows your code
4
+ wrote.
5
+
6
+ ## Introduction
7
+
8
+ By default, each test runs on a new in-memory SQLite database (see [Environment](testing.md#environment)), which is
9
+ empty: no tables until a test migrates it.
10
+
11
+ ### Resetting the database after each test
12
+
13
+ `RefreshDatabase` gives each test a migrated database. Add it to the file's test case:
14
+
15
+ ```ts title="tests/Feature/Posts.test.ts"
16
+ import { RefreshDatabase } from '@orieljs/framework/testing'
17
+ import { test } from 'vitest'
18
+ import { TestCase } from '../TestCase'
19
+
20
+ const t = TestCase.boot({ uses: [RefreshDatabase] })
21
+
22
+ test('the posts page loads', async () => {
23
+ const response = await t.get('/posts')
24
+
25
+ response.assertOk()
26
+ })
27
+ ```
28
+
29
+ On the in-memory database, it runs your migrations for each test, on that test's new database. On a database in a
30
+ file or on a server, it runs `migrate:fresh` once, and then runs each test inside a transaction that it rolls back
31
+ afterwards, so no test sees another's rows.
32
+
33
+ | Trait | What it does |
34
+ | --- | --- |
35
+ | `RefreshDatabase` | Migrates as above |
36
+ | `DatabaseMigrations` | Runs `migrate:fresh` before each test and `migrate:rollback` after it |
37
+ | `DatabaseTransactions` | Runs each test in a transaction that is rolled back, without migrating |
38
+
39
+ To use a trait in every test, set it on your `TestCase`:
40
+
41
+ ```ts title="tests/TestCase.ts"
42
+ import type { Application } from '@orieljs/framework'
43
+ import { RefreshDatabase, TestCase as BaseTestCase } from '@orieljs/framework/testing'
44
+ import { afterEach, beforeEach } from 'vitest'
45
+ import { createApplication } from '../bootstrap/app'
46
+
47
+ export class TestCase extends BaseTestCase {
48
+ static override hooks = { beforeEach, afterEach }
49
+
50
+ override readonly uses = [RefreshDatabase]
51
+
52
+ override createApplication(): Promise<Application> {
53
+ return createApplication()
54
+ }
55
+ }
56
+ ```
57
+
58
+ `connectionsToTransact` lists the connections the transaction traits wrap, the default one unless you name others.
59
+
60
+ > [!NOTE]
61
+ > Laravel's traits are PHP traits, added with `use RefreshDatabase;`. Oriel's are objects you list in `uses`.
62
+
63
+ ## Model factories
64
+
65
+ [Factories](eloquent-factories.md) make the rows a test needs:
66
+
67
+ ```ts title="tests/Feature/Users.test.ts"
68
+ import { RefreshDatabase } from '@orieljs/framework/testing'
69
+ import { test } from 'vitest'
70
+ import { User } from '../../app/Models/User'
71
+ import { TestCase } from '../TestCase'
72
+
73
+ const t = TestCase.boot({ uses: [RefreshDatabase] })
74
+
75
+ test('models can be instantiated', async () => {
76
+ const user = await User.factory().create()
77
+
78
+ const response = await t.actingAs(user).get('/dashboard')
79
+
80
+ response.assertOk()
81
+ })
82
+ ```
83
+
84
+ Oriel finds the factories in `database/factories` when your tests start.
85
+
86
+ ## Running seeders
87
+
88
+ `t.runSeeder()` runs `DatabaseSeeder` in a test, or the seeder you name:
89
+
90
+ ```ts
91
+ import { TestCase } from './tests/TestCase'
92
+ const t = TestCase.boot()
93
+ // ---cut---
94
+ await t.runSeeder()
95
+
96
+ await t.runSeeder('OrderStatusSeeder')
97
+ ```
98
+
99
+ To seed before every test, set `seed`, or `seeder` for another seeder, on your `TestCase`. `RefreshDatabase` and
100
+ `DatabaseMigrations` then seed after they migrate:
101
+
102
+ ```ts
103
+ import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
104
+ // ---cut---
105
+ export class TestCase extends BaseTestCase {
106
+ override seed = true
107
+ }
108
+ ```
109
+
110
+ > [!NOTE]
111
+ > Laravel's `$this->seed()` is `runSeeder()` in Oriel, since `seed` is the option.
112
+
113
+ ## Available assertions
114
+
115
+ The test case has these assertions. They read the database, so `await` each one:
116
+
117
+ ```ts
118
+ import { TestCase } from './tests/TestCase'
119
+ import type { User } from './app/Models/User'
120
+ const t = TestCase.boot()
121
+ declare const user: User
122
+ // ---cut---
123
+ await t.assertDatabaseHas('users', { email: 'sally@example.com' })
124
+
125
+ await t.assertModelExists(user)
126
+ ```
127
+
128
+ | Assertion | Passes when |
129
+ | --- | --- |
130
+ | `assertDatabaseHas(table, data)` | A row of the table has the values; `null` matches a null column |
131
+ | `assertDatabaseMissing(table, data)` | No row has them |
132
+ | `assertDatabaseCount(table, count)` | The table has that many rows |
133
+ | `assertDatabaseEmpty(table)` | The table has no rows |
134
+ | `assertModelExists(model)`, `assertModelMissing(model)` | The model's row is in the database, or isn't |
135
+ | `assertSoftDeleted(tableOrModel, data?)`, `assertNotSoftDeleted(tableOrModel, data?)` | The row is soft deleted, or isn't |
136
+
137
+ `table` is a table's name, or a model, whose table and connection the assertion uses. `assertDatabaseHas()`,
138
+ `assertDatabaseMissing()`, `assertDatabaseCount()` and `assertDatabaseEmpty()` take a connection name as their last
139
+ argument, and `assertSoftDeleted()` and `assertNotSoftDeleted()` after the data. A failed `assertDatabaseHas()` shows a
140
+ few rows of the table, to help you see what differs.
141
+
142
+ > [!NOTE]
143
+ > Laravel's assertions also take a model's class, `User::class`. Oriel's take a table name or a model: write
144
+ > `assertDatabaseHas('users', ...)`, or pass a model you have.