roda-kabk 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f5508b1dac33bf7629b9c147a7f1798a67e78280862aaacc77fa6114944df277
4
+ data.tar.gz: 459908e88eb476e96592aa97f8832c53c5bbcd01b446ace3e977b6aca14c1bea
5
+ SHA512:
6
+ metadata.gz: e3be59177e76c02daad67d4d00c1c6b6c6effd42ad0acee508e1b75b85516760552f0e1839c47e9d9eccc4bec16c6f2f171619f14c533b5dfc4afa001f1302dc
7
+ data.tar.gz: 1f322e901a43c63bb5ceaec3ebb6314bed76c3384a951e144b79ddf68c7b6b13c189768f03f57d516da856d3bc9680f7a56923dc9e62b248264b6bc617d7d587
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "sqlite3"
6
+ gem "rackup"
7
+ gem "rspec"
8
+ gem "rack-test"
9
+ gem "logger"
10
+ gem "puma"
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ roda-kabk (0.2.0)
5
+ kabk (~> 0.2.0)
6
+ roda (~> 3.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ bigdecimal (4.1.2)
12
+ concurrent-ruby (1.3.8)
13
+ diff-lcs (1.6.2)
14
+ kabk (0.2.0)
15
+ concurrent-ruby (~> 1.1)
16
+ sequel (~> 5.0)
17
+ logger (1.7.0)
18
+ nio4r (2.7.5)
19
+ puma (8.0.2)
20
+ nio4r (~> 2.0)
21
+ rack (3.2.6)
22
+ rack-test (2.2.0)
23
+ rack (>= 1.3)
24
+ rackup (2.3.1)
25
+ rack (>= 3)
26
+ roda (3.106.0)
27
+ rack
28
+ rspec (3.13.2)
29
+ rspec-core (~> 3.13.0)
30
+ rspec-expectations (~> 3.13.0)
31
+ rspec-mocks (~> 3.13.0)
32
+ rspec-core (3.13.6)
33
+ rspec-support (~> 3.13.0)
34
+ rspec-expectations (3.13.5)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.13.0)
37
+ rspec-mocks (3.13.8)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.13.0)
40
+ rspec-support (3.13.7)
41
+ sequel (5.107.0)
42
+ bigdecimal
43
+ sqlite3 (2.9.5-x64-mingw-ucrt)
44
+
45
+ PLATFORMS
46
+ x64-mingw-ucrt
47
+
48
+ DEPENDENCIES
49
+ logger
50
+ puma
51
+ rack-test
52
+ rackup
53
+ roda-kabk!
54
+ rspec
55
+ sqlite3
56
+
57
+ CHECKSUMS
58
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
59
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
60
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
61
+ kabk (0.2.0) sha256=96b8cbea6b28d8ef0e5b9b4d935851833a617d87f14c7b0c39bc50142bf78118
62
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
63
+ nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
64
+ puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
65
+ rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
66
+ rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
67
+ rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
68
+ roda (3.106.0) sha256=1a1cfb13c6690db158a3929e9c9ece88e697fb23b05baae63f07b0773e3bbade
69
+ roda-kabk (0.2.0)
70
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
71
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
72
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
73
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
74
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
75
+ sequel (5.107.0) sha256=019629313205295b007f6df2c5eab19ddc40710d549fd445ac300f2853a370db
76
+ sqlite3 (2.9.5-x64-mingw-ucrt) sha256=b00d5697994ee8589b6096694a2130aa5567db64373baca55ea98c9bf958f46a
77
+
78
+ BUNDLED WITH
79
+ 4.0.16
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Parham Taheri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,267 @@
1
+ <h1 align="center">
2
+ <img src="roda-kabk.svg" alt="Roda Kabk Logo" width="300" />
3
+ <br>
4
+ Roda Kabk
5
+ </h1>
6
+
7
+ <p align="center">
8
+ <a href="https://rubygems.org/gems/roda-kabk"><img src="https://img.shields.io/gem/v/roda-kabk.svg?color=blue" alt="Gem Version"></a>
9
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
10
+ </p>
11
+
12
+ > **Kabk carries the Ruby to Simorgh**
13
+
14
+ `roda-kabk` is the official [Roda](https://roda.jeremyevans.net/) plugin for [Kabk](https://github.com/par-taheri/Kabk) — providing a **Smart Dispatch** routing architecture for the Simurgh Panel protocol (v1.6.0). It uses `api_path` defined in `Kabk.register` as the Single Source of Truth for all routes, giving you full control over middleware, authentication, and routing branches.
15
+
16
+ ---
17
+
18
+ ## Features
19
+
20
+ - **Smart Dispatch Routing (`r.kabk.route`)**: Automatically routes incoming requests to the appropriate `Kabk::RestEngine` resource based on each resource's `api_path`.
21
+ - **Global Static Assets (`r.kabk.statics`)**: Serves pre-built assets (JS, CSS, SVGs, images, fonts, videos) under `/assets/*` and `/simurgh-logo.svg`.
22
+ - **Vue.js SPA Server (`r.kabk.server`)**: Serves the single-page admin dashboard SPA `index.html` with embedded schema injection.
23
+ - **Protocol Schema Manifest (`r.kabk.schema`)**: Exposes dynamic protocol schema JSON.
24
+ - **Pluggable File Uploads (`r.kabk.upload`)**: Seamless integration with **Shrine**, ActiveStorage, or custom storage handlers via configurable procs.
25
+ - **Context Injection**: Pass `context` into `r.kabk.route(context: ...)` to support Kabk's lifecycle hooks and audit fields based on the current user.
26
+ - **Optimistic Concurrency Control (OCC)**: Built-in support for preventing concurrent record overwrite conflicts (`updated_at` / `version`).
27
+ - **Client-Side Data Export**: CSV exports are natively generated on the client-side (frontend) using existing table data and active filters without backend overhead.
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ Add this line to your application's `Gemfile`:
34
+
35
+ ```ruby
36
+ gem 'roda-kabk'
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quick Start (Smart Dispatch Architecture)
48
+
49
+ In the new decoupled architecture, `Kabk` no longer manages authentication internally. You handle authentication in Roda and dispatch protected CRUD requests via `r.kabk.route(context: ...)`.
50
+
51
+ ### 1. Setup DB and Sequel Model
52
+
53
+ Define your user and resource models with `api_path` as the Single Source of Truth:
54
+
55
+ ```ruby
56
+ require 'roda'
57
+ require 'sequel'
58
+ require 'kabk'
59
+ require 'roda/plugins/kabk'
60
+ require 'bcrypt'
61
+
62
+ DB = Sequel.sqlite
63
+
64
+ # Authentication Model
65
+ DB.create_table :admin_users do
66
+ primary_key :id
67
+ String :username, null: false, unique: true
68
+ String :password_digest, null: false
69
+ String :full_name
70
+ String :avatar_url
71
+ String :role, default: "admin"
72
+ end
73
+
74
+ class AdminUser < Sequel::Model
75
+ plugin :secure_password
76
+ end
77
+
78
+ DB.create_table :articles do
79
+ primary_key :id
80
+ String :title, null: false
81
+ String :content
82
+ DateTime :updated_at
83
+ end
84
+
85
+ class Article < Sequel::Model; end
86
+
87
+ # Register Entity with Kabk (api_path is the Single Source of Truth)
88
+ Kabk.register(name: "article", table: Article) do
89
+ title "Articles"
90
+ plural_name "articles"
91
+ api_path "/admin/api/articles"
92
+ concurrency_field "updated_at"
93
+
94
+ field :id, type: :number, form_type: :number, primary_key: true
95
+ field :title, type: :string, form_type: :text, required: true
96
+ field :content, type: :string, form_type: :textarea
97
+ field :updated_at, type: :datetime, form_type: :datetime, readonly: true
98
+ end
99
+ ```
100
+
101
+ ### 2. Implement Roda Application
102
+
103
+ Use Roda's routing tree with `r.kabk` helper methods:
104
+
105
+ ```ruby
106
+ class App < Roda
107
+ plugin :sessions, secret: ENV.fetch("SESSION_SECRET", "super_secret_key_that_is_long_enough_for_session")
108
+ plugin :json
109
+ plugin :kabk,
110
+ system_config: {
111
+ title: { en: "Admin Dashboard", fa: "پنل مدیریت" },
112
+ default_locale: "en",
113
+ supported_locales: ["en", "fa"],
114
+ direction: "ltr",
115
+ show_demo_credentials: false,
116
+ endpoints: {
117
+ upload: "/admin/api/uploads"
118
+ },
119
+ auth: {
120
+ strategy: "session",
121
+ sso_redirect_url: nil,
122
+ login_url: "/admin/api/auth/login",
123
+ me_url: "/admin/api/auth/me",
124
+ logout_url: "/admin/api/auth/logout",
125
+ refresh_url: "/admin/api/auth/refresh",
126
+ show_demo_credentials: false,
127
+ login_fields: [
128
+ { name: "username", label: { en: "Username", fa: "نام کاربری" }, type: "text", required: true },
129
+ { name: "password", label: { en: "Password", fa: "کلمه عبور" }, type: "password", required: true }
130
+ ]
131
+ }
132
+ },
133
+ upload_handler: ->(file_param, req) {
134
+ {
135
+ url: "/uploads/#{file_param[:filename]}",
136
+ file_name: file_param[:filename],
137
+ size: file_param[:tempfile].size,
138
+ mime_type: file_param[:type]
139
+ }
140
+ }
141
+
142
+ route do |r|
143
+ r.kabk.statics
144
+
145
+ r.on "admin" do
146
+ r.is do
147
+ r.kabk.server
148
+ end
149
+
150
+ r.on "api" do
151
+ r.get "schema" do
152
+ r.kabk.schema
153
+ end
154
+
155
+ r.post "uploads" do
156
+ r.kabk.upload
157
+ end
158
+
159
+ r.on "auth" do
160
+ r.post "login" do
161
+ body = JSON.parse(r.body.read) rescue {}
162
+ user = AdminUser.first(username: body["username"])
163
+ if user && user.authenticate(body["password"])
164
+ session[:user_id] = user.id
165
+ { success: true, data: { id: user.id, username: user.username, role: user.role } }
166
+ else
167
+ response.status = 401
168
+ { success: false, error: { message: "Invalid credentials" } }
169
+ end
170
+ end
171
+
172
+ r.get "me" do
173
+ user = AdminUser[session[:user_id]]
174
+ if user
175
+ { success: true, data: { id: user.id, username: user.username, role: user.role } }
176
+ else
177
+ response.status = 401
178
+ { success: false, error: { message: "Unauthorized" } }
179
+ end
180
+ end
181
+
182
+ r.post "logout" do
183
+ session.clear
184
+ { success: true, message: "Logged out" }
185
+ end
186
+ end
187
+
188
+ current_user = AdminUser[session[:user_id]]
189
+ unless current_user
190
+ response.status = 401
191
+ r.halt({ success: false, error: { message: "Unauthorized" } }.to_json)
192
+ end
193
+
194
+ r.kabk.route(context: { current_user: current_user })
195
+ end
196
+ end
197
+ end
198
+ end
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Configuration Reference
204
+
205
+ The `plugin :kabk` method accepts the following options:
206
+
207
+ | Option | Type | Default | Description |
208
+ | :--- | :--- | :--- | :--- |
209
+ | `system_config` | `Hash` / `NilClass` | `nil` | Custom system branding, custom fonts, and localization settings. |
210
+ | `upload_handler` | `Proc` / `Callable` | `nil` | Custom file upload handler block/callable. Processes uploaded file params. |
211
+
212
+ ---
213
+
214
+ ## Request Methods (`r.kabk.*`)
215
+
216
+ | Method | Description |
217
+ | :--- | :--- |
218
+ | `r.kabk.statics` | Serves static assets (JS, CSS, SVGs, images, fonts, videos) from `/assets/*` and `simurgh-logo.svg`. |
219
+ | `r.kabk.server` | Serves `index.html` (with memoized schema injection) for the Vue.js SPA dashboard. |
220
+ | `r.kabk.schema` | Returns the dynamic protocol schema JSON manifest. |
221
+ | `r.kabk.upload` | Handles multipart file uploads using `upload_handler` or fallback. |
222
+ | `r.kabk.route(context: {})` | Smart Dispatcher: matches the request path against registered `api_path`s and executes CRUD actions (list, get, create, update, delete) on `Kabk::RestEngine`. |
223
+
224
+ ---
225
+
226
+ ## Context Injection & Hooks
227
+
228
+ By passing `context: { current_user: ... }` to `r.kabk.route`, `Kabk`'s `RestEngine` propagates this context to lifecycle hooks and audit fields. For example, you can write entity hooks in `Kabk` core that verify if a user has permission to delete a record or automatically assign the `created_by` field based on `context[:current_user]`.
229
+
230
+ ---
231
+
232
+ ## File Upload Integration (Shrine Example)
233
+
234
+ You can connect **Shrine** directly using the `upload_handler` option:
235
+
236
+ ```ruby
237
+ plugin :kabk,
238
+ upload_handler: ->(file_param, req) {
239
+ # file_param contains Rack tempfile params: { tempfile: #<File>, filename: "...", type: "..." }
240
+ uploader = ImageUploader.upload(file_param[:tempfile], :store)
241
+
242
+ {
243
+ url: uploader.url,
244
+ file_name: file_param[:filename],
245
+ size: file_param[:tempfile].size,
246
+ mime_type: file_param[:type]
247
+ }
248
+ }
249
+ ```
250
+
251
+ If no `upload_handler` is provided and no file is attached, `r.kabk.upload` returns an `HTTP 422 Unprocessable Entity` error.
252
+
253
+ ---
254
+
255
+ ## Testing
256
+
257
+ Run the test suite using `rspec`:
258
+
259
+ ```bash
260
+ bundle exec rspec
261
+ ```
262
+
263
+ ---
264
+
265
+ ## License
266
+
267
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
data/TUTORIAL.md ADDED
@@ -0,0 +1,271 @@
1
+ # Roda-Kabk Ultimate Tutorial: Building a Comprehensive Simurgh Admin Panel
2
+
3
+ Welcome to the **Roda-Kabk Tutorial**. This guide will walk you through building a fully-featured, production-ready admin panel using `roda-kabk` and the Simurgh Panel protocol.
4
+
5
+ We will translate a complex, real-world schema—including relations, advanced validations, JSON editors, custom fonts, and file uploads—directly into the Ruby Kabk DSL.
6
+
7
+ ---
8
+
9
+ ## 1. Setting Up the Application & System Config
10
+
11
+ First, we need to mount the plugin in our Roda application and configure the `system_config`. This is where we define custom branding, multilingual support, and custom fonts.
12
+
13
+ ```ruby
14
+ require 'roda'
15
+ require 'sequel'
16
+ require 'kabk'
17
+ require 'roda/plugins/kabk'
18
+
19
+ class AdminApp < Roda
20
+ plugin :sessions, secret: ENV.fetch("SESSION_SECRET", "super_secret_key_that_is_long_enough_for_session")
21
+ plugin :json
22
+ plugin :kabk,
23
+ system_config: {
24
+ title: { en: "Simurgh Panel", fa: "پنل مدیریت سیمرغ" },
25
+ logo_url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=100&h=100&fit=crop&crop=faces",
26
+ default_locale: "en",
27
+ supported_locales: ["en", "fa"],
28
+ direction: "ltr",
29
+ show_demo_credentials: false,
30
+ custom_fonts: [
31
+ {
32
+ name: "Inter",
33
+ url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2",
34
+ format: "woff2",
35
+ label: { en: "Inter (English)", fa: "اینتر (انگلیسی)" }
36
+ },
37
+ {
38
+ name: "Sahel",
39
+ url: "https://cdn.jsdelivr.net/gh/rastikerdar/sahel-font@v3.4.0/dist/Sahel.woff2",
40
+ format: "woff2",
41
+ label: { en: "Sahel (Persian)", fa: "ساحل (فارسی)" }
42
+ }
43
+ ],
44
+ custom_font_sizes: [
45
+ { size: "14px", label: { en: "Standard", fa: "عادی" } },
46
+ { size: "18px", label: { en: "Medium", fa: "متوسط" } },
47
+ { size: "28px", label: { en: "Large Heading", fa: "تیتر بزرگ" } }
48
+ ]
49
+ }
50
+
51
+ route do |r|
52
+ r.kabk.statics
53
+
54
+ r.on "admin" do
55
+ r.is do
56
+ r.kabk.server
57
+ end
58
+
59
+ r.on "api" do
60
+ r.get "schema" do
61
+ r.kabk.schema
62
+ end
63
+
64
+ r.post "uploads" do
65
+ r.kabk.upload
66
+ end
67
+
68
+ r.on "auth" do
69
+ r.post "login" do
70
+ body = JSON.parse(r.body.read) rescue {}
71
+ user = User.first(email: body["username"] || body["email"])
72
+ if user && user.authenticate(body["password"])
73
+ session[:user_id] = user.id
74
+ { success: true, data: { id: user.id, username: user.email, role: user.role } }
75
+ else
76
+ response.status = 401
77
+ { success: false, error: { message: "Invalid credentials" } }
78
+ end
79
+ end
80
+
81
+ r.get "me" do
82
+ user = User[session[:user_id]]
83
+ if user
84
+ { success: true, data: { id: user.id, username: user.email, role: user.role } }
85
+ else
86
+ response.status = 401
87
+ { success: false, error: { message: "Unauthorized" } }
88
+ end
89
+ end
90
+
91
+ r.post "logout" do
92
+ session.clear
93
+ { success: true, message: "Logged out" }
94
+ end
95
+ end
96
+
97
+ current_user = User[session[:user_id]]
98
+ unless current_user
99
+ response.status = 401
100
+ r.halt({ error: "Unauthorized" }.to_json)
101
+ end
102
+
103
+ r.kabk.route(context: { current_user: current_user })
104
+ end
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 2. Defining Resources (The DSL)
113
+
114
+ We will define 5 major resources to showcase different capabilities: `Users`, `Categories`, `Products`, `Support Tickets`, and `News`.
115
+
116
+ ### Resource 1: Users (Badges, Textareas, and Unique Validation)
117
+
118
+ ```ruby
119
+ Kabk.register(name: "users", table: User) do
120
+ title en: "User Directory", fa: "مدیریت کاربران"
121
+ icon "Users"
122
+ api_path "/admin/api/users"
123
+ group "Access & Users"
124
+ order 2
125
+ searchable_fields [:full_name, :email]
126
+ sortable_fields [:id, :full_name, :role, :birth_date, :created_at]
127
+ concurrency_field "updated_at"
128
+
129
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
130
+
131
+ field :full_name, type: :string, form_type: :text, required: true, validation: { min_length: 3, max_length: 100 }
132
+
133
+ field :email, type: :string, form_type: :text, required: true, validation: {
134
+ pattern: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
135
+ unique: true,
136
+ custom_message: { en: "Email invalid or taken", fa: "فرمت ایمیل معتبر نیست یا قبلاً ثبت شده است" }
137
+ }
138
+
139
+ field :avatar, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true
140
+
141
+ field :role, type: :string, form_type: :select, display_as: "badge", required: true, default_value: "editor", options: [
142
+ { label: { en: "Administrator", fa: "مدیر کل" }, value: "admin", color: "purple" },
143
+ { label: { en: "Editor", fa: "ویرایشگر" }, value: "editor", color: "blue" },
144
+ { label: { en: "Author", fa: "نویسنده" }, value: "author", color: "emerald" },
145
+ { label: { en: "Viewer", fa: "مشاهده‌کننده" }, value: "viewer", color: "gray" }
146
+ ]
147
+
148
+ field :bio, type: :string, form_type: :textarea, hidden_in_table: true, accordion: true, rows: 3
149
+ field :is_active, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: true
150
+
151
+ field :birth_date, type: :date, form_type: :date, nullable: true
152
+ field :created_at, type: :date, form_type: :date, readonly: true, hidden_in_form: true
153
+ end
154
+ ```
155
+
156
+ ### Resource 2: Categories (Simple Text and Badges)
157
+
158
+ ```ruby
159
+ Kabk.register(name: "categories", table: Category) do
160
+ title en: "Categories", fa: "دسته‌بندی‌ها"
161
+ icon "Tags"
162
+ api_path "/admin/api/categories"
163
+ group "Content Management"
164
+ order 3
165
+ searchable_fields [:title, :slug]
166
+
167
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
168
+ field :title, type: :string, form_type: :text, required: true, col_width: 6
169
+ field :slug, type: :string, form_type: :text, required: true, col_width: 6, validation: { pattern: "^[a-z0-9-]+$" }
170
+ field :color_code, type: :string, form_type: :text, display_as: "badge", col_width: 6
171
+ end
172
+ ```
173
+
174
+ ### Resource 3: Products (JSON Editors and Advanced Layout)
175
+
176
+ ```ruby
177
+ Kabk.register(name: "products", table: Product) do
178
+ title en: "Products Catalog", fa: "محصولات و کالاها"
179
+ icon "Package"
180
+ api_path "/admin/api/products"
181
+ group "Store & Services"
182
+ order 4
183
+ searchable_fields [:title, :sku]
184
+
185
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
186
+ field :title, type: :string, form_type: :text, required: true, col_width: 8
187
+ field :sku, type: :string, form_type: :text, required: true, col_width: 4
188
+
189
+ field :price, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0, max: 1000000000 }
190
+ field :stock, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0 }
191
+
192
+ field :specs_json, type: :json, form_type: :json_editor, hidden_in_table: true, accordion: true, default_value: {
193
+ weight_kg: 1.2,
194
+ brand: "Acme Corp",
195
+ warranty_months: 24,
196
+ dimensions: { length: 30, width: 20, height: 10 }
197
+ }
198
+
199
+ field :status, type: :string, form_type: :select, display_as: "badge", options: [
200
+ { label: { en: "In Stock", fa: "موجود در انبار" }, value: "in_stock", color: "green" },
201
+ { label: { en: "Low Stock", fa: "موجودی محدود" }, value: "low_stock", color: "amber" },
202
+ { label: { en: "Out of Stock", fa: "ناموجود" }, value: "out_of_stock", color: "red" }
203
+ ]
204
+ end
205
+ ```
206
+
207
+ ### Resource 4: News (Relations, File Uploads, WYSIWYG)
208
+
209
+ This entity brings it all together by referencing `users` and `categories`, and handling complex file configurations.
210
+
211
+ ```ruby
212
+ Kabk.register(name: "news_item", table: NewsItem) do
213
+ title en: "News & Announcements", fa: "اخبار و اطلاعیه‌ها"
214
+ icon "Newspaper"
215
+ api_path "/admin/api/news"
216
+ group "Content Management"
217
+ order 1
218
+ concurrency_field "updated_at"
219
+
220
+ searchable_fields [:title, :content]
221
+ filterable_fields [:published, :category_ids]
222
+
223
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true, col_width: 2
224
+
225
+ field :title, type: :string, form_type: :text, required: true, col_width: 12, validation: {
226
+ min_length: 5, max_length: 150,
227
+ custom_message: { en: "Title 5-150 chars", fa: "عنوان بین ۵ تا ۱۵۰ کاراکتر" }
228
+ }
229
+
230
+ # Many-to-One Relation (Author -> User)
231
+ field :author_id, type: :relation, form_type: :relation_select, required: true, col_width: 6, relation: {
232
+ resource: "users", cardinality: :many_to_one,
233
+ value_field: "id", label_field: "full_name",
234
+ display_key: "author_id_display", fetch_endpoint: "/admin/api/users",
235
+ search_param: "search", debounce_ms: 300, per_page: 20
236
+ }
237
+
238
+ # Many-to-Many Relation (News -> Categories)
239
+ field :category_ids, type: :relation, form_type: :multiselect, required: true, col_width: 6, relation: {
240
+ resource: "categories", cardinality: :many_to_many,
241
+ value_field: "id", label_field: "title",
242
+ display_key: "category_ids_display", fetch_endpoint: "/admin/api/categories"
243
+ }
244
+
245
+ # Single Image Upload (with constraints)
246
+ field :cover_image, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true, col_width: 6, upload_config: {
247
+ allowed_extensions: ["jpg", "jpeg", "png", "webp"],
248
+ max_size_mb: 3
249
+ }
250
+
251
+ # Multi-Image Gallery
252
+ field :gallery, type: :array, form_type: :image_multi, hidden_in_table: true, col_width: 6, upload_config: {
253
+ allowed_extensions: ["jpg", "png", "webp"], max_size_mb: 5
254
+ }
255
+
256
+ field :content, type: :string, form_type: :wysiwyg, hidden_in_table: true, accordion: true, required: true
257
+
258
+ field :published, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: false
259
+
260
+ field :publish_date, type: :datetime, form_type: :datetime, nullable: true, col_width: 6, depends_on: {
261
+ field: "published", value: true
262
+ }, validation: { min_date: "2020-01-01T00:00:00Z" }
263
+
264
+ field :updated_at, type: :datetime, form_type: :datetime, readonly: true, hidden_in_form: true
265
+ end
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 3. What to do next?
271
+ Now that the schema and DSL have been mapped out, simply run your Roda application and navigate to `/admin` in your browser. The Simurgh panel will dynamically parse this metadata and render all interactive UI components, relationships, validations, and custom fonts.