ar_serializer 1.0.0

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.
@@ -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