rails_api_base 1.0.6 → 1.0.7

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: 2da130825a1eb93bcdb92862f36f4e3f62ccc037b0727d6f8c936a79ebf369ca
4
- data.tar.gz: fa81a3cd03d67dd6696f0ce21f6b314e932d0e5bea4ab93d805087ec5eff8d92
3
+ metadata.gz: 4b6c866039a21cff850edb5dafc20def6aa0dc08dc80915c9e5e9c8a91cb3d67
4
+ data.tar.gz: 83d17238894d5fbf5b9c10bd46de22bc75240921dba22905a59c5cd610a8b227
5
5
  SHA512:
6
- metadata.gz: 6bdb9e7db069e7da7370d1516786fdc21936b401971b82b3d6558aeb626eb958523eac3ad3efce309f13341fef6b3e2e066170569fa8e4087ceb5b60400dac49
7
- data.tar.gz: 32752db03260bd4dba4a25dd1a9cc439f53ff04983aaa2fea7219d70623d44add188e23c7d510ac5b0fea5b9a067b83e5cb7a9d656c89492aad5a91706e9db24
6
+ metadata.gz: b8791bc3f3118651b51963ecc773e43ddd5d5b4ec1f7d6fd3ab3a7f5c3fc22a2107d4c6e8baffa6ec425621106a4212e6989a1d2329c093ca01b031dd0420539
7
+ data.tar.gz: 97174e7dfc9bbd446ae8186361349805beb01810f67e3e9e7bbfbf92ffca2e42ed001a99ec464470c7f768174d1aedcf3e86e2dfd2d9f7d60f0978238ab5e59c
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
+ ```
@@ -52,4 +52,4 @@ module BlueprintOptionsSupport
52
52
  # def blueprint_options_for_locale
53
53
  # { locale: I18n.locale }
54
54
  # end
55
- end
55
+ end
@@ -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,7 +2,8 @@
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
 
@@ -32,8 +33,11 @@ module RailsApiBase
32
33
  before_action :set_resource, only: %i[show update destroy]
33
34
 
34
35
  def index
35
- items = collection
36
- data = serialize_collection(items)
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")
@@ -108,4 +130,4 @@ module RailsApiBase
108
130
  raise NotImplementedError, "Subclass must implement ##{controller_name.singularize}_params"
109
131
  end
110
132
  end
111
- end
133
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsApiBase
2
- VERSION = "1.0.6"
2
+ VERSION = "1.0.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_api_base
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - aric.zheng
@@ -49,6 +49,7 @@ files:
49
49
  - Rakefile
50
50
  - app/assets/stylesheets/rails_api_base/application.css
51
51
  - app/controllers/concerns/blueprint_options_support.rb
52
+ - app/controllers/concerns/queryable.rb
52
53
  - app/controllers/rails_api_base/application_controller.rb
53
54
  - app/controllers/rails_api_base/base_controller.rb
54
55
  - app/helpers/rails_api_base/application_helper.rb