@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,494 @@
1
+ # Eloquent: getting started
2
+
3
+ Each table of your database has a model, a class that reads its rows as objects, writes them back, and knows its
4
+ relations to the other tables. Oriel's models follow Laravel's Eloquent.
5
+
6
+ ## Introduction
7
+
8
+ A model extends `Model`, and its class name gives its table: `Flight` reads and writes the `flights` table. A model
9
+ query takes every method of the [query builder](queries.md), and returns models:
10
+
11
+ ```ts
12
+ const flights = await Flight.where('active', 1).orderBy('name').limit(10).get()
13
+ ```
14
+
15
+ Before you start, set your database connection in `config/database.ts` (see [Getting started](database.md)).
16
+
17
+ ## Generating model classes
18
+
19
+ `make:model` writes a model in `app/Models`:
20
+
21
+ ```sh
22
+ node oriel make:model Flight
23
+ ```
24
+
25
+ Its options write the model's other files too: a migration (`-m`), a factory (`-f`), a seeder (`-s`), a controller
26
+ (`-c`, a resource controller with `-r`, an API one with `--api`), form requests for it (`-R`), a policy (`--policy`),
27
+ or all of them (`-a`). Short options combine:
28
+
29
+ ```sh
30
+ node oriel make:model Flight -mfs
31
+ ```
32
+
33
+ ### Inspecting models
34
+
35
+ `node oriel model:types` writes `types/models.d.ts`: each model's columns, read from your database, with their types, so
36
+ `flight.name` is a `string` and `Flight.where('nme', …)` is a type error. `migrate`, `migrate:fresh` and
37
+ `migrate:refresh` run it for you when they finish, outside production, in an app with a `types` folder, as a new app
38
+ has. It reads your tables, so run your migrations first. A model must be a named export of its file to get its types.
39
+
40
+ ## Model conventions
41
+
42
+ A model's settings are fields of its class, most of them `protected override` fields:
43
+
44
+ ```ts title="app/Models/Flight.ts"
45
+ import { Model } from '@orieljs/framework'
46
+
47
+ export class Flight extends Model {
48
+ protected override table = 'my_flights'
49
+ }
50
+ ```
51
+
52
+ ### Table names
53
+
54
+ A model's table is the plural, in snake case, of its class name: `Flight` uses `flights`, and `AirTrafficController`
55
+ `air_traffic_controllers`. `table` names another.
56
+
57
+ ### Primary keys
58
+
59
+ A model's primary key is `id`, an auto-incrementing integer. `primaryKey` names another column; a key that doesn't
60
+ increment sets `override incrementing = false` (a public field, as `timestamps` is), and a string key
61
+ `protected override keyType = 'string' as const`:
62
+
63
+ ```ts
64
+ export class Flight extends Model {
65
+ protected override primaryKey = 'flight_id'
66
+ }
67
+ ```
68
+
69
+ #### UUID and ULID keys
70
+
71
+ The `HasUuids` mixin gives a model a UUID key in place of an integer, made when the model is created: a version 7 UUID,
72
+ which sorts by time, so the index stays compact. `HasUlids` gives a lower-case ULID:
73
+
74
+ ```ts title="app/Models/Article.ts"
75
+ import { HasUuids, Model } from '@orieljs/framework'
76
+
77
+ export class Article extends HasUuids(Model) {}
78
+ ```
79
+
80
+ Their tables need a `uuid('id')` or `ulid('id')` primary key column (see [Migrations](migrations.md)). A route
81
+ parameter or a `find()` with a malformed id finds nothing, without a query.
82
+
83
+ > [!NOTE]
84
+ > Laravel adds abilities to a model with traits (`use HasUuids;`). TypeScript classes have one parent, so Oriel's are
85
+ > mixins, functions that wrap the class you extend: `HasUuids(Model)`, and a new app's
86
+ > `HasApiTokens(Notifiable(Authorizable(AuthenticatableModel(Model))))`.
87
+
88
+ ### Timestamps
89
+
90
+ A model sets `created_at` and `updated_at` when it is created and saved. `timestamps = false` turns this off, and the
91
+ static `CREATED_AT` and `UPDATED_AT` rename the columns, or turn one off with `null`:
92
+
93
+ ```ts
94
+ export class Flight extends Model {
95
+ override timestamps = false
96
+ }
97
+
98
+ export class Booking extends Model {
99
+ static override UPDATED_AT = null
100
+ }
101
+ ```
102
+
103
+ Timestamps are stored in UTC, to the second.
104
+
105
+ ### Database connections
106
+
107
+ A model uses the default connection, unless `connection` names another:
108
+
109
+ ```ts
110
+ export class Flight extends Model {
111
+ protected override connection = 'pgsql'
112
+ }
113
+ ```
114
+
115
+ ### Default attribute values
116
+
117
+ `attributes` gives a new model's default values:
118
+
119
+ ```ts
120
+ export class Flight extends Model {
121
+ protected override attributes = { options: '[]', delayed: false }
122
+ }
123
+ ```
124
+
125
+ ### Attributes and their types
126
+
127
+ A model's attributes are its row's columns, read and written as properties: `flight.name`. Their types come from
128
+ `types/models.d.ts`. Without it, declare them yourself, with `declare`:
129
+
130
+ ```ts
131
+ export class Flight extends Model {
132
+ declare name: string
133
+ declare delayed: boolean
134
+ }
135
+ ```
136
+
137
+ > [!WARNING]
138
+ > Never give an attribute a class field, such as `name = ''` or `name!: string`: a field hides the attribute, so
139
+ > the model never reads or writes the column. `declare` adds only the type.
140
+
141
+ A column named like one of `Model`'s own members, such as `table`, `exists` or `update`, can't be read as a property:
142
+ read it with `getAttribute('update')`.
143
+
144
+ ## Retrieving models
145
+
146
+ `all()` returns every row of the model's table, as models in a collection:
147
+
148
+ ```ts
149
+ for (const flight of await Flight.all()) {
150
+ console.log(flight.name)
151
+ }
152
+ ```
153
+
154
+ #### Building queries
155
+
156
+ A query starts with a static method of the model, such as `where()`, and runs with `get()`:
157
+
158
+ ```ts
159
+ const flights = await Flight.where('active', 1).orderBy('name').limit(10).get()
160
+ ```
161
+
162
+ `Flight.query()` starts an empty one.
163
+
164
+ #### Refreshing models
165
+
166
+ `fresh()` reads the model again from the database and returns a new one, and `refresh()` reads it into the model
167
+ itself, its loaded relations included:
168
+
169
+ ```ts
170
+ const flight = await Flight.where('number', 'FR 900').firstOrFail()
171
+
172
+ const freshFlight = await flight.fresh()
173
+
174
+ await flight.refresh()
175
+ ```
176
+
177
+ ### Collections
178
+
179
+ `get()` and `all()` return an `EloquentCollection`, a [collection](collections.md) with methods for models (see
180
+ [Eloquent collections](eloquent-collections.md)). It is iterable:
181
+
182
+ ```ts
183
+ const flights = await Flight.where('destination', 'Paris').get()
184
+
185
+ const names = flights.map((flight) => flight.name).all()
186
+ ```
187
+
188
+ ### Chunking results
189
+
190
+ `chunk()` reads a large table a part at a time, ordered by the key, and `chunkById()` pages by the key, for a function
191
+ that changes the rows:
192
+
193
+ ```ts
194
+ await Flight.chunk(200, async (flights) => {
195
+ for (const flight of flights) {
196
+ // ...
197
+ }
198
+ })
199
+ ```
200
+
201
+ ### Streaming results lazily
202
+
203
+ `lazy()` returns a lazy collection, which reads the rows a part at a time as you go through it:
204
+
205
+ ```ts
206
+ for await (const flight of Flight.lazy()) {
207
+ // ...
208
+ }
209
+ ```
210
+
211
+ ## Retrieving single models / aggregates
212
+
213
+ `find()` returns the model with that key, and `first()` the first of a query, or `undefined`:
214
+
215
+ ```ts
216
+ const flight = await Flight.find(1)
217
+
218
+ const active = await Flight.where('active', 1).first()
219
+
220
+ const first = await Flight.firstWhere('active', 1)
221
+ ```
222
+
223
+ #### Not found exceptions
224
+
225
+ `findOrFail()` and `firstOrFail()` throw a `ModelNotFoundError` when there is no model. Uncaught, it answers 404:
226
+
227
+ ```ts
228
+ const flight = await Flight.findOrFail(1)
229
+
230
+ const flightOver = await Flight.where('legs', '>', 3).firstOrFail()
231
+ ```
232
+
233
+ ### Retrieving or creating models
234
+
235
+ `firstOrCreate()` finds the model that matches its first argument, or creates it with both arguments. `firstOrNew()`
236
+ does the same, but leaves the new model unsaved:
237
+
238
+ ```ts
239
+ const flight = await Flight.firstOrCreate({ name: 'London to Paris' }, { delayed: 1, arrival_time: '11:30' })
240
+
241
+ const unsaved = await Flight.firstOrNew({ name: 'London to Paris' })
242
+ ```
243
+
244
+ ### Retrieving aggregates
245
+
246
+ ```ts
247
+ const count = await Flight.where('active', 1).count()
248
+
249
+ const max = await Flight.where('active', 1).max('price')
250
+ ```
251
+
252
+ ## Inserting and updating models
253
+
254
+ ### Inserts
255
+
256
+ `create()` saves a new model, with the attributes you give, and returns it:
257
+
258
+ ```ts title="app/Http/Controllers/FlightController.ts"
259
+ import { redirect, request } from '@orieljs/framework'
260
+ import { Flight } from '../../Models/Flight'
261
+ import { Controller } from './Controller'
262
+
263
+ export class FlightController extends Controller {
264
+ /** Store a new flight in the database. */
265
+ async store() {
266
+ // Validate the request...
267
+
268
+ await Flight.create({ name: request().string('name').toString() })
269
+
270
+ return redirect('/flights')
271
+ }
272
+ }
273
+ ```
274
+
275
+ `make()` builds the model without saving it, and `save()` saves it:
276
+
277
+ ```ts
278
+ const flight = Flight.make({ name: 'London to Paris' })
279
+
280
+ await flight.save()
281
+ ```
282
+
283
+ > [!NOTE]
284
+ > Laravel builds a model with `new Flight(['name' => …])`. An Oriel model's constructor takes nothing: use
285
+ > `Flight.make()` or `Flight.create()`.
286
+
287
+ ### Updates
288
+
289
+ `save()` saves a model you changed. Only the changed attributes are written, and `updated_at` is set:
290
+
291
+ ```ts
292
+ const flight = await Flight.findOrFail(1)
293
+
294
+ flight.name = 'Paris to London'
295
+
296
+ await flight.save()
297
+ ```
298
+
299
+ `update()` sets attributes and saves, on a model or on every row a query matches:
300
+
301
+ ```ts
302
+ declare const flight: Flight
303
+ // ---cut---
304
+ await flight.update({ delayed: 1 })
305
+
306
+ await Flight.where('active', 1).where('destination', 'San Diego').update({ delayed: 1 })
307
+ ```
308
+
309
+ A query's `update()` writes the rows directly: their models aren't read, and their events don't fire.
310
+
311
+ `updateOrCreate()` updates the model that matches its first argument, or creates one:
312
+
313
+ ```ts
314
+ const flight = await Flight.updateOrCreate({ departure: 'Oakland', destination: 'San Diego' }, { price: 99, discounted: 1 })
315
+ ```
316
+
317
+ #### Examining attribute changes
318
+
319
+ `isDirty()` tells you whether a model, or an attribute, changed since it was read, and `isClean()` the opposite.
320
+ `wasChanged()` tells you whether the last `save()` changed it, and `getOriginal()` gives the values as they were read:
321
+
322
+ ```ts
323
+ const user = await User.create({ name: 'Taylor', email: 'taylor@example.com', password: '' })
324
+
325
+ user.name = 'Painter'
326
+
327
+ user.isDirty() // true
328
+ user.isDirty('name') // true
329
+ user.isDirty('email') // false
330
+ user.getOriginal('name') // 'Taylor'
331
+
332
+ await user.save()
333
+
334
+ user.wasChanged('name') // true
335
+ ```
336
+
337
+ ### Mass assignment
338
+
339
+ `create()`, `make()`, `update()` and `fill()` set only the attributes listed in the model's `fillable`, so a request
340
+ can't set a column you didn't mean it to, such as `is_admin`:
341
+
342
+ ```ts
343
+ export class Flight extends Model {
344
+ protected override fillable = ['name']
345
+ }
346
+ ```
347
+
348
+ Other attributes are left out without an error. A model with neither `fillable` nor a `guarded` of its own takes none,
349
+ and `create()` with attributes throws a `MassAssignmentError`. `guarded` lists the attributes that can't be set, in
350
+ place of `fillable`; `guarded = []` lets every attribute through.
351
+
352
+ `forceFill()` and `forceCreate()` set every attribute you give, and `Model.unguarded()` runs a function with the check
353
+ off, as your seeders do.
354
+
355
+ ### Upserts
356
+
357
+ `upsert()` on a query inserts the rows, and updates those that exist, as on the [query builder](queries.md#upserts).
358
+
359
+ ## Deleting models
360
+
361
+ `delete()` deletes a model:
362
+
363
+ ```ts
364
+ const flight = await Flight.findOrFail(1)
365
+
366
+ await flight.delete()
367
+ ```
368
+
369
+ `destroy()` deletes by key. It reads each model and deletes it, so their events fire:
370
+
371
+ ```ts
372
+ await Flight.destroy(1)
373
+
374
+ await Flight.destroy(1, 2, 3)
375
+ ```
376
+
377
+ A query's `delete()` deletes the rows directly, without events:
378
+
379
+ ```ts
380
+ const deleted = await Flight.where('active', 0).delete()
381
+ ```
382
+
383
+ ### Soft deleting
384
+
385
+ A soft-deleted model stays in its table, with a `deleted_at` date, and queries leave it out. Turn it on with
386
+ `softDeletes = true`, and give the table a `deleted_at` column with `table.softDeletes()` in a migration:
387
+
388
+ ```ts title="app/Models/Flight.ts"
389
+ import { Model } from '@orieljs/framework'
390
+
391
+ export class Flight extends Model {
392
+ protected override softDeletes = true
393
+ }
394
+ ```
395
+
396
+ `delete()` then sets `deleted_at`, `trashed()` tells you whether a model is soft-deleted, `restore()` brings it back,
397
+ and `forceDelete()` deletes it for good:
398
+
399
+ ```ts
400
+ const flight = await Flight.findOrFail(1)
401
+
402
+ await flight.delete()
403
+
404
+ if (flight.trashed()) {
405
+ await flight.restore()
406
+ }
407
+
408
+ await flight.forceDelete()
409
+ ```
410
+
411
+ > [!NOTE]
412
+ > Laravel's soft deletes are a trait, `SoftDeletes`. Oriel's are a setting of the model, `softDeletes = true`.
413
+
414
+ ### Querying soft deleted models
415
+
416
+ `withTrashed()` includes soft-deleted models in a query, and `onlyTrashed()` returns only them:
417
+
418
+ ```ts
419
+ const flights = await Flight.withTrashed().where('account_id', 1).get()
420
+
421
+ const trashed = await Flight.onlyTrashed().get()
422
+ ```
423
+
424
+ > [!NOTE]
425
+ > Laravel's prunable models (`Prunable`, `model:prune`) and `replicate()` aren't in Oriel.
426
+
427
+ ## Query scopes
428
+
429
+ ### Global scopes
430
+
431
+ A global scope adds a condition to every query of a model. Add one in the model's static `booted()`, with a name and
432
+ a function, or with a class that has an `apply()` method:
433
+
434
+ ```ts title="app/Models/Flight.ts"
435
+ import { Model } from '@orieljs/framework'
436
+
437
+ export class Flight extends Model {
438
+ static override booted(): void {
439
+ this.addGlobalScope('active', (query) => {
440
+ query.where('active', 1)
441
+ })
442
+ }
443
+ }
444
+ ```
445
+
446
+ `withoutGlobalScope('active')` leaves one out of a query, and `withoutGlobalScopes()` all of them:
447
+
448
+ ```ts
449
+ const flights = await Flight.withoutGlobalScope('active').get()
450
+ ```
451
+
452
+ ### Local scopes
453
+
454
+ A local scope is a static method whose name starts with `scope`. It receives the query, and the rest of the
455
+ arguments:
456
+
457
+ ```ts title="app/Models/Post.ts"
458
+ import { Model, type Builder } from '@orieljs/framework'
459
+
460
+ export class Post extends Model {
461
+ static scopePublished(query: Builder<Post>) {
462
+ return query.whereNotNull('published_at')
463
+ }
464
+ }
465
+ ```
466
+
467
+ Call it without the prefix, on the model or on a query:
468
+
469
+ ```ts
470
+ const posts = await Post.published().orderBy('created_at').get()
471
+
472
+ const recent = await Post.where('author_id', 1).published().get()
473
+ ```
474
+
475
+ `model:types` adds the scopes to the model's types, so `Post.published()` is typed.
476
+
477
+ ## Comparing models
478
+
479
+ `is()` tells you whether two models are the same row: the same key, table and connection. `isNot()` is its
480
+ opposite:
481
+
482
+ ```ts
483
+ declare const post: Post
484
+ declare const another: Post
485
+ // ---cut---
486
+ if (post.is(another)) {
487
+ // ...
488
+ }
489
+ ```
490
+
491
+ ## Events
492
+
493
+ Models dispatch events as they are created, updated, saved, deleted and restored, which you can listen for with
494
+ closures or observers: see [Observers](eloquent-observers.md).
@@ -0,0 +1,104 @@
1
+ # Encryption
2
+
3
+ Encrypt values your app stores or sends, such as another service's API token, with your app's key, and decrypt them
4
+ when you need them back.
5
+
6
+ ## Introduction
7
+
8
+ Oriel's encrypter uses AES-256 or AES-128, through Node's own `crypto` module. Every value it encrypts is
9
+ authenticated, so a value that was changed after it was encrypted can't be decrypted. Your app's cookies, its
10
+ sessions when `session.encrypt` is on, and the `Crypt` facade all use it.
11
+
12
+ ## Configuration
13
+
14
+ The encrypter's key is `key` in `config/app.ts`, which reads `APP_KEY` from `.env`. The installer generates it, and
15
+ `node oriel key:generate` writes a new one: a random key for your cipher, written as `base64:` and its bytes (see
16
+ [The application key](configuration.md#the-application-key)).
17
+
18
+ `cipher` in `config/app.ts` names the cipher, `AES-256-GCM` in a new app. `AES-128-GCM`, `AES-256-CBC` and `AES-128-CBC`
19
+ are the others, in upper or lower case. The key must have the length the cipher takes: 32 bytes for AES-256 and 16 for
20
+ AES-128.
21
+
22
+ > [!NOTE]
23
+ > Laravel's default cipher is AES-256-CBC, which signs each value with an HMAC. Oriel's is AES-256-GCM, which
24
+ > authenticates each value itself. Both are supported, and both encrypt a value in the same format as Laravel, so a
25
+ > value encrypted with `encryptString()` by a Laravel app with the same key and cipher decrypts in Oriel, and the
26
+ > other way round.
27
+
28
+ An app without a key fails the first time it needs the encrypter, such as on its first request to a page of the `web`
29
+ group, whose cookies it encrypts, with "No application encryption key has been specified.".
30
+
31
+ ### Gracefully rotating encryption keys
32
+
33
+ A new key can't decrypt what the old one encrypted, so changing it signs every user out, as their session cookies no
34
+ longer decrypt, and makes every value you encrypted with the old key unreadable. List your previous keys in
35
+ `APP_PREVIOUS_KEYS`, separated by commas:
36
+
37
+ ```dotenv
38
+ APP_KEY="base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY="
39
+ APP_PREVIOUS_KEYS="base64:2nLsGFGzyoae2ax3EF2Lyq/hH6QghBGLIq5uL+Gp8/w="
40
+ ```
41
+
42
+ Oriel then encrypts with the current key only, and decrypts with the current key or any previous one. Your users stay
43
+ signed in, and the values you stored stay readable, while you encrypt them again with the new key.
44
+
45
+ ## Using the encrypter
46
+
47
+ #### Encrypting a value
48
+
49
+ The `Crypt` facade's `encryptString()` encrypts a string:
50
+
51
+ ```ts title="app/Http/Controllers/DigitalOceanTokenController.ts"
52
+ import { Crypt, redirect, request } from '@orieljs/framework'
53
+ import type { User } from '../../Models/User'
54
+ import { Controller } from './Controller'
55
+
56
+ export class DigitalOceanTokenController extends Controller {
57
+ /** Store a DigitalOcean API token for the user. */
58
+ async store() {
59
+ const user = (await request().user<User>())!
60
+
61
+ await user.forceFill({ token: Crypt.encryptString(request().string('token').toString()) }).save()
62
+
63
+ return redirect('/secrets')
64
+ }
65
+ }
66
+ ```
67
+
68
+ `encrypt()` takes any value that JSON can hold, such as an object, and turns it into JSON before it encrypts it. The
69
+ `encrypt()` and `decrypt()` helpers do the same as the facade's methods:
70
+
71
+ ```ts
72
+ const payload = Crypt.encrypt({ userId: 1, plan: 'pro' })
73
+
74
+ const sameKind = encrypt({ userId: 1, plan: 'pro' })
75
+ ```
76
+
77
+ > [!NOTE]
78
+ > Laravel's `encrypt()` turns a value into a PHP string with `serialize()`. Oriel's uses JSON, so a value from
79
+ > `encrypt()` doesn't decrypt in the other framework; `encryptString()` values do. As JSON, a `Date` comes back as its
80
+ > ISO string.
81
+
82
+ #### Decrypting a value
83
+
84
+ `decryptString()` decrypts what `encryptString()` encrypted, and `decrypt()` what `encrypt()` did, giving back the
85
+ value. A value that can't be decrypted, because it was changed, was encrypted with another key, or isn't a payload at
86
+ all, throws a `DecryptError`:
87
+
88
+ ```ts
89
+ declare const encryptedValue: string
90
+ // ---cut---
91
+ try {
92
+ const decrypted = Crypt.decryptString(encryptedValue)
93
+ } catch (error) {
94
+ if (!(error instanceof DecryptError)) throw error
95
+
96
+ // ...
97
+ }
98
+ ```
99
+
100
+ `decrypt()` takes the type of the value it gives back, which is a cast, not a check:
101
+ `Crypt.decrypt<{ userId: number; plan: string }>(payload)`.
102
+
103
+ The encrypter's methods are synchronous, and the facade and helpers need your app to be running, as in a request, a
104
+ job or a command.