dami 1.0.0

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +306 -0
  3. data/LICENSE +21 -0
  4. data/README.md +106 -0
  5. data/bin/dami +10 -0
  6. data/docs/01.Getting_Started.md +220 -0
  7. data/docs/02.Models_and_Fields.md +244 -0
  8. data/docs/03.Querying.md +387 -0
  9. data/docs/04.Creating_Updating_Deleting.md +226 -0
  10. data/docs/05.Validation.md +259 -0
  11. data/docs/06.Protection.md +160 -0
  12. data/docs/07.Associations.md +239 -0
  13. data/docs/08.Scopes.md +99 -0
  14. data/docs/09.Migrations.md +165 -0
  15. data/docs/10.Flows_and_Commands.md +181 -0
  16. data/docs/11.Localization.md +435 -0
  17. data/docs/12.TheDamiWay.md +227 -0
  18. data/docs/Manifesto.md +305 -0
  19. data/docs/site.md +477 -0
  20. data/lib/dami/actions/command.rb +80 -0
  21. data/lib/dami/actions/context.rb +63 -0
  22. data/lib/dami/actions/draft.rb +36 -0
  23. data/lib/dami/actions/flow.rb +42 -0
  24. data/lib/dami/adapters/base.rb +40 -0
  25. data/lib/dami/adapters/sqlite/connection.rb +122 -0
  26. data/lib/dami/adapters/sqlite/core.rb +17 -0
  27. data/lib/dami/adapters/sqlite/query.rb +353 -0
  28. data/lib/dami/adapters/sqlite/schema.rb +135 -0
  29. data/lib/dami/cli.rb +117 -0
  30. data/lib/dami/configuration.rb +246 -0
  31. data/lib/dami/core.rb +98 -0
  32. data/lib/dami/dsl_guardrails.rb +45 -0
  33. data/lib/dami/errors.rb +89 -0
  34. data/lib/dami/inflector.rb +245 -0
  35. data/lib/dami/localization.rb +131 -0
  36. data/lib/dami/migration.rb +84 -0
  37. data/lib/dami/migrator.rb +105 -0
  38. data/lib/dami/plugins/associations.rb +284 -0
  39. data/lib/dami/plugins/nested_attributes.rb +180 -0
  40. data/lib/dami/plugins/protection.rb +30 -0
  41. data/lib/dami/plugins/validations.rb +90 -0
  42. data/lib/dami/query/builder.rb +132 -0
  43. data/lib/dami/query/enumerable.rb +62 -0
  44. data/lib/dami/query/persistence.rb +133 -0
  45. data/lib/dami/record_proxy.rb +32 -0
  46. data/lib/dami/result.rb +27 -0
  47. data/lib/dami/schema/diff.rb +84 -0
  48. data/lib/dami/schema/dumper.rb +60 -0
  49. data/lib/dami/schema/generator.rb +69 -0
  50. data/lib/dami/schema/introspector.rb +34 -0
  51. data/lib/dami/schema/loader.rb +58 -0
  52. data/lib/dami/schema.rb +13 -0
  53. data/lib/dami/validation_rules.rb +72 -0
  54. data/lib/dami/version.rb +3 -0
  55. data/lib/dami.rb +32 -0
  56. metadata +218 -0
@@ -0,0 +1,226 @@
1
+ # Chapter 4: Creating, Updating, and Deleting
2
+
3
+ This chapter covers all operations that modify data in your database. Dami provides a simple yet secure API for creating, updating, and deleting records, with built-in protections against common vulnerabilities.
4
+
5
+ ## Creating Records
6
+
7
+ ### Creating a Single Record
8
+
9
+ The most common way to insert a new record is with the `.create` method. It accepts a hash of attributes and, if successful, returns a `RecordProxy` instance of the newly created record.
10
+
11
+ ```ruby
12
+ # Create a new user
13
+ new_user = Dami.db(:users).create(
14
+ name: 'Carol',
15
+ email: 'carol@example.com',
16
+ status: 'active'
17
+ )
18
+
19
+ puts new_user[:id] # => 43
20
+ puts new_user[:name] # => "Carol"
21
+ ```
22
+
23
+ If the data fails validation, `.create` will raise a `Dami::ValidationError` instead of creating the record.
24
+
25
+ ### Creating Multiple Records (Bulk Inserts) ✨
26
+
27
+ For inserting multiple records at once, always use `.create_many`. It is significantly more performant as it inserts all records with a single SQL statement. It accepts an array of hashes.
28
+
29
+ ```ruby
30
+ # Create two new posts in a single database query
31
+ posts_to_create = [
32
+ { user_id: 1, title: 'My First Post' },
33
+ { user_id: 1, title: 'My Second Post' }
34
+ ]
35
+
36
+ Dami.db(:posts).create_many(posts_to_create)
37
+ ```
38
+
39
+ ## Updating Records
40
+
41
+ The `.update` method is called on a query chain and modifies all records that match the preceding `where` conditions. It returns the first matching record (re-read after the update), so for multi-record updates treat the return value as a sample, not a summary. Validations run against the attributes you pass; conditional rules (`if:` / `unless:` / `when:`) see the first matching record as context.
42
+
43
+ ### Updating a Single Record
44
+
45
+ The most common pattern is to find a record by its ID and then update it.
46
+
47
+ ```ruby
48
+ # Find user with ID 1 and update their name
49
+ Dami.db(:users).where(id: 1).update(name: 'Alice Smith')
50
+
51
+ # You can also update multiple attributes at once
52
+ Dami.db(:users).where(id: 1).update(
53
+ name: 'Alice Smith',
54
+ email: 'alicesmith@example.com'
55
+ )
56
+ ```
57
+
58
+ ### Updating Multiple Records
59
+
60
+ You can efficiently update many records that match a condition in a single query.
61
+
62
+ ```ruby
63
+ # Promote all 'pending' users to 'active'
64
+ Dami.db(:users).where(status: 'pending').update(status: 'active')
65
+
66
+ # Archive all posts older than one year
67
+ Dami.db(:posts)
68
+ .where('published_at < ?', 1.year.ago)
69
+ .update(status: 'archived')
70
+ ```
71
+
72
+ ## Deleting Records
73
+
74
+ Similar to `update`, the `.delete` method is called on a query chain and removes all records that match the conditions.
75
+
76
+ ### Deleting a Single Record
77
+
78
+ ```ruby
79
+ # Delete the user with ID 42
80
+ Dami.db(:users).where(id: 42).delete
81
+ ```
82
+
83
+ ### Deleting Multiple Records
84
+
85
+ ```ruby
86
+ # Clean up all spam comments in one query
87
+ Dami.db(:comments).where(is_spam: true).delete
88
+
89
+ # Delete all posts by a specific user
90
+ Dami.db(:posts).where(user_id: 15).delete
91
+ ```
92
+
93
+ ## Security: Attribute Protection 🛡️
94
+
95
+ A critical feature of any ORM is protection against **mass-assignment vulnerabilities**. This happens when a user submits extra data in a form (e.g., `role: 'admin'`) and the application blindly saves it.
96
+
97
+ Dami protects you by default. You must explicitly define which attributes are sensitive using a `protection` block in your model.
98
+
99
+ ```ruby
100
+ # In your behavior definition
101
+ Dami.behavrior :users do
102
+ # ... fields ...
103
+ protection do
104
+ protect :role, :account_balance # These fields cannot be changed by default
105
+ end
106
+ end
107
+
108
+ # An attacker tries to make themselves an admin
109
+ malicious_params = { name: 'Eve', email: 'eve@hacker.com', role: 'admin' }
110
+
111
+ # This will fail with an error!
112
+ Dami.db(:users).where(id: 10).update(malicious_params)
113
+ # => raises Dami::ProtectionError: Protected fields not permitted: role
114
+ ```
115
+
116
+ To safely update protected attributes in controlled parts of your code (like an admin panel), you must use the `permit:` option.
117
+
118
+ ```ruby
119
+ # In an admin controller, you can safely permit the role change.
120
+ admin_params = { role: 'moderator' }
121
+ Dami.db(:users).where(id: 10).update(admin_params, permit: [:role]) # This now works
122
+ ```
123
+
124
+ ⚠️ For rare cases where you need to bypass all protections, you can use `protect: false`. Use this with extreme caution.
125
+
126
+ ```ruby
127
+ # Disables all protections for this single operation
128
+ Dami.db(:users).create(name: 'Root', role: 'super_admin', protect: false)
129
+ ```
130
+
131
+ ## Handling Validations
132
+
133
+ If `create` or `update` is called with data that violates a model's validation rules, a `Dami::ValidationError` is raised. You can rescue this error to handle the failure gracefully, for example, by re-rendering a form with error messages.
134
+
135
+ ```ruby
136
+ begin
137
+ # Attempt to create a user with a missing name
138
+ user_params = { email: 'test@example.com', status: 'active' }
139
+ Dami.db(:users).create(user_params)
140
+ rescue Dami::ValidationError => e
141
+ # The operation failed, and e.errors contains the details
142
+ puts e.errors # => { name: ["is required"] }
143
+
144
+ # You can now use this hash to display errors to the user
145
+ render_form_with_errors(e.errors)
146
+ end
147
+ ```
148
+
149
+ ## Nested Attributes
150
+
151
+ Dami provides a powerful way to create, update, and destroy associated records through a parent record in a single, atomic operation. This is particularly useful for complex forms where a user might edit a blog post and its comments at the same time.
152
+
153
+
154
+ ### Enabling Nested Attributes
155
+
156
+ To enable this feature, you must explicitly declare it on the parent model using the `nests` keyword. This is a security measure to prevent accidental modification of associations.
157
+
158
+ ```ruby
159
+ Dami.model :posts do
160
+ fields { field :title, :string }
161
+ relationships { has_many :comments }
162
+
163
+ # Enable nested attributes for the :comments association
164
+ nests :comments, allow_destroy: true
165
+ end
166
+
167
+ Dami.model :comments do
168
+ fields { field :post_id, :integer; field :content, :text }
169
+ relationships { belongs_to :post }
170
+ end
171
+ ````
172
+
173
+ The `allow_destroy: true` option is required if you want to allow nested records to be deleted.
174
+
175
+ ### Usage
176
+
177
+ You can now modify the parent and its children by passing a special `_attributes` key in your `create` or `update` call.
178
+
179
+ #### **Creating Records**
180
+
181
+ To create a parent with new children, provide an array of hashes.
182
+
183
+ ```ruby
184
+ @db[:posts].create(
185
+ title: 'My New Post',
186
+ comments_attributes: [
187
+ { content: 'This is the first comment.' },
188
+ { content: 'And this is the second.' }
189
+ ]
190
+ )
191
+ ```
192
+
193
+ #### **Updating and Destroying Records**
194
+
195
+ To update existing children or destroy them, you must provide their `id`. To mark a record for destruction, add a `_destroy: '1'` key.
196
+
197
+ ```ruby
198
+ @post = @db[:posts].find(1)
199
+ @comment_to_update = @post.comments.first
200
+ @comment_to_destroy = @post.comments.last
201
+
202
+ @db[:posts].where(id: @post[:id]).update(
203
+ title: 'An Updated Title',
204
+ comments_attributes: [
205
+ # To UPDATE, provide the ID
206
+ { id: @comment_to_update[:id], content: 'This comment has been edited.' },
207
+
208
+ # To CREATE, omit the ID
209
+ { content: 'This is a brand new comment.' },
210
+
211
+ # To DESTROY, provide the ID and _destroy flag
212
+ { id: @comment_to_destroy[:id], _destroy: '1' }
213
+ ]
214
+ )
215
+ ```
216
+
217
+ ### Key Guarantees
218
+
219
+ * **Atomicity**: The entire operation—saving the parent and all children—is wrapped in a single database transaction. If *any* part fails (e.g., a single child comment fails its validation), the **entire operation is rolled back**, and no records are saved or changed.
220
+ * **Validation**: All nested records are validated against their own model's rules. If any fail, a single `Dami::ValidationError` is raised with a nested hash of all errors from all records, perfect for displaying in a form.
221
+ * **Protection**: Nested records respect their own `protection` rules. You cannot change a protected attribute on a child model unless you use the `permit:` option on the top-level `update` call.
222
+
223
+ ## What's Next?
224
+
225
+ * **Chapter 5: Validation** - A deep dive into defining data quality rules.
226
+ * **Chapter 6: Protection** - Develop permit, protect, field-level security
@@ -0,0 +1,259 @@
1
+ # Chapter 5: Validation
2
+
3
+ Validation is the cornerstone of data integrity. Dami provides a powerful and declarative DSL to ensure that only valid data is saved to your database. If a record fails its validation rules, Dami will raise a `Dami::ValidationError` and prevent the `create` or `update` operation from proceeding.
4
+
5
+ **Validations live in `Dami.behavior`**, keeping write-side rules separate from structure.
6
+
7
+ ## Defining Validations
8
+
9
+ Validations are defined within a `validate` block in your `Dami.behavior` block. You use the `rule` method to apply one or more validation rules to a field.
10
+
11
+ ```ruby
12
+ Dami.model :users do
13
+ fields do
14
+ field :name, :string
15
+ field :email, :string
16
+ field :status, :string
17
+ end
18
+ end
19
+
20
+ Dami.behavior :users do
21
+ validate do
22
+ rule :name, :required
23
+ rule :email, :required, :email
24
+ rule :status, inclusion: %w[active inactive pending]
25
+ end
26
+ end
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Registering Validation Rules
32
+
33
+ Before you can use validation rules, register them in your initializer:
34
+
35
+ ```ruby
36
+ # config/initializers/dami.rb
37
+ Dami.connect(:default, adapter: :sqlite, path: 'db.sqlite')
38
+ Dami.register_default_rules!
39
+ ```
40
+
41
+ This registers these common rules:
42
+
43
+ | Rule | Description |
44
+ |---------------|----------------------------------------------------|
45
+ | `:required`, `:presence` | Ensures the value is not `nil` or an empty string. |
46
+ | `:email` | Checks for a valid email format. |
47
+ | `:inclusion` | Ensures the value is within the given list. |
48
+ | `:format` | Checks that the value matches a regex. |
49
+ | `:min_length` | Ensures the string has at least N characters. |
50
+ | `:max_length` | Ensures the string has at most N characters. |
51
+ | `:min` | Ensures the number is at least N. |
52
+ | `:max` | Ensures the number is at most N. |
53
+
54
+ ### Customizing Error Messages
55
+
56
+ **Global Message Overrides:**
57
+
58
+ Change error messages for all validations without modifying rules:
59
+
60
+ ```ruby
61
+ # config/initializers/dami.rb
62
+ Dami.register_default_rules!
63
+ Dami.override_messages :default, {
64
+ required: "cannot be blank",
65
+ email: "must be a valid email address",
66
+ min_length: ->(min) { "too short (minimum: #{min} characters)" }
67
+ }
68
+ ```
69
+
70
+ Messages are checked in this priority order:
71
+ 1. Custom rule's message (when registering a new rule)
72
+ 2. Global override (via `override_messages`)
73
+ 3. Default message (from `register_default_rules!`)
74
+
75
+ ### Custom Validation Rules
76
+
77
+ Register your own rules for domain-specific validations:
78
+
79
+ ```ruby
80
+ Dami.rules :default, {
81
+ phone: {
82
+ check: ->(v) { v.to_s =~ /\A\d{3}-\d{3}-\d{4}\z/ },
83
+ message: "must be a valid phone number (XXX-XXX-XXXX)"
84
+ },
85
+ url: {
86
+ check: ->(v) { v.to_s =~ /\Ahttps?:\/\// },
87
+ message: "must be a valid URL"
88
+ }
89
+ }
90
+ ```
91
+
92
+ **Parameterized Rules:**
93
+
94
+ For rules that take options, return a hash from a lambda:
95
+
96
+ ```ruby
97
+ Dami.rules :default, {
98
+ exact_length: ->(len) {
99
+ {
100
+ check: ->(v) { v.to_s.length == len },
101
+ message: "must be exactly #{len} characters"
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ```ruby
108
+ # Usage
109
+ Dami.behavior :users do
110
+ validate do
111
+ rule :zip_code, exact_length: 5
112
+ end
113
+ end
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Validating Multiple Fields
119
+
120
+ The `all` helper applies the same rules to multiple fields at once:
121
+
122
+ ```ruby
123
+ Dami.behavior :users do
124
+ validate do
125
+ # Apply :required to all fields except :age
126
+ on :create do
127
+ all except: [:age] do
128
+ rule :required
129
+ end
130
+ end
131
+ end
132
+ end
133
+ ```
134
+
135
+ **Options:**
136
+ - `only: [:field1, :field2]` - Apply rules only to these fields
137
+ - `except: [:field3]` - Apply rules to all fields except these
138
+ - `**kwargs` - Apply rules directly: `all presence: true, min_length: 3`
139
+
140
+ ```ruby
141
+ Dami.behavior :users do
142
+ validate do
143
+ # Apply presence validation to specific fields
144
+ all only: [:name, :email], presence: true
145
+
146
+ # Apply multiple rules to all fields except one
147
+ all except: [:bio] do
148
+ rule :required
149
+ rule :min_length, 3
150
+ end
151
+ end
152
+ end
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Contextual Validation
158
+
159
+ Sometimes a rule should only apply during a specific operation. You can scope rules to `:create` or `:update` using an `on` block.
160
+
161
+ ```ruby
162
+ Dami.behavior :users do
163
+ validate do
164
+ rule :name, :required
165
+
166
+ on :create do
167
+ rule :email, :required
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Conditional Validation
176
+
177
+ For more complex logic, you can make a validation conditional based on the values of other attributes.
178
+
179
+ ### Using `if:` and `unless:`
180
+
181
+ Use `if:` or `unless:` with a lambda (`->`) for dynamic conditions. The lambda receives a hash of all the attributes being updated.
182
+
183
+ ```ruby
184
+ Dami.behavior :users do
185
+ validate do
186
+ rule :ssn, :required, if: ->(attrs) { attrs[:account_type] == 'business' }
187
+ end
188
+ end
189
+ ```
190
+
191
+ ### Using `when:`
192
+
193
+ For simple equality checks, `when:` provides a convenient shortcut. The rule only runs if the attributes in the `when` hash match the record's attributes.
194
+
195
+ ```ruby
196
+ Dami.behavior :orders do
197
+ validate do
198
+ rule :shipping_address, :required, when: { delivery_type: 'physical' }
199
+ end
200
+ end
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Validating Virtual Fields
206
+
207
+ A common use case for validation is to check fields that you don't actually save to the database, like a password confirmation. These are called **virtual fields**.
208
+
209
+ ```ruby
210
+ Dami.model :users do
211
+ fields do
212
+ field :password_hash, :string
213
+ end
214
+ virtual do
215
+ field :password, :string
216
+ field :password_confirmation, :string
217
+ end
218
+ end
219
+
220
+ Dami.behavior :users do
221
+ validate do
222
+ rule :password, :required, min_length: 8
223
+ rule :password_confirmation do |confirmation, attrs|
224
+ confirmation == attrs[:password]
225
+ end
226
+ end
227
+ end
228
+ ```
229
+
230
+ In this example, `password` and `password_confirmation` are accepted as input and validated, but only the `password_hash` (which you would generate from the password) is saved to the database.
231
+
232
+ ---
233
+
234
+ ## Handling Errors
235
+
236
+ When a validation fails, Dami raises a `Dami::ValidationError`. You can `rescue` this error to access a hash of all the validation failures.
237
+
238
+ ```ruby
239
+ begin
240
+ Dami.db(:users).create(name: '', email: 'invalid-email')
241
+ rescue Dami::ValidationError => e
242
+ puts e.message # => "Validation failed"
243
+
244
+ puts e.errors
245
+ # => {
246
+ # :name=>["is required"],
247
+ # :email=>["must be valid email"]
248
+ # }
249
+ end
250
+ ```
251
+
252
+ This error hash is designed to be easily used to display feedback to a user in a web form or API response.
253
+
254
+ ---
255
+
256
+ ## What's Next?
257
+
258
+ * **Chapter 6: Protection** - Learn how to secure your models from mass-assignment vulnerabilities
259
+ * **Chapter 7: Associations** - Define and query relationships between your models
@@ -0,0 +1,160 @@
1
+ # Chapter 6: Protection
2
+
3
+ Data security is not optional. A common web application vulnerability is **mass-assignment**, where a malicious user submits unexpected fields in a form—like `role: 'admin'`—and the application blindly saves them. Dami prevents this by default through a simple and powerful attribute protection system.
4
+
5
+ **Protection rules live in `Dami.behavior`**, keeping security policies separate from structure.
6
+
7
+ ## The Problem: Mass-Assignment
8
+
9
+ Imagine a user signs up with the following data:
10
+
11
+ ```ruby
12
+ malicious_params = {
13
+ name: 'Eve',
14
+ email: 'eve@hacker.com',
15
+ role: 'admin' # Uh oh!
16
+ }
17
+
18
+ # If the app is not secure, this might create an admin user!
19
+ Dami.db(:users).create(malicious_params)
20
+ ```
21
+
22
+ Without protection, this simple action could grant unauthorized administrative access. Dami's protection layer is designed to stop this.
23
+
24
+ ---
25
+
26
+ ## Protecting Attributes
27
+
28
+ You declare which attributes are sensitive using the `protect` method inside a `protection` block in your `Dami.behavior` block.
29
+
30
+ ```ruby
31
+ Dami.model :users do
32
+ fields do
33
+ field :name, :string
34
+ field :email, :string
35
+ field :role, :string
36
+ end
37
+ end
38
+
39
+ Dami.behavior :users do
40
+ protection do
41
+ protect :role # Cannot be mass-assigned
42
+ end
43
+ end
44
+ ```
45
+
46
+ Now, if someone attempts the same malicious action, Dami will raise an error and the operation will fail, keeping your application secure.
47
+
48
+ ```ruby
49
+ malicious_params = { name: 'Eve', role: 'admin' }
50
+
51
+ Dami.db(:users).create(malicious_params)
52
+ # => raises Dami::ProtectionError: Protected fields not permitted: role
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Permitting Attributes
58
+
59
+ In addition to `protect`, you can use `permit` to explicitly allow certain fields. This is useful for fields that are safe for users to modify:
60
+
61
+ ```ruby
62
+ Dami.behavior :users do
63
+ protection do
64
+ protect :role # Always blocked
65
+ permit :status # Always allowed
66
+ end
67
+ end
68
+ ```
69
+
70
+ ```ruby
71
+ # This works - status is permitted
72
+ Dami.db(:users).create(name: 'Alice', status: 'active')
73
+
74
+ # This fails - role is protected
75
+ Dami.db(:users).create(name: 'Eve', role: 'admin')
76
+ # => Dami::ProtectionError
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Safely Updating Protected Fields
82
+
83
+ There are legitimate times when you need to change a protected attribute, such as in an admin panel or a background job. To do this safely, you must explicitly grant permission for the operation using the `permit:` option.
84
+
85
+ The `permit:` option takes an array of symbols representing the protected fields you are allowing to be changed for this **single operation**.
86
+
87
+ ```ruby
88
+ # In an admin controller, an authorized user is changing another user's role
89
+ user_id = 42
90
+ update_params = { role: 'moderator' }
91
+
92
+ # The permit option grants temporary permission
93
+ Dami.db(:users)
94
+ .where(id: user_id)
95
+ .update(update_params, permit: [:role]) # ✅ This is safe and will succeed
96
+ ```
97
+
98
+ This makes your security intentions explicit. You are telling Dami, "For this one call, I know what I'm doing and I am intentionally modifying a protected field."
99
+
100
+ ---
101
+
102
+ ## Bypassing All Protections (Use with Caution)
103
+
104
+ In very rare circumstances, like in a seed script or a system migration, you may need to bypass all protections entirely. For this, you can use the `protect: false` option.
105
+
106
+ ⚠️ **This is the nuclear option.** It disables all `protect` rules for the operation. Use it with extreme care and never with parameters coming directly from a user.
107
+
108
+ ```ruby
109
+ # Creating a system-level user during initial setup
110
+ Dami.db(:users).create(
111
+ name: 'Root',
112
+ role: 'super_admin',
113
+ status: 'active',
114
+ protect: false # Disables all protections for this one call
115
+ )
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Protection in Batch Operations
121
+
122
+ Protection is enforced in batch operations like `create_many`:
123
+
124
+ ```ruby
125
+ Dami.behavior :users do
126
+ protection do
127
+ protect :role
128
+ end
129
+ end
130
+
131
+ # This will raise Dami::ProtectionError
132
+ Dami.db(:users).create_many([
133
+ { name: 'Alice', role: 'admin' },
134
+ { name: 'Bob', role: 'admin' }
135
+ ])
136
+
137
+ # To allow it, use permit:
138
+ Dami.db(:users).create_many([
139
+ { name: 'Alice', role: 'admin' },
140
+ { name: 'Bob', role: 'admin' }
141
+ ], permit: [:role])
142
+ ```
143
+
144
+ **Important:** `create_many` validates all records before any database writes. If any record fails protection or validation, the entire batch is rejected atomically.
145
+
146
+ ---
147
+
148
+ ## Best Practices
149
+
150
+ * **Protect by Default**: Be aggressive with the `protect` rule. If an attribute should not be changed by a regular user, protect it. Common examples include `role`, `is_admin`, `account_balance`, `verified`
151
+ * **Use `permit` for Safe Fields**: Explicitly mark fields that are always safe to modify, like `status` or user preferences
152
+ * **Permit Explicitly in Code**: Use the `permit: [...]` option in the specific, secure parts of your code (like admin controllers or service objects) where you intend to modify sensitive data
153
+ * **Avoid `protect: false`**: Reserve `protect: false` for trusted, internal scripts where you have full control over the input data
154
+
155
+ ---
156
+
157
+ ## What's Next?
158
+
159
+ * **Chapter 7: Associations** - Learn how to define, query, and manage relationships between your models
160
+ * **Chapter 8: Scopes** - Create reusable query shortcuts to keep your code clean and readable