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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +550 -0
- data/Rakefile +6 -0
- data/app/controllers/concerns/rails_curd_base/queryable.rb +178 -0
- data/app/controllers/rails_curd_base/application_controller.rb +4 -0
- data/app/controllers/rails_curd_base/curd_controller.rb +103 -0
- data/config/routes.rb +2 -0
- data/lib/rails_curd_base/engine.rb +5 -0
- data/lib/rails_curd_base/version.rb +3 -0
- data/lib/rails_curd_base.rb +8 -0
- data/lib/tasks/rails_curd_base_tasks.rake +4 -0
- data/llms.txt +369 -0
- metadata +108 -0
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
|
+

|
|
528
|
+

|
|
529
|
+

|
|
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,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,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
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: []
|