ar_serializer 1.1.1 → 1.2.1
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 +4 -4
- data/.github/workflows/test.yml +23 -0
- data/.gitignore +2 -0
- data/Gemfile +1 -0
- data/README.md +4 -3
- data/ar_serializer.gemspec +1 -1
- data/bin/console +2 -2
- data/gemfiles/Gemfile-rails-6 +9 -0
- data/gemfiles/Gemfile-rails-7-0 +9 -0
- data/gemfiles/Gemfile-rails-7-1 +9 -0
- data/lib/ar_serializer/field.rb +129 -63
- data/lib/ar_serializer/graphql/types.rb +9 -7
- data/lib/ar_serializer/serializer.rb +131 -80
- data/lib/ar_serializer/version.rb +1 -1
- data/lib/ar_serializer.rb +33 -29
- metadata +10 -21
- data/Gemfile.lock +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee32f3c64753466283134306f9c8bcae7435cd8cfb337591455de2455de5fc15
|
4
|
+
data.tar.gz: eab2dc2c198ef804b6d0f1070057131480dae5fcae0f394d2a4aee065b647e0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d1aef11e66bca294d04217f7046d902063ba382e0ccbd4c11c0e843ad527929941f22b874753fefc24c550efbc28a3226939c61dc82fe81ed7e0352963f0e7e
|
7
|
+
data.tar.gz: 6f62a16b9bf8f971f9379c0860772e921844e8123506cbbb89e92f759d8f97f1b686ac803cfd1092bce8078be656d96ff2ceaa66377cc080685fdcf19b072a7d
|
@@ -0,0 +1,23 @@
|
|
1
|
+
name: Test
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
strategy:
|
6
|
+
fail-fast: false
|
7
|
+
matrix:
|
8
|
+
ruby: [ '3.1', '3.2', '3.3' ]
|
9
|
+
gemfiles:
|
10
|
+
- gemfiles/Gemfile-rails-6
|
11
|
+
- gemfiles/Gemfile-rails-7-0
|
12
|
+
- gemfiles/Gemfile-rails-7-1
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v4
|
16
|
+
- uses: ruby/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: ${{ matrix.ruby }}
|
19
|
+
- run: |
|
20
|
+
sudo apt-get update
|
21
|
+
sudo apt-get install -y libsqlite3-dev
|
22
|
+
- run: bundle install --gemfile ${{ matrix.gemfiles }} --jobs 4 --retry 3
|
23
|
+
- run: bundle exec --gemfile ${{ matrix.gemfiles }} rake
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -71,10 +71,10 @@ end
|
|
71
71
|
|
72
72
|
# preloader
|
73
73
|
class Foo < ActiveRecord::Base
|
74
|
-
|
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:
|
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,7 +85,8 @@ 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: {
|
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
|
data/ar_serializer.gemspec
CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_dependency 'activerecord'
|
25
25
|
spec.add_dependency 'top_n_loader'
|
26
|
-
%w[rake
|
26
|
+
%w[rake sqlite3 minitest simplecov].each do |gem_name|
|
27
27
|
spec.add_development_dependency gem_name
|
28
28
|
end
|
29
29
|
end
|
data/bin/console
CHANGED
data/lib/ar_serializer/field.rb
CHANGED
@@ -1,19 +1,36 @@
|
|
1
1
|
require 'ar_serializer/error'
|
2
2
|
require 'top_n_loader'
|
3
|
+
require 'set'
|
3
4
|
|
4
5
|
class ArSerializer::Field
|
5
|
-
attr_reader :includes, :preloaders, :data_block, :only, :except, :order_column
|
6
|
-
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
|
7
10
|
@includes = includes
|
8
11
|
@preloaders = preloaders
|
9
12
|
@only = only && [*only].map(&:to_s)
|
10
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
|
11
18
|
@data_block = data_block
|
12
19
|
@order_column = order_column
|
20
|
+
@orderable = orderable
|
13
21
|
@type = type
|
14
22
|
@params_type = params_type
|
15
23
|
end
|
16
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
|
+
|
17
34
|
def type
|
18
35
|
type = @type.is_a?(Proc) ? @type.call : @type
|
19
36
|
splat = lambda do |t|
|
@@ -34,7 +51,7 @@ class ArSerializer::Field
|
|
34
51
|
end
|
35
52
|
|
36
53
|
def arguments
|
37
|
-
return @params_type if @params_type
|
54
|
+
return @params_type.is_a?(Proc) ? @params_type.call : @params_type if @params_type
|
38
55
|
@preloaders.size
|
39
56
|
@data_block.parameters
|
40
57
|
parameters_list = [@data_block.parameters.drop(@preloaders.size + 1)]
|
@@ -64,10 +81,19 @@ class ArSerializer::Field
|
|
64
81
|
end
|
65
82
|
return :any if any && arguments.empty?
|
66
83
|
arguments.map do |key, req|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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]
|
71
97
|
end.to_h
|
72
98
|
end
|
73
99
|
|
@@ -79,14 +105,14 @@ class ArSerializer::Field
|
|
79
105
|
raise ArSerializer::InvalidQuery, "unpermitted attribute: #{invalid_keys}"
|
80
106
|
end
|
81
107
|
|
82
|
-
def self.count_field(klass, association_name)
|
108
|
+
def self.count_field(klass, name, association_name, permission:)
|
83
109
|
preloader = lambda do |models|
|
84
110
|
klass.joins(association_name).where(id: models.map(&:id)).group(:id).count
|
85
111
|
end
|
86
112
|
data_block = lambda do |preloaded, _context, **_params|
|
87
113
|
preloaded[id] || 0
|
88
114
|
end
|
89
|
-
new preloaders: [preloader], data_block: data_block, type: :int
|
115
|
+
new klass, name, preloaders: [preloader], data_block: data_block, type: :int, orderable: false, permission: permission, fallback: 0
|
90
116
|
end
|
91
117
|
|
92
118
|
def self.type_from_column_type(klass, name)
|
@@ -98,7 +124,7 @@ class ArSerializer::Field
|
|
98
124
|
def self.type_from_attribute_type(klass, name)
|
99
125
|
attr_type = klass.attribute_types[name]
|
100
126
|
if attr_type.is_a?(ActiveRecord::Enum::EnumType) && klass.respond_to?(name.pluralize)
|
101
|
-
values = klass.
|
127
|
+
values = klass.__send__(name.pluralize).keys.compact
|
102
128
|
values = values.map { |v| v.is_a?(Symbol) ? v.to_s : v }.uniq
|
103
129
|
valid_classes = [TrueClass, FalseClass, String, Integer, Float]
|
104
130
|
return if values.empty? || (values.map(&:class) - valid_classes).present?
|
@@ -111,7 +137,7 @@ class ArSerializer::Field
|
|
111
137
|
decimal: :float,
|
112
138
|
string: :string,
|
113
139
|
text: :string,
|
114
|
-
json: :
|
140
|
+
json: :unknown,
|
115
141
|
binary: :string,
|
116
142
|
time: :string,
|
117
143
|
date: :string,
|
@@ -119,105 +145,145 @@ class ArSerializer::Field
|
|
119
145
|
}[attr_type.type]
|
120
146
|
end
|
121
147
|
|
122
|
-
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
|
123
150
|
if count_of
|
124
|
-
if includes || preload || data_block || only || except
|
125
|
-
raise ArgumentError, '
|
151
|
+
if includes || preload || data_block || only || except || order_column || orderable || scoped_access != nil || fallback
|
152
|
+
raise ArgumentError, 'wrong options for count_of field'
|
126
153
|
end
|
127
|
-
return count_field klass, count_of
|
154
|
+
return count_field klass, name, count_of, permission: permission
|
128
155
|
end
|
129
|
-
|
130
|
-
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
|
131
157
|
if association
|
132
158
|
if association.collection?
|
133
159
|
type ||= -> { [association.klass] }
|
160
|
+
fallback ||= []
|
134
161
|
elsif (association.belongs_to? && association.options[:optional] == true) || (association.has_one? && association.options[:required] != true)
|
135
162
|
type ||= -> { [association.klass, nil] }
|
136
163
|
else
|
137
164
|
type ||= -> { association.klass }
|
138
165
|
end
|
139
|
-
return association_field klass,
|
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
|
140
167
|
end
|
141
168
|
type ||= lambda do
|
142
169
|
if klass.respond_to? :column_for_attribute
|
143
|
-
type_from_column_type klass,
|
170
|
+
type_from_column_type klass, name.underscore
|
144
171
|
elsif klass.respond_to? :attribute_types
|
145
|
-
type_from_attribute_type(klass,
|
172
|
+
type_from_attribute_type(klass, name.underscore) || :any
|
146
173
|
else
|
147
174
|
:any
|
148
175
|
end
|
149
176
|
end
|
150
|
-
custom_field klass,
|
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
|
151
178
|
end
|
152
179
|
|
153
|
-
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
|
154
182
|
if preload
|
155
|
-
preloaders =
|
156
|
-
next preloader if preloader.is_a? Proc
|
157
|
-
unless klass._custom_preloaders.has_key?(preloader)
|
158
|
-
raise ArgumentError, "preloader not found: #{preloader}"
|
159
|
-
end
|
160
|
-
klass._custom_preloaders[preloader]
|
161
|
-
end
|
183
|
+
preloaders = [*preload]
|
162
184
|
else
|
163
185
|
preloaders = []
|
164
|
-
includes ||=
|
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
|
165
198
|
end
|
166
|
-
data_block ||= ->(preloaded, _context, **_params) { preloaded[id] } if preloaders.size == 1
|
167
199
|
raise ArgumentError, 'data_block needed if multiple preloaders are present' if !preloaders.empty? && data_block.nil?
|
168
200
|
new(
|
169
|
-
|
170
|
-
|
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 }
|
171
207
|
)
|
172
208
|
end
|
173
209
|
|
174
|
-
def self.parse_order(klass, order)
|
175
|
-
|
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
|
176
216
|
case order
|
177
217
|
when Hash
|
178
218
|
raise ArSerializer::InvalidQuery, 'invalid order' unless order.size == 1
|
179
|
-
order.first
|
219
|
+
order_by, direction = order.first.map(&:to_sym)
|
180
220
|
when Symbol, 'asc', 'desc'
|
181
|
-
|
182
|
-
|
183
|
-
|
221
|
+
direction = order.to_sym
|
222
|
+
else
|
223
|
+
raise ArSerializer::InvalidQuery, 'invalid order'
|
184
224
|
end
|
185
225
|
end
|
186
|
-
info = klass._serializer_field_info
|
187
|
-
|
188
|
-
raise ArSerializer::InvalidQuery, "
|
189
|
-
raise ArSerializer::InvalidQuery, "
|
190
|
-
[
|
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]
|
191
231
|
end
|
192
232
|
|
193
|
-
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
|
194
237
|
if collection
|
195
|
-
preloader = lambda do |models, _context, limit: nil, order: nil, **_option|
|
196
|
-
preload_association klass, models,
|
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)
|
197
257
|
end
|
198
|
-
params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
|
199
258
|
else
|
200
259
|
preloader = lambda do |models, _context, **_params|
|
201
|
-
preload_association klass, models,
|
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)
|
202
264
|
end
|
203
265
|
end
|
204
|
-
data_block
|
205
|
-
preloaded ? preloaded[id] || [] : send(name)
|
206
|
-
end
|
207
|
-
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
|
208
267
|
end
|
209
268
|
|
210
|
-
def self.preload_association(klass, models, name, limit: nil, order: nil)
|
211
|
-
limit
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
279
|
+
end
|
280
|
+
ArSerializer.preload_associations models, name
|
281
|
+
return models.map { |m| [m.id || m, m.__send__(name)] }.to_h if !order && !order_by && !direction
|
216
282
|
models.map do |model|
|
217
|
-
records_nonnils, records_nils = model.
|
218
|
-
records = records_nils.sort_by(&:id) + records_nonnils.sort_by { |r| [r[
|
219
|
-
records.reverse! if
|
220
|
-
[model.id, 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]
|
221
287
|
end.to_h
|
222
288
|
end
|
223
289
|
end
|
@@ -103,7 +103,7 @@ module ArSerializer::GraphQL
|
|
103
103
|
class InvalidType < StandardError; end
|
104
104
|
|
105
105
|
def validate!
|
106
|
-
valid_symbols = %i[number int float string boolean any]
|
106
|
+
valid_symbols = %i[number int float string boolean any unknown]
|
107
107
|
invalids = []
|
108
108
|
recursive_validate = lambda do |t|
|
109
109
|
case t
|
@@ -153,8 +153,8 @@ module ArSerializer::GraphQL
|
|
153
153
|
end
|
154
154
|
|
155
155
|
def self.from(type, only = nil, except = nil)
|
156
|
-
type = [type[0...-1].to_sym, nil] if type.is_a?(Symbol) && type.to_s.
|
157
|
-
type = [type[0...-1], nil] if type.is_a?(String) && type.
|
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?('?')
|
158
158
|
case type
|
159
159
|
when Class
|
160
160
|
SerializableTypeClass.new type, only, except
|
@@ -164,7 +164,7 @@ module ArSerializer::GraphQL
|
|
164
164
|
if type.size == 1
|
165
165
|
ListTypeClass.new type.first, only, except
|
166
166
|
elsif type.size == 2 && type.last.nil?
|
167
|
-
OptionalTypeClass.new type
|
167
|
+
OptionalTypeClass.new type, only, except
|
168
168
|
else
|
169
169
|
OrTypeClass.new type, only, except
|
170
170
|
end
|
@@ -195,6 +195,8 @@ module ArSerializer::GraphQL
|
|
195
195
|
:boolean
|
196
196
|
when :other
|
197
197
|
:other
|
198
|
+
when :unknown
|
199
|
+
:unknown
|
198
200
|
else
|
199
201
|
:any
|
200
202
|
end
|
@@ -216,7 +218,7 @@ module ArSerializer::GraphQL
|
|
216
218
|
''
|
217
219
|
when 'boolean'
|
218
220
|
true
|
219
|
-
when 'any'
|
221
|
+
when 'any', 'unknown'
|
220
222
|
nil
|
221
223
|
else
|
222
224
|
type
|
@@ -227,7 +229,7 @@ module ArSerializer::GraphQL
|
|
227
229
|
case type
|
228
230
|
when :int, :float
|
229
231
|
'number'
|
230
|
-
when :string, :number, :boolean
|
232
|
+
when :string, :number, :boolean, :unknown
|
231
233
|
type.to_s
|
232
234
|
when Symbol
|
233
235
|
'any'
|
@@ -266,7 +268,7 @@ module ArSerializer::GraphQL
|
|
266
268
|
end
|
267
269
|
|
268
270
|
def sample
|
269
|
-
type.reject { |k| k.to_s.
|
271
|
+
type.reject { |k| k.to_s.end_with? '?' }.transform_values do |v|
|
270
272
|
TypeClass.from(v).sample
|
271
273
|
end
|
272
274
|
end
|
@@ -1,66 +1,55 @@
|
|
1
1
|
require 'ar_serializer/error'
|
2
2
|
|
3
|
-
class ArSerializer::
|
4
|
-
|
5
|
-
|
6
|
-
@
|
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
|
10
|
-
|
10
|
+
def ar_custom_serializable_data(result)
|
11
|
+
@block.call result
|
11
12
|
end
|
12
13
|
end
|
13
14
|
|
14
|
-
module ArSerializer::
|
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, query, context: 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
34
|
attributes = parse_args(query)[:attributes]
|
43
35
|
if model.is_a?(ArSerializer::Serializable)
|
44
|
-
|
45
|
-
|
46
|
-
output
|
36
|
+
result = _serialize [model], attributes, context, permission: permission
|
37
|
+
result[model]
|
47
38
|
else
|
48
|
-
|
49
|
-
|
50
|
-
|
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(
|
58
|
-
|
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
|
61
|
-
value_outputs.each { |value, output| output[:id] = value.id } if include_id && klass.method_defined?(:id)
|
50
|
+
models.uniq!
|
62
51
|
if attributes.any? { |k, _| k == :* }
|
63
|
-
all_keys = klass._serializer_field_keys.map(&:to_sym)
|
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
55
|
attributes = all_keys.map { |k| [k, {}] } + attributes.reject { |k, _| k == :* }
|
@@ -68,85 +57,147 @@ module ArSerializer::Serializer
|
|
68
57
|
attributes.each do |name, sub_args|
|
69
58
|
field_name = sub_args[:field_name] || name
|
70
59
|
field = klass._serializer_field_info field_name
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
preloader_params = attributes.flat_map do |name, sub_args|
|
76
|
-
field_name = sub_args[:field_name] || name
|
77
|
-
klass._serializer_field_info(field_name).preloaders.map do |p|
|
78
|
-
[p, sub_args[:params]]
|
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
|
79
63
|
end
|
64
|
+
ArSerializer.preload_associations models, field.includes if field.includes.present?
|
80
65
|
end
|
81
|
-
|
82
|
-
|
83
|
-
preloader_params += defaults.preloaders.map { |p| [p] }
|
84
|
-
end
|
85
|
-
preloader_values = preloader_params.compact.uniq.map do |key|
|
86
|
-
preloader, params = key
|
66
|
+
|
67
|
+
preload = lambda do |preloader, params|
|
87
68
|
has_keyword = preloader.parameters.any? { |type, _name| %i[key keyreq keyrest].include? type }
|
88
69
|
arity = preloader.arity.abs
|
89
70
|
arguments = [models]
|
90
71
|
if has_keyword
|
91
72
|
arguments << context unless arity == 2
|
92
|
-
|
73
|
+
preloader.call(*arguments, **(params || {}))
|
93
74
|
else
|
94
75
|
arguments << context unless arity == 1
|
95
|
-
|
76
|
+
preloader.call(*arguments)
|
96
77
|
end
|
97
|
-
end
|
78
|
+
end
|
98
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
|
86
|
+
end
|
87
|
+
if permission_field
|
88
|
+
preloadeds = permission_field.preloaders.map do |p|
|
89
|
+
preloader_values[[p, nil]] ||= preload.call p, nil
|
90
|
+
end
|
91
|
+
models = models.select do |model|
|
92
|
+
model.instance_exec(*preloadeds, context, {}, &permission_field.data_block)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
defaults = klass._serializer_field_info :defaults
|
99
97
|
if defaults
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
98
|
+
defaults.preloaders.each do |p|
|
99
|
+
preloader_values[[p, nil]] ||= preload.call p, nil
|
100
|
+
end
|
101
|
+
end
|
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
|
104
108
|
end
|
105
109
|
end
|
106
110
|
|
111
|
+
models.each do |model|
|
112
|
+
output_for_model[model] = {}
|
113
|
+
end
|
114
|
+
|
107
115
|
attributes.each do |name, sub_arg|
|
108
116
|
params = sub_arg[:params]
|
109
|
-
sub_calls = []
|
110
117
|
column_name = sub_arg[:column_name] || name
|
111
118
|
field_name = sub_arg[:field_name] || name
|
112
119
|
info = klass._serializer_field_info field_name
|
113
120
|
preloadeds = info.preloaders.map { |p| preloader_values[[p, params]] } || []
|
114
121
|
data_block = info.data_block
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
127
135
|
elsif child.is_a? ArSerializer::Serializable
|
128
|
-
|
129
|
-
|
130
|
-
|
136
|
+
sub_results[model] = [:single, child]
|
137
|
+
sub_models << child
|
138
|
+
else
|
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
|
131
177
|
else
|
132
|
-
|
178
|
+
data[column_name] = fallback
|
133
179
|
end
|
134
180
|
end
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
139
189
|
end
|
140
190
|
end
|
191
|
+
output_for_model
|
141
192
|
end
|
142
193
|
|
143
|
-
def self.
|
194
|
+
def self.deep_underscore_keys params
|
144
195
|
case params
|
145
196
|
when Array
|
146
|
-
params.map { |v|
|
197
|
+
params.map { |v| deep_underscore_keys v }
|
147
198
|
when Hash
|
148
|
-
params.transform_keys
|
149
|
-
|
199
|
+
params.transform_keys { |k| k.to_s.underscore.to_sym }.transform_values! do |v|
|
200
|
+
deep_underscore_keys v
|
150
201
|
end
|
151
202
|
else
|
152
203
|
params
|
@@ -171,7 +222,7 @@ module ArSerializer::Serializer
|
|
171
222
|
elsif !only_attributes && %i[attributes query].include?(sym_key)
|
172
223
|
attributes.concat parse_args(value, only_attributes: true)
|
173
224
|
elsif !only_attributes && sym_key == :params
|
174
|
-
params =
|
225
|
+
params = deep_underscore_keys value
|
175
226
|
else
|
176
227
|
attributes << [sym_key, value == true ? {} : parse_args(value)]
|
177
228
|
end
|
data/lib/ar_serializer.rb
CHANGED
@@ -7,6 +7,16 @@ module ArSerializer
|
|
7
7
|
def self.serialize(model, query, **option)
|
8
8
|
Serializer.serialize(model, query, **option)
|
9
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
|
19
|
+
end
|
10
20
|
end
|
11
21
|
|
12
22
|
module ArSerializer::Serializable
|
@@ -18,34 +28,32 @@ module ArSerializer::Serializable
|
|
18
28
|
end
|
19
29
|
|
20
30
|
def _serializer_field_info(name)
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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 |=
|
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|
|
@@ -56,22 +64,18 @@ module ArSerializer::Serializable
|
|
56
64
|
end
|
57
65
|
end
|
58
66
|
|
59
|
-
def
|
60
|
-
|
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
71
|
def serializer_defaults(**args, &block)
|
68
|
-
serializer_field
|
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::
|
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.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- tompng
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-07-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -52,20 +52,6 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: pry
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: sqlite3
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,16 +101,19 @@ executables: []
|
|
115
101
|
extensions: []
|
116
102
|
extra_rdoc_files: []
|
117
103
|
files:
|
104
|
+
- ".github/workflows/test.yml"
|
118
105
|
- ".gitignore"
|
119
106
|
- ".travis.yml"
|
120
107
|
- Gemfile
|
121
|
-
- Gemfile.lock
|
122
108
|
- LICENSE.txt
|
123
109
|
- README.md
|
124
110
|
- Rakefile
|
125
111
|
- ar_serializer.gemspec
|
126
112
|
- bin/console
|
127
113
|
- bin/setup
|
114
|
+
- gemfiles/Gemfile-rails-6
|
115
|
+
- gemfiles/Gemfile-rails-7-0
|
116
|
+
- gemfiles/Gemfile-rails-7-1
|
128
117
|
- lib/ar_serializer.rb
|
129
118
|
- lib/ar_serializer/error.rb
|
130
119
|
- lib/ar_serializer/field.rb
|
@@ -138,7 +127,7 @@ homepage: https://github.com/tompng/ar_serializer
|
|
138
127
|
licenses:
|
139
128
|
- MIT
|
140
129
|
metadata: {}
|
141
|
-
post_install_message:
|
130
|
+
post_install_message:
|
142
131
|
rdoc_options: []
|
143
132
|
require_paths:
|
144
133
|
- lib
|
@@ -153,8 +142,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
142
|
- !ruby/object:Gem::Version
|
154
143
|
version: '0'
|
155
144
|
requirements: []
|
156
|
-
rubygems_version: 3.
|
157
|
-
signing_key:
|
145
|
+
rubygems_version: 3.5.9
|
146
|
+
signing_key:
|
158
147
|
specification_version: 4
|
159
148
|
summary: ActiveRecord serializer, avoid N+1
|
160
149
|
test_files: []
|
data/Gemfile.lock
DELETED
@@ -1,59 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
ar_serializer (1.1.1)
|
5
|
-
activerecord
|
6
|
-
top_n_loader
|
7
|
-
|
8
|
-
GEM
|
9
|
-
remote: https://rubygems.org/
|
10
|
-
specs:
|
11
|
-
activemodel (6.0.2.1)
|
12
|
-
activesupport (= 6.0.2.1)
|
13
|
-
activerecord (6.0.2.1)
|
14
|
-
activemodel (= 6.0.2.1)
|
15
|
-
activesupport (= 6.0.2.1)
|
16
|
-
activesupport (6.0.2.1)
|
17
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
-
i18n (>= 0.7, < 2)
|
19
|
-
minitest (~> 5.1)
|
20
|
-
tzinfo (~> 1.1)
|
21
|
-
zeitwerk (~> 2.2)
|
22
|
-
coderay (1.1.2)
|
23
|
-
concurrent-ruby (1.1.5)
|
24
|
-
docile (1.3.1)
|
25
|
-
i18n (1.7.0)
|
26
|
-
concurrent-ruby (~> 1.0)
|
27
|
-
json (2.1.0)
|
28
|
-
method_source (0.9.2)
|
29
|
-
minitest (5.13.0)
|
30
|
-
pry (0.12.2)
|
31
|
-
coderay (~> 1.1.0)
|
32
|
-
method_source (~> 0.9.0)
|
33
|
-
rake (12.3.2)
|
34
|
-
simplecov (0.16.1)
|
35
|
-
docile (~> 1.1)
|
36
|
-
json (>= 1.8, < 3)
|
37
|
-
simplecov-html (~> 0.10.0)
|
38
|
-
simplecov-html (0.10.2)
|
39
|
-
sqlite3 (1.4.0)
|
40
|
-
thread_safe (0.3.6)
|
41
|
-
top_n_loader (1.0.1)
|
42
|
-
activerecord
|
43
|
-
tzinfo (1.2.6)
|
44
|
-
thread_safe (~> 0.1)
|
45
|
-
zeitwerk (2.2.2)
|
46
|
-
|
47
|
-
PLATFORMS
|
48
|
-
ruby
|
49
|
-
|
50
|
-
DEPENDENCIES
|
51
|
-
ar_serializer!
|
52
|
-
minitest
|
53
|
-
pry
|
54
|
-
rake
|
55
|
-
simplecov
|
56
|
-
sqlite3
|
57
|
-
|
58
|
-
BUNDLED WITH
|
59
|
-
2.1.2
|