ar_serializer 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e9144cab6ef520e2c0734bbe98a13a046f3b7b8411310ccc522dc999bae5365
4
- data.tar.gz: d938b8684bbb22937c97c2572994b6bd11b38c72011f1b19975d96c7633656c7
3
+ metadata.gz: 87a5c3a6d707be1abec6c92335dbb5888b120a53b6e429eb1849b2aca42a1f1d
4
+ data.tar.gz: bdd382384bd5452fc909b26cb25fbefd252117fb552b0fc42bc4eddad016a13d
5
5
  SHA512:
6
- metadata.gz: a7ff7ed23abaabef6893c31dc259ce18eeb3111df652c4e4a2d717523853e4a4f27119ddb0d027583ae788b218fdea73abebfc5de99480447e51bbabe04929f9
7
- data.tar.gz: 8bceb19d2f319772e575c440870d074431f21efeab16375dced2e87637791e3d94927f0024c01bfd9669f0e0fb9743539f17ac47b12233a056db64e12b952264
6
+ metadata.gz: 86a9f5895dd2c9f5a9d3a1e51781d5286c4cfbb02687d2d6a9ec502ed85c0971a0343a08218ee0be3233a8f93e0be993e1ce7ab68b5fe75548a76e068247b3a9
7
+ data.tar.gz: 829e8871640abf2a4863d77df8ce703d9e85b38cf46496bd8573f6913572c5ebe18fc1a6c799e1b68124835abc1398841e810413b30cbddb5fe174b0301c85b1
@@ -0,0 +1,22 @@
1
+ name: Test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ ruby: [ '2.7', '3.0', '3.1' ]
9
+ gemfiles:
10
+ - gemfiles/Gemfile-rails-6
11
+ - gemfiles/Gemfile-rails-7
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ - run: |
19
+ sudo apt-get update
20
+ sudo apt-get install -y libsqlite3-dev
21
+ - run: bundle install --gemfile ${{ matrix.gemfiles }} --jobs 4 --retry 3
22
+ - run: bundle exec --gemfile ${{ matrix.gemfiles }} rake
data/Gemfile.lock CHANGED
@@ -1,48 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ar_serializer (1.0.0)
4
+ ar_serializer (1.2.0)
5
5
  activerecord
6
6
  top_n_loader
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activemodel (5.2.3)
12
- activesupport (= 5.2.3)
13
- activerecord (5.2.3)
14
- activemodel (= 5.2.3)
15
- activesupport (= 5.2.3)
16
- arel (>= 9.0)
17
- activesupport (5.2.3)
11
+ activemodel (7.0.2.3)
12
+ activesupport (= 7.0.2.3)
13
+ activerecord (7.0.2.3)
14
+ activemodel (= 7.0.2.3)
15
+ activesupport (= 7.0.2.3)
16
+ activesupport (7.0.2.3)
18
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (>= 0.7, < 2)
20
- minitest (~> 5.1)
21
- tzinfo (~> 1.1)
22
- arel (9.0.0)
23
- coderay (1.1.2)
24
- concurrent-ruby (1.1.5)
25
- docile (1.3.1)
26
- i18n (1.6.0)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ coderay (1.1.3)
22
+ concurrent-ruby (1.1.10)
23
+ docile (1.4.0)
24
+ i18n (1.10.0)
27
25
  concurrent-ruby (~> 1.0)
28
- json (2.1.0)
29
- method_source (0.9.2)
30
- minitest (5.11.3)
31
- pry (0.12.2)
32
- coderay (~> 1.1.0)
33
- method_source (~> 0.9.0)
34
- rake (12.3.2)
35
- simplecov (0.16.1)
26
+ method_source (1.0.0)
27
+ minitest (5.15.0)
28
+ pry (0.14.1)
29
+ coderay (~> 1.1)
30
+ method_source (~> 1.0)
31
+ rake (13.0.6)
32
+ simplecov (0.21.2)
36
33
  docile (~> 1.1)
37
- json (>= 1.8, < 3)
38
- simplecov-html (~> 0.10.0)
39
- simplecov-html (0.10.2)
40
- sqlite3 (1.4.0)
41
- thread_safe (0.3.6)
42
- top_n_loader (1.0.0)
34
+ simplecov-html (~> 0.11)
35
+ simplecov_json_formatter (~> 0.1)
36
+ simplecov-html (0.12.3)
37
+ simplecov_json_formatter (0.1.4)
38
+ sqlite3 (1.4.2)
39
+ top_n_loader (1.0.2)
43
40
  activerecord
44
- tzinfo (1.2.5)
45
- thread_safe (~> 0.1)
41
+ tzinfo (2.0.4)
42
+ concurrent-ruby (~> 1.0)
46
43
 
47
44
  PLATFORMS
48
45
  ruby
@@ -56,4 +53,4 @@ DEPENDENCIES
56
53
  sqlite3
57
54
 
58
55
  BUNDLED WITH
59
- 1.17.2
56
+ 2.3.3
data/README.md CHANGED
@@ -71,10 +71,10 @@ end
71
71
 
72
72
  # preloader
73
73
  class Foo < ActiveRecord::Base
74
- define_preloader :bar_count_loader do |models|
74
+ bar_count_loader = ->(models) do
75
75
  Bar.where(foo_id: models.map(&:id)).group(:foo_id).count
76
76
  end
77
- serializer_field :bar_count, preload: preloader_name_or_proc do |preloaded|
77
+ serializer_field :bar_count, preload: bar_count_loader do |preloaded|
78
78
  preloaded[id] || 0
79
79
  end
80
80
  # data_blockが `do |preloaded| preloaded[id] end` の場合は省略可能
@@ -85,11 +85,12 @@ class Post < ActiveRecord::Base
85
85
  has_many :comments
86
86
  serializer_field :comments
87
87
  end
88
- ArSerializer.serialize Post.all, comments: [:id, params: { order: { id: :desc }, limit: 2 }]
88
+ ArSerializer.serialize Post.all, { comments: [:id, params: { order_by: :createdAt, direction: :desc, first: 10 }] }
89
+ ArSerializer.serialize Post.all, { comments: [:id, params: { order_by: :updatedAt, last: 10 }] }
89
90
 
90
91
  # context and params
91
92
  class Post < ActiveRecord::Base
92
- serializer_field :created_at do |context, params|
93
+ serializer_field :created_at do |context, **params|
93
94
  created_at.in_time_zone(context[:tz]).strftime params[:format]
94
95
  end
95
96
  end
@@ -123,10 +124,10 @@ class User < ActiveRecord::Base
123
124
  serializer_field :o_posts, association: :posts, only: :title
124
125
  serializer_field :e_posts, association: :posts, except: :comments
125
126
  end
126
- ArSerializer.serialize user, o_posts: :title, e_posts: :body
127
- ArSerializer.serialize user, o_posts: :*, e_posts: :*
128
- ArSerializer.serialize user, o_posts: :body #=> Error
129
- ArSerializer.serialize user, e_posts: :comments #=> Error
127
+ ArSerializer.serialize user, { o_posts: :title, e_posts: :body }
128
+ ArSerializer.serialize user, { o_posts: :*, e_posts: :* }
129
+ ArSerializer.serialize user, { o_posts: :body } #=> Error
130
+ ArSerializer.serialize user, { e_posts: :comments } #=> Error
130
131
 
131
132
  # types
132
133
  class User < ActiveRecord::Base
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_serializer.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "activerecord", "~> 6.0.0"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_serializer.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "activerecord", "~> 7.0.0"
@@ -1,18 +1,36 @@
1
1
  require 'ar_serializer/error'
2
+ require 'top_n_loader'
3
+ require 'set'
2
4
 
3
5
  class ArSerializer::Field
4
- attr_reader :includes, :preloaders, :data_block, :only, :except, :order_column
5
- def initialize includes: nil, preloaders: [], data_block:, only: nil, except: nil, order_column: nil, type: nil, params_type: nil
6
+ attr_reader :includes, :preloaders, :data_block, :only, :except, :scoped_access, :order_column, :permission, :fallback
7
+ def initialize klass, name, includes: nil, preloaders: [], data_block:, only: nil, except: nil, private: false, scoped_access: nil, permission: nil, fallback: nil, order_column: nil, orderable: nil, type: nil, params_type: nil
8
+ @klass = klass
9
+ @name = name
6
10
  @includes = includes
7
11
  @preloaders = preloaders
8
12
  @only = only && [*only].map(&:to_s)
9
13
  @except = except && [*except].map(&:to_s)
14
+ @private = private
15
+ @scoped_access = scoped_access.nil? ? true : scoped_access
16
+ @permission = permission
17
+ @fallback = fallback
10
18
  @data_block = data_block
11
19
  @order_column = order_column
20
+ @orderable = orderable
12
21
  @type = type
13
22
  @params_type = params_type
14
23
  end
15
24
 
25
+ def orderable?
26
+ return @orderable unless @orderable.nil?
27
+ @orderable = !@permission && @klass.has_attribute?((@order_column || @name).to_s.underscore)
28
+ end
29
+
30
+ def private?
31
+ @private
32
+ end
33
+
16
34
  def type
17
35
  type = @type.is_a?(Proc) ? @type.call : @type
18
36
  splat = lambda do |t|
@@ -33,7 +51,7 @@ class ArSerializer::Field
33
51
  end
34
52
 
35
53
  def arguments
36
- return @params_type if @params_type
54
+ return @params_type.is_a?(Proc) ? @params_type.call : @params_type if @params_type
37
55
  @preloaders.size
38
56
  @data_block.parameters
39
57
  parameters_list = [@data_block.parameters.drop(@preloaders.size + 1)]
@@ -63,39 +81,38 @@ class ArSerializer::Field
63
81
  end
64
82
  return :any if any && arguments.empty?
65
83
  arguments.map do |key, req|
66
- type = key.to_s.match?(/^(.+_)?id|Id$/) ? :int : :any
67
- name = key.to_s.underscore
68
- type = [type] if name.singularize.pluralize == name
69
- [req ? key : "#{key}?", type]
84
+ camelcase = key.to_s.camelcase :lower
85
+ type = (
86
+ case key
87
+ when /^(.+_)?id$/
88
+ :int
89
+ when /^(.+_)?ids$/
90
+ [:int]
91
+ else
92
+ name = key.to_s.underscore
93
+ name.singularize.pluralize == name ? [:any] : :any
94
+ end
95
+ )
96
+ [req ? camelcase : "#{camelcase}?", type]
70
97
  end.to_h
71
98
  end
72
99
 
73
100
  def validate_attributes(attributes)
74
101
  return unless @only || @except
75
- keys = attributes.keys.map(&:to_s) - ['*']
102
+ keys = attributes.map(&:first).map(&:to_s) - ['*']
76
103
  return unless (@only && (keys - @only).present?) || (@except && (keys & @except).present?)
77
104
  invalid_keys = [*(@only && keys - @only), *(@except && keys & @except)].uniq
78
105
  raise ArSerializer::InvalidQuery, "unpermitted attribute: #{invalid_keys}"
79
106
  end
80
107
 
81
- def self.count_field(klass, association_name)
108
+ def self.count_field(klass, name, association_name, permission:)
82
109
  preloader = lambda do |models|
83
110
  klass.joins(association_name).where(id: models.map(&:id)).group(:id).count
84
111
  end
85
- data_block = lambda do |preloaded, _context, _params|
112
+ data_block = lambda do |preloaded, _context, **_params|
86
113
  preloaded[id] || 0
87
114
  end
88
- new preloaders: [preloader], data_block: data_block, type: :int
89
- end
90
-
91
- def self.top_n_loader_available?
92
- return @top_n_loader_available unless @top_n_loader_available.nil?
93
- @top_n_loader_available = begin
94
- require 'top_n_loader'
95
- true
96
- rescue LoadError
97
- nil
98
- end
115
+ new klass, name, preloaders: [preloader], data_block: data_block, type: :int, orderable: false, permission: permission, fallback: 0
99
116
  end
100
117
 
101
118
  def self.type_from_column_type(klass, name)
@@ -107,7 +124,7 @@ class ArSerializer::Field
107
124
  def self.type_from_attribute_type(klass, name)
108
125
  attr_type = klass.attribute_types[name]
109
126
  if attr_type.is_a?(ActiveRecord::Enum::EnumType) && klass.respond_to?(name.pluralize)
110
- values = klass.send(name.pluralize).keys.compact
127
+ values = klass.__send__(name.pluralize).keys.compact
111
128
  values = values.map { |v| v.is_a?(Symbol) ? v.to_s : v }.uniq
112
129
  valid_classes = [TrueClass, FalseClass, String, Integer, Float]
113
130
  return if values.empty? || (values.map(&:class) - valid_classes).present?
@@ -120,7 +137,7 @@ class ArSerializer::Field
120
137
  decimal: :float,
121
138
  string: :string,
122
139
  text: :string,
123
- json: :string,
140
+ json: :unknown,
124
141
  binary: :string,
125
142
  time: :string,
126
143
  date: :string,
@@ -128,107 +145,145 @@ class ArSerializer::Field
128
145
  }[attr_type.type]
129
146
  end
130
147
 
131
- def self.create(klass, name, type: nil, params_type: nil, count_of: nil, includes: nil, preload: nil, only: nil, except: nil, order_column: nil, &data_block)
148
+ def self.create(klass, name, type: nil, params_type: nil, count_of: nil, includes: nil, preload: nil, only: nil, except: nil, private: nil, scoped_access: nil, permission: nil, fallback: nil, order_column: nil, orderable: nil, &data_block)
149
+ name = name.to_s
132
150
  if count_of
133
- if includes || preload || data_block || only || except
134
- raise ArgumentError, 'includes, preload block cannot be used with count_of'
151
+ if includes || preload || data_block || only || except || order_column || orderable || scoped_access != nil || fallback
152
+ raise ArgumentError, 'wrong options for count_of field'
135
153
  end
136
- return count_field klass, count_of
154
+ return count_field klass, name, count_of, permission: permission
137
155
  end
138
- underscore_name = name.to_s.underscore
139
- association = klass.reflect_on_association underscore_name if klass.respond_to? :reflect_on_association
156
+ association = klass.reflect_on_association name.underscore if klass.respond_to? :reflect_on_association
140
157
  if association
141
158
  if association.collection?
142
159
  type ||= -> { [association.klass] }
160
+ fallback ||= []
143
161
  elsif (association.belongs_to? && association.options[:optional] == true) || (association.has_one? && association.options[:required] != true)
144
162
  type ||= -> { [association.klass, nil] }
145
163
  else
146
164
  type ||= -> { association.klass }
147
165
  end
148
- return association_field klass, underscore_name, only: only, except: except, type: type, collection: association.collection? if !includes && !preload && !data_block && !params_type
166
+ return association_field klass, name, only: only, except: except, scoped_access: scoped_access, permission: permission, fallback: fallback, type: type, collection: association.collection? if !includes && !preload && !data_block && !params_type
149
167
  end
150
168
  type ||= lambda do
151
169
  if klass.respond_to? :column_for_attribute
152
- type_from_column_type klass, underscore_name
170
+ type_from_column_type klass, name.underscore
153
171
  elsif klass.respond_to? :attribute_types
154
- type_from_attribute_type(klass, underscore_name) || :any
172
+ type_from_attribute_type(klass, name.underscore) || :any
155
173
  else
156
174
  :any
157
175
  end
158
176
  end
159
- custom_field klass, underscore_name, includes: includes, preload: preload, only: only, except: except, order_column: order_column, type: type, params_type: params_type, &data_block
177
+ custom_field klass, name, includes: includes, preload: preload, only: only, except: except, private: private, scoped_access: scoped_access, permission: permission, fallback: fallback, order_column: order_column, orderable: orderable, type: type, params_type: params_type, &data_block
160
178
  end
161
179
 
162
- def self.custom_field(klass, name, includes:, preload:, only:, except:, order_column:, type:, params_type:, &data_block)
180
+ def self.custom_field(klass, name, includes:, preload:, only:, except:, private:, scoped_access:, permission:, fallback:, order_column:, orderable:, type:, params_type:, &data_block)
181
+ underscore_name = name.underscore
163
182
  if preload
164
- preloaders = Array(preload).map do |preloader|
165
- next preloader if preloader.is_a? Proc
166
- unless klass._custom_preloaders.has_key?(preloader)
167
- raise ArgumentError, "preloader not found: #{preloader}"
168
- end
169
- klass._custom_preloaders[preloader]
170
- end
183
+ preloaders = [*preload]
171
184
  else
172
185
  preloaders = []
173
- includes ||= name if klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(name)
186
+ includes ||= underscore_name if klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(underscore_name)
187
+ end
188
+ if !data_block && preloaders.size == 1
189
+ data_block = ->(preloaded, _context, **_params) do
190
+ if preloaded.is_a? Set
191
+ preloaded.include? id
192
+ elsif fallback.nil?
193
+ preloaded[id] # returns preloaded.default unless preloaded.has_key?(id)
194
+ else
195
+ preloaded.has_key?(id) ? preloaded[id] : fallback
196
+ end
197
+ end
174
198
  end
175
- data_block ||= ->(preloaded, _context, _params) { preloaded[id] } if preloaders.size == 1
176
199
  raise ArgumentError, 'data_block needed if multiple preloaders are present' if !preloaders.empty? && data_block.nil?
177
200
  new(
178
- includes: includes, preloaders: preloaders, only: only, except: except, order_column: order_column, type: type, params_type: params_type,
179
- data_block: data_block || ->(_context, _params) { send name }
201
+ klass,
202
+ name,
203
+ includes: includes, preloaders: preloaders, only: only, except: except,
204
+ private: private, scoped_access: scoped_access, permission: permission, fallback: fallback,
205
+ order_column: order_column, orderable: orderable, type: type, params_type: params_type,
206
+ data_block: data_block || ->(_context, **_params) { __send__ underscore_name }
180
207
  )
181
208
  end
182
209
 
183
- def self.parse_order(klass, order)
184
- key, mode = begin
210
+ def self.parse_order(klass, order: nil, order_by: nil, direction: nil, only: nil, except: nil)
211
+ raise ArSerializer::InvalidQuery, 'invalid order' if order && (order_by || direction)
212
+ primary_key = klass.primary_key.to_sym
213
+ order_by = order_by&.to_s&.to_sym || primary_key
214
+ direction = direction&.to_s&.to_sym || :asc
215
+ if order # deprecated
185
216
  case order
186
217
  when Hash
187
218
  raise ArSerializer::InvalidQuery, 'invalid order' unless order.size == 1
188
- order.first
219
+ order_by, direction = order.first.map(&:to_sym)
189
220
  when Symbol, 'asc', 'desc'
190
- [klass.primary_key, order]
191
- when NilClass
192
- [klass.primary_key, :asc]
221
+ direction = order.to_sym
222
+ else
223
+ raise ArSerializer::InvalidQuery, 'invalid order'
193
224
  end
194
225
  end
195
- info = klass._serializer_field_info(key)
196
- key = info&.order_column || key.to_s.underscore
197
- raise ArSerializer::InvalidQuery, "unpermitted order key: #{key}" unless klass.has_attribute?(key) && info
198
- raise ArSerializer::InvalidQuery, "invalid order mode: #{mode.inspect}" unless [:asc, :desc, 'asc', 'desc'].include? mode
199
- [key.to_sym, mode.to_sym]
226
+ info = klass._serializer_field_info order_by
227
+ order_column = (info&.order_column || order_by).to_s.underscore.to_sym
228
+ raise ArSerializer::InvalidQuery, "invalid order direction: #{direction}" unless [:asc, :desc].include? direction
229
+ raise ArSerializer::InvalidQuery, "unpermitted order field: #{order_by}" unless order_by == primary_key || (info&.orderable? && (!only || only.include?(order_by)) && !except&.include?(order_by))
230
+ [order_column, direction]
200
231
  end
201
232
 
202
- def self.association_field(klass, name, only:, except:, type:, collection:)
233
+ def self.association_field(klass, name, only:, except:, scoped_access:, permission:, fallback:, type:, collection:)
234
+ underscore_name = name.underscore
235
+ only = [*only] if only
236
+ except = [*except] if except
203
237
  if collection
204
- preloader = lambda do |models, _context, limit: nil, order: nil, **_option|
205
- preload_association klass, models, name, limit: limit, order: order
238
+ preloader = lambda do |models, _context, limit: nil, order: nil, first: nil, last: nil, order_by: nil, direction: nil, **_option|
239
+ preload_association klass, models, underscore_name, limit: limit, order: order, first: first, last: last, order_by: order_by, direction: direction, only: only, except: except
240
+ end
241
+ params_type = -> {
242
+ orderable_keys = klass.reflect_on_association(underscore_name).klass._serializer_orderable_field_keys
243
+ orderable_keys &= only.map(&:to_s) if only
244
+ orderable_keys -= except.map(&:to_s) if except
245
+ orderable_keys |= ['id']
246
+ orderable_keys.sort!
247
+ modes = %w[asc desc]
248
+ {
249
+ first?: :int,
250
+ last?: :int,
251
+ orderBy?: orderable_keys.size == 1 ? orderable_keys.first : orderable_keys,
252
+ direction?: modes
253
+ }
254
+ }
255
+ data_block = lambda do |preloaded, _context, **_params|
256
+ preloaded ? preloaded[id || self] || [] : __send__(underscore_name)
206
257
  end
207
- params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
208
258
  else
209
- preloader = lambda do |models, _context, _params|
210
- preload_association klass, models, name
259
+ preloader = lambda do |models, _context, **_params|
260
+ preload_association klass, models, underscore_name
261
+ end
262
+ data_block = lambda do |preloaded, _context, **_params|
263
+ preloaded ? preloaded[id || self] : __send__(underscore_name)
211
264
  end
212
265
  end
213
- data_block = lambda do |preloaded, _context, _params|
214
- preloaded ? preloaded[id] || [] : send(name)
215
- end
216
- new preloaders: [preloader], data_block: data_block, only: only, except: except, type: type, params_type: params_type
266
+ new klass, name, preloaders: [preloader], data_block: data_block, only: only, except: except, scoped_access: scoped_access, permission: permission, fallback: fallback, type: type, params_type: params_type, orderable: false
217
267
  end
218
268
 
219
- def self.preload_association(klass, models, name, limit: nil, order: nil)
220
- limit = limit&.to_i
221
- order_key, order_mode = parse_order klass.reflect_on_association(name).klass, order
222
- if limit && top_n_loader_available?
223
- return TopNLoader.load_associations klass, models.map(&:id), name, limit: limit, order: { order_key => order_mode }
269
+ def self.preload_association(klass, models, name, limit: nil, order: nil, first: nil, last: nil, order_by: nil, direction: nil, only: nil, except: nil)
270
+ raise ArSerializer::InvalidQuery, 'invalid count option' if (limit && (first || last)) || (first && last)
271
+ first = (first || limit)&.to_i
272
+ last = last&.to_i
273
+ order_column, order_direction = parse_order klass.reflect_on_association(name).klass, order: order, order_by: order_by, direction: direction, only: only, except: except
274
+ if first || last
275
+ order_option = { order_column => first ? order_direction : (order_direction == :asc ? :desc : :asc) }
276
+ result = TopNLoader.load_associations klass, models.map(&:id), name, limit: first || last, order: order_option
277
+ result = result.transform_values!(&:reverse!) if last
278
+ return result
224
279
  end
225
- ActiveRecord::Associations::Preloader.new.preload models, name
226
- return if limit.nil? && order.nil?
280
+ ArSerializer.preload_associations models, name
281
+ return models.map { |m| [m.id || m, m.__send__(name)] }.to_h if !order && !order_by && !direction
227
282
  models.map do |model|
228
- records_nonnils, records_nils = model.send(name).partition(&order_key)
229
- records = records_nils.sort_by(&:id) + records_nonnils.sort_by { |r| [r[order_key], r.id] }
230
- records.reverse! if order_mode == :desc
231
- [model.id, limit ? records.take(limit) : records]
283
+ records_nonnils, records_nils = model.__send__(name).partition(&order_column)
284
+ records = records_nils.sort_by(&:id) + records_nonnils.sort_by { |r| [r[order_column], r.id] }
285
+ records.reverse! if order_direction == :desc
286
+ [model.id || model, records]
232
287
  end.to_h
233
288
  end
234
289
  end
@@ -9,8 +9,8 @@ class ArSerializer::GraphQL::Parser
9
9
  @chars = query.chars
10
10
  end
11
11
 
12
- def self.parse(*args)
13
- new(*args).parse
12
+ def self.parse(query, **option)
13
+ new(query, **option).parse
14
14
  end
15
15
 
16
16
  def parse
@@ -194,7 +194,7 @@ class ArSerializer::GraphQL::Parser
194
194
  args = parse_args
195
195
  consume_blank
196
196
  fields = parse_fields
197
- [name, { as: alias_name, params: args, attributes: fields }.compact]
197
+ [(alias_name || name), { field: name, params: args, attributes: fields }.compact]
198
198
  end
199
199
 
200
200
  def parse_fields
@@ -38,7 +38,15 @@ module ArSerializer::GraphQL
38
38
  type.collect_types types
39
39
  end
40
40
 
41
+ def args_required?
42
+ return false if field.arguments == :any
43
+ field.arguments.any? do |key, type|
44
+ !key.match?(/\?$/) && !(type.is_a?(Array) && type.include?(nil))
45
+ end
46
+ end
47
+
41
48
  def args_ts_type
49
+ return 'any' if field.arguments == :any
42
50
  arg_types = field.arguments.map do |key, type|
43
51
  "#{key}: #{TypeClass.from(type).ts_type}"
44
52
  end
@@ -95,7 +103,7 @@ module ArSerializer::GraphQL
95
103
  class InvalidType < StandardError; end
96
104
 
97
105
  def validate!
98
- valid_symbols = %i[number int float string boolean any]
106
+ valid_symbols = %i[number int float string boolean any unknown]
99
107
  invalids = []
100
108
  recursive_validate = lambda do |t|
101
109
  case t
@@ -145,8 +153,8 @@ module ArSerializer::GraphQL
145
153
  end
146
154
 
147
155
  def self.from(type, only = nil, except = nil)
148
- type = [type[0...-1].to_sym, nil] if type.is_a?(Symbol) && type.to_s.ends_with?('?')
149
- type = [type[0...-1], nil] if type.is_a?(String) && type.ends_with?('?')
156
+ type = [type[0...-1].to_sym, nil] if type.is_a?(Symbol) && type.to_s.end_with?('?')
157
+ type = [type[0...-1], nil] if type.is_a?(String) && type.end_with?('?')
150
158
  case type
151
159
  when Class
152
160
  SerializableTypeClass.new type, only, except
@@ -187,6 +195,8 @@ module ArSerializer::GraphQL
187
195
  :boolean
188
196
  when :other
189
197
  :other
198
+ when :unknown
199
+ :unknown
190
200
  else
191
201
  :any
192
202
  end
@@ -208,7 +218,7 @@ module ArSerializer::GraphQL
208
218
  ''
209
219
  when 'boolean'
210
220
  true
211
- when 'any'
221
+ when 'any', 'unknown'
212
222
  nil
213
223
  else
214
224
  type
@@ -219,7 +229,7 @@ module ArSerializer::GraphQL
219
229
  case type
220
230
  when :int, :float
221
231
  'number'
222
- when :string, :number, :boolean
232
+ when :string, :number, :boolean, :unknown
223
233
  type.to_s
224
234
  when Symbol
225
235
  'any'
@@ -258,7 +268,7 @@ module ArSerializer::GraphQL
258
268
  end
259
269
 
260
270
  def sample
261
- type.reject { |k| k.to_s.ends_with? '?' }.transform_values do |v|
271
+ type.reject { |k| k.to_s.end_with? '?' }.transform_values do |v|
262
272
  TypeClass.from(v).sample
263
273
  end
264
274
  end
@@ -1,149 +1,203 @@
1
1
  require 'ar_serializer/error'
2
2
 
3
- class ArSerializer::CompositeValue
4
- def initialize(pairs:, output:)
5
- @pairs = pairs
6
- @output = output
3
+ class ArSerializer::CustomSerializable
4
+ attr_reader :ar_custom_serializable_models
5
+ def initialize(models, &block)
6
+ @ar_custom_serializable_models = models
7
+ @block = block
7
8
  end
8
9
 
9
- def ar_serializer_build_sub_calls
10
- [@output, @pairs]
10
+ def ar_custom_serializable_data(result)
11
+ @block.call result
11
12
  end
12
13
  end
13
14
 
14
- module ArSerializer::ArrayLikeCompositeValue
15
- def ar_serializer_build_sub_calls
16
- output = []
17
- record_elements = []
18
- each do |record|
19
- data = {}
20
- output << data
21
- record_elements << [record, data]
22
- end
23
- [output, record_elements]
24
- end
15
+ module ArSerializer::ArrayLikeSerializable
25
16
  end
26
17
 
27
18
  module ArSerializer::Serializer
28
19
  def self.current_namespaces
29
- Thread.current[:ar_serializer_current_namespaces]
20
+ Thread.current[:ar_serializer_current_namespaces] || [nil]
30
21
  end
31
22
 
32
23
  def self.with_namespaces(namespaces)
33
24
  namespaces_was = Thread.current[:ar_serializer_current_namespaces]
34
- Thread.current[:ar_serializer_current_namespaces] = namespaces
25
+ Thread.current[:ar_serializer_current_namespaces] = Array(namespaces) | [nil]
35
26
  yield
36
27
  ensure
37
28
  Thread.current[:ar_serializer_current_namespaces] = namespaces_was
38
29
  end
39
30
 
40
- def self.serialize(model, args, context: nil, include_id: false, use: nil)
31
+ def self.serialize(model, query, context: nil, use: nil, permission: true)
32
+ return nil if model.nil?
41
33
  with_namespaces use do
42
- attributes = parse_args(args)[:attributes]
34
+ attributes = parse_args(query)[:attributes]
43
35
  if model.is_a?(ArSerializer::Serializable)
44
- output = {}
45
- _serialize [[model, output]], attributes, context, include_id
46
- output
36
+ result = _serialize [model], attributes, context, permission: permission
37
+ result[model]
47
38
  else
48
- sets = model.to_a.map do |record|
49
- [record, {}]
50
- end
51
- _serialize sets, attributes, context, include_id
52
- sets.map(&:last)
39
+ models = model.to_a
40
+ result = _serialize models, attributes, context, permission: permission
41
+ models.map { |m| result[m] }.compact
53
42
  end
54
43
  end
55
44
  end
56
45
 
57
- def self._serialize(mixed_value_outputs, attributes, context, include_id, only = nil, except = nil)
58
- mixed_value_outputs.group_by { |v, _o| v.class }.each do |klass, value_outputs|
46
+ def self._serialize(mixed_models, attributes, context, only: nil, except: nil, permission: true)
47
+ output_for_model = {}
48
+ mixed_models.group_by(&:class).each do |klass, models|
59
49
  next unless klass.respond_to? :_serializer_field_info
60
- models = value_outputs.map(&:first)
61
- value_outputs.each { |value, output| output[:id] = value.id } if include_id && klass.method_defined?(:id)
62
- if attributes[:*]
50
+ models.uniq!
51
+ if attributes.any? { |k, _| k == :* }
63
52
  all_keys = klass._serializer_field_keys.map(&:to_sym)
64
53
  all_keys &= only.map(&:to_sym) if only
65
54
  all_keys -= except.map(&:to_sym) if except
66
- attributes = all_keys.map { |k| [k, {}] }.to_h.merge attributes
67
- attributes.delete :*
55
+ attributes = all_keys.map { |k| [k, {}] } + attributes.reject { |k, _| k == :* }
68
56
  end
69
- attributes.each_key do |name|
70
- field = klass._serializer_field_info name
71
- raise ArSerializer::InvalidQuery, "No serializer field `#{name}`#{" namespaces: #{current_namespaces}" if current_namespaces} for #{klass}" unless field
72
- ActiveRecord::Associations::Preloader.new.preload models, field.includes if field.includes.present?
57
+ attributes.each do |name, sub_args|
58
+ field_name = sub_args[:field_name] || name
59
+ field = klass._serializer_field_info field_name
60
+ if field.nil? || field.private?
61
+ message = "No serializer field `#{field_name}`#{" namespaces: #{current_namespaces.compact}" if current_namespaces.any?} for #{klass}"
62
+ raise ArSerializer::InvalidQuery, message
63
+ end
64
+ ArSerializer.preload_associations models, field.includes if field.includes.present?
73
65
  end
74
66
 
75
- preloader_params = attributes.flat_map do |name, sub_args|
76
- klass._serializer_field_info(name).preloaders.map do |p|
77
- [p, sub_args[:params]]
67
+ preload = lambda do |preloader, params|
68
+ has_keyword = preloader.parameters.any? { |type, _name| %i[key keyreq keyrest].include? type }
69
+ arity = preloader.arity.abs
70
+ arguments = [models]
71
+ if has_keyword
72
+ arguments << context unless arity == 2
73
+ preloader.call(*arguments, **(params || {}))
74
+ else
75
+ arguments << context unless arity == 1
76
+ preloader.call(*arguments)
78
77
  end
79
78
  end
80
- defaults = klass._serializer_field_info(:defaults)
81
- if defaults
82
- preloader_params += defaults.preloaders.map { |p| [p] }
79
+
80
+ preloader_values = {}
81
+ if permission == true
82
+ permission_field = klass._serializer_field_info :permission
83
+ elsif permission
84
+ permission_field = klass._serializer_field_info permission
85
+ raise ArgumentError, "No permission field #{permission} for #{klass}" unless permission_field
83
86
  end
84
- preloader_values = preloader_params.compact.uniq.map do |key|
85
- preloader, params = key
86
- if preloader.arity < 0
87
- [key, preloader.call(models, context, params || {})]
88
- else
89
- [key, preloader.call(*[models, context, params || {}].take(preloader.arity))]
87
+ if permission_field
88
+ preloadeds = permission_field.preloaders.map do |p|
89
+ preloader_values[[p, nil]] ||= preload.call p, nil
90
90
  end
91
- end.to_h
91
+ models = models.select do |model|
92
+ model.instance_exec(*preloadeds, context, {}, &permission_field.data_block)
93
+ end
94
+ end
92
95
 
96
+ defaults = klass._serializer_field_info :defaults
93
97
  if defaults
94
- preloadeds = defaults.preloaders.map { |p| preloader_values[[p]] } || []
95
- value_outputs.each do |value, output|
96
- data = value.instance_exec(*preloadeds, context, {}, &defaults.data_block)
97
- output.update data
98
+ defaults.preloaders.each do |p|
99
+ preloader_values[[p, nil]] ||= preload.call p, nil
98
100
  end
99
101
  end
100
102
 
103
+ attributes.each do |name, sub_args|
104
+ field_name = sub_args[:field_name] || name
105
+ klass._serializer_field_info(field_name).preloaders.each do |p|
106
+ params = sub_args[:params]
107
+ preloader_values[[p, params]] ||= preload.call p, params
108
+ end
109
+ end
110
+
111
+ models.each do |model|
112
+ output_for_model[model] = {}
113
+ end
114
+
101
115
  attributes.each do |name, sub_arg|
102
116
  params = sub_arg[:params]
103
- sub_calls = []
104
117
  column_name = sub_arg[:column_name] || name
105
- info = klass._serializer_field_info name
118
+ field_name = sub_arg[:field_name] || name
119
+ info = klass._serializer_field_info field_name
106
120
  preloadeds = info.preloaders.map { |p| preloader_values[[p, params]] } || []
107
121
  data_block = info.data_block
108
- value_outputs.each do |value, output|
109
- child = value.instance_exec(*preloadeds, context, params || {}, &data_block)
110
- if child.is_a?(Array) && child.all? { |el| el.is_a? ArSerializer::Serializable }
111
- output[column_name] = child.map do |record|
112
- data = {}
113
- sub_calls << [record, data]
114
- data
115
- end
116
- elsif child.respond_to? :ar_serializer_build_sub_calls
117
- sub_output, record_elements = child.ar_serializer_build_sub_calls
118
- record_elements.each { |o| sub_calls << o }
119
- output[column_name] = sub_output
120
- elsif child.is_a? ArSerializer::CompositeValue
121
- sub_output, record_elements = child.build
122
- record_elements.each { |o| sub_calls << o }
123
- output[column_name] = sub_output
122
+ permission_block = info.permission
123
+ fallback = info.fallback
124
+ sub_results = {}
125
+ sub_models = []
126
+ models.each do |model|
127
+ next if permission_block && !model.instance_exec(context, **(params || {}), &permission_block)
128
+ child = model.instance_exec(*preloadeds, context, **(params || {}), &data_block)
129
+ if child.is_a?(ArSerializer::ArrayLikeSerializable) || (child.is_a?(Array) && child.any? { |el| el.is_a? ArSerializer::Serializable })
130
+ sub_results[model] = [:multiple, child]
131
+ sub_models << child.grep(ArSerializer::Serializable)
132
+ elsif child.respond_to?(:ar_custom_serializable_models) && child.respond_to?(:ar_custom_serializable_data)
133
+ sub_results[model] = [:custom, child]
134
+ sub_models << child.ar_custom_serializable_models
124
135
  elsif child.is_a? ArSerializer::Serializable
125
- data = {}
126
- sub_calls << [child, data]
127
- output[column_name] = data
136
+ sub_results[model] = [:single, child]
137
+ sub_models << child
128
138
  else
129
- output[column_name] = child
139
+ sub_results[model] = [:data, child]
140
+ end
141
+ end
142
+
143
+ sub_models.flatten!
144
+ sub_models.uniq!
145
+ unless sub_models.empty?
146
+ sub_attributes = sub_arg[:attributes] || {}
147
+ info.validate_attributes sub_attributes
148
+ result = _serialize(
149
+ sub_models,
150
+ sub_attributes,
151
+ context,
152
+ only: info.only,
153
+ except: info.except,
154
+ permission: info.scoped_access
155
+ )
156
+ end
157
+
158
+ models.each do |model|
159
+ data = output_for_model[model]
160
+ type, res = sub_results[model]
161
+ case type
162
+ when :single
163
+ data[column_name] = result[res]
164
+ when :multiple
165
+ arr = data[column_name] = []
166
+ res.each do |r|
167
+ if r.is_a? ArSerializer::Serializable
168
+ arr << result[r] if result.key? r
169
+ else
170
+ arr << r
171
+ end
172
+ end
173
+ when :custom
174
+ data[column_name] = res.ar_custom_serializable_data result || {}
175
+ when :data
176
+ data[column_name] = res
177
+ else
178
+ data[column_name] = fallback
130
179
  end
131
180
  end
132
- next if sub_calls.empty?
133
- sub_attributes = sub_arg[:attributes] || {}
134
- info.validate_attributes sub_attributes
135
- _serialize sub_calls, sub_attributes, context, include_id, info.only, info.except
181
+ end
182
+
183
+ if defaults
184
+ preloadeds = defaults.preloaders.map { |p| preloader_values[[p]] } || []
185
+ models.each do |model|
186
+ data = model.instance_exec(*preloadeds, context, {}, &defaults.data_block)
187
+ output_for_model[model].update data
188
+ end
136
189
  end
137
190
  end
191
+ output_for_model
138
192
  end
139
193
 
140
- def self.deep_with_indifferent_access params
194
+ def self.deep_underscore_keys params
141
195
  case params
142
196
  when Array
143
- params.map { |v| deep_with_indifferent_access v }
197
+ params.map { |v| deep_underscore_keys v }
144
198
  when Hash
145
- params.transform_keys(&:to_sym).transform_values! do |v|
146
- deep_with_indifferent_access v
199
+ params.transform_keys { |k| k.to_s.underscore.to_sym }.transform_values! do |v|
200
+ deep_underscore_keys v
147
201
  end
148
202
  else
149
203
  params
@@ -151,27 +205,26 @@ module ArSerializer::Serializer
151
205
  end
152
206
 
153
207
  def self.parse_args(args, only_attributes: false)
154
- attributes = {}
208
+ attributes = []
155
209
  params = nil
156
210
  column_name = nil
211
+ field_name = nil
157
212
  (args.is_a?(Array) ? args : [args]).each do |arg|
158
213
  if arg.is_a?(Symbol) || arg.is_a?(String)
159
- attributes[arg.to_sym] = {}
214
+ attributes << [arg.to_sym, {}]
160
215
  elsif arg.is_a? Hash
161
216
  arg.each do |key, value|
162
217
  sym_key = key.to_sym
163
- if only_attributes
164
- attributes[sym_key] = value == true ? {} : parse_args(value)
165
- next
166
- end
167
- if sym_key == :as
218
+ if !only_attributes && sym_key == :field
219
+ field_name = value
220
+ elsif !only_attributes && sym_key == :as
168
221
  column_name = value
169
- elsif sym_key == :attributes
170
- attributes.update parse_args(value, only_attributes: true)
171
- elsif sym_key == :params
172
- params = deep_with_indifferent_access value
222
+ elsif !only_attributes && %i[attributes query].include?(sym_key)
223
+ attributes.concat parse_args(value, only_attributes: true)
224
+ elsif !only_attributes && sym_key == :params
225
+ params = deep_underscore_keys value
173
226
  else
174
- attributes[sym_key] = value == true ? {} : parse_args(value)
227
+ attributes << [sym_key, value == true ? {} : parse_args(value)]
175
228
  end
176
229
  end
177
230
  else
@@ -179,6 +232,6 @@ module ArSerializer::Serializer
179
232
  end
180
233
  end
181
234
  return attributes if only_attributes
182
- { attributes: attributes, column_name: column_name, params: params || {} }
235
+ { attributes: attributes, column_name: column_name, field_name: field_name, params: params || {} }
183
236
  end
184
237
  end
@@ -4,32 +4,60 @@ module ArSerializer::TypeScript
4
4
  def self.generate_type_definition(*classes)
5
5
  types = related_serializer_types classes.flatten
6
6
  [
7
+ 'type NonAliasQuery = true | string | string[] | ({ field?: undefined } & { [key: string]: any })',
7
8
  types.map { |t| data_type_definition t },
8
9
  types.map { |t| query_type_definition t }
9
10
  ].join "\n"
10
11
  end
11
12
 
13
+ FieldInfo = Struct.new :name, :params_required?, :params_type, :query_type, :sub_query_params
14
+
12
15
  def self.query_type_definition(type)
13
16
  field_definitions = type.fields.map do |field|
14
17
  association_type = field.type.association_type
15
- if association_type
16
- qname = "Type#{association_type.name}Query"
17
- if field.args.empty?
18
- "#{field.name}?: true | #{qname} | { as?: string; attributes?: #{qname} }"
19
- else
20
- "#{field.name}?: true | #{qname} | { as?: string; params: #{field.args_ts_type}; attributes?: #{qname} }"
21
- end
22
- else
23
- "#{field.name}?: true | { as: string }"
24
- end
18
+ query_type = "Type#{association_type.name}Query" if association_type
19
+ params_type = field.args_ts_type unless field.args.empty?
20
+ params_required = field.args_required?
21
+ attrs = []
22
+ attrs << "query?: #{query_type}" if query_type
23
+ attrs << "params#{'?' unless params_required}: #{params_type}" if params_type
24
+ sub_query_params = attrs
25
+ FieldInfo.new field.name, params_required, params_type, query_type, sub_query_params
25
26
  end
26
- field_definitions << "'*'?: true"
27
+ accept_wildcard = !field_definitions.any?(&:params_required?)
27
28
  query_type_name = "Type#{type.name}Query"
29
+ standalone_fields_name = "Type#{type.name}StandaloneFields"
30
+ alias_query_type_name = "Type#{type.name}AliasFieldQuery"
28
31
  base_query_type_name = "Type#{type.name}QueryBase"
32
+ standalone_fields_definition = field_definitions.reject(&:params_required?).map do |info|
33
+ "'#{info.name}'"
34
+ end.join(' | ')
35
+ standalone_fields_definition += " | '*'" if accept_wildcard
36
+ alias_query_type_definition = field_definitions.map do |info|
37
+ attrs = ["field: '#{info.name}'", *info.sub_query_params].join('; ')
38
+ " | { #{attrs} }\n"
39
+ end.join
40
+ base_query_type_definition = field_definitions.map do |info|
41
+ types = []
42
+ unless info.params_required?
43
+ types << true
44
+ types << info.query_type if info.query_type
45
+ end
46
+ types << "{ field: never; #{info.sub_query_params.join('; ')} }" unless info.sub_query_params.empty?
47
+ " #{info.name}: #{types.join(' | ')}"
48
+ end.join("\n")
49
+ base_query_type_definition += "\n '*': true" if accept_wildcard
29
50
  <<~TYPE
30
- export type #{query_type_name} = keyof (#{base_query_type_name}) | Readonly<(keyof (#{base_query_type_name}))[]> | #{base_query_type_name}
51
+ export type #{query_type_name} = #{standalone_fields_name} | Readonly<#{standalone_fields_name}[]>
52
+ | (
53
+ { [key in keyof #{base_query_type_name}]?: key extends '*' ? true : #{base_query_type_name}[key] | #{alias_query_type_name} }
54
+ & { [key: string]: #{alias_query_type_name} | NonAliasQuery }
55
+ )
56
+ export type #{standalone_fields_name} = #{standalone_fields_definition.presence || 'never'}
57
+ export type #{alias_query_type_name} =
58
+ #{alias_query_type_definition.presence || 'never'}
31
59
  export interface #{base_query_type_name} {
32
- #{field_definitions.map { |line| " #{line}" }.join("\n")}
60
+ #{base_query_type_definition}
33
61
  }
34
62
  TYPE
35
63
  end
@@ -1,3 +1,3 @@
1
1
  module ArSerializer
2
- VERSION = '1.0.0'
2
+ VERSION = '1.2.0'
3
3
  end
data/lib/ar_serializer.rb CHANGED
@@ -4,8 +4,18 @@ require 'ar_serializer/field'
4
4
  require 'active_record'
5
5
 
6
6
  module ArSerializer
7
- def self.serialize(*args)
8
- Serializer.serialize(*args)
7
+ def self.serialize(model, query, **option)
8
+ Serializer.serialize(model, query, **option)
9
+ end
10
+
11
+ if ActiveRecord::VERSION::MAJOR >= 7
12
+ def self.preload_associations(models, associations)
13
+ ActiveRecord::Associations::Preloader.new(records: models, associations: associations).call
14
+ end
15
+ else
16
+ def self.preload_associations(models, associations)
17
+ ActiveRecord::Associations::Preloader.new.preload models, associations
18
+ end
9
19
  end
10
20
  end
11
21
 
@@ -18,60 +28,54 @@ module ArSerializer::Serializable
18
28
  end
19
29
 
20
30
  def _serializer_field_info(name)
21
- namespaces = ArSerializer::Serializer.current_namespaces
22
- if namespaces
23
- Array(namespaces).each do |ns|
24
- field = _serializer_namespace(ns)[name.to_s]
25
- return field if field
26
- end
27
- end
28
- field = _serializer_namespace(nil)[name.to_s]
29
- if field
30
- field
31
- elsif superclass < ArSerializer::Serializable
32
- superclass._serializer_field_info name
31
+ ArSerializer::Serializer.current_namespaces.each do |ns|
32
+ field = _serializer_namespace(ns)[name.to_s]
33
+ return field if field
33
34
  end
35
+ superclass._serializer_field_info name if superclass < ArSerializer::Serializable
34
36
  end
35
37
 
36
- def _serializer_field_keys
37
- namespaces = ArSerializer::Serializer.current_namespaces
38
- keys = []
39
- if namespaces
40
- Array(namespaces).each do |ns|
41
- keys |= _serializer_namespace(ns).keys
38
+ def _serializer_field_keys(public_only = true)
39
+ keys = ArSerializer::Serializer.current_namespaces.map do |ns|
40
+ if public_only
41
+ fields = _serializer_namespace(ns)
42
+ fields.keys.reject { |key| fields[key].private? }
43
+ else
44
+ _serializer_namespace(ns).keys
42
45
  end
43
- end
44
- keys |= _serializer_namespace(nil).keys
45
- keys |= superclass._serializer_field_keys if superclass < ArSerializer::Serializable
46
+ end.inject(:|)
47
+ keys |= superclass._serializer_field_keys(public_only) if superclass < ArSerializer::Serializable
46
48
  keys
47
49
  end
48
50
 
51
+ def _serializer_orderable_field_keys
52
+ _serializer_field_keys.select do |name|
53
+ _serializer_field_info(name).orderable?
54
+ end
55
+ end
56
+
49
57
  def serializer_field(*names, namespace: nil, association: nil, **option, &data_block)
50
58
  namespaces = namespace.is_a?(Array) ? namespace : [namespace]
51
59
  namespaces.each do |ns|
52
60
  names.each do |name|
53
- field = ArSerializer::Field.create(self, association || name, option, &data_block)
61
+ field = ArSerializer::Field.create(self, association || name, **option, &data_block)
54
62
  _serializer_namespace(ns)[name.to_s] = field
55
63
  end
56
64
  end
57
65
  end
58
66
 
59
- def _custom_preloaders
60
- @_custom_preloaders ||= {}
61
- end
62
-
63
- def define_preloader(name, &block)
64
- _custom_preloaders[name] = block
67
+ def serializer_permission(**args, &data_block)
68
+ serializer_field(:permission, **args, private: true, &data_block)
65
69
  end
66
70
 
67
- def serializer_defaults(*args, &block)
68
- serializer_field :defaults, *args, &block
71
+ def serializer_defaults(**args, &block)
72
+ serializer_field(:defaults, **args, private: true, &block)
69
73
  end
70
74
  end
71
75
  end
72
76
 
73
77
  ActiveRecord::Base.include ArSerializer::Serializable
74
- ActiveRecord::Relation.include ArSerializer::ArrayLikeCompositeValue
78
+ ActiveRecord::Relation.include ArSerializer::ArrayLikeSerializable
75
79
 
76
80
  require 'ar_serializer/graphql'
77
81
  require 'ar_serializer/type_script'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tompng
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-05-11 00:00:00.000000000 Z
11
+ date: 2022-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -115,6 +115,7 @@ executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - ".github/workflows/test.yml"
118
119
  - ".gitignore"
119
120
  - ".travis.yml"
120
121
  - Gemfile
@@ -125,6 +126,8 @@ files:
125
126
  - ar_serializer.gemspec
126
127
  - bin/console
127
128
  - bin/setup
129
+ - gemfiles/Gemfile-rails-6
130
+ - gemfiles/Gemfile-rails-7
128
131
  - lib/ar_serializer.rb
129
132
  - lib/ar_serializer/error.rb
130
133
  - lib/ar_serializer/field.rb
@@ -138,7 +141,7 @@ homepage: https://github.com/tompng/ar_serializer
138
141
  licenses:
139
142
  - MIT
140
143
  metadata: {}
141
- post_install_message:
144
+ post_install_message:
142
145
  rdoc_options: []
143
146
  require_paths:
144
147
  - lib
@@ -153,8 +156,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
156
  - !ruby/object:Gem::Version
154
157
  version: '0'
155
158
  requirements: []
156
- rubygems_version: 3.0.1
157
- signing_key:
159
+ rubygems_version: 3.3.3
160
+ signing_key:
158
161
  specification_version: 4
159
162
  summary: ActiveRecord serializer, avoid N+1
160
163
  test_files: []