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: 2da130825a1eb93bcdb92862f36f4e3f62ccc037b0727d6f8c936a79ebf369ca
4
- data.tar.gz: fa81a3cd03d67dd6696f0ce21f6b314e932d0e5bea4ab93d805087ec5eff8d92
3
+ metadata.gz: e18fba691675d980ac2aa985b2e390aa45acae166602faf8601ced9820b31dc0
4
+ data.tar.gz: 615cfd471a33904767ef6502690355d984541f57644676905759900076de2307
5
5
  SHA512:
6
- metadata.gz: 6bdb9e7db069e7da7370d1516786fdc21936b401971b82b3d6558aeb626eb958523eac3ad3efce309f13341fef6b3e2e066170569fa8e4087ceb5b60400dac49
7
- data.tar.gz: 32752db03260bd4dba4a25dd1a9cc439f53ff04983aaa2fea7219d70623d44add188e23c7d510ac5b0fea5b9a067b83e5cb7a9d656c89492aad5a91706e9db24
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
+ ```
@@ -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,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, data: data }, status: status
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, errors: errors }, status: status
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
- 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")
@@ -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
@@ -1,3 +1,3 @@
1
1
  module RailsApiBase
2
- VERSION = "1.0.6"
2
+ VERSION = "1.0.8"
3
3
  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.6
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-06 00:00:00.000000000 Z
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.6.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: []