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,239 @@
1
+ # Chapter 7: Associations
2
+
3
+ Associations are powerful shortcuts that link your models together, allowing you to easily query and interact with related records. Instead of writing manual joins and foreign key lookups, you can navigate your data's relationships intuitively.
4
+
5
+ All associations are **lazy-loaded** by default, meaning the related data is only fetched from the database when you first access it.
6
+
7
+ ## One-to-Many Relationships
8
+
9
+ This is the most common relationship type, connecting one model to many others.
10
+
11
+ ### `belongs_to`
12
+
13
+ This association indicates that a model has a foreign key that points to another model. For example, a `Post` belongs to a `User`. This assumes your `posts` table has a `user_id` column.
14
+
15
+ #### **Definition**
16
+
17
+ ```ruby
18
+ Dami.model :posts do
19
+ fields { field :user_id, :integer; field :title, :string }
20
+ relationships do
21
+ belongs_to :user
22
+ end
23
+ end
24
+ ```
25
+
26
+ #### **Usage**
27
+
28
+ When you access the association, Dami automatically runs a query to find the related record.
29
+
30
+ ```ruby
31
+ post = Dami.db(:posts).find(1)
32
+
33
+ # This triggers a new query: SELECT * FROM users WHERE id = ... LIMIT 1
34
+ author = post.user
35
+
36
+ puts author[:name] # => "Alice"
37
+ ```
38
+
39
+ ✍️
40
+
41
+ -----
42
+
43
+ ### `has_many`
44
+
45
+ This is the inverse of `belongs_to`. It indicates that a model can be associated with many instances of another model. A `User` has many `Posts`.
46
+
47
+ #### **Definition**
48
+
49
+ ```ruby
50
+ Dami.model :users do
51
+ fields { field :name, :string }
52
+ relationships do
53
+ has_many :posts
54
+ end
55
+ end
56
+ ```
57
+
58
+ #### **Usage**
59
+
60
+ Accessing a `has_many` association returns a `Query::Builder` instance, allowing you to chain additional queries before fetching the data.
61
+
62
+ ```ruby
63
+ user = Dami.db(:users).find(1)
64
+
65
+ # This does NOT run a query yet. It returns a query builder.
66
+ user_posts_query = user.posts
67
+
68
+ # Now you can chain more conditions...
69
+ published_posts = user_posts_query.where(status: 'published').to_a
70
+
71
+ # Or just get them all. This executes the query.
72
+ all_posts = user.posts.to_a
73
+ puts all_posts.length # => 5
74
+ ```
75
+
76
+ -----
77
+
78
+ ## One-to-One Relationships
79
+
80
+ ### `has_one`
81
+
82
+ A `has_one` association is a special case where a model is associated with at most one other record. For example, a `User` has one `Profile`. This assumes your `profiles` table has a `user_id` column.
83
+
84
+ #### **Definition**
85
+
86
+ ```ruby
87
+ Dami.model :users do
88
+ fields { field :name, :string }
89
+ relationships do
90
+ has_one :profile
91
+ end
92
+ end
93
+ ```
94
+
95
+ #### **Usage**
96
+
97
+ ```ruby
98
+ user = Dami.db(:users).find(1)
99
+
100
+ # This triggers a query: SELECT * FROM profiles WHERE user_id = ... LIMIT 1
101
+ user_profile = user.profile
102
+
103
+ puts user_profile[:bio] # => "Loves writing about Dami."
104
+ ```
105
+
106
+ -----
107
+
108
+ ## Eager Loading with `.preload`
109
+
110
+ Lazy loading is convenient, but it can lead to a major performance issue known as the **N+1 Query Problem**.
111
+
112
+ ⚠️ **The N+1 Problem:** Imagine you fetch 10 posts and then loop through them to print the author's name.
113
+
114
+ ```ruby
115
+ posts = Dami.db(:posts).limit(10).to_a # 1 query
116
+
117
+ posts.each do |post|
118
+ puts post.user[:name] # N (10) additional queries! One for each post.
119
+ end
120
+ # Total Queries: 1 + 10 = 11
121
+ ```
122
+
123
+ This is incredibly inefficient. The solution is **eager loading**. Use `.preload` to tell Dami to fetch the associated data in advance using the minimum number of queries.
124
+
125
+ ```ruby
126
+ # This runs only TWO queries, no matter how many posts there are.
127
+ posts = Dami.db(:posts).preload(:user).limit(10).to_a
128
+ # Query 1: SELECT * FROM posts LIMIT 10
129
+ # Query 2: SELECT * FROM users WHERE id IN (1, 2, 5, ...)
130
+
131
+ posts.each do |post|
132
+ # No new query is run here! The user data is already loaded.
133
+ puts post.user[:name]
134
+ end
135
+ # Total Queries: 2
136
+ ```
137
+
138
+ You can preload multiple associations at once:
139
+ `Dami.db(:users).preload(:posts, :profile).to_a`
140
+
141
+ -----
142
+
143
+ ## Many-to-Many Relationships
144
+
145
+ ### `has_many :through`
146
+
147
+ This association is used for many-to-many relationships, which require a third "join" table. For example, a `Post` can have many `Tags`, and a `Tag` can be on many `Posts`. The connection is made through a `posts_tags` table.
148
+
149
+ #### **Definition**
150
+
151
+ You must define the associations on all three models.
152
+
153
+ ```ruby
154
+ Dami.model :posts do
155
+ relationships do
156
+ has_many :posts_tags
157
+ has_many :tags, through: :posts_tags
158
+ end
159
+ end
160
+
161
+ Dami.model :tags do
162
+ relationships do
163
+ has_many :posts_tags
164
+ has_many :posts, through: :posts_tags
165
+ end
166
+ end
167
+
168
+ Dami.model :posts_tags do # The join model
169
+ relationships do
170
+ belongs_to :post
171
+ belongs_to :tag
172
+ end
173
+ end
174
+ ```
175
+
176
+ #### **Usage**
177
+
178
+ You can now access the association directly, and Dami handles the join automatically.
179
+
180
+ ```ruby
181
+ post = Dami.db(:posts).find(1)
182
+ puts post.tags.map { |t| t[:name] } # => ["ruby", "orm"]
183
+
184
+ tag = Dami.db(:tags).find(5)
185
+ puts tag.posts.count # => 42
186
+ ```
187
+
188
+ -----
189
+
190
+ ## Polymorphic Associations
191
+
192
+ A polymorphic association allows a model to belong to more than one type of other model on a single association. A classic example is a `Comment` which can belong to a `Post`, an `Article`, or a `Video`.
193
+
194
+ This requires two columns on the "belonging" model: `commentable_id` (an integer) and `commentable_type` (a string like "Post" or "Article").
195
+
196
+ #### **Definition**
197
+
198
+ ```ruby
199
+ # The polymorphic model
200
+ Dami.model :comments do
201
+ relationships do
202
+ belongs_to commentable: { polymorphic: true }
203
+ end
204
+ end
205
+
206
+ # The models that can be commented on
207
+ Dami.model :posts do
208
+ relationships do
209
+ has_many comments: { as: :commentable, model: :comments }
210
+ end
211
+ end
212
+
213
+ Dami.model :articles do
214
+ relationships do
215
+ has_many comments: { as: :commentable, model: :comments }
216
+ end
217
+ end
218
+ ```
219
+
220
+ #### **Usage**
221
+
222
+ Dami automatically figures out which type of record to fetch.
223
+
224
+ ```ruby
225
+ comment = Dami.db(:comments).find(1) # This one is on a Post
226
+ parent = comment.commentable
227
+ puts parent[:title] # => "My Awesome Post"
228
+
229
+ comment2 = Dami.db(:comments).find(2) # This one is on an Article
230
+ parent2 = comment2.commentable
231
+ puts parent2[:headline] # => "A Great Article"
232
+ ```
233
+
234
+
235
+
236
+ ## What's Next?
237
+
238
+ * **Chapter 8: Scopes** - Create reusable query shortcuts to keep your code clean and readable.
239
+ * **Chapter 9: Migrations** - Learn how to manage your database schema over time.
data/docs/08.Scopes.md ADDED
@@ -0,0 +1,99 @@
1
+ # Chapter 8: Scopes
2
+
3
+ Scopes are named query shortcuts that let you encapsulate commonly used query logic directly into your model definitions. They allow you to turn complex, repetitive query chains into clean, readable, and reusable methods.
4
+
5
+ Think of scopes as custom filters for your models. Instead of writing `.where(status: 'active')` everywhere, you can simply call `.active`.
6
+
7
+ -----
8
+
9
+ ## Defining Scopes
10
+
11
+ Scopes are defined within a `scopes` block in your model using the `.scope` method. Each scope takes a name and a lambda that returns a `Query::Builder` instance.
12
+
13
+ ```ruby
14
+ Dami.scopes :posts do
15
+ # A simple scope without arguments
16
+ scope :published, -> { where(status: 'published') }
17
+
18
+ # A scope that chains other query methods
19
+ scope :recent, -> { order(created_at: :desc).limit(10) }
20
+
21
+ end
22
+ ```
23
+
24
+ -----
25
+
26
+ ## Using Scopes
27
+
28
+ Once defined, a scope can be called just like any other method on your query builder.
29
+
30
+ ```ruby
31
+ # Get all published posts
32
+ published_posts = Dami.db(:posts).published.to_a
33
+
34
+ # Get the 10 most recent posts
35
+ recent_posts = Dami.db(:posts).recent.to_a
36
+ ```
37
+
38
+ ### Chaining Scopes 🔗
39
+
40
+ The real power of scopes comes from chaining them together. Dami combines them using `AND`, allowing you to compose complex queries from simple, reusable parts.
41
+
42
+ ```ruby
43
+ # Get the 10 most recent PUBLISHED posts
44
+ recent_published = Dami.db(:posts).published.recent.to_a
45
+ ```
46
+
47
+ This is far more readable than chaining the underlying `where`, `order`, and `limit` calls every time.
48
+
49
+ -----
50
+
51
+ ## Scopes with Arguments
52
+
53
+ Scopes can also accept arguments, making them even more flexible. The arguments you pass to the scope method will be passed directly to the lambda.
54
+
55
+ #### **Definition**
56
+
57
+ ```ruby
58
+ Dami.scopes :users do
59
+
60
+ # Find users by a specific status
61
+ scope :by_status, ->(status) { where(status: status) }
62
+
63
+ # Find users created after a certain date
64
+ scope :created_after, ->(date) { where('created_at > ?', date) }
65
+
66
+ end
67
+ ```
68
+
69
+ #### **Usage**
70
+
71
+ ```ruby
72
+ # Get all pending users
73
+ pending_users = Dami.db(:users).by_status('pending').to_a
74
+
75
+ # Get users created in the last week and chain it with another scope
76
+ recent_active_users = Dami.db(:users)
77
+ .active # Assuming an :active scope exists
78
+ .created_after(1.week.ago)
79
+ .to_a
80
+ ```
81
+
82
+ -----
83
+
84
+ ## Best Practices & Why Use Scopes?
85
+
86
+ Scopes are a fundamental tool for writing clean and maintainable data-access code.
87
+
88
+ * **DRY (Don't Repeat Yourself)**: Scopes centralize your query logic. If the definition of a "published" post changes (e.g., to `status IN ('live', 'promoted')`), you only need to update the scope in one place, and your entire application will reflect the change.
89
+
90
+ * **Readability**: Scopes make your code self-documenting. `Dami.db(:posts).published.recent` is immediately understandable, whereas the underlying chain of `where` and `order` clauses is not.
91
+
92
+ * **Composability**: Scopes are like building blocks. You can define small, focused scopes and chain them together to build sophisticated queries without duplicating logic.
93
+
94
+ -----
95
+
96
+ ## What's Next?
97
+
98
+ * **Chapter 9: Migrations** - Learn how to manage your database schema over time with versioned changes.
99
+ * **Chapter 10: Advanced Topics** - Explore transactions, flows, and other powerful features.
@@ -0,0 +1,165 @@
1
+ # Chapter 9: Migrations
2
+
3
+ Migrations are the definitive history of your database schema, allowing you to evolve its structure in a safe, repeatable, and collaborative way. Dami provides a powerful migrator and an **intelligent migration generator** that automates the most tedious parts of schema management, letting you focus on your models.
4
+
5
+ -----
6
+
7
+ ## The Magic Workflow: Let Dami Write Your Migrations ✨
8
+
9
+ With Dami, you rarely need to write migrations by hand. The primary workflow is simple and incredibly fast:
10
+
11
+ 1. **Modify Your Models**: Make changes directly in your `Dami.model` definitions. Add a field, remove a field, or add a `belongs_to` relationship.
12
+ 2. **Generate the Migration**: Run a single command in your terminal.
13
+ 3. **Review and Migrate**: Inspect the auto-generated migration file, then run it.
14
+
15
+ #### **Example: Adding an `age` Column to Users**
16
+
17
+ **Step 1: Edit the Model**
18
+ You decide `users` need an `age` column. You simply add it to the `fields` block.
19
+
20
+ ```ruby
21
+ # app/models/user.rb
22
+ Dami.model :users do
23
+ fields do
24
+ field :name, :string
25
+ field :email, :string
26
+ field :age, :integer # <-- You add this line
27
+ end
28
+ end
29
+ ```
30
+
31
+ **Step 2: Generate the Migration**
32
+ Run the `generate` command from your terminal.
33
+
34
+ ```bash
35
+ dami generate migration AddAgeToUsers
36
+ ```
37
+
38
+ Dami introspects your models, compares them to the last known schema (`db/schema.rb`), and detects that the `age` column is missing. It then writes the perfect migration file for you.
39
+
40
+ ```
41
+ ✅ New migration created: db/migrations/20251015194500_add_age_to_users.rb
42
+ ```
43
+
44
+ The generated file looks like this:
45
+
46
+ ```ruby
47
+ # db/migrations/20251015194500_add_age_to_users.rb
48
+ # This file is auto-generated.
49
+
50
+ def up
51
+ add_column(:users, :age, :integer)
52
+ end
53
+
54
+ def down
55
+ remove_column(:users, :age)
56
+ end
57
+ ```
58
+
59
+ **Step 3: Run the Migration**
60
+ The generated file is perfect. Now, just apply it to your database.
61
+
62
+ ```bash
63
+ dami db migrate
64
+ ```
65
+
66
+ That's it\! Your database is now in sync with your models. This workflow also handles removing columns, adding tables, and even **automatically creating indexes** when you add a `belongs_to` relationship.
67
+
68
+ -----
69
+
70
+ ## The CLI Commands 🚀
71
+
72
+ All migration and schema tasks are handled through the `dami` executable.
73
+
74
+ | Command | Description |
75
+ |----------------------------------|--------------------------------------------------------------------------|
76
+ | `dami db migrate` | Runs all pending migrations and updates `db/schema.rb`. |
77
+ | `dami db rollback [STEPS]` | Reverts the last migration (or the last `STEPS` migrations). |
78
+ | `dami generate migration NAME` | **(Your primary tool)** Generates a new migration from model changes. |
79
+ | `dami db schema_dump` | Manually regenerates `db/schema.rb` from the current database state. |
80
+
81
+ -----
82
+
83
+ ## Manual Migrations
84
+
85
+ For complex changes that the generator can't infer—like data migrations, complex constraints, or custom SQL—you can always write a migration by hand.
86
+
87
+ 1. Create a new file: `db/migrations/TIMESTAMP_do_something_special.rb`.
88
+ 2. Write your `up` and `down` methods using the Schema DSL.
89
+
90
+ ### The Schema DSL
91
+
92
+ The DSL provides simple methods for all common schema operations.
93
+
94
+ | Method | Example Usage |
95
+ |------------------|--------------------------------------------------|
96
+ | `create_table` | `create_table(:posts) { \|t\| t.field ... }` |
97
+ | `drop_table` | `drop_table(:posts)` |
98
+ | `add_column` | `add_column(:users, :status, :string)` |
99
+ | `remove_column` | `remove_column(:users, :status)` |
100
+ | `add_index` | `add_index(:posts, :user_id)` / `add_index(:users, :email, unique: true)` |
101
+ | `remove_index` | `remove_index(:posts, :user_id)` |
102
+ | `execute(sql)` | Runs a raw SQL command for special cases. |
103
+
104
+ ### Column options
105
+
106
+ Every `t.field` (and `add_column`) accepts these options, which become real SQL constraints:
107
+
108
+ | Option | Effect |
109
+ |---------------------------|------------------------------------------------------------|
110
+ | `null: false` | `NOT NULL` |
111
+ | `unique: true` | `UNIQUE` |
112
+ | `default: value` | `DEFAULT value` (`default: :current_timestamp` for `CURRENT_TIMESTAMP`) |
113
+ | `references: :users` | `REFERENCES users(id)` — enforced, Dami turns foreign keys on |
114
+ | `on_delete: :cascade` | Adds `ON DELETE CASCADE` to a `references:` column |
115
+
116
+ ```ruby
117
+ create_table(:expenses) do |t|
118
+ t.field :id, :primary_key
119
+ t.field :amount_cents, :integer, null: false
120
+ t.field :currency, :string, null: false, default: 'EUR'
121
+ t.field :note, :text
122
+ t.references :batch, on_delete: :cascade # batch_id INTEGER REFERENCES batches(id) ON DELETE CASCADE
123
+ t.timestamps # created_at / updated_at, filled automatically
124
+ end
125
+ add_index(:expenses, :batch_id)
126
+ ```
127
+
128
+ Violations raise `Dami::NotNullViolation`, `Dami::UniqueConstraintViolation` (with `.column`) and `Dami::ForeignKeyViolation`.
129
+
130
+ **SQLite limits on `add_column`:** SQLite cannot add a `UNIQUE` or `PRIMARY KEY` column to an existing table, and a `NOT NULL` column added later needs a `default:`. Put those constraints in `create_table`, or add the column and then an index with `add_index(..., unique: true)`.
131
+
132
+ ### Types
133
+
134
+ | Type(s) | SQL type |
135
+ |----------------------------------|------------|
136
+ | `:primary_key` | `INTEGER PRIMARY KEY AUTOINCREMENT` |
137
+ | `:integer`, `:boolean` | `INTEGER` |
138
+ | `:float`, `:decimal` | `REAL` |
139
+ | `:datetime`, `:date`, `:time` | `DATETIME` |
140
+ | `:string`, `:text`, `:json` | `TEXT` |
141
+
142
+ Values come back from SQLite as they are stored: booleans are converted for fields declared `:boolean`, JSON is parsed for fields declared `:json`, dates and times come back as strings (`'2026-09-12'`, `'2026-09-12 18:40:00'` UTC).
143
+
144
+ -----
145
+
146
+ ## The Role of `db/schema.rb`
147
+
148
+ This file is a critical part of the generator. It's an auto-generated snapshot of your database's canonical structure after the last successful migration.
149
+
150
+ * **DO NOT EDIT THIS FILE MANUALLY.**
151
+ * Dami uses it as the "before" picture to compare against your "after" models.
152
+ * It's automatically updated for you every time you run `dami db:migrate`.
153
+ * You should commit this file to your version control system (Git).
154
+
155
+ -----
156
+
157
+ ## Best Practices
158
+
159
+ * **Trust the Generator First**: Always try to use `dami generate migration` as your primary workflow. It's faster and less error-prone.
160
+ * **Write Manual Migrations for Data Changes**: The generator handles structure (schema), but you should write manual migrations for data changes (e.g., backfilling a `status` column for existing users).
161
+ * **Never Edit a Run Migration**: Once a migration has been run on other machines, do not edit it. Create a *new* migration to make further changes.
162
+
163
+ ## What's Next?
164
+
165
+ * **Chapter 10: Advanced Topics** - Explore transactions, flows, and other powerful features for building complex application logic.
@@ -0,0 +1,181 @@
1
+ ## Chapter 10: Drafts, Flows & Commands
2
+
3
+ In most frameworks, handling business logic is an afterthought. It gets mixed into models, controllers, or scattered across service objects. Dami treats business logic as a first-class citizen with a simple, powerful philosophy: the **"Spectrum of Complexity."**
4
+
5
+ A great framework shouldn't give you a sledgehammer for every task. It should give you a full toolbox. Dami provides the perfect tool for every job, scaling gracefully from a simple one-liner to a complex, orchestrated workflow.
6
+
7
+ This chapter will guide you through the three tiers of Dami's business logic layer.
8
+
9
+ -----
10
+
11
+ ### Tier 1: The Foot Soldier - Simple, Direct CRUD
12
+
13
+ **When to use it:** For trusted, internal operations where no complex validation is needed. An admin script, a database seed, or a simple counter update.
14
+
15
+ For these tasks, Dami gets out of your way. The tool is a direct, one-liner database command. There is no ceremony.
16
+
17
+ ```ruby
18
+ # Fast, simple, and explicit.
19
+ # No extra layers needed for trusted operations.
20
+
21
+ # Activate a user
22
+ db(:users).where(id: 1337).update(status: 'active')
23
+
24
+ # Log a user's last login time
25
+ db(:users).find(42).update(last_seen_at: Time.now)
26
+
27
+ # Delete archived records
28
+ db(:orders).where(status: 'archived').delete
29
+ ```
30
+
31
+ This is the baseline. It's clean, performant, and the right tool for simple jobs.
32
+
33
+ -----
34
+
35
+ ### Tier 2: The Elite Guard - Inline Drafts
36
+
37
+ **When to use it:** For the 80% of typical web controller actions. A user updates their profile, submits a form, or changes a setting. The input is untrusted and requires contextual validation and transformation.
38
+
39
+ For these tasks, Dami provides an **inline `draft` block** on `create`, `update`, and `delete`. This is a massive leap in power with minimal ceremony. It gives you a temporary `draft` object to prepare and verify your data within a single, atomic transaction.
40
+
41
+ ```ruby
42
+ # app/controllers/accounts_controller.rb
43
+ def update
44
+ # ...
45
+ # The block yields a `draft` object. The entire block is one transaction.
46
+ updated_account = db(:accounts).where(id: params[:id]).update(params) do |draft|
47
+ # --- The Inline Draft DSL ---
48
+
49
+ # 1. Use declarative helpers for common patterns.
50
+ draft.prevent_changes :subdomain, message: "Subdomain cannot be changed."
51
+
52
+ # 2. Apply reusable transformations.
53
+ draft.prepare(AccountPolicies::NORMALIZE_NAME)
54
+
55
+ # 3. Add ad-hoc, contextual validation.
56
+ draft.verify :name_is_unique, error: "This name is already taken." do
57
+ # `changed?` and `original` are available helpers.
58
+ !draft.changed?(:name) || db(:accounts).where(name: draft.get(:name)).none?
59
+ end
60
+ end
61
+
62
+ if updated_account
63
+ # The block passed, and the update was successful.
64
+ redirect_to account_path, notice: "Account updated!"
65
+ else
66
+ # The block failed validation and halted. `update` returns nil.
67
+ # The database was never touched.
68
+ render :edit, status: :unprocessable_entity
69
+ end
70
+ end
71
+ ```
72
+
73
+ This is the workhorse of your application. It provides contextual validation and transformation right where it's needed, using a consistent, declarative DSL.
74
+
75
+ -----
76
+
77
+ ### Tier 3: The Final Boss - Commands & Orchestration
78
+
79
+ **When to use it:** For the 20% of critical, multi-step business processes. A user signup, an e-commerce checkout, a subscription cancellation. These workflows involve multiple database tables, external API calls, and side effects that **cannot be rolled back**.
80
+
81
+ For these tasks, Dami provides two powerful tools: **Commands** and **`Dami.run`**.
82
+
83
+ #### The Command (The Brains 🧠)
84
+
85
+ A `Dami::Command` encapsulates complex validation and transformation logic into a single, reusable, and testable class. It's how you turn the logic from an inline block into a formal, named "recipe."
86
+
87
+ ```ruby
88
+ # app/commands/create_account_and_owner_command.rb
89
+ class CreateAccountAndOwnerCommand < Dami::Command
90
+ # Fails early if `signup_source` is not provided in the context.
91
+ requires_context :signup_source
92
+
93
+ # Declarative validation rules for the incoming data.
94
+ validate :account_name_is_unique, error: "Account name is taken" do
95
+ db(:accounts).where(name: data[:account][:name]).none?
96
+ end
97
+
98
+ validate :owner_password_confirmation, error: "Passwords do not match" do
99
+ data[:user][:password] == data[:user][:password_confirmation]
100
+ end
101
+
102
+ # A data transformation step.
103
+ transform :prepare_records do |data|
104
+ data[:user][:password_hash] = BCrypt::Password.create(data[:user][:password])
105
+ data[:account][:subdomain] = data[:account][:name].downcase.strip.gsub(/\s+/, '-')
106
+ data
107
+ end
108
+ end
109
+ ```
110
+
111
+ #### The Orchestrator (`Dami.run`)
112
+
113
+ This is the final boss. It's a top-to-bottom, transaction-safe orchestrator for the entire business process. It reads like a clear, imperative script.
114
+
115
+ ```ruby
116
+ # app/flows/onboarding_flow.rb
117
+ Dami.run(context: { signup_source: 'web' }) do |run_context|
118
+ # 1. PREPARE the data. `prepare` uses the command to validate and transform.
119
+ # It halts automatically if validation fails.
120
+ clean_data = prepare CreateAccountAndOwnerCommand, with: params, context: run_context
121
+
122
+ # 2. Interact with the DB. These commands are now part of the `run` block's
123
+ # single, atomic transaction.
124
+ account = db(:accounts).create(clean_data[:account])
125
+ user = db(:users).create(clean_data[:user].merge(account_id: account[:id]))
126
+
127
+ # 3. Perform an external task with resilience.
128
+ perform "Create Billing Trial", retry: { on: [Billing::ApiError], times: 3 } do
129
+ BillingService.start_trial(account_id: account[:id], user_email: user[:email])
130
+ end
131
+
132
+ # 4. SUCCEED. The final, declarative exit point for the entire orchestration.
133
+ succeed with: { account: account, user: user }, and_then: [
134
+ WelcomeEmailJob.with(user_id: user.id),
135
+ Analytics.track("Team Onboarded", account_id: account.id)
136
+ ]
137
+ end
138
+ ```
139
+
140
+ -----
141
+
142
+ ### **The Dami Guarantee: Atomic Operations & Safe Side Effects**
143
+
144
+ You might be asking: "Why do I need `succeed with: and_then:`? Can't I just put the email job in a `call` block?" This is where Dami's most powerful feature comes into play.
145
+
146
+ #### The Problem: The "Email Sent, User Never Created" Bug
147
+
148
+ A `Dami.run` block guarantees that all **database operations** are atomic—they all succeed, or they all roll back. That includes a `Failure` halt from `prepare`: every write the flow made before the halt is rolled back. A flow that calls `run(:other_flow)` runs the inner flow in a savepoint, so an inner failure rolls back only the inner writes and hands the `Failure` back to the outer flow. But what about actions that can't be rolled back, like sending an email or enqueuing a background job?
149
+
150
+ **Consider this dangerous sequence of events:**
151
+
152
+ 1. A user is created in the database transaction.
153
+ 2. A welcome email job is immediately enqueued.
154
+ 3. The *next step*, calling the `BillingService`, fails.
155
+ 4. **Disaster:** The entire database transaction is **rolled back**. The user record is deleted. But it's too late—the email job is already in the queue, and a welcome email gets sent to a user who doesn't exist.
156
+
157
+ #### The Dami Solution: A Transaction-Aware Holding Area
158
+
159
+ The `succeed with: and_then:` block is not just another step. It is a **transaction-aware gatekeeper** for your irreversible side effects.
160
+
161
+ > It's a "holding area" for actions that must only run *after* the database transaction has been successfully and permanently committed.
162
+
163
+ **This is the safe, correct sequence:**
164
+
165
+ 1. A user is created in the database transaction.
166
+ 2. The call to the `BillingService` succeeds.
167
+ 3. The `succeed with:` block is reached. The `WelcomeEmailJob` is placed in the holding area.
168
+ 4. The `Dami.run` block finishes. Dami commits the database transaction.
169
+ 5. **Only now**, after the commit is successful, does Dami execute the actions in the `and_then:` block.
170
+
171
+ If the `BillingService` had failed, the transaction would have rolled back, and the `and_then:` block would have been discarded. No email would ever be sent.
172
+
173
+ ---
174
+
175
+ ### The Dami Way vs. The Rails Way
176
+
177
+ * **The Problem: Callback Hell & Fat Models.** In Rails, a complex process like this is often a chain of `after_save` hooks. An `Account` model might trigger a user creation, which in turn triggers a welcome email. This chain of hidden actions is brittle and incredibly hard to debug. If an API call to the `BillingService` fails, the `Account` and `User` records are already in the database, leaving your system in an inconsistent, half-finished state.
178
+
179
+ * **The Dami Solution: An Explicit, Transactional Orchestrator.** `Dami.run` makes the entire business process visible and atomic. The "semantic trio" of `prepare`, `db()`, and `call` makes the intent of each line instantly clear. If the `call` to the `BillingService` fails, the entire transaction is **automatically rolled back**, and the `Account` and `User` records are never saved. The `succeed with: and_then:` block guarantees that side effects like sending an email happen **only after** everything has been successfully committed to the database, eliminating an entire class of common bugs.
180
+
181
+ This is the pinnacle of the Dami philosophy: a system that scales from the simplicity of a one-liner to the robust, transactional, and resilient power needed for mission-critical business logic, all while maintaining a consistent and joyful developer experience.