rails_api_base 1.0.6 → 1.0.8
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e18fba691675d980ac2aa985b2e390aa45acae166602faf8601ced9820b31dc0
|
|
4
|
+
data.tar.gz: 615cfd471a33904767ef6502690355d984541f57644676905759900076de2307
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5863ec6fdd7373b4d91d1c05fc6dc0b87f4766ba00f7a565ae499ab8e793812c1ee644f98df81791f7bc575aa9122de15aad153676a278425da3174e600ec85
|
|
7
|
+
data.tar.gz: 3faa7cdf902519e04b11cb6843f988131611be27f5cb595635603d8cfe4ef2541fe3740e8e5bff896399fe7c37f1e878581d7089b50b4e493449da3cf6b24544
|
data/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
> Standardized JSON API base controller for Rails with Blueprinter support.
|
|
3
3
|
|
|
4
4
|
## Features
|
|
5
|
-
|
|
6
5
|
- Unified response format: `{ code, msg, data }`
|
|
7
6
|
- Field selection via `?fields=title,user`
|
|
8
7
|
- Auto Blueprinter integration (`PostBlueprint`)
|
|
@@ -11,9 +10,46 @@
|
|
|
11
10
|
- N+1 safe (with proper `includes` in controller)
|
|
12
11
|
|
|
13
12
|
## Installation
|
|
14
|
-
|
|
15
13
|
Add to your Gemfile:
|
|
16
14
|
|
|
17
15
|
```ruby
|
|
18
16
|
gem 'rails_api_base'
|
|
19
|
-
gem 'blueprinter'
|
|
17
|
+
gem 'blueprinter'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
- ./app/blueprints/post_blueprint.rb
|
|
22
|
+
- ./app/controllers/posts_controller.rb
|
|
23
|
+
|
|
24
|
+
```rb
|
|
25
|
+
# blueprint file - optimize version(dynamic_fields)
|
|
26
|
+
class PostBlueprint < Blueprinter::Base
|
|
27
|
+
identifier :id
|
|
28
|
+
fields :title, :content
|
|
29
|
+
|
|
30
|
+
# === 动态注册可选字段 ===
|
|
31
|
+
dynamic_fields = [:user, :tags]
|
|
32
|
+
|
|
33
|
+
dynamic_fields.each do |field_name|
|
|
34
|
+
field field_name, if: ->(_, model, options) {
|
|
35
|
+
Array(options[:fields]).include?(field_name)
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# posts controller
|
|
41
|
+
class PostsController < RailsApiBase::BaseController
|
|
42
|
+
blueprint_options_default :fields
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
def collection
|
|
46
|
+
Post.includes(:user, :tags)
|
|
47
|
+
.page(params[:page])
|
|
48
|
+
.per(params[:size] || 10)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def post_params
|
|
52
|
+
params.require(:post).permit(:title, :content)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# app/controllers/concerns/rails_api_base/queryable.rb
|
|
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 sanitize_sql_order(field, direction)
|
|
109
|
+
direction = %i[asc desc].include?(direction) ? direction : :asc
|
|
110
|
+
"#{connection.quote_column_name(field)} #{direction}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply_searching(scope)
|
|
114
|
+
config = query_config[:searching]
|
|
115
|
+
term = params[config[:search_param]]&.strip
|
|
116
|
+
return scope unless term.present?
|
|
117
|
+
|
|
118
|
+
searchable_fields = config[:searchable_fields]
|
|
119
|
+
return scope if searchable_fields.empty?
|
|
120
|
+
|
|
121
|
+
connection = scope.connection
|
|
122
|
+
|
|
123
|
+
terms = Array.new(searchable_fields.size, "%#{term}%")
|
|
124
|
+
conditions = searchable_fields.map do |f|
|
|
125
|
+
"LOWER(#{connection.quote_column_name(f)}) LIKE LOWER(?)"
|
|
126
|
+
end.join(' OR ')
|
|
127
|
+
scope.where(conditions, *terms)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def apply_filtering(scope)
|
|
131
|
+
config = query_config[:filtering]
|
|
132
|
+
raw_filters = params[config[:filter_param]]
|
|
133
|
+
return scope if raw_filters.blank?
|
|
134
|
+
|
|
135
|
+
# 1. 获取允许过滤的字段名(字符串数组)
|
|
136
|
+
filterable_fields = config[:filterable_fields].map(&:to_s)
|
|
137
|
+
|
|
138
|
+
# 2. permit 这些字段(包括嵌套操作符,如 filter[status][eq])
|
|
139
|
+
permitted = raw_filters.permit(filterable_fields.map { |f| "#{f}" } +
|
|
140
|
+
filterable_fields.map { |f| "#{f}.*" })
|
|
141
|
+
|
|
142
|
+
# 3. 转为普通 Hash
|
|
143
|
+
filters = permitted.to_h
|
|
144
|
+
|
|
145
|
+
connection = scope.connection
|
|
146
|
+
|
|
147
|
+
filters.inject(scope) do |s, (field, value)|
|
|
148
|
+
if value.is_a?(Hash)
|
|
149
|
+
op = value.keys.first&.to_sym
|
|
150
|
+
val = value.values.first
|
|
151
|
+
apply_filter_operation(s, field, op, val)
|
|
152
|
+
else
|
|
153
|
+
s.where("#{connection.quote_column_name(field)} = ?", val_or_array(value))
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def apply_filter_operation(scope, field, op, value)
|
|
159
|
+
connection = scope.connection
|
|
160
|
+
quoted = connection.quote_column_name(field)
|
|
161
|
+
case op
|
|
162
|
+
when :eq then scope.where("#{quoted} = ?", val_or_array(value))
|
|
163
|
+
when :neq then scope.where("#{quoted} != ?", val_or_array(value))
|
|
164
|
+
when :gt then scope.where("#{quoted} > ?", value)
|
|
165
|
+
when :gte then scope.where("#{quoted} >= ?", value)
|
|
166
|
+
when :lt then scope.where("#{quoted} < ?", value)
|
|
167
|
+
when :lte then scope.where("#{quoted} <= ?", value)
|
|
168
|
+
when :in then scope.where(quoted => Array(value))
|
|
169
|
+
when :nin then scope.where.not(quoted => Array(value))
|
|
170
|
+
else scope
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def val_or_array(val)
|
|
175
|
+
return val unless val.is_a?(String) && val.include?(',')
|
|
176
|
+
val.split(',').map(&:strip)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
module RailsApiBase
|
|
3
3
|
class BaseController < ActionController::API
|
|
4
4
|
include BlueprintOptionsSupport
|
|
5
|
-
|
|
5
|
+
include Queryable
|
|
6
|
+
|
|
6
7
|
# 默认开启 :defaults 模式
|
|
7
8
|
blueprint_options_default :defaults
|
|
8
9
|
|
|
9
10
|
# === 统一成功响应 ===
|
|
10
11
|
def render_success(data, status: :ok, message: "success", code: nil)
|
|
11
12
|
code ||= response_code_for(action_name, status: status)
|
|
12
|
-
render json: { code: code, msg: message,
|
|
13
|
+
render json: { code: code, msg: message, data: data }, status: status
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
# === 统一错误响应 ===
|
|
16
17
|
def render_error(message: "Unprocessable Entity", status: :unprocessable_entity, errors: nil)
|
|
17
18
|
code = response_code_for(action_name, status: status)
|
|
18
|
-
render json: { code: code, msg: message,
|
|
19
|
+
render json: { code: code, msg: message, errors: errors }, status: status
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# === 允许子类自定义响应 code ===
|
|
@@ -32,8 +33,11 @@ module RailsApiBase
|
|
|
32
33
|
before_action :set_resource, only: %i[show update destroy]
|
|
33
34
|
|
|
34
35
|
def index
|
|
35
|
-
|
|
36
|
-
data =
|
|
36
|
+
result = apply_query_with_meta(collection)
|
|
37
|
+
data = {
|
|
38
|
+
query_config[:meta][:rows_key] => serialize_collection(result[:collection]),
|
|
39
|
+
}
|
|
40
|
+
data.merge!(result[:meta]) if result[:meta]
|
|
37
41
|
render_success(data)
|
|
38
42
|
end
|
|
39
43
|
|
|
@@ -44,7 +48,8 @@ module RailsApiBase
|
|
|
44
48
|
|
|
45
49
|
def create
|
|
46
50
|
resource = resource_class.new(resource_params)
|
|
47
|
-
if resource.save
|
|
51
|
+
if before_save(resource) && resource.save
|
|
52
|
+
after_save(resource)
|
|
48
53
|
data = serialize_resource(resource)
|
|
49
54
|
render_success(data, status: :created, message: "Created successfully")
|
|
50
55
|
else
|
|
@@ -53,7 +58,8 @@ module RailsApiBase
|
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
def update
|
|
56
|
-
if resource.update(resource_params)
|
|
61
|
+
if before_save(resource) && resource.update(resource_params)
|
|
62
|
+
after_save(resource)
|
|
57
63
|
data = serialize_resource(resource)
|
|
58
64
|
render_success(data, message: "Updated successfully")
|
|
59
65
|
else
|
|
@@ -61,6 +67,22 @@ module RailsApiBase
|
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
|
|
70
|
+
# === 可选:通用钩子(默认调用 create/update 钩子)===
|
|
71
|
+
def before_save(resource)
|
|
72
|
+
action_name == "create" ? before_create(resource) : before_update(resource)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def after_save(resource)
|
|
76
|
+
action_name == "create" ? after_create(resource) : after_update(resource)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# === 子类覆盖这些 ===
|
|
80
|
+
def before_create(resource); true; end
|
|
81
|
+
def after_create(resource); end
|
|
82
|
+
|
|
83
|
+
def before_update(resource); true; end
|
|
84
|
+
def after_update(resource); end
|
|
85
|
+
|
|
64
86
|
def destroy
|
|
65
87
|
resource.destroy
|
|
66
88
|
render_success(nil, message: "Deleted successfully")
|
|
@@ -90,6 +112,10 @@ module RailsApiBase
|
|
|
90
112
|
controller_name.singularize.classify.constantize
|
|
91
113
|
end
|
|
92
114
|
|
|
115
|
+
def resource_key
|
|
116
|
+
controller_name.singularize.underscore.to_sym
|
|
117
|
+
end
|
|
118
|
+
|
|
93
119
|
def resource
|
|
94
120
|
instance_variable_get("@#{controller_name.singularize}")
|
|
95
121
|
end
|
|
@@ -105,7 +131,9 @@ module RailsApiBase
|
|
|
105
131
|
end
|
|
106
132
|
|
|
107
133
|
def resource_params
|
|
108
|
-
raise NotImplementedError, "Subclass must implement ##{controller_name.singularize}_params"
|
|
134
|
+
# raise NotImplementedError, "Subclass must implement ##{controller_name.singularize}_params"
|
|
135
|
+
permitted_params = params.require(resource_key)
|
|
136
|
+
permitted_params.permit!
|
|
109
137
|
end
|
|
110
138
|
end
|
|
111
|
-
end
|
|
139
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_api_base
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- aric.zheng
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-08 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -49,6 +50,7 @@ files:
|
|
|
49
50
|
- Rakefile
|
|
50
51
|
- app/assets/stylesheets/rails_api_base/application.css
|
|
51
52
|
- app/controllers/concerns/blueprint_options_support.rb
|
|
53
|
+
- app/controllers/concerns/queryable.rb
|
|
52
54
|
- app/controllers/rails_api_base/application_controller.rb
|
|
53
55
|
- app/controllers/rails_api_base/base_controller.rb
|
|
54
56
|
- app/helpers/rails_api_base/application_helper.rb
|
|
@@ -69,6 +71,7 @@ metadata:
|
|
|
69
71
|
homepage_uri: https://js.work
|
|
70
72
|
source_code_uri: https://github.com/afeiship/rails_api_base
|
|
71
73
|
changelog_uri: https://github.com/afeiship/rails_api_base/blob/master/CHANGELOG.md
|
|
74
|
+
post_install_message:
|
|
72
75
|
rdoc_options: []
|
|
73
76
|
require_paths:
|
|
74
77
|
- lib
|
|
@@ -83,7 +86,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
86
|
- !ruby/object:Gem::Version
|
|
84
87
|
version: '0'
|
|
85
88
|
requirements: []
|
|
86
|
-
rubygems_version: 3.
|
|
89
|
+
rubygems_version: 3.5.22
|
|
90
|
+
signing_key:
|
|
87
91
|
specification_version: 4
|
|
88
92
|
summary: Standardized JSON API foundation for Rails apps.
|
|
89
93
|
test_files: []
|