ar_serializer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7e9144cab6ef520e2c0734bbe98a13a046f3b7b8411310ccc522dc999bae5365
4
+ data.tar.gz: d938b8684bbb22937c97c2572994b6bd11b38c72011f1b19975d96c7633656c7
5
+ SHA512:
6
+ metadata.gz: a7ff7ed23abaabef6893c31dc259ce18eeb3111df652c4e4a2d717523853e4a4f27119ddb0d027583ae788b218fdea73abebfc5de99480447e51bbabe04929f9
7
+ data.tar.gz: 8bceb19d2f319772e575c440870d074431f21efeab16375dced2e87637791e3d94927f0024c01bfd9669f0e0fb9743539f17ac47b12233a056db64e12b952264
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.sqlite3
10
+ .ruby-version
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ar_serializer (1.0.0)
5
+ activerecord
6
+ top_n_loader
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
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)
18
+ 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)
27
+ 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)
36
+ 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)
43
+ activerecord
44
+ tzinfo (1.2.5)
45
+ thread_safe (~> 0.1)
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
+ 1.17.2
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 tompng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,160 @@
1
+ # ArSerializer
2
+
3
+ - JSONの形をclientからリクエストできる
4
+ - N+1 SQLを避ける
5
+
6
+ ## Install
7
+
8
+ ```ruby
9
+ gem 'ar_serializer'
10
+ ```
11
+
12
+ ## Field定義
13
+ ```ruby
14
+ class User < ActiveRecord::Base
15
+ has_many :posts
16
+ serializer_field :id, :name, :posts
17
+ end
18
+
19
+ class Post < ActiveRecord::Base
20
+ has_many :comments
21
+ serializer_field :id, :title, :body, :comments
22
+ serializer_field :comment_count, count_of: :comments
23
+ end
24
+
25
+ class Comment < ActiveRecord::Base
26
+ serializer_field :id, :body
27
+ end
28
+ ```
29
+
30
+ ## Serialize
31
+ ```ruby
32
+ ArSerializer.serialize Post.find(params[:id]), params[:query]
33
+ ```
34
+
35
+ ## Query
36
+ ```ruby
37
+ ArSerializer.serialize user, :*
38
+ # => {
39
+ # id: 1,
40
+ # name: "user1",
41
+ # posts: [{}, {}]
42
+ # }
43
+
44
+ ArSerializer.serialize user, [:id, :name, posts: [:id, :title, comments: :id]]
45
+ ArSerializer.serialize user, { id: true, name: true, posts: { id: true, title: true, comments: :id } }
46
+ # => {
47
+ # id: 1,
48
+ # name: "user1",
49
+ # posts: [
50
+ # { id: 2, title: "title1", comments: [{ id: 5 }, { id: 17 }] },
51
+ # { id: 3, title: "title2", comments: [] }
52
+ # ]
53
+ # }
54
+ ArSerializer.serialize posts, [:title, :body, comment_count: { as: :num_replies }]
55
+ # => [
56
+ # { title: "title1", body: "body1", num_replies: 3 },
57
+ # { title: "title2", body: "body2", num_replies: 2 },
58
+ # { title: "title3", body: "body3", num_replies: 0 },
59
+ # { title: "title4", body: "body4", num_replies: 4 }
60
+ # ]
61
+ ```
62
+
63
+ ## その他
64
+ ```ruby
65
+ # data block, include
66
+ class Comment < ActiveRecord::Base
67
+ serializer_field :username, includes: :user do
68
+ { ja: user.name + '先生', en: 'Dr.' + user.name }
69
+ end
70
+ end
71
+
72
+ # preloader
73
+ class Foo < ActiveRecord::Base
74
+ define_preloader :bar_count_loader do |models|
75
+ Bar.where(foo_id: models.map(&:id)).group(:foo_id).count
76
+ end
77
+ serializer_field :bar_count, preload: preloader_name_or_proc do |preloaded|
78
+ preloaded[id] || 0
79
+ end
80
+ # data_blockが `do |preloaded| preloaded[id] end` の場合は省略可能
81
+ end
82
+
83
+ # order and limits
84
+ class Post < ActiveRecord::Base
85
+ has_many :comments
86
+ serializer_field :comments
87
+ end
88
+ ArSerializer.serialize Post.all, comments: [:id, params: { order: { id: :desc }, limit: 2 }]
89
+
90
+ # context and params
91
+ class Post < ActiveRecord::Base
92
+ serializer_field :created_at do |context, params|
93
+ created_at.in_time_zone(context[:tz]).strftime params[:format]
94
+ end
95
+ end
96
+ ArSerializer.serialize post, { created_at: { params: { format: '%H:%M:%S' } } }, context: { tz: 'Tokyo' }
97
+
98
+ # camelcase
99
+ class Foo < ActiveRecord::Base
100
+ def foo_bar; end
101
+ serializer_field :fooBar
102
+ end
103
+
104
+ # non activerecord class
105
+ class Foo
106
+ include ArSerializer::Serializable
107
+ def bar; end
108
+ serializer_field :bar
109
+ end
110
+
111
+ # namespace
112
+ class User < ActiveRecord::Base
113
+ serializer_field :name
114
+ serializer_field(:foo, namespace: :admin) { :foo }
115
+ serializer_field(:bar, namespace: :superadmin) { :bar }
116
+ end
117
+ ArSerializer.serialize user, [:name, :foo] #=> Error
118
+ ArSerializer.serialize user, [:name, :foo], use: :admin
119
+ ArSerializer.serialize user, [:name, :foo, :bar], use: [:admin, :superadmin]
120
+
121
+ # only, except
122
+ class User < ActiveRecord::Base
123
+ serializer_field :o_posts, association: :posts, only: :title
124
+ serializer_field :e_posts, association: :posts, except: :comments
125
+ 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
130
+
131
+ # types
132
+ class User < ActiveRecord::Base
133
+ serializer_field(:posts, params_type: { title: :string? }) do |title: nil|
134
+ title ? posts.where(title: title) : posts
135
+ end
136
+ serializer_field :foobar, type: ['foo', 'bar', { foobar: [:string, nil] }] do
137
+ ['foo', 'bar', { foobar: nil }, { foobar: 'foobar' }].sample
138
+ end
139
+ serializer_field :published_posts, type: -> { [Post] }
140
+ end
141
+ ArSerializer::TypeScript.generate_type_definition User
142
+ # => export type TypeUser {...}; export type TypePost {...}; ...
143
+
144
+ # graphql
145
+ class MySchema
146
+ include ArSerializer::Serializable
147
+ serializer_field :post, type: Post do |context, id:|
148
+ Post.find id
149
+ end
150
+ serializer_field :user, type: :string, params_type: { name: :string } do |context, params|
151
+ User.find_by name: params[:name]
152
+ end
153
+ serializer_field :__schema do
154
+ ArSerializer::GraphQL::SchemaClass.new self.class
155
+ end
156
+ end
157
+ ArSerializer::GraphQL.definition MySchema # schema.graphql
158
+ ArSerializer::GraphQL.serialize MySchema.new, '{post(id: 1){title} user(name: user1){id name}}'
159
+ ArSerializer::GraphQL.serialize MySchema.new, '{__schema{types{name fields{ name}}}}', operation_name: nil, variables: {}
160
+ ```
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,29 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ar_serializer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ar_serializer'
8
+ spec.version = ArSerializer::VERSION
9
+ spec.authors = ['tompng']
10
+ spec.email = ['tomoyapenguin@gmail.com']
11
+
12
+ spec.summary = %(ActiveRecord serializer, avoid N+1)
13
+ spec.description = %(ActiveRecord serializer, avoid N+1)
14
+ spec.homepage = "https://github.com/tompng/#{spec.name}"
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'activerecord'
25
+ spec.add_dependency 'top_n_loader'
26
+ %w[rake pry sqlite3 minitest simplecov].each do |gem_name|
27
+ spec.add_development_dependency gem_name
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'ar_serializer'
5
+ require 'pry'
6
+ require_relative '../test/db'
7
+
8
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,77 @@
1
+ require 'ar_serializer/version'
2
+ require 'ar_serializer/serializer'
3
+ require 'ar_serializer/field'
4
+ require 'active_record'
5
+
6
+ module ArSerializer
7
+ def self.serialize(*args)
8
+ Serializer.serialize(*args)
9
+ end
10
+ end
11
+
12
+ module ArSerializer::Serializable
13
+ extend ActiveSupport::Concern
14
+
15
+ module ClassMethods
16
+ def _serializer_namespace(ns)
17
+ (@_serializer_field_info ||= {})[ns] ||= {}
18
+ end
19
+
20
+ 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
33
+ end
34
+ end
35
+
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
42
+ end
43
+ end
44
+ keys |= _serializer_namespace(nil).keys
45
+ keys |= superclass._serializer_field_keys if superclass < ArSerializer::Serializable
46
+ keys
47
+ end
48
+
49
+ def serializer_field(*names, namespace: nil, association: nil, **option, &data_block)
50
+ namespaces = namespace.is_a?(Array) ? namespace : [namespace]
51
+ namespaces.each do |ns|
52
+ names.each do |name|
53
+ field = ArSerializer::Field.create(self, association || name, option, &data_block)
54
+ _serializer_namespace(ns)[name.to_s] = field
55
+ end
56
+ end
57
+ end
58
+
59
+ def _custom_preloaders
60
+ @_custom_preloaders ||= {}
61
+ end
62
+
63
+ def define_preloader(name, &block)
64
+ _custom_preloaders[name] = block
65
+ end
66
+
67
+ def serializer_defaults(*args, &block)
68
+ serializer_field :defaults, *args, &block
69
+ end
70
+ end
71
+ end
72
+
73
+ ActiveRecord::Base.include ArSerializer::Serializable
74
+ ActiveRecord::Relation.include ArSerializer::ArrayLikeCompositeValue
75
+
76
+ require 'ar_serializer/graphql'
77
+ require 'ar_serializer/type_script'
@@ -0,0 +1,3 @@
1
+ module ArSerializer
2
+ class InvalidQuery < ::StandardError; end
3
+ end
@@ -0,0 +1,234 @@
1
+ require 'ar_serializer/error'
2
+
3
+ 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
+ @includes = includes
7
+ @preloaders = preloaders
8
+ @only = only && [*only].map(&:to_s)
9
+ @except = except && [*except].map(&:to_s)
10
+ @data_block = data_block
11
+ @order_column = order_column
12
+ @type = type
13
+ @params_type = params_type
14
+ end
15
+
16
+ def type
17
+ type = @type.is_a?(Proc) ? @type.call : @type
18
+ splat = lambda do |t|
19
+ case t
20
+ when Array
21
+ if t.size == 1 || (t.size == 2 && t.compact.size == 1)
22
+ t.map(&splat)
23
+ else
24
+ t.map { |v| v.is_a?(String) ? v : splat.call(v) }
25
+ end
26
+ when Hash
27
+ t.transform_values(&splat)
28
+ else
29
+ t
30
+ end
31
+ end
32
+ splat.call type
33
+ end
34
+
35
+ def arguments
36
+ return @params_type if @params_type
37
+ @preloaders.size
38
+ @data_block.parameters
39
+ parameters_list = [@data_block.parameters.drop(@preloaders.size + 1)]
40
+ @preloaders.each do |preloader|
41
+ parameters_list << preloader.parameters.drop(2)
42
+ end
43
+ arguments = {}
44
+ any = false
45
+ parameters_list.each do |parameters|
46
+ ftype, fname = parameters.first
47
+ if %i[opt req rest].include? ftype
48
+ any = true unless fname.match?(/^_/)
49
+ next
50
+ end
51
+ parameters.each do |type, name|
52
+ case type
53
+ when :keyreq
54
+ arguments[name] ||= true
55
+ when :key
56
+ arguments[name] ||= false
57
+ when :keyrest
58
+ any = true unless name.match?(/^_/)
59
+ when :opt, :req
60
+ break
61
+ end
62
+ end
63
+ end
64
+ return :any if any && arguments.empty?
65
+ 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]
70
+ end.to_h
71
+ end
72
+
73
+ def validate_attributes(attributes)
74
+ return unless @only || @except
75
+ keys = attributes.keys.map(&:to_s) - ['*']
76
+ return unless (@only && (keys - @only).present?) || (@except && (keys & @except).present?)
77
+ invalid_keys = [*(@only && keys - @only), *(@except && keys & @except)].uniq
78
+ raise ArSerializer::InvalidQuery, "unpermitted attribute: #{invalid_keys}"
79
+ end
80
+
81
+ def self.count_field(klass, association_name)
82
+ preloader = lambda do |models|
83
+ klass.joins(association_name).where(id: models.map(&:id)).group(:id).count
84
+ end
85
+ data_block = lambda do |preloaded, _context, _params|
86
+ preloaded[id] || 0
87
+ 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
99
+ end
100
+
101
+ def self.type_from_column_type(klass, name)
102
+ type = type_from_attribute_type klass, name.to_s
103
+ return :any if type.nil?
104
+ klass.column_for_attribute(name).null ? [*type, nil] : type
105
+ end
106
+
107
+ def self.type_from_attribute_type(klass, name)
108
+ attr_type = klass.attribute_types[name]
109
+ if attr_type.is_a?(ActiveRecord::Enum::EnumType) && klass.respond_to?(name.pluralize)
110
+ values = klass.send(name.pluralize).keys.compact
111
+ values = values.map { |v| v.is_a?(Symbol) ? v.to_s : v }.uniq
112
+ valid_classes = [TrueClass, FalseClass, String, Integer, Float]
113
+ return if values.empty? || (values.map(&:class) - valid_classes).present?
114
+ return values
115
+ end
116
+ {
117
+ boolean: :boolean,
118
+ integer: :int,
119
+ float: :float,
120
+ decimal: :float,
121
+ string: :string,
122
+ text: :string,
123
+ json: :string,
124
+ binary: :string,
125
+ time: :string,
126
+ date: :string,
127
+ datetime: :string
128
+ }[attr_type.type]
129
+ end
130
+
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)
132
+ if count_of
133
+ if includes || preload || data_block || only || except
134
+ raise ArgumentError, 'includes, preload block cannot be used with count_of'
135
+ end
136
+ return count_field klass, count_of
137
+ end
138
+ underscore_name = name.to_s.underscore
139
+ association = klass.reflect_on_association underscore_name if klass.respond_to? :reflect_on_association
140
+ if association
141
+ if association.collection?
142
+ type ||= -> { [association.klass] }
143
+ elsif (association.belongs_to? && association.options[:optional] == true) || (association.has_one? && association.options[:required] != true)
144
+ type ||= -> { [association.klass, nil] }
145
+ else
146
+ type ||= -> { association.klass }
147
+ end
148
+ return association_field klass, underscore_name, only: only, except: except, type: type, collection: association.collection? if !includes && !preload && !data_block && !params_type
149
+ end
150
+ type ||= lambda do
151
+ if klass.respond_to? :column_for_attribute
152
+ type_from_column_type klass, underscore_name
153
+ elsif klass.respond_to? :attribute_types
154
+ type_from_attribute_type(klass, underscore_name) || :any
155
+ else
156
+ :any
157
+ end
158
+ 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
160
+ end
161
+
162
+ def self.custom_field(klass, name, includes:, preload:, only:, except:, order_column:, type:, params_type:, &data_block)
163
+ 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
171
+ else
172
+ preloaders = []
173
+ includes ||= name if klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(name)
174
+ end
175
+ data_block ||= ->(preloaded, _context, _params) { preloaded[id] } if preloaders.size == 1
176
+ raise ArgumentError, 'data_block needed if multiple preloaders are present' if !preloaders.empty? && data_block.nil?
177
+ 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 }
180
+ )
181
+ end
182
+
183
+ def self.parse_order(klass, order)
184
+ key, mode = begin
185
+ case order
186
+ when Hash
187
+ raise ArSerializer::InvalidQuery, 'invalid order' unless order.size == 1
188
+ order.first
189
+ when Symbol, 'asc', 'desc'
190
+ [klass.primary_key, order]
191
+ when NilClass
192
+ [klass.primary_key, :asc]
193
+ end
194
+ 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]
200
+ end
201
+
202
+ def self.association_field(klass, name, only:, except:, type:, collection:)
203
+ if collection
204
+ preloader = lambda do |models, _context, limit: nil, order: nil, **_option|
205
+ preload_association klass, models, name, limit: limit, order: order
206
+ end
207
+ params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
208
+ else
209
+ preloader = lambda do |models, _context, _params|
210
+ preload_association klass, models, name
211
+ end
212
+ 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
217
+ end
218
+
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 }
224
+ end
225
+ ActiveRecord::Associations::Preloader.new.preload models, name
226
+ return if limit.nil? && order.nil?
227
+ 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]
232
+ end.to_h
233
+ end
234
+ end