rails_curd_base 0.1.2

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: 45ac1efb0de272c8bfc202ab29ec4f216582e23fcd16cfc85dbe1ce3bfbb9534
4
+ data.tar.gz: 64dcac6b6270ce247d77192370a306f683c3d1b04df6b54fb0b32a86d5c8919b
5
+ SHA512:
6
+ metadata.gz: 9da65048981078282d064ad920534a761a1bd33932571ba74d5cfe89e1726d15d5e9f0cb6a13e7942eea08f2623e058ef955f3f6bdfe2e2c0d17bcf244a65c2b
7
+ data.tar.gz: e2d92aba652e4a9cb808e3af848666515fbf4a61dc2cb35565ddae0ea10ba5682118b6fe84a5a6b579c93fd21155e80921aa06438ead5b02389d562db4b1cff5
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright aric.zheng
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,550 @@
1
+ # RailsCurdBase
2
+
3
+ > RailsCurdBase 是一个用于 Ruby on Rails 的 CRUD 基础控制器 gem,提供开箱即用的增删改查功能、查询能力、分页、排序、搜索和过滤。它使用 RailsWarp 提供统一的 JSON 响应格式,并使用 Kaminari 处理分页。
4
+
5
+ ## 特性
6
+
7
+ - 🔥 **零配置 CRUD** - 继承 `CurdController` 即可获取完整的增删改查功能
8
+ - 🔍 **强大查询** - 内置分页、排序、搜索、过滤功能,通过 `supports_query` 轻松配置
9
+ - 📦 **统一响应** - 基于 RailsWarp 的统一 JSON 响应格式
10
+ - 🎣 **生命周期钩子** - 提供 `before_create`, `after_create`, `before_update`, `after_update` 四个钩子
11
+ - 🤖 **AI 友助** - 配备完整的 LLM 提示文档 (`llms.txt`),方便 AI 辅助编程
12
+ - ⚙️ **零依赖蓝图** - 不依赖任何序列化 gem,使用 Rails 原生 `as_json`
13
+ - 🔧 **高度可定制** - 轻松覆盖 `collection`、序列化方法等实现自定义逻辑
14
+
15
+ ## 兼容性
16
+
17
+ - ✅ Rails 6.0+
18
+ - ✅ Ruby 2.7+
19
+ - ✅ API 模式 (`ActionController::API`)
20
+
21
+ ## 安装
22
+
23
+ 将这行添加到你的应用的 Gemfile 中:
24
+
25
+ ```ruby
26
+ gem "rails_curd_base"
27
+ ```
28
+
29
+ 然后执行:
30
+
31
+ ```bash
32
+ $ bundle install
33
+ ```
34
+
35
+ 或者手动安装:
36
+
37
+ ```bash
38
+ $ gem install rails_curd_base
39
+ ```
40
+
41
+ ## 快速开始
42
+
43
+ ### 1. 创建模型
44
+
45
+ ```ruby
46
+ # app/models/post.rb
47
+ class Post < ApplicationRecord
48
+ validates :title, presence: true
49
+ validates :content, presence: true
50
+ end
51
+ ```
52
+
53
+ ### 2. 创建控制器
54
+
55
+ ```ruby
56
+ # app/controllers/api/posts_controller.rb
57
+ class Api::PostsController < RailsCurdBase::CurdController
58
+ # 启用查询功能
59
+ supports_query(
60
+ pagination: { enabled: true, default_per: 10, max_per: 50 },
61
+ sorting: { enabled: true, allowed_fields: [:id, :title, :created_at] },
62
+ searching: { enabled: true, searchable_fields: [:title, :content] },
63
+ filtering: { enabled: true, filterable_fields: [:status] }
64
+ )
65
+ end
66
+ ```
67
+
68
+ ### 3. 配置路由
69
+
70
+ ```ruby
71
+ # config/routes.rb
72
+ Rails.application.routes.draw do
73
+ namespace :api do
74
+ resources :posts
75
+ end
76
+ end
77
+ ```
78
+
79
+ 就这么简单!现在你拥有了完整的 CRUD API:
80
+
81
+ - `GET /api/posts` - 列表(支持分页、排序、搜索、过滤)
82
+ - `GET /api/posts/1` - 详情
83
+ - `POST /api/posts` - 创建
84
+ - `PUT /api/posts/1` - 更新
85
+ - `DELETE /api/posts/1` - 删除
86
+
87
+ ## API 使用示例
88
+
89
+ ### 获取列表(基础)
90
+
91
+ ```bash
92
+ GET /api/posts
93
+ ```
94
+
95
+ **响应:**
96
+ ```json
97
+ {
98
+ "success": true,
99
+ "code": 200,
100
+ "msg": "Retrieved successfully",
101
+ "data": {
102
+ "rows": [...],
103
+ "total": 100
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### 分页查询
109
+
110
+ ```bash
111
+ GET /api/posts?page=1&size=20
112
+ ```
113
+
114
+ ### 排序
115
+
116
+ ```bash
117
+ # 升序
118
+ GET /api/posts?sort=title
119
+
120
+ # 降序(使用 - 前缀)
121
+ GET /api/posts?sort=-created_at
122
+ ```
123
+
124
+ ### 搜索
125
+
126
+ ```bash
127
+ GET /api/posts?q=hello
128
+ ```
129
+
130
+ 在 `title` 和 `content` 字段中模糊搜索 "hello"。
131
+
132
+ ### 过滤
133
+
134
+ ```bash
135
+ # 等于
136
+ GET /api/posts?filter[status]=published
137
+
138
+ # 大于等于
139
+ GET /api/posts?filter[views][gte]=100
140
+
141
+ # 在数组中
142
+ GET /api/posts?filter[category_id][in]=1,2,3
143
+ ```
144
+
145
+ 支持的过滤操作符:`eq`(默认), `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`
146
+
147
+ ### 组合查询
148
+
149
+ ```bash
150
+ GET /api/posts?page=1&size=20&sort=-created_at&q=rails&filter[status]=published
151
+ ```
152
+
153
+ ### 创建记录
154
+
155
+ ```bash
156
+ POST /api/posts
157
+ Content-Type: application/json
158
+
159
+ {
160
+ "post": {
161
+ "title": "Hello World",
162
+ "content": "My first post",
163
+ "status": "published"
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### 更新记录
169
+
170
+ ```bash
171
+ PUT /api/posts/1
172
+ Content-Type: application/json
173
+
174
+ {
175
+ "post": {
176
+ "title": "Updated Title"
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### 删除记录
182
+
183
+ ```bash
184
+ DELETE /api/posts/1
185
+ ```
186
+
187
+ ## 高级用法
188
+
189
+ ### 生命周期钩子
190
+
191
+ ```ruby
192
+ class Api::PostsController < RailsCurdBase::CurdController
193
+ supports_query(
194
+ pagination: { enabled: true, default_per: 10 }
195
+ )
196
+
197
+ # 创建前钩子
198
+ def before_create(resource)
199
+ resource.author = current_user
200
+ resource.published_at = Time.current if resource.status == 'published'
201
+ true # 必须返回 true,否则会中止保存
202
+ end
203
+
204
+ # 创建后钩子
205
+ def after_create(resource)
206
+ NotificationService.notify_followers(resource)
207
+ end
208
+
209
+ # 更新前钩子
210
+ def before_update(resource)
211
+ resource.editor = current_user
212
+ true
213
+ end
214
+
215
+ # 更新后钩子
216
+ def after_update(resource)
217
+ CacheService.invalidate(resource)
218
+ end
219
+ end
220
+ ```
221
+
222
+ ### 自定义数据范围
223
+
224
+ ```ruby
225
+ class Api::PostsController < RailsCurdBase::CurdController
226
+ # 只返回当前用户的文章
227
+ def collection
228
+ current_user.posts
229
+ end
230
+ end
231
+ ```
232
+
233
+ ### 嵌套资源
234
+
235
+ ```ruby
236
+ class Api::UserPostsController < RailsCurdBase::CurdController
237
+ def collection
238
+ user.posts # 假设 params[:user_id] 存在
239
+ end
240
+ end
241
+ ```
242
+
243
+ ### 自定义序列化
244
+
245
+ ```ruby
246
+ class Api::PostsController < RailsCurdBase::CurdController
247
+ private
248
+
249
+ def serialize_resource(resource)
250
+ {
251
+ id: resource.id,
252
+ title: resource.title,
253
+ summary: resource.content.truncate(100),
254
+ author_name: resource.author.name,
255
+ created_at: resource.created_at.iso8601
256
+ }
257
+ end
258
+
259
+ def serialize_collection(collection)
260
+ collection.map { |resource| serialize_resource(resource) }
261
+ end
262
+ end
263
+ ```
264
+
265
+ ### 添加认证和授权
266
+
267
+ ```ruby
268
+ class Api::PostsController < RailsCurdBase::CurdController
269
+ before_action :authenticate_user!
270
+ before_action :set_post, only: [:show, :update, :destroy]
271
+
272
+ private
273
+
274
+ def authenticate_user!
275
+ head :unauthorized unless current_user
276
+ end
277
+
278
+ def set_post
279
+ @post = current_user.posts.find_by(id: params[:id])
280
+ end
281
+ end
282
+ ```
283
+
284
+ ## Queryable 配置详解
285
+
286
+ `supports_query` 方法接受四个配置项:
287
+
288
+ ### 分页配置
289
+
290
+ ```ruby
291
+ pagination: {
292
+ enabled: true, # 是否启用
293
+ default_per: 20, # 默认每页数量
294
+ max_per: 100, # 最大每页数量
295
+ page_param: :page, # 页码参数名
296
+ per_param: :size # 每页数量参数名
297
+ }
298
+ ```
299
+
300
+ ### 排序配置
301
+
302
+ ```ruby
303
+ sorting: {
304
+ enabled: true, # 是否启用
305
+ allowed_fields: [:id, :title], # 允许排序的字段
306
+ sort_param: :sort, # 排序参数名
307
+ default_direction: :asc # 默认排序方向
308
+ }
309
+ ```
310
+
311
+ 使用:`?sort=title`(升序)或 `?sort=-title`(降序)
312
+
313
+ ### 搜索配置
314
+
315
+ ```ruby
316
+ searching: {
317
+ enabled: true, # 是否启用
318
+ searchable_fields: [:title, :content], # 可搜索的字段
319
+ search_param: :q # 搜索参数名
320
+ }
321
+ ```
322
+
323
+ 搜索会对所有 `searchable_fields` 执行 `LIKE %term%` 查询。
324
+
325
+ ### 过滤配置
326
+
327
+ ```ruby
328
+ filtering: {
329
+ enabled: true, # 是否启用
330
+ filterable_fields: [:status, :category_id], # 可过滤的字段
331
+ filter_param: :filter # 过滤参数名
332
+ }
333
+ ```
334
+
335
+ 支持的过滤操作符:
336
+ - `?filter[field]=value` - 等于 (eq)
337
+ - `?filter[field][neq]=value` - 不等于 (neq)
338
+ - `?filter[field][gt]=value` - 大于 (gt)
339
+ - `?filter[field][gte]=value` - 大于等于 (gte)
340
+ - `?filter[field][lt]=value` - 小于 (lt)
341
+ - `?filter[field][lte]=value` - 小于等于 (lte)
342
+ - `?filter[field][in]=1,2,3` - 在数组中 (in)
343
+ - `?filter[field][nin]=1,2,3` - 不在数组中 (nin)
344
+
345
+ ## 资源自推导
346
+
347
+ `CurdController` 会自动从控制器名称推导资源类:
348
+
349
+ | 控制器 | 资源类 | 参数键 | 实例变量 |
350
+ |---------|---------|---------|-----------|
351
+ | `Api::PostsController` | `Post` | `:post` | `@post` |
352
+ | `Api::UsersController` | `User` | `:user` | `@user` |
353
+ | `Api::CommentsController` | `Comment` | `:comment` | `@comment` |
354
+
355
+ 如需自定义,可覆盖以下方法:
356
+
357
+ ```ruby
358
+ def resource_class
359
+ CustomPost # 覆盖自动推导
360
+ end
361
+
362
+ def resource_key
363
+ :article # 覆盖 :post
364
+ end
365
+ ```
366
+
367
+ ## 响应格式
368
+
369
+ 所有 API 响应都遵循统一的格式(基于 RailsWarp):
370
+
371
+ ### 成功响应
372
+
373
+ ```json
374
+ {
375
+ "success": true,
376
+ "code": 200,
377
+ "msg": "Retrieved successfully",
378
+ "data": { ... }
379
+ }
380
+ ```
381
+
382
+ ### 错误响应
383
+
384
+ ```json
385
+ {
386
+ "success": false,
387
+ "code": 422,
388
+ "msg": "Validation failed",
389
+ "data": {
390
+ "errors": ["Title can't be blank"]
391
+ }
392
+ }
393
+ ```
394
+
395
+ ### HTTP 状态码
396
+
397
+ | 场景 | 状态码 |
398
+ |------|---------|
399
+ | 获取列表 | 200 |
400
+ | 获取详情 | 200 |
401
+ | 创建成功 | 201 |
402
+ | 更新成功 | 200 |
403
+ | 删除成功 | 204 |
404
+ | 验证失败 | 422 |
405
+ | 未找到 | 404 |
406
+ | 未授权 | 401 |
407
+
408
+ ## 示例应用
409
+
410
+ 完整的示例应用请查看 `test/dummy/` 目录:
411
+
412
+ ```bash
413
+ cd test/dummy
414
+ bin/rails db:migrate
415
+ bin/rails db:seed
416
+ bin/rails server
417
+ ```
418
+
419
+ 然后访问 `http://localhost:3000/api/posts`
420
+
421
+ 详细文档请查看 [test/dummy/EXAMPLE_USAGE.md](test/dummy/EXAMPLE_USAGE.md)
422
+
423
+ ## AI 辅助编程
424
+
425
+ 本项目包含完整的 LLM 提示文档 `llms.txt`,帮助 Claude、ChatGPT 等 AI 更好地理解和使用本项目。
426
+
427
+ ## 依赖项
428
+
429
+ - **rails** >= 6.0
430
+ - **kaminari** >= 0.16 - 分页功能
431
+ - **rails_warp** >= 0.1.0 - 统一响应格式
432
+
433
+ ## 性能优化建议
434
+
435
+ 1. **数据库索引** - 在排序和过滤的字段上添加索引
436
+ ```ruby
437
+ add_index :posts, :status
438
+ add_index :posts, :created_at
439
+ add_index :posts, :author_id
440
+ ```
441
+
442
+ 2. **限制查询字段** - 通过覆盖 `serialize_resource` 只返回需要的字段
443
+
444
+ 3. **使用缓存** - 在钩子方法中实现缓存逻辑
445
+
446
+ 4. **优化 SQL** - 覆盖 `collection` 方法优化复杂查询
447
+
448
+ ## 常见问题
449
+
450
+ ### 1. 如何添加额外的查询逻辑?
451
+
452
+ ```ruby
453
+ def collection
454
+ base = super
455
+ base = base.where(published: true) unless current_user&.admin?
456
+ base
457
+ end
458
+ ```
459
+
460
+ ### 2. 如何实现软删除?
461
+
462
+ ```ruby
463
+ def destroy
464
+ resource.update!(deleted_at: Time.current)
465
+ ok message: "Deleted successfully", code: 204
466
+ end
467
+ ```
468
+
469
+ ### 3. 如何添加版本控制?
470
+
471
+ ```ruby
472
+ # config/routes.rb
473
+ namespace :api do
474
+ namespace :v1 do
475
+ resources :posts
476
+ end
477
+ end
478
+
479
+ # app/controllers/api/v1/posts_controller.rb
480
+ module Api
481
+ module V1
482
+ class PostsController < RailsCurdBase::CurdController
483
+ # ...
484
+ end
485
+ end
486
+ end
487
+ ```
488
+
489
+ ### 4. 如何自定义错误处理?
490
+
491
+ ```ruby
492
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
493
+ rescue_from StandardError, with: :handle_error
494
+
495
+ private
496
+
497
+ def handle_not_found
498
+ fail message: "Record not found", code: 404
499
+ end
500
+
501
+ def handle_error(e)
502
+ Rails.logger.error "Error: #{e.class} - #{e.message}"
503
+ fail message: "Internal server error", code: 500
504
+ end
505
+ ```
506
+
507
+ ## 开发
508
+
509
+ 欢迎贡献!请遵循以下步骤:
510
+
511
+ 1. Fork 本仓库
512
+ 2. 创建你的特性分支 (`git checkout -b my-amazing-feature`)
513
+ 3. 提交你的修改 (`git commit -am 'Add some amazing feature'`)
514
+ 4. 推送到分支 (`git push origin my-amazing-feature`)
515
+ 5. 创建新的 Pull Request
516
+
517
+ ### 运行测试
518
+
519
+ ```bash
520
+ cd test/dummy
521
+ bin/rails db:migrate RAILS_ENV=test
522
+ bin/rails test
523
+ ```
524
+
525
+ ## 路线图
526
+
527
+ ![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)
528
+ ![Rails](https://img.shields.io/badge/rails-%3E%206.0-red.svg)
529
+ ![Ruby](https://img.shields.io/badge/ruby-%3E%3D2.7-red.svg)
530
+
531
+ ## 作者
532
+
533
+ - **aric.zheng** - 1290657123@qq.com
534
+
535
+ ## 许可证
536
+
537
+ 本 gem 以 MIT 许可证开源 - 详见 [MIT-LICENSE](MIT-LICENSE) 文件。
538
+
539
+ ## 致谢
540
+
541
+ - [RailsWarp](https://github.com/afeiship/rails_warp) - 统一的响应格式
542
+ - [Kaminari](https://github.com/kaminari/kaminari) - 分页功能
543
+ - [rails_api_base](https://github.com/afeiship/rails_api_base) - 参考实现
544
+
545
+ ## 支持
546
+
547
+ 如有问题或建议,请:
548
+ - 提交 [Issue](https://github.com/afeiship/rails_curd_base/issues)
549
+ - 发送 Pull Request
550
+ - 联系作者:1290657123@qq.com
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
@@ -0,0 +1,178 @@
1
+ module RailsCurdBase
2
+ module Queryable
3
+ extend ActiveSupport::Concern
4
+
5
+ DEFAULT_CONFIG = {
6
+ pagination: {
7
+ enabled: false,
8
+ page_param: :page,
9
+ per_param: :size,
10
+ default_per: 10,
11
+ max_per: 100
12
+ },
13
+ sorting: {
14
+ enabled: false,
15
+ sort_param: :sort,
16
+ default_direction: :asc,
17
+ allowed_fields: []
18
+ },
19
+ searching: {
20
+ enabled: false,
21
+ search_param: :q,
22
+ searchable_fields: []
23
+ },
24
+ filtering: {
25
+ enabled: false,
26
+ filter_param: :filter,
27
+ filterable_fields: []
28
+ },
29
+ meta: {
30
+ enabled: true,
31
+ rows_key: :rows,
32
+ total_key: :total
33
+ }
34
+ }.freeze
35
+
36
+ class_methods do
37
+ def supports_query(user_config = {})
38
+ config = DEFAULT_CONFIG.deep_dup
39
+ user_config.each do |key, value|
40
+ if config.key?(key) && value.is_a?(Hash)
41
+ config[key].merge!(value)
42
+ else
43
+ config[key] = value
44
+ end
45
+ end
46
+
47
+ define_method(:query_config) { config }
48
+ include InstanceMethods
49
+ end
50
+ end
51
+
52
+ module InstanceMethods
53
+ def apply_query_with_meta(scope)
54
+ config = query_config
55
+ meta = {}
56
+
57
+ # 应用过滤、搜索、排序(不分页)
58
+ base_scope = scope
59
+ base_scope = apply_filtering(base_scope) if config[:filtering][:enabled]
60
+ base_scope = apply_searching(base_scope) if config[:searching][:enabled]
61
+ base_scope = apply_sorting(base_scope) if config[:sorting][:enabled]
62
+
63
+ # 获取总数(用于 meta)
64
+ if config[:meta][:enabled]
65
+ meta[config[:meta][:total_key]] = base_scope.count
66
+ end
67
+
68
+ # 应用分页
69
+ paginated_scope = base_scope
70
+ if config[:pagination][:enabled]
71
+ paginated_scope = apply_pagination(base_scope)
72
+ end
73
+
74
+ {
75
+ collection: paginated_scope,
76
+ meta: config[:meta][:enabled] ? meta : nil
77
+ }
78
+ end
79
+
80
+ private
81
+
82
+ def apply_pagination(scope)
83
+ config = query_config[:pagination]
84
+ page = [params[config[:page_param]].to_i, 1].max
85
+ per_input = params[config[:per_param]].to_i
86
+ per = per_input <= 0 ? config[:default_per] : [per_input, config[:max_per]].min
87
+ per = [per, 1].max
88
+ scope.page(page).per(per)
89
+ end
90
+
91
+ def apply_sorting(scope)
92
+ config = query_config[:sorting]
93
+ sort_param = params[config[:sort_param]]
94
+ return scope unless sort_param.present?
95
+
96
+ direction = sort_param.start_with?('-') ? :desc : :asc
97
+ field = sort_param.delete_prefix('-').to_sym
98
+ connection = scope.connection
99
+
100
+ if config[:allowed_fields].empty? || config[:allowed_fields].include?(field)
101
+ quoted_field = connection.quote_column_name(field)
102
+ return scope.order(Arel.sql("#{quoted_field} #{direction}"))
103
+ else
104
+ return scope
105
+ end
106
+ end
107
+
108
+ def apply_searching(scope)
109
+ config = query_config[:searching]
110
+ term = params[config[:search_param]]&.strip
111
+ return scope unless term.present?
112
+
113
+ searchable_fields = config[:searchable_fields]
114
+ return scope if searchable_fields.empty?
115
+
116
+ connection = scope.connection
117
+
118
+ terms = Array.new(searchable_fields.size, "%#{term}%")
119
+ conditions = searchable_fields.map do |f|
120
+ "LOWER(#{connection.quote_column_name(f)}) LIKE LOWER(?)"
121
+ end.join(' OR ')
122
+ scope.where(conditions, *terms)
123
+ end
124
+
125
+ def apply_filtering(scope)
126
+ config = query_config[:filtering]
127
+ raw_filters = params[config[:filter_param]]
128
+ return scope if raw_filters.blank?
129
+
130
+ # 1. 获取允许过滤的字段名(字符串数组)
131
+ filterable_fields = config[:filterable_fields].map(&:to_s)
132
+
133
+ # 2. permit 这些字段(包括嵌套操作符)
134
+ # 支持两种格式:filter[status]=published 或 filter[status][eq]=published
135
+ permitted = raw_filters.permit(
136
+ *filterable_fields.map { |f| f.to_sym },
137
+ *filterable_fields.map { |f| { f.to_sym => [:eq, :neq, :gt, :gte, :lt, :lte, :in, :nin] } }
138
+ )
139
+
140
+ # 3. 转为普通 Hash
141
+ filters = permitted.to_h
142
+
143
+ connection = scope.connection
144
+
145
+ filters.inject(scope) do |s, (field, value)|
146
+ if value.is_a?(Hash)
147
+ op = value.keys.first&.to_sym
148
+ val = value.values.first
149
+ apply_filter_operation(s, field, op, val)
150
+ else
151
+ s.where("#{connection.quote_column_name(field)} = ?", val_or_array(value))
152
+ end
153
+ end
154
+ end
155
+
156
+ def apply_filter_operation(scope, field, op, value)
157
+ connection = scope.connection
158
+ quoted = connection.quote_column_name(field)
159
+ case op
160
+ when :eq then scope.where("#{quoted} = ?", val_or_array(value))
161
+ when :neq then scope.where("#{quoted} != ?", val_or_array(value))
162
+ when :gt then scope.where("#{quoted} > ?", value)
163
+ when :gte then scope.where("#{quoted} >= ?", value)
164
+ when :lt then scope.where("#{quoted} < ?", value)
165
+ when :lte then scope.where("#{quoted} <= ?", value)
166
+ when :in then scope.where(quoted => Array(value))
167
+ when :nin then scope.where.not(quoted => Array(value))
168
+ else scope
169
+ end
170
+ end
171
+
172
+ def val_or_array(val)
173
+ return val unless val.is_a?(String) && val.include?(',')
174
+ val.split(',').map(&:strip)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,4 @@
1
+ module RailsCurdBase
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,103 @@
1
+ module RailsCurdBase
2
+ class CurdController < ActionController::API
3
+ include Queryable
4
+
5
+ # === CRUD 基础动作 ===
6
+ before_action :set_resource, only: %i[show update destroy]
7
+
8
+ def index
9
+ result = apply_query_with_meta(collection)
10
+ data = {
11
+ query_config[:meta][:rows_key] => serialize_collection(result[:collection]),
12
+ }
13
+ data.merge!(result[:meta]) if result[:meta]
14
+ ok data: data, message: "Retrieved successfully"
15
+ end
16
+
17
+ def show
18
+ data = serialize_resource(resource)
19
+ ok data: data
20
+ end
21
+
22
+ def create
23
+ @resource = resource_class.new(resource_params)
24
+ if before_save(@resource) && @resource.save
25
+ after_save(@resource)
26
+ data = serialize_resource(@resource)
27
+ ok data: data, message: "Created successfully", code: 201
28
+ else
29
+ fail message: "Validation failed", code: 422, data: { errors: @resource.errors.full_messages }
30
+ end
31
+ end
32
+
33
+ def update
34
+ if before_save(resource) && resource.update(resource_params)
35
+ after_save(resource)
36
+ data = serialize_resource(resource)
37
+ ok data: data, message: "Updated successfully"
38
+ else
39
+ fail message: "Validation failed", code: 422, data: { errors: resource.errors.full_messages }
40
+ end
41
+ end
42
+
43
+ # === 可选:通用钩子(默认调用 create/update 钩子)===
44
+ def before_save(resource)
45
+ action_name == "create" ? before_create(resource) : before_update(resource)
46
+ end
47
+
48
+ def after_save(resource)
49
+ action_name == "create" ? after_create(resource) : after_update(resource)
50
+ end
51
+
52
+ # === 子类覆盖这些 ===
53
+ def before_create(resource); true; end
54
+ def after_create(resource); end
55
+
56
+ def before_update(resource); true; end
57
+ def after_update(resource); end
58
+
59
+ def destroy
60
+ resource.destroy
61
+ ok message: "Deleted successfully", code: 204
62
+ end
63
+
64
+ private
65
+
66
+ # === 序列化逻辑 ===
67
+ def serialize_resource(resource)
68
+ resource.as_json
69
+ end
70
+
71
+ def serialize_collection(collection)
72
+ collection.as_json
73
+ end
74
+
75
+ # === 资源推导 ===
76
+ def resource_class
77
+ controller_name.singularize.classify.constantize
78
+ end
79
+
80
+ def resource_key
81
+ controller_name.singularize.underscore.to_sym
82
+ end
83
+
84
+ def resource
85
+ instance_variable_get("@#{controller_name.singularize}")
86
+ end
87
+
88
+ def set_resource
89
+ instance_variable_set("@#{controller_name.singularize}", resource_class.find(params[:id]))
90
+ rescue ActiveRecord::RecordNotFound
91
+ fail message: "Record not found", code: 404
92
+ end
93
+
94
+ def collection
95
+ resource_class.all
96
+ end
97
+
98
+ def resource_params
99
+ permitted_params = params.require(resource_key)
100
+ permitted_params.permit!
101
+ end
102
+ end
103
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ RailsCurdBase::Engine.routes.draw do
2
+ end
@@ -0,0 +1,5 @@
1
+ module RailsCurdBase
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RailsCurdBase
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module RailsCurdBase
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "rails_curd_base/version"
2
+ require "rails_curd_base/engine"
3
+ require "kaminari"
4
+ require "rails_warp"
5
+
6
+ module RailsCurdBase
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :rails_curd_base do
3
+ # # Task goes here
4
+ # end
data/llms.txt ADDED
@@ -0,0 +1,369 @@
1
+ # RailsCurdBase - AI Assistant Guide
2
+
3
+ > RailsCurdBase is a Ruby on Rails CRUD base controller gem providing ready-to-use Create, Read, Update, Delete functionality with advanced query capabilities including pagination, sorting, searching, and filtering. It integrates RailsWarp for unified JSON responses and Kaminari for pagination.
4
+
5
+ **IMPORTANT NOTES FOR AI ASSISTANTS:**
6
+ - RailsCurdBase inherits from ActionController::API for Rails API applications
7
+ - All responses use RailsWarp unified format: { success: boolean, code: integer, msg: string, data: object }
8
+ - Controllers automatically derive resource class from controller name (Api::PostsController => Post model)
9
+ - Built-in Queryable concern provides pagination, sorting, searching, filtering
10
+ - Four lifecycle hooks: before_create, after_create, before_update, after_update
11
+ - Resource automatically derived: controller_name.singularize.classify.constantize
12
+
13
+ ## DOCUMENTATION LINKS
14
+
15
+ - Repository: https://github.com/afeiship/rails_curd_base
16
+ - Usage Guide: test/dummy/EXAMPLE_USAGE.md (complete API documentation and examples)
17
+ - Test/Demo App: test/dummy/ directory (fully working example)
18
+
19
+ ## CORE FEATURES
20
+
21
+ ### 1. Zero-Configuration CRUD
22
+ Inherit from CurdController to get complete CRUD:
23
+ - index: List with pagination, sorting, searching, filtering
24
+ - show: Single record retrieval
25
+ - create: Create new records
26
+ - update: Update existing records
27
+ - destroy: Delete records
28
+
29
+ ### 2. Queryable Module (supports_query)
30
+ Configure via supports_query:
31
+ - Pagination: page, size parameters (Kaminari)
32
+ - Sorting: sort parameter (supports -field for desc)
33
+ - Searching: q parameter (multi-field LIKE search)
34
+ - Filtering: filter parameter (eq, neq, gt, gte, lt, lte, in, nin operators)
35
+
36
+ ### 3. Unified Response Format (RailsWarp)
37
+ - ok: success response with data, message, code
38
+ - fail: error response with message, code, errors data
39
+
40
+ ### 4. Lifecycle Hooks
41
+ - before_create(resource): called before create, return false to abort
42
+ - after_create(resource): called after successful create
43
+ - before_update(resource): called before update, return false to abort
44
+ - after_update(resource): called after successful update
45
+
46
+ ### 5. Automatic Resource Derivation
47
+ - resource_class: Model class (auto-derived from controller name)
48
+ - resource_key: Parameter symbol (:post, :user, etc.)
49
+ - resource: Current resource instance (@post, @user, etc.)
50
+
51
+ ## INSTALLATION
52
+
53
+ Add to Gemfile:
54
+ ```ruby
55
+ gem 'rails_curd_base'
56
+ ```
57
+
58
+ Run:
59
+ ```bash
60
+ bundle install
61
+ ```
62
+
63
+ ## BASIC USAGE
64
+
65
+ ### Create Controller
66
+ ```ruby
67
+ class Api::PostsController < RailsCurdBase::CurdController
68
+ supports_query(
69
+ pagination: { enabled: true, default_per: 10, max_per: 50 },
70
+ sorting: { enabled: true, allowed_fields: [:id, :title, :created_at] },
71
+ searching: { enabled: true, searchable_fields: [:title, :content] },
72
+ filtering: { enabled: true, filterable_fields: [:status] }
73
+ )
74
+ end
75
+ ```
76
+
77
+ ### API Endpoints
78
+ ```bash
79
+ # List (with pagination, sorting, searching, filtering)
80
+ GET /api/posts?page=1&size=10&sort=-created_at&q=rails&filter[status]=published
81
+
82
+ # Show single
83
+ GET /api/posts/1
84
+
85
+ # Create
86
+ POST /api/posts
87
+ {"post": {"title": "Hello", "content": "World", "status": "published"}}
88
+
89
+ # Update
90
+ PUT /api/posts/1
91
+ {"post": {"title": "Updated"}}
92
+
93
+ # Delete
94
+ DELETE /api/posts/1
95
+ ```
96
+
97
+ ### Response Format
98
+ Success:
99
+ ```json
100
+ {
101
+ "success": true,
102
+ "code": 200,
103
+ "msg": "Retrieved successfully",
104
+ "data": {
105
+ "rows": [...],
106
+ "total": 100
107
+ }
108
+ }
109
+ ```
110
+
111
+ Error:
112
+ ```json
113
+ {
114
+ "success": false,
115
+ "code": 422,
116
+ "msg": "Validation failed",
117
+ "data": {
118
+ "errors": ["Title can't be blank"]
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## QUERYABLE CONFIGURATION
124
+
125
+ ### Pagination
126
+ ```ruby
127
+ supports_query(
128
+ pagination: {
129
+ enabled: true,
130
+ default_per: 20,
131
+ max_per: 100,
132
+ page_param: :page,
133
+ per_param: :size
134
+ }
135
+ )
136
+ ```
137
+
138
+ ### Sorting
139
+ ```ruby
140
+ supports_query(
141
+ sorting: {
142
+ enabled: true,
143
+ allowed_fields: [:id, :title, :created_at],
144
+ sort_param: :sort
145
+ }
146
+ )
147
+ # Usage: ?sort=field or ?sort=-field (desc)
148
+ ```
149
+
150
+ ### Searching
151
+ ```ruby
152
+ supports_query(
153
+ searching: {
154
+ enabled: true,
155
+ searchable_fields: [:title, :content],
156
+ search_param: :q
157
+ }
158
+ )
159
+ # Usage: ?q=search_term
160
+ # Performs: WHERE field LIKE '%term%' OR field2 LIKE '%term%'
161
+ ```
162
+
163
+ ### Filtering
164
+ ```ruby
165
+ supports_query(
166
+ filtering: {
167
+ enabled: true,
168
+ filterable_fields: [:status, :category_id],
169
+ filter_param: :filter
170
+ }
171
+ )
172
+ # Usage:
173
+ # ?filter[status]=published (eq)
174
+ # ?filter[status][neq]=draft (not equals)
175
+ # ?filter[views][gte]=100 (greater than or equal)
176
+ # ?filter[views][lte]=1000 (less than or equal)
177
+ # ?filter[category_id][in]=1,2,3 (in array)
178
+ # ?filter[status][nin]=draft,archived (not in array)
179
+ ```
180
+
181
+ ## CUSTOMIZATION
182
+
183
+ ### Override Collection (Scope Data)
184
+ ```ruby
185
+ class Api::PostsController < RailsCurdBase::CurdController
186
+ def collection
187
+ current_user.posts # Only current user's posts
188
+ end
189
+ end
190
+ ```
191
+
192
+ ### Lifecycle Hooks
193
+ ```ruby
194
+ class Api::PostsController < RailsCurdBase::CurdController
195
+ before_action :authenticate_user!
196
+
197
+ def before_create(resource)
198
+ resource.author = current_user
199
+ resource.published_at = Time.current if resource.status == 'published'
200
+ true # Must return true to continue
201
+ end
202
+
203
+ def after_create(resource)
204
+ NotificationService.notify(resource)
205
+ end
206
+ end
207
+ ```
208
+
209
+ ### Custom Resource Class
210
+ ```ruby
211
+ class Api::PostsController < RailsCurdBase::CurdController
212
+ def resource_class
213
+ CustomPost # Override auto-derivation
214
+ end
215
+ end
216
+ ```
217
+
218
+ ### Custom Serialization
219
+ ```ruby
220
+ class Api::PostsController < RailsCurdBase::CurdController
221
+ private
222
+
223
+ def serialize_resource(resource)
224
+ {
225
+ id: resource.id,
226
+ title: resource.title,
227
+ summary: resource.content.truncate(100)
228
+ }
229
+ end
230
+
231
+ def serialize_collection(collection)
232
+ collection.map { |r| serialize_resource(r) }
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Add Authentication/Authorization
238
+ ```ruby
239
+ class Api::PostsController < RailsCurdBase::CurdController
240
+ before_action :authenticate_user!
241
+ before_action :set_post, only: [:show, :update, :destroy]
242
+
243
+ private
244
+
245
+ def authenticate_user!
246
+ head :unauthorized unless current_user
247
+ end
248
+
249
+ def set_post
250
+ @post = current_user.posts.find(params[:id])
251
+ end
252
+ end
253
+ ```
254
+
255
+ ## NAMING CONVENTIONS
256
+
257
+ - Controller: Api::ResourcesController or ResourcesController
258
+ - Model: Resource (auto-derived)
259
+ - Parameter: :resource (auto-derived)
260
+ - Instance: @resource (auto-derived)
261
+
262
+ Examples:
263
+ - Api::UsersController => User model, :user param, @user instance
264
+ - Api::PostsController => Post model, :post param, @post instance
265
+ - Api::CommentsController => Comment model, :comment param, @comment instance
266
+
267
+ ## COMMON PATTERNS
268
+
269
+ ### Nested Resources
270
+ ```ruby
271
+ def collection
272
+ user.posts # Assumes params[:user_id] or set_user before_action
273
+ end
274
+ ```
275
+
276
+ ### Add Query Scopes
277
+ ```ruby
278
+ def collection
279
+ base = super
280
+ base = base.where(published: true) unless current_user.admin?
281
+ base
282
+ end
283
+ ```
284
+
285
+ ### Custom Error Handling
286
+ ```ruby
287
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
288
+
289
+ private
290
+
291
+ def not_found
292
+ fail message: 'Record not found', code: 404
293
+ end
294
+ ```
295
+
296
+ ### Soft Deletes
297
+ ```ruby
298
+ def destroy
299
+ resource.update!(deleted_at: Time.current)
300
+ ok message: 'Deleted successfully', code: 204
301
+ end
302
+ ```
303
+
304
+ ## TESTING
305
+
306
+ Run the dummy application:
307
+ ```bash
308
+ cd test/dummy
309
+ bin/rails db:migrate
310
+ bin/rails db:seed
311
+ bin/rails server
312
+ ```
313
+
314
+ Test API:
315
+ ```bash
316
+ curl http://localhost:3000/api/posts
317
+ curl http://localhost:3000/api/posts/1
318
+ curl -X POST http://localhost:3000/api/posts -H 'Content-Type: application/json' -d '{"post":{"title":"Test"}}'
319
+ ```
320
+
321
+ ## DEPENDENCIES
322
+
323
+ - rails >= 6.0
324
+ - kaminari >= 0.16 (pagination)
325
+ - rails_warp >= 0.1.0 (unified responses)
326
+
327
+ ## IMPORTANT NOTES FOR AI ASSISTANTS
328
+
329
+ 1. ALWAYS inherit from RailsCurdBase::CurdController for API controllers
330
+ 2. Use supports_query for query configuration (pagination, sorting, searching, filtering)
331
+ 3. Use ok() for success responses, fail() for error responses
332
+ 4. Resource is auto-derived: controller_name.singularize.classify.constantize
333
+ 5. Override collection() to scope data (current_user.posts, etc.)
334
+ 6. Use hooks (before_create, after_create, etc.) for callbacks
335
+ 7. Parameters via resource_params() method (auto-permits :resource param)
336
+ 8. Query parameters: page, size, sort, q, filter[field]
337
+ 9. All query features are optional (enable/disable in supports_query)
338
+ 10. Filter operators: eq (default), neq, gt, gte, lt, lte, in, nin
339
+
340
+ ## PERFORMANCE TIPS
341
+
342
+ - Add database indexes on filtered/sorted fields
343
+ - Use select() to limit fields in queries
344
+ - Override serialize_resource for custom JSON output
345
+ - Use caching in hooks for expensive operations
346
+ - Optimize collection() for complex queries
347
+
348
+ ## FILE STRUCTURE
349
+
350
+ ```
351
+ app/controllers/rails_curd_base/
352
+ curd_controller.rb # Main controller
353
+ application_controller.rb
354
+
355
+ app/controllers/concerns/rails_curd_base/
356
+ queryable.rb # Query functionality
357
+ ```
358
+
359
+ ## CONTRIBUTE
360
+
361
+ Contributions welcome! Please:
362
+ - Follow existing code style
363
+ - Add tests for new features
364
+ - Update documentation
365
+ - Submit PR with clear description
366
+
367
+ ## LICENSE
368
+
369
+ MIT
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_curd_base
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - aric.zheng
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: kaminari
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.16'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails_warp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.0
55
+ description: |
56
+ RailsCurdBase provides a ready-to-use CRUD base controller for Ruby on Rails API applications.
57
+ It offers zero-configuration CRUD operations, powerful query capabilities (pagination, sorting,
58
+ searching, filtering), unified JSON response format via RailsWarp, and flexible lifecycle hooks.
59
+ Perfect for building RESTful APIs quickly with Rails 6.0+.
60
+ email:
61
+ - 1290657123@qq.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - MIT-LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - app/controllers/concerns/rails_curd_base/queryable.rb
70
+ - app/controllers/rails_curd_base/application_controller.rb
71
+ - app/controllers/rails_curd_base/curd_controller.rb
72
+ - config/routes.rb
73
+ - lib/rails_curd_base.rb
74
+ - lib/rails_curd_base/engine.rb
75
+ - lib/rails_curd_base/version.rb
76
+ - lib/tasks/rails_curd_base_tasks.rake
77
+ - llms.txt
78
+ homepage: https://github.com/aferic/rails_curd_base
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/aferic/rails_curd_base
83
+ source_code_uri: https://github.com/aferic/rails_curd_base
84
+ changelog_uri: https://github.com/aferic/rails_curd_base/blob/main/CHANGELOG.md
85
+ bug_tracker_uri: https://github.com/aferic/rails_curd_base/issues
86
+ documentation_uri: https://github.com/aferic/rails_curd_base/blob/main/README.md
87
+ wiki_uri: https://github.com/aferic/rails_curd_base/wiki
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 2.7.0
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.5.22
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: A Rails CRUD base controller with query capabilities, pagination, sorting,
107
+ searching, and filtering
108
+ test_files: []