roda-kabk 0.1.5

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.

Potentially problematic release.


This version of roda-kabk might be problematic. Click here for more details.

checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 29d71b61867e52a48284e539f00194c7094a5a6b3c80282f0a4ed6702085132b
4
+ data.tar.gz: 3432f771d7e78f564bf86bf5be72e636725b92185180940d1cce07e71fdfeb8a
5
+ SHA512:
6
+ metadata.gz: 8a0ebee193374a27891b1416081a3fd60c2039eab8f8d1886fe579676da0787105efbe4921dcd0ee6606d77c6bfe04cee1e3e352ddbd57adfa7da15b613cf1fd
7
+ data.tar.gz: 0d3510f11f3becae6b999b1a8a5e2d9458b553d971877e527867625c2038da8746b0de3cc388acd05a5d9f7587e6a9507fa1d1dea191658a4cb06c0ec63e49a4
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,88 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ roda-kabk (0.1.2)
5
+ kabk (~> 0.1.2)
6
+ roda (~> 3.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ base64 (0.3.0)
12
+ bigdecimal (4.1.2)
13
+ concurrent-ruby (1.3.8)
14
+ csv (3.3.6)
15
+ diff-lcs (1.6.2)
16
+ jwt (2.10.3)
17
+ base64
18
+ kabk (0.1.2)
19
+ concurrent-ruby (~> 1.1)
20
+ csv
21
+ jwt (~> 2.7)
22
+ sequel (~> 5.0)
23
+ logger (1.7.0)
24
+ nio4r (2.7.5)
25
+ puma (8.0.2)
26
+ nio4r (~> 2.0)
27
+ rack (3.2.6)
28
+ rack-test (2.2.0)
29
+ rack (>= 1.3)
30
+ rackup (2.3.1)
31
+ rack (>= 3)
32
+ roda (3.106.0)
33
+ rack
34
+ rspec (3.13.2)
35
+ rspec-core (~> 3.13.0)
36
+ rspec-expectations (~> 3.13.0)
37
+ rspec-mocks (~> 3.13.0)
38
+ rspec-core (3.13.6)
39
+ rspec-support (~> 3.13.0)
40
+ rspec-expectations (3.13.5)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.13.0)
43
+ rspec-mocks (3.13.8)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (~> 3.13.0)
46
+ rspec-support (3.13.7)
47
+ sequel (5.107.0)
48
+ bigdecimal
49
+ sqlite3 (2.9.5-x64-mingw-ucrt)
50
+
51
+ PLATFORMS
52
+ x64-mingw-ucrt
53
+
54
+ DEPENDENCIES
55
+ logger
56
+ puma
57
+ rack-test
58
+ rackup
59
+ roda-kabk!
60
+ rspec
61
+ sqlite3
62
+
63
+ CHECKSUMS
64
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
65
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
66
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
67
+ csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456
68
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
69
+ jwt (2.10.3) sha256=e4d9352fbc7309b1a7448c7dd713dfe4d8c47077af80759cdbed8f878ea0b484
70
+ kabk (0.1.2)
71
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
72
+ nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
73
+ puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
74
+ rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
75
+ rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
76
+ rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
77
+ roda (3.106.0) sha256=1a1cfb13c6690db158a3929e9c9ece88e697fb23b05baae63f07b0773e3bbade
78
+ roda-kabk (0.1.2)
79
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
80
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
81
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
82
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
83
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
84
+ sequel (5.107.0) sha256=019629313205295b007f6df2c5eab19ddc40710d549fd445ac300f2853a370db
85
+ sqlite3 (2.9.5-x64-mingw-ucrt) sha256=b00d5697994ee8589b6096694a2130aa5567db64373baca55ea98c9bf958f46a
86
+
87
+ BUNDLED WITH
88
+ 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,188 @@
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) — seamlessly binding Roda applications to the Simurgh Panel protocol (v1.6.0) for schema-driven dynamic REST APIs without duplicating backend logic.
15
+
16
+ ---
17
+
18
+ ## Features
19
+
20
+ - **Embedded Simurgh Panel UI**: Bundles and serves the pre-built single-page admin dashboard SPA directly at `mount_at` (`/api/admin`) with embedded schema injection.
21
+ - **Generic REST Engine**: Auto-generates full CRUD, search, multi-field filtering, sorting, and pagination for registered Sequel models.
22
+ - **Pluggable File Uploads**: Seamless integration with **Shrine**, ActiveStorage, or custom storage handlers via configurable procs.
23
+ - **Authentication & Security**: Integrated JWT authentication (`login`, `refresh`, `me`, `change-password`) and Role-Based Access Control (RBAC).
24
+ - **Optimistic Concurrency Control (OCC)**: Built-in support for preventing concurrent record overwrite conflicts (`updated_at` / `version`).
25
+ - **Data Export**: Exposes CSV and XLSX export endpoints out of the box (`/export`).
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's `Gemfile`:
32
+
33
+ ```ruby
34
+ gem 'roda-kabk'
35
+ ```
36
+
37
+ And then execute:
38
+
39
+ ```bash
40
+ bundle install
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quick Start
46
+
47
+ ```ruby
48
+ require 'roda'
49
+ require 'sequel'
50
+ require 'kabk'
51
+ require 'roda/plugins/kabk'
52
+
53
+ # 1. Setup DB and Sequel Model
54
+ DB = Sequel.sqlite
55
+ DB.create_table :articles do
56
+ primary_key :id
57
+ String :title, null: false
58
+ String :content
59
+ DateTime :updated_at
60
+ end
61
+
62
+ class Article < Sequel::Model; end
63
+
64
+ # 2. Register Entity with Kabk
65
+ Kabk.register(name: "article", table: Article) do
66
+ title "Articles"
67
+ concurrency_field "updated_at"
68
+
69
+ field :id, type: :number, form_type: :number, primary_key: true
70
+ field :title, type: :string, form_type: :text, required: true
71
+ field :content, type: :string, form_type: :textarea
72
+ field :updated_at, type: :datetime, form_type: :datetime, readonly: true, calendar: :jalali
73
+ end
74
+
75
+ # 3. Mount Kabk Plugin in Roda
76
+ class App < Roda
77
+ plugin :kabk,
78
+ mount_at: "/api/admin",
79
+ serve_ui: true,
80
+ auth_strategy: Kabk::Auth::JwtStrategy.new(secret: ENV.fetch("JWT_SECRET", "super_secret_key")),
81
+ login_handler: ->(username, password) {
82
+ # Fetch user from DB and verify password
83
+ # return user_data hash if valid, nil otherwise
84
+ user = User.first(email: username)
85
+ return nil unless user && user.authenticate(password)
86
+ { id: user.id, username: user.email, role: user.role }
87
+ },
88
+ change_password_handler: ->(user_context, old_pw, new_pw) {
89
+ # Verify old password and save new password
90
+ user = User[user_context[:id]]
91
+ raise Kabk::InvalidOldPasswordError.new(fields: { old_password: ["Incorrect password"] }) unless user.authenticate(old_pw)
92
+ user.update(password: new_pw)
93
+ },
94
+ upload_handler: ->(file_param, req) {
95
+ # Integration point for Shrine / ActiveStorage
96
+ {
97
+ url: "/uploads/#{file_param[:filename]}",
98
+ file_name: file_param[:filename],
99
+ size: file_param[:tempfile].size,
100
+ mime_type: file_param[:type]
101
+ }
102
+ }
103
+
104
+ route do |r|
105
+ # Exposes Dashboard UI, Auth, CRUD, Export, and Upload endpoints under /api/admin
106
+ r.kabk
107
+ end
108
+ end
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Configuration Reference
114
+
115
+ The `plugin :kabk` method accepts the following options:
116
+
117
+ | Option | Type | Default | Description |
118
+ | :--- | :--- | :--- | :--- |
119
+ | `mount_at` | `String` | `"/api/admin"` | Base URI path under which all Kabk admin endpoints and UI are mounted. |
120
+ | `serve_ui` | `Boolean` | `true` | Serves the bundled Simurgh Panel single-page dashboard UI directly at `mount_at`. |
121
+ | `system_config` | `Hash` / `NilClass` | `nil` | Custom system branding and localization settings (overrides default locale/title). |
122
+ | `auth_strategy` | `Kabk::Auth::JwtStrategy` / `NilClass` | `nil` | Authentication strategy object. Set to `nil` to disable auth for development. |
123
+ | `upload_handler` | `Proc` / `Callable` | `nil` | Custom file upload handler block/callable. Processes uploaded file params. |
124
+
125
+ ---
126
+
127
+ ## File Upload Integration (Shrine Example)
128
+
129
+ You can connect **Shrine** directly using the `upload_handler` option:
130
+
131
+ ```ruby
132
+ plugin :kabk,
133
+ mount_at: "/api/admin",
134
+ upload_handler: ->(file_param, req) {
135
+ # file_param contains Rack tempfile params: { tempfile: #<File>, filename: "...", type: "..." }
136
+ uploader = ImageUploader.upload(file_param[:tempfile], :store)
137
+
138
+ {
139
+ url: uploader.url,
140
+ file_name: file_param[:filename],
141
+ size: file_param[:tempfile].size,
142
+ mime_type: file_param[:type]
143
+ }
144
+ }
145
+ ```
146
+
147
+ If no `upload_handler` is provided and no file is attached, the endpoint returns an `HTTP 400 Bad Request` with error code `BAD_REQUEST`.
148
+
149
+ ---
150
+
151
+ ## Endpoints Reference
152
+
153
+ All endpoints are exposed under the configured `mount_at` prefix (e.g. `/api/admin`):
154
+
155
+ ### Authentication
156
+ - `POST /api/admin/auth/login` — Authenticates user credentials & returns JWT access/refresh tokens.
157
+ - `POST /api/admin/auth/refresh` — Generates new access token from refresh token.
158
+ - `GET /api/admin/auth/me` — Returns current authenticated user context.
159
+ - `POST /api/admin/auth/logout` — Revokes active session.
160
+ - `POST /api/admin/auth/change-password` — Updates user password (requires `old_password` verification).
161
+
162
+ ### Resource Operations (Generic REST)
163
+ For any registered entity `plural_name` (e.g., `/articles`, `/users`):
164
+ - `GET /api/admin/:plural_name` — List records (supports `page`, `per_page`, `sort`, `search`, `filter[field]`).
165
+ - `GET /api/admin/:plural_name/:id` — Retrieve single record details.
166
+ - `POST /api/admin/:plural_name` — Create new record.
167
+ - `PUT /api/admin/:plural_name/:id` — Update record (with Optimistic Concurrency Control).
168
+ - `DELETE /api/admin/:plural_name/:id` — Delete record.
169
+ - `GET /api/admin/:plural_name/export` — Export data in `csv` or `xlsx` format (`?format=xlsx`).
170
+
171
+ ### Media Uploads
172
+ - `POST /api/admin/uploads` — Upload file via multipart/form-data.
173
+
174
+ ---
175
+
176
+ ## Testing
177
+
178
+ Run the test suite using `rspec`:
179
+
180
+ ```bash
181
+ bundle exec rspec
182
+ ```
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
data/TUTORIAL.md ADDED
@@ -0,0 +1,227 @@
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, jalali calendars, 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 (like Sahel and Lalezar).
12
+
13
+ ```ruby
14
+ require 'roda'
15
+ require 'roda/plugins/kabk'
16
+
17
+ class AdminApp < Roda
18
+ plugin :kabk,
19
+ mount_at: "/api/admin",
20
+ system_config: {
21
+ title: { fa: "پنل مدیریت سیمرغ", en: "Simurgh Panel" },
22
+ logo_url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=100&h=100&fit=crop&crop=faces",
23
+ default_locale: "fa",
24
+ supported_locales: ["fa", "en"],
25
+ direction: "rtl",
26
+ custom_fonts: [
27
+ {
28
+ name: "Sahel",
29
+ url: "https://cdn.jsdelivr.net/gh/rastikerdar/sahel-font@v3.4.0/dist/Sahel.woff2",
30
+ format: "woff2",
31
+ label: { fa: "ساحل (فارسی)", en: "Sahel (Persian)" }
32
+ },
33
+ {
34
+ name: "Lalezar",
35
+ url: "https://cdn.jsdelivr.net/gh/rastikerdar/lalezar-font@v1.0.0/dist/Lalezar.woff2",
36
+ format": "woff2",
37
+ label: { fa: "لاله‌زار (عنوان فانتزی)", en: "Lalezar (Display)" }
38
+ }
39
+ ],
40
+ custom_font_sizes: [
41
+ { size: "15px", label: { fa: "عادی ویژه", en: "Special Normal" } },
42
+ { size: "20px", label: { fa: "برجسته متوسط", en: "Highlighted Medium" } },
43
+ { size: "32px", label: { fa: "تیتر بزرگ", en: "Hero Banner" } }
44
+ ]
45
+ },
46
+ auth_strategy: Kabk::Auth::JwtStrategy.new(secret: ENV.fetch("JWT_SECRET", "secret")),
47
+ login_handler: ->(username, password) {
48
+ user = User.first(email: username)
49
+ return nil unless user && user.authenticate(password)
50
+ { id: user.id, username: user.email, role: user.role }
51
+ },
52
+ change_password_handler: ->(user_context, old_pw, new_pw) {
53
+ user = User[user_context[:id]]
54
+ raise Kabk::InvalidOldPasswordError.new(fields: { old_password: ["Incorrect password"] }) unless user.authenticate(old_pw)
55
+ user.update(password: new_pw)
56
+ }
57
+
58
+ route do |r|
59
+ r.kabk
60
+ end
61
+ end
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 2. Defining Resources (The DSL)
67
+
68
+ We will define 5 major resources to showcase different capabilities: `Users`, `Categories`, `Products`, `Support Tickets`, and `News`.
69
+
70
+ ### Resource 1: Users (Badges, Textareas, and Unique Validation)
71
+
72
+ ```ruby
73
+ Kabk.register(name: "users", table: User) do
74
+ title fa: "مدیریت کاربران", en: "User Directory"
75
+ icon "Users"
76
+ api_path "/api/admin/users"
77
+ group "دسترسی و کاربران"
78
+ order 2
79
+ searchable_fields [:full_name, :email]
80
+ sortable_fields [:id, :full_name, :role, :birth_date, :created_at]
81
+ concurrency_field "updated_at"
82
+
83
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
84
+
85
+ field :full_name, type: :string, form_type: :text, required: true, validation: { min_length: 3, max_length: 100 }
86
+
87
+ field :email, type: :string, form_type: :text, required: true, validation: {
88
+ pattern: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
89
+ unique: true,
90
+ custom_message: { fa: "فرمت ایمیل معتبر نیست یا قبلاً ثبت شده است", en: "Email invalid or taken" }
91
+ }
92
+
93
+ field :avatar, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true
94
+
95
+ field :role, type: :string, form_type: :select, display_as: "badge", required: true, default_value: "editor", options: [
96
+ { label: { fa: "مدیر کل", en: "Administrator" }, value: "admin", color: "purple" },
97
+ { label: { fa: "ویرایشگر", en: "Editor" }, value: "editor", color: "blue" },
98
+ { label: { fa: "نویسنده", en: "Author" }, value: "author", color: "emerald" },
99
+ { label: { fa: "مشاهده‌کننده", en: "Viewer" }, value: "viewer", color: "gray" }
100
+ ]
101
+
102
+ field :bio, type: :string, form_type: :textarea, hidden_in_table: true, accordion: true, rows: 3
103
+ field :is_active, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: true
104
+
105
+ field :birth_date, type: :date, form_type: :date, calendar: :jalali, nullable: true
106
+ field :created_at, type: :date, form_type: :date, calendar: :jalali, readonly: true, hidden_in_form: true
107
+ end
108
+ ```
109
+
110
+ ### Resource 2: Categories (Simple Text and Badges)
111
+
112
+ ```ruby
113
+ Kabk.register(name: "categories", table: Category) do
114
+ title fa: "دسته‌بندی‌ها", en: "Categories"
115
+ icon "Tags"
116
+ api_path "/api/admin/categories"
117
+ group "مدیریت محتوا"
118
+ order 3
119
+ searchable_fields [:title, :slug]
120
+
121
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
122
+ field :title, type: :string, form_type: :text, required: true, col_width: 6
123
+ field :slug, type: :string, form_type: :text, required: true, col_width: 6, validation: { pattern: "^[a-z0-9-]+$" }
124
+ field :color_code, type: :string, form_type: :text, display_as: "badge", col_width: 6
125
+ end
126
+ ```
127
+
128
+ ### Resource 3: Products (JSON Editors and Advanced Layout)
129
+
130
+ ```ruby
131
+ Kabk.register(name: "products", table: Product) do
132
+ title fa: "محصولات و کالاها", en: "Products Catalog"
133
+ icon "Package"
134
+ api_path "/api/admin/products"
135
+ group "فروشگاه و خدمات"
136
+ order 4
137
+ searchable_fields [:title, :sku]
138
+
139
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
140
+ field :title, type: :string, form_type: :text, required: true, col_width: 8
141
+ field :sku, type: :string, form_type: :text, required: true, col_width: 4
142
+
143
+ field :price, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0, max: 1000000000 }
144
+ field :stock, type: :number, form_type: :number, required: true, col_width: 6, validation: { min: 0 }
145
+
146
+ # Using JSON Editor with accordion and default nested structures
147
+ field :specs_json, type: :json, form_type: :json_editor, hidden_in_table: true, accordion: true, default_value: {
148
+ weight_kg: 1.2,
149
+ brand: "Shayan Group",
150
+ warranty_months: 24,
151
+ dimensions: { length: 30, width: 20, height: 10 }
152
+ }
153
+
154
+ field :status, type: :string, form_type: :select, display_as: "badge", options: [
155
+ { label: { fa: "موجود در انبار", en: "In Stock" }, value: "in_stock", color: "green" },
156
+ { label: { fa: "موجودی محدود", en: "Low Stock" }, value: "low_stock", color: "amber" },
157
+ { label: { fa: "ناموجود", en: "Out of Stock" }, value: "out_of_stock", color: "red" }
158
+ ]
159
+ end
160
+ ```
161
+
162
+ ### Resource 4: News (Relations, File Uploads, WYSIWYG, Jalali Date)
163
+
164
+ This entity brings it all together by referencing `users` and `categories`, and handling complex file configurations.
165
+
166
+ ```ruby
167
+ Kabk.register(name: "news_item", table: NewsItem) do
168
+ title fa: "اخبار و اطلاعیه‌ها", en: "News & Announcements"
169
+ icon "Newspaper"
170
+ api_path "/api/admin/news"
171
+ group "مدیریت محتوا"
172
+ order 1
173
+ concurrency_field "updated_at"
174
+
175
+ searchable_fields [:title, :content]
176
+ filterable_fields [:published, :category_ids]
177
+
178
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true, col_width: 2
179
+
180
+ field :title, type: :string, form_type: :text, required: true, col_width: 12, validation: {
181
+ min_length: 5, max_length: 150,
182
+ custom_message: { fa: "عنوان بین ۵ تا ۱۵۰ کاراکتر", en: "Title 5-150 chars" }
183
+ }
184
+
185
+ # Many-to-One Relation (Author -> User)
186
+ field :author_id, type: :relation, form_type: :relation_select, required: true, col_width: 6, relation: {
187
+ resource: "users", cardinality: :many_to_one,
188
+ value_field: "id", label_field: "full_name",
189
+ display_key: "author_id_display", fetch_endpoint: "/api/admin/users",
190
+ search_param: "search", debounce_ms: 300, per_page: 20
191
+ }
192
+
193
+ # Many-to-Many Relation (News -> Categories)
194
+ field :category_ids, type: :relation, form_type: :multiselect, required: true, col_width: 6, relation: {
195
+ resource: "categories", cardinality: :many_to_many,
196
+ value_field: "id", label_field: "title",
197
+ display_key: "category_ids_display", fetch_endpoint: "/api/admin/categories"
198
+ }
199
+
200
+ # Single Image Upload (with constraints)
201
+ field :cover_image, type: :file, form_type: :image_single, display_as: "thumbnail", nullable: true, col_width: 6, upload_config: {
202
+ allowed_extensions: ["jpg", "jpeg", "png", "webp"],
203
+ max_size_mb: 3
204
+ }
205
+
206
+ # Multi-Image Gallery
207
+ field :gallery, type: :array, form_type: :image_multi, hidden_in_table: true, col_width: 6, upload_config: {
208
+ allowed_extensions: ["jpg", "png", "webp"], max_size_mb: 5
209
+ }
210
+
211
+ field :content, type: :string, form_type: :wysiwyg, hidden_in_table: true, accordion: true, required: true
212
+
213
+ field :published, type: :boolean, form_type: :switch, display_as: "boolean_icon", default_value: false
214
+
215
+ # Dependent Field with Jalali Calendar
216
+ field :publish_date, type: :datetime, form_type: :datetime, calendar: :jalali, nullable: true, col_width: 6, depends_on: {
217
+ field: "published", value: true
218
+ }, validation: { min_date: "2020-01-01T00:00:00Z" }
219
+
220
+ field :updated_at, type: :datetime, form_type: :datetime, calendar: :jalali, readonly: true, hidden_in_form: true
221
+ end
222
+ ```
223
+
224
+ ---
225
+
226
+ ## 3. What to do next?
227
+ Now that the schema and DSL have been mapped out, simply run your Roda application and navigate to `/api/admin` in your browser. The Simurgh panel will dynamically parse this metadata and render all interactive UI components, Jalali calendars, relationships, validations, and custom fonts.
data/examples/app.rb ADDED
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roda"
4
+ require "sequel"
5
+ require "logger"
6
+
7
+ # Setup memory database
8
+ DB = Sequel.sqlite
9
+ DB.extension :pagination
10
+ DB.logger = Logger.new($stdout)
11
+
12
+ # Create tables matching Schema_Sample.json entities
13
+ DB.create_table :users do
14
+ primary_key :id
15
+ String :full_name
16
+ String :email
17
+ String :avatar
18
+ String :role, default: "editor"
19
+ String :bio
20
+ TrueClass :is_active, default: true
21
+ DateTime :created_at
22
+ DateTime :updated_at
23
+ end
24
+
25
+ DB.create_table :categories do
26
+ primary_key :id
27
+ String :title
28
+ String :slug
29
+ String :color_code
30
+ end
31
+
32
+ DB.create_table :news_items do
33
+ primary_key :id
34
+ String :title
35
+ Integer :author_id
36
+ String :category_ids # Simplified many-to-many as JSON string or comma-separated for SQLite
37
+ String :cover_image
38
+ String :gallery
39
+ String :content
40
+ TrueClass :published, default: false
41
+ DateTime :publish_date
42
+ DateTime :updated_at
43
+ DateTime :created_at
44
+ end
45
+
46
+ # Require Kabk core
47
+ $LOAD_PATH.unshift File.expand_path("../../kabk/lib", __dir__)
48
+ require "kabk"
49
+
50
+ # Define Sequel Models
51
+ class User < Sequel::Model; end
52
+ class Category < Sequel::Model; end
53
+ class NewsItem < Sequel::Model; end
54
+
55
+ # Register Resources using DSL
56
+ Kabk.register(name: "user", table: User) do
57
+ title fa: "مدیریت کاربران", en: "User Directory"
58
+ plural_name "users"
59
+ icon "Users"
60
+ api_path "/api/admin/users"
61
+ searchable_fields ["full_name", "email"]
62
+ sortable_fields ["id", "full_name", "role", "created_at"]
63
+ concurrency_field "updated_at"
64
+
65
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
66
+ field :full_name, type: :string, form_type: :text, required: true, validation: { min_length: 3, max_length: 100 }
67
+ field :email, type: :string, form_type: :text, required: true, validation: { unique: true }
68
+ field :avatar, type: :file, form_type: :image_single, display_as: :thumbnail
69
+ field :role, type: :string, form_type: :select, required: true, display_as: :badge, default_value: "editor"
70
+ field :bio, type: :string, form_type: :textarea, hidden_in_table: true, accordion: true
71
+ field :is_active, type: :boolean, form_type: :switch, display_as: :boolean_icon, default_value: true
72
+ field :created_at, type: :date, form_type: :date, calendar: :jalali, readonly: true, hidden_in_form: true
73
+ end
74
+
75
+ Kabk.register(name: "category", table: Category) do
76
+ title fa: "دسته‌بندی‌ها", en: "Categories"
77
+ plural_name "categories"
78
+ icon "Tags"
79
+ searchable_fields ["title", "slug"]
80
+
81
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
82
+ field :title, type: :string, form_type: :text, required: true
83
+ field :slug, type: :string, form_type: :text, required: true
84
+ field :color_code, type: :string, form_type: :text, display_as: :badge
85
+ end
86
+
87
+ Kabk.register(name: "news_item", table: NewsItem) do
88
+ title fa: "اخبار و اطلاعیه‌ها", en: "News & Announcements"
89
+ plural_name "news"
90
+ icon "Newspaper"
91
+ searchable_fields ["title", "content"]
92
+ concurrency_field "updated_at"
93
+
94
+ field :id, type: :number, form_type: :number, primary_key: true, hidden_in_form: true
95
+ field :title, type: :string, form_type: :text, required: true
96
+ field :author_id, type: :relation, form_type: :relation_select, relation: { resource: "user", cardinality: "many_to_one", value_field: "id", label_field: "full_name" }
97
+ field :category_ids, type: :relation, form_type: :multiselect, relation: { resource: "category", cardinality: "many_to_many", value_field: "id", label_field: "title" }
98
+ field :cover_image, type: :file, form_type: :image_single, display_as: :thumbnail
99
+ field :content, type: :string, form_type: :wysiwyg, hidden_in_table: true, accordion: true
100
+ field :published, type: :boolean, form_type: :switch, display_as: :boolean_icon
101
+ field :publish_date, type: :datetime, form_type: :datetime, calendar: :jalali, depends_on: { field: "published", value: true }
102
+ field :updated_at, type: :datetime, form_type: :datetime, calendar: :jalali, readonly: true, hidden_in_form: true
103
+ end
104
+
105
+ # Seed some mock data
106
+ User.create(full_name: "Admin User", email: "admin@example.com", role: "admin")
107
+ User.create(full_name: "Reza Mohammadi", email: "reza@example.com", role: "editor")
108
+ Category.create(title: "Announcements", slug: "announcements")
109
+ Category.create(title: "Events", slug: "events")
110
+ NewsItem.create(title: "First Post", author_id: 2, category_ids: "[1,2]", content: "Hello World", updated_at: Time.now)
111
+
112
+ # Main App using Roda
113
+ class App < Roda
114
+ # Setup plugin with mock JWT Strategy
115
+ plugin :kabk,
116
+ mount_at: "/api/admin",
117
+ auth_strategy: Kabk::Auth::JwtStrategy.new(secret: "test_secret"),
118
+ system_config: { logo_url: "/kabk_logo.png" }
119
+
120
+ route do |r|
121
+ # This mounts all the standard protocol endpoints and UI automatically under /api/admin
122
+ r.kabk
123
+
124
+ r.root do
125
+ "Welcome to Kabk Simurgh Protocol API."
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "app"
2
+ run App.freeze.app