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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/chunk-7ZQZYDOC.js +3253 -0
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +535 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +2202 -5952
  16. package/dist/index.js +4900 -944
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +19 -5
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-A5XRLEWM.js.map +0 -1
  83. package/dist/chunk-BSQKOLSK.js.map +0 -1
  84. package/dist/chunk-PM5YTG57.js +0 -880
  85. package/dist/chunk-PM5YTG57.js.map +0 -1
  86. package/dist/chunk-WNJP46B7.js.map +0 -1
  87. package/dist/index.d-DOlTeG-8.d.ts +0 -99
@@ -0,0 +1,254 @@
1
+ # Eloquent: mutators and casting
2
+
3
+ Accessors, mutators and casts change attribute values as your models read and write them: a JSON column comes back as
4
+ an object, a date as a `Date`, and a name in the case you chose.
5
+
6
+ ## Introduction
7
+
8
+ A cast turns a column's stored value into the type your code works with, and back when the model is saved. An accessor
9
+ is a method that computes an attribute when you read it, and a mutator one that sets it when you write it.
10
+
11
+ ## Accessors and mutators
12
+
13
+ ### Defining an accessor
14
+
15
+ An accessor is a method named `get`, the attribute's name in StudlyCase, and `Attribute`. It receives the stored value,
16
+ and returns what reading the attribute gives:
17
+
18
+ ```ts title="app/Models/User.ts"
19
+ import { Model } from '@orieljs/framework'
20
+
21
+ export class User extends Model {
22
+ /** Get the user's first name. */
23
+ getFirstNameAttribute(value: unknown): string {
24
+ const name = String(value ?? '')
25
+
26
+ return name.charAt(0).toUpperCase() + name.slice(1)
27
+ }
28
+ }
29
+ ```
30
+
31
+ `user.getAttribute('first_name')` then calls it, and so does reading `user.first_name`. `model:types` keeps the column's
32
+ type for it; an accessor with no column, below, is typed from its return type.
33
+
34
+ An accessor can also make an attribute that has no column, computed from others:
35
+
36
+ ```ts
37
+ export class User extends Model {
38
+ getFullNameAttribute(): string {
39
+ return `${String(this.getAttribute('first_name'))} ${String(this.getAttribute('last_name'))}`
40
+ }
41
+ }
42
+ ```
43
+
44
+ To have it in the model's `toArray()` and JSON, add it to `appends` (see
45
+ [Appending values to JSON](#appending-values-to-json)).
46
+
47
+ ### Defining a mutator
48
+
49
+ A mutator is a method named `set`, the attribute's name in StudlyCase, and `Attribute`. It receives the value being
50
+ set, and stores what it decides in `this.attributes`:
51
+
52
+ ```ts title="app/Models/User.ts"
53
+ import { Model } from '@orieljs/framework'
54
+
55
+ export class User extends Model {
56
+ /** Interact with the user's first name. */
57
+ setFirstNameAttribute(value: string): void {
58
+ this.attributes.first_name = value.toLowerCase()
59
+ }
60
+ }
61
+ ```
62
+
63
+ Every write goes through it: `user.first_name = 'Sally'`, `fill()`, `create()` and `update()`.
64
+
65
+ > [!NOTE]
66
+ > Laravel's accessors and mutators are methods returning an `Attribute` (`Attribute::make(get: …, set: …)`), or the
67
+ > older `getXAttribute()` and `setXAttribute()` methods. Oriel has the older form.
68
+
69
+ ## Attribute casting
70
+
71
+ `casts` maps attributes to the type they are read as:
72
+
73
+ ```ts title="app/Models/User.ts"
74
+ import { Model, type CastDefinition } from '@orieljs/framework'
75
+
76
+ export class User extends Model {
77
+ protected override casts: Readonly<Record<string, CastDefinition>> = {
78
+ is_admin: 'boolean',
79
+ options: 'json',
80
+ email_verified_at: 'datetime',
81
+ }
82
+ }
83
+ ```
84
+
85
+ A model with `model:types` types each attribute from its cast: `is_admin` as a `boolean`, `email_verified_at` as a
86
+ `Date | null`.
87
+
88
+ | Cast | Read as | Stored as |
89
+ | --- | --- | --- |
90
+ | `int`, `integer` | A number | The number |
91
+ | `float`, `double`, `real` | A number | The number |
92
+ | `string` | A string | The string |
93
+ | `bool`, `boolean` | `true` or `false` | `1` or `0`, or a boolean on PostgreSQL |
94
+ | `array`, `json`, `object` | The parsed JSON | JSON text |
95
+ | `collection` | A [collection](collections.md) of the parsed JSON list | JSON text |
96
+ | `date` | A `Date` at midnight UTC | `'2026-03-04'` |
97
+ | `datetime` | A `Date` | `'2026-01-02 03:04:05'`, in UTC |
98
+ | `immutable_date`, `immutable_datetime` | A `Date`, as `date` and `datetime` | The same |
99
+ | `timestamp` | A Unix time in seconds | The date and time |
100
+ | `decimal:2` | A string with that many decimals, `'9.50'` | The number |
101
+ | `hashed` | The hash | A hash of the value, made when the model is saved |
102
+
103
+ A built-in cast of `null` stays `null`; a custom cast's `get()` receives it. The incrementing key is always cast as an
104
+ integer, and `created_at` and `updated_at` (and `deleted_at` with soft deletes) as datetimes.
105
+
106
+ A cast string Oriel doesn't know, such as Laravel's `datetime:Y-m-d` with a format, throws an `InvalidCastError` when
107
+ the attribute is read or written: "Call to undefined cast [datetime:Y-m-d] on column [launched_at] in model
108
+ [Product].".
109
+
110
+ ### Array and JSON casting
111
+
112
+ A `json` attribute is read as the parsed value. Changing it in place counts as a change, so `save()` writes it:
113
+
114
+ ```ts
115
+ declare const user: User & { options: { theme: string } }
116
+ // ---cut---
117
+ user.options.theme = 'dark'
118
+
119
+ await user.save()
120
+ ```
121
+
122
+ ### Date casting
123
+
124
+ A date is stored in UTC, as `'2026-01-02 03:04:05'`, and read as a `Date`. In `toArray()` and JSON, dates are written
125
+ as Laravel writes them: `'2026-01-02T03:04:05.000000Z'`.
126
+
127
+ ### Enum casting
128
+
129
+ A TypeScript enum casts an attribute to its values:
130
+
131
+ ```ts title="app/Models/Server.ts"
132
+ import { Model, type CastDefinition } from '@orieljs/framework'
133
+
134
+ export enum ServerStatus {
135
+ Provisioned = 'provisioned',
136
+ Ready = 'ready',
137
+ }
138
+
139
+ export class Server extends Model {
140
+ protected override casts: Readonly<Record<string, CastDefinition>> = { status: ServerStatus }
141
+ }
142
+ ```
143
+
144
+ Setting a value that isn't one of the enum's throws: "\"nope\" is not a valid backing value for the cast of [status]
145
+ on [Server].".
146
+
147
+ ### Hashed casting
148
+
149
+ The `hashed` cast hashes a value when the model is saved, with your [hashing](hashing.md) driver, so you can set a
150
+ plain password:
151
+
152
+ ```ts
153
+ export class User extends Model {
154
+ protected override casts: Readonly<Record<string, CastDefinition>> = { password: 'hashed' }
155
+ }
156
+ ```
157
+
158
+ Until `save()`, reading the attribute gives what you set. A value that is already a hash made with your current
159
+ settings is kept as it is; one made with other settings is refused.
160
+
161
+ > [!NOTE]
162
+ > Laravel hashes as soon as you set the attribute. Hashing is asynchronous in Oriel, so it happens in `save()`, before
163
+ > the model's `saving` event, which sees the hash.
164
+
165
+ ### Query time casting
166
+
167
+ `withCasts()` adds casts for one query, such as for a column a raw select adds:
168
+
169
+ ```ts
170
+ const users = await User.query()
171
+ .select('users.*')
172
+ .selectRaw('(select max(created_at) from posts where posts.author_id = users.id) as last_posted_at')
173
+ .withCasts({ last_posted_at: 'datetime' })
174
+ .get()
175
+ ```
176
+
177
+ > [!NOTE]
178
+ > Laravel's encrypted casts, `AsArrayObject`, `AsCollection`, `AsStringable`, `AsEnumCollection` and castable value
179
+ > objects aren't in Oriel; write a custom cast for them.
180
+
181
+ ## Custom casts
182
+
183
+ A custom cast is a class with a `get()` method, which turns the stored value into what the model gives, and a `set()`
184
+ method, which turns a value back into what is stored. `make:cast` writes one in `app/Casts`:
185
+
186
+ ```sh
187
+ node oriel make:cast Money
188
+ ```
189
+
190
+ ```ts title="app/Casts/Money.ts"
191
+ import type { CastsAttributes } from '@orieljs/framework'
192
+
193
+ /** A price kept in cents, read in units: 1234 is 12.34. */
194
+ export class Money implements CastsAttributes<number, number> {
195
+ get(_model: object, _key: string, value: unknown): number {
196
+ return Number(value) / 100
197
+ }
198
+
199
+ set(_model: object, _key: string, value: number): unknown {
200
+ return Math.round(value * 100)
201
+ }
202
+ }
203
+ ```
204
+
205
+ Name the class in `casts`, or give an instance:
206
+
207
+ ```ts title="app/Models/Product.ts"
208
+ // @filename: app/Casts/Money.ts
209
+ import type { CastsAttributes } from '@orieljs/framework'
210
+ export declare class Money implements CastsAttributes<number, number> {
211
+ get(model: object, key: string, value: unknown): number
212
+ set(model: object, key: string, value: number): unknown
213
+ }
214
+ // ---cut---
215
+ import { Model, type CastDefinition } from '@orieljs/framework'
216
+ import { Money } from '../Casts/Money'
217
+
218
+ export class Product extends Model {
219
+ protected override casts: Readonly<Record<string, CastDefinition>> = { price: Money }
220
+ }
221
+ ```
222
+
223
+ Both methods also receive the model, the attribute's name and all its stored attributes.
224
+
225
+ ## Serialization
226
+
227
+ ### Hiding attributes from JSON
228
+
229
+ `hidden` lists the attributes left out of the model's `toArray()` and JSON, such as a password, and `visible` lists
230
+ the only ones left in:
231
+
232
+ ```ts
233
+ export class User extends Model {
234
+ protected override hidden = ['password', 'remember_token']
235
+ }
236
+ ```
237
+
238
+ `makeVisible()` and `makeHidden()` change the lists for one model: `user.makeVisible('password').toArray()`.
239
+
240
+ ### Appending values to JSON
241
+
242
+ An accessor with no column isn't in `toArray()` and JSON unless you add it to `appends`:
243
+
244
+ ```ts
245
+ export class User extends Model {
246
+ protected override appends = ['is_admin']
247
+
248
+ getIsAdminAttribute(): boolean {
249
+ return this.getAttribute('role') === 'admin'
250
+ }
251
+ }
252
+ ```
253
+
254
+ `append()` adds one for one model: `user.append('is_admin').toArray()`.
@@ -0,0 +1,145 @@
1
+ # Eloquent: observers
2
+
3
+ Models dispatch events as they are created, updated, deleted and restored. Listen for them with a closure, or group
4
+ the listeners for one model in an observer class.
5
+
6
+ ## Introduction
7
+
8
+ A model dispatches these events, each with the model:
9
+
10
+ | Event | When |
11
+ | --- | --- |
12
+ | `retrieved` | A model was read from the database |
13
+ | `creating`, `created` | A new model is about to be saved for the first time, and was |
14
+ | `updating`, `updated` | A model that exists is about to be saved with changes, and was |
15
+ | `saving`, `saved` | A model is about to be saved, new or not, changed or not, and was |
16
+ | `deleting`, `deleted` | A model is about to be deleted, or soft-deleted, and was |
17
+ | `restoring`, `restored` | A soft-deleted model is about to be restored, and was |
18
+ | `forceDeleting`, `forceDeleted` | A soft-deleted model is about to be deleted for good, and was |
19
+
20
+ Creating a model dispatches `saving`, `creating`, `created`, then `saved`; updating one dispatches `saving`,
21
+ `updating`, `updated`, then `saved`. Saving a model with no changes dispatches only `saving` and `saved`.
22
+
23
+ A listener of a `-ing` event that returns `false` stops the operation: the model isn't saved, deleted or restored.
24
+
25
+ > [!WARNING]
26
+ > A query's `update()` and `delete()`, such as `Post.where('active', 0).delete()`, write the rows directly: no model
27
+ > is read, and no event is dispatched.
28
+
29
+ ## Using closures
30
+
31
+ Register a closure for an event in the model's static `booted()`, with the method named after the event:
32
+
33
+ ```ts title="app/Models/User.ts"
34
+ import { Model } from '@orieljs/framework'
35
+
36
+ export class User extends Model {
37
+ /** The "booted" method of the model. */
38
+ static override booted(): void {
39
+ this.created((user) => {
40
+ // ...
41
+ })
42
+ }
43
+ }
44
+ ```
45
+
46
+ `booted()` runs once, the first time the model is used in your app.
47
+
48
+ ## Observers
49
+
50
+ ### Defining observers
51
+
52
+ An observer is a class whose methods are named after the events they handle. `make:observer` writes one in
53
+ `app/Observers`, with `created()`, `updated()` and `deleted()` methods; give their parameter your model's type:
54
+
55
+ ```sh
56
+ node oriel make:observer UserObserver
57
+ ```
58
+
59
+ ```ts title="app/Observers/UserObserver.ts"
60
+ import type { User } from '../Models/User'
61
+
62
+ export class UserObserver {
63
+ /** Handle the User "created" event. */
64
+ created(user: User): void {
65
+ // ...
66
+ }
67
+
68
+ /** Handle the User "updated" event. */
69
+ updated(user: User): void {
70
+ // ...
71
+ }
72
+
73
+ /** Handle the User "deleted" event. */
74
+ deleted(user: User): void {
75
+ // ...
76
+ }
77
+ }
78
+ ```
79
+
80
+ Register it with the model's `observe()`, in a service provider's `boot()`:
81
+
82
+ ```ts title="app/Providers/AppServiceProvider.ts"
83
+ // @filename: app/Observers/UserObserver.ts
84
+ export declare class UserObserver {}
85
+ // ---cut---
86
+ import { ServiceProvider } from '@orieljs/framework'
87
+ import { User } from '../Models/User'
88
+ import { UserObserver } from '../Observers/UserObserver'
89
+
90
+ export class AppServiceProvider extends ServiceProvider {
91
+ override boot(): void {
92
+ User.observe(UserObserver)
93
+ }
94
+ }
95
+ ```
96
+
97
+ Every method named after an event is registered. Oriel makes the observer through your app's container, so it can take
98
+ a service with `inject()`; an instance works too: `User.observe(new UserObserver())`.
99
+
100
+ A `-ing` method that returns `false` cancels the operation, as a closure does:
101
+
102
+ ```ts title="app/Observers/PostObserver.ts"
103
+ import { Str } from '@orieljs/framework'
104
+ import type { Post } from '../Models/Post'
105
+
106
+ export class PostObserver {
107
+ /** Give a new post its slug. */
108
+ creating(post: Post): void {
109
+ post.slug = Str.slug(post.title)
110
+ }
111
+
112
+ /** A published post can't be deleted. */
113
+ deleting(post: Post): boolean {
114
+ return post.published_at === null
115
+ }
116
+ }
117
+ ```
118
+
119
+ > [!NOTE]
120
+ > Laravel registers an observer with an attribute on the model (`#[ObservedBy]`), and can run its methods after the
121
+ > transaction commits (`ShouldHandleEventsAfterCommit`). Oriel registers observers with `observe()`, and runs them as
122
+ > the events happen. Laravel's `trashed` and `replicating` events aren't in Oriel.
123
+
124
+ ### Event names
125
+
126
+ A model event is dispatched as `eloquent.<event>: <Model>`, such as `eloquent.created: User`, so you can also listen
127
+ for it with `Event.listen()`. Laravel uses the model's full class name; Oriel uses the class's name.
128
+
129
+ ## Muting events
130
+
131
+ `Model.withoutEvents()` runs a function with model events off, for the code it runs:
132
+
133
+ ```ts
134
+ const user = await Model.withoutEvents(() => User.findOrFail(1))
135
+ ```
136
+
137
+ `saveQuietly()` and `deleteQuietly()` save or delete one model without events:
138
+
139
+ ```ts
140
+ const user = await User.findOrFail(1)
141
+
142
+ user.name = 'Victoria Faith'
143
+
144
+ await user.saveQuietly()
145
+ ```