ar_serializer 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de3115e702a13b938be93babf38a51d84da70ed9b69f24f4e785677b8a69c69a
4
- data.tar.gz: 640e1bd6f87589be333e2f76885a351081461a95955cb746659dd77dcf1b0ad8
3
+ metadata.gz: ef5e6277909a8e2573068c3c8c2e19275c8763c9395f610cb0e8aebbce566eee
4
+ data.tar.gz: ad0b8f4f8961dcf576c402071f79b312e93c0a4da958bab46b420adb70ea5416
5
5
  SHA512:
6
- metadata.gz: 5943c272b4a00c091fc002bec7aaa0c5f4ed4da98f0050da4b19fb7cbc3622e0dc539f9ef2c0f661bc7e0718c24f13f2ee84957392d5ea6f7bb12a4652cd2083
7
- data.tar.gz: 39183107e382d5d8dbbfe157feba890e3d4a7b47bbadff80d524fa193db8d812125f44786a6056009b0bae57d52b39dd6f9b4e6aff5c716913bf055467f720ae
6
+ metadata.gz: 285562de1dc23efb39552eb7b184fb2004dfdf0a63314d161f5397060ca79c22136140612cde7f3d46bc76f3b649e016671fd15561d9829e4c14f22a42cf7c72
7
+ data.tar.gz: 3081e60e717cefe07b8629934babeacecef06c5ee059702cf8371e3214dd5c6fb7a489dd09cc8f5b31d9f8571ef4b8722c607dcce02145e2a5197bcd8a42c7cf
@@ -5,7 +5,7 @@ jobs:
5
5
  strategy:
6
6
  fail-fast: false
7
7
  matrix:
8
- ruby: [ '3.2', '3.3', '3.4' ]
8
+ ruby: [ '3.2', '3.3', '3.4', '4.0' ]
9
9
  gemfiles:
10
10
  - gemfiles/Gemfile-rails-6
11
11
  - gemfiles/Gemfile-rails-7
data/Gemfile CHANGED
@@ -5,3 +5,4 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
5
5
  # Specify your gem's dependencies in ar_serializer.gemspec
6
6
  gemspec
7
7
  gem "sqlite3"
8
+ gem "irb"
data/README.md CHANGED
@@ -1,15 +1,19 @@
1
1
  # ArSerializer
2
2
 
3
- - JSONの形をclientからリクエストできる
4
- - N+1 SQLを避ける
3
+ A serializer for ActiveRecord (and plain Ruby objects) where the **client requests the shape of the JSON**, GraphQL-style.
5
4
 
6
- ## Install
5
+ - The client decides which fields and associations to fetch, with a query.
6
+ - Associations are batch-loaded, so deeply nested queries avoid N+1 SQL.
7
+ - Generates TypeScript type definitions and can serve a GraphQL endpoint.
8
+
9
+ ## Installation
7
10
 
8
11
  ```ruby
9
12
  gem 'ar_serializer'
10
13
  ```
11
14
 
12
- ## Field定義
15
+ ## Defining fields
16
+
13
17
  ```ruby
14
18
  class User < ActiveRecord::Base
15
19
  has_many :posts
@@ -27,20 +31,26 @@ class Comment < ActiveRecord::Base
27
31
  end
28
32
  ```
29
33
 
30
- ## Serialize
34
+ ## Serializing
35
+
36
+ ```ruby
37
+ ArSerializer.serialize(model, query, context: nil, use: nil)
38
+ ```
39
+
31
40
  ```ruby
32
41
  ArSerializer.serialize Post.find(params[:id]), params[:query]
33
42
  ```
34
43
 
35
44
  ## Query
45
+
46
+ A query selects fields. Use `:*` for all fields, an array, or a hash; nested
47
+ associations take a nested query.
48
+
36
49
  ```ruby
37
50
  ArSerializer.serialize user, :*
38
- # => {
39
- # id: 1,
40
- # name: "user1",
41
- # posts: [{}, {}]
42
- # }
51
+ # => { id: 1, name: "user1", posts: [{}, {}] }
43
52
 
53
+ # Array form and hash form are equivalent:
44
54
  ArSerializer.serialize user, [:id, :name, posts: [:id, :title, comments: :id]]
45
55
  ArSerializer.serialize user, { id: true, name: true, posts: { id: true, title: true, comments: :id } }
46
56
  # => {
@@ -51,25 +61,36 @@ ArSerializer.serialize user, { id: true, name: true, posts: { id: true, title: t
51
61
  # { id: 3, title: "title2", comments: [] }
52
62
  # ]
53
63
  # }
64
+ ```
65
+
66
+ Rename a field in the output with `as:`:
67
+
68
+ ```ruby
54
69
  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
- # ]
70
+ # => [{ title: "title1", body: "body1", num_replies: 3 }, ...]
61
71
  ```
62
72
 
63
- ## その他
73
+ ## Field options
74
+
75
+ ### Computed fields (`data block`, `includes`)
76
+
77
+ Pass a block to compute the value. `includes:` eager-loads the associations the
78
+ block needs.
79
+
64
80
  ```ruby
65
- # data block, include
66
81
  class Comment < ActiveRecord::Base
67
- serializer_field :username, includes: :user do
68
- { ja: user.name + '先生', en: 'Dr.' + user.name }
82
+ serializer_field :title, includes: :user do
83
+ "#{user.name}'s comment"
69
84
  end
70
85
  end
86
+ ```
87
+
88
+ ### Preloading (avoid N+1)
71
89
 
72
- # preloader
90
+ `preload:` receives **all** records being serialized and returns a lookup; the
91
+ data block then reads from it per record.
92
+
93
+ ```ruby
73
94
  class Foo < ActiveRecord::Base
74
95
  bar_count_loader = ->(models) do
75
96
  Bar.where(foo_id: models.map(&:id)).group(:foo_id).count
@@ -77,59 +98,136 @@ class Foo < ActiveRecord::Base
77
98
  serializer_field :bar_count, preload: bar_count_loader do |preloaded|
78
99
  preloaded[id] || 0
79
100
  end
80
- # data_blockが `do |preloaded| preloaded[id] end` の場合は省略可能
101
+ # When the data block is exactly `do |preloaded| preloaded[id] end`, it can be omitted.
81
102
  end
103
+ ```
82
104
 
83
- # order and limits
84
- class Post < ActiveRecord::Base
85
- has_many :comments
86
- serializer_field :comments
87
- end
105
+ ### Counts
106
+
107
+ ```ruby
108
+ serializer_field :comment_count, count_of: :comments
109
+ ```
110
+
111
+ ### Order and limits
112
+
113
+ Associations accept `order_by`, `direction`, `first`/`last` params.
114
+
115
+ ```ruby
88
116
  ArSerializer.serialize Post.all, { comments: [:id, params: { order_by: :createdAt, direction: :desc, first: 10 }] }
89
117
  ArSerializer.serialize Post.all, { comments: [:id, params: { order_by: :updatedAt, last: 10 }] }
118
+ ```
119
+
120
+ ### Context and params
90
121
 
91
- # context and params
122
+ The block receives the serialize-time `context` and any query `params`.
123
+
124
+ ```ruby
92
125
  class Post < ActiveRecord::Base
93
126
  serializer_field :created_at do |context, **params|
94
127
  created_at.in_time_zone(context[:tz]).strftime params[:format]
95
128
  end
96
129
  end
97
130
  ArSerializer.serialize post, { created_at: { params: { format: '%H:%M:%S' } } }, context: { tz: 'Tokyo' }
131
+ ```
98
132
 
99
- # camelcase
133
+ ### camelCase field names
134
+
135
+ ```ruby
100
136
  class Foo < ActiveRecord::Base
101
137
  def foo_bar; end
102
138
  serializer_field :fooBar
103
139
  end
140
+ ```
104
141
 
105
- # non activerecord class
106
- class Foo
107
- include ArSerializer::Serializable
108
- def bar; end
109
- serializer_field :bar
142
+ ### Aliasing an association (`association:`)
143
+
144
+ Expose an association under a different name.
145
+
146
+ ```ruby
147
+ class User < ActiveRecord::Base
148
+ serializer_field :articles, association: :posts
110
149
  end
150
+ ArSerializer.serialize user, { articles: :title }
151
+ ```
111
152
 
112
- # namespace
153
+ ### Restricting fields (`only` / `except`)
154
+
155
+ Limit which fields of the associated records may be queried. Combine with
156
+ `association:` when the field name differs from the association.
157
+
158
+ ```ruby
159
+ class User < ActiveRecord::Base
160
+ serializer_field :posts, only: :title # restrict
161
+ serializer_field :entries, association: :posts, except: :body # alias + restrict
162
+ end
163
+ ArSerializer.serialize user, { posts: :title } #=> ok
164
+ ArSerializer.serialize user, { posts: :body } #=> Error (not allowed by `only`)
165
+ ArSerializer.serialize user, { entries: :title } #=> ok
166
+ ArSerializer.serialize user, { entries: :body } #=> Error (excluded by `except`)
167
+ ```
168
+
169
+ ## Access control
170
+
171
+ ### Field-level: `permission:`
172
+
173
+ Guards a single field. When the predicate returns false the field is omitted
174
+ from the output.
175
+
176
+ ```ruby
177
+ serializer_field :email, permission: ->(current_user) { current_user&.admin? }
178
+ ```
179
+
180
+ ### Model-level: `serializer_permission`
181
+
182
+ Guards every instance of a class, **no matter which query path reaches it**.
183
+ When serializing, each candidate object is checked and objects failing the
184
+ predicate are dropped — a single reference becomes `null`, an array drops the
185
+ element. Useful because any field reachable through the query graph is otherwise
186
+ fetchable.
187
+
188
+ ```ruby
189
+ class Document < ActiveRecord::Base
190
+ belongs_to :user
191
+ serializer_permission do |current_user|
192
+ current_user && current_user.id == user_id
193
+ end
194
+ serializer_field :id, :title
195
+ end
196
+ ```
197
+
198
+ ## Namespaces
199
+
200
+ Fields can be grouped into namespaces and exposed only when requested via `use:`.
201
+
202
+ ```ruby
113
203
  class User < ActiveRecord::Base
114
204
  serializer_field :name
115
205
  serializer_field(:foo, namespace: :admin) { :foo }
116
206
  serializer_field(:bar, namespace: :superadmin) { :bar }
117
207
  end
118
- ArSerializer.serialize user, [:name, :foo] #=> Error
208
+ ArSerializer.serialize user, [:name, :foo] #=> Error
119
209
  ArSerializer.serialize user, [:name, :foo], use: :admin
120
210
  ArSerializer.serialize user, [:name, :foo, :bar], use: [:admin, :superadmin]
211
+ ```
121
212
 
122
- # only, except
123
- class User < ActiveRecord::Base
124
- serializer_field :o_posts, association: :posts, only: :title
125
- serializer_field :e_posts, association: :posts, except: :comments
213
+ ## Non-ActiveRecord classes
214
+
215
+ Include `ArSerializer::Serializable` to use the DSL on plain Ruby objects.
216
+
217
+ ```ruby
218
+ class Foo
219
+ include ArSerializer::Serializable
220
+ def bar; end
221
+ serializer_field :bar
126
222
  end
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
223
+ ```
131
224
 
132
- # types
225
+ ## TypeScript types
226
+
227
+ Declare types with `type:` / `params_type:`, then generate `.d.ts`-style
228
+ definitions.
229
+
230
+ ```ruby
133
231
  class User < ActiveRecord::Base
134
232
  serializer_field(:posts, params_type: { title: :string? }) do |title: nil|
135
233
  title ? posts.where(title: title) : posts
@@ -139,10 +237,16 @@ class User < ActiveRecord::Base
139
237
  end
140
238
  serializer_field :published_posts, type: -> { [Post] }
141
239
  end
240
+
142
241
  ArSerializer::TypeScript.generate_type_definition User
143
- # => export type TypeUser {...}; export type TypePost {...}; ...
242
+ # => export type TypeUser = {...}; export type TypePost = {...}; ...
243
+ ```
244
+
245
+ ## GraphQL
144
246
 
145
- # graphql
247
+ Expose a schema object and serve GraphQL queries against it.
248
+
249
+ ```ruby
146
250
  class MySchema
147
251
  include ArSerializer::Serializable
148
252
  serializer_field :post, type: Post do |context, id:|
@@ -155,7 +259,12 @@ class MySchema
155
259
  ArSerializer::GraphQL::SchemaClass.new self.class
156
260
  end
157
261
  end
262
+
158
263
  ArSerializer::GraphQL.definition MySchema # schema.graphql
159
- ArSerializer::GraphQL.serialize MySchema.new, '{post(id: 1){title} user(name: user1){id name}}'
160
- ArSerializer::GraphQL.serialize MySchema.new, '{__schema{types{name fields{ name}}}}', operation_name: nil, variables: {}
264
+ ArSerializer::GraphQL.serialize MySchema.new, '{ post(id: 1){ title } user(name: "user1"){ id name } }'
265
+ ArSerializer::GraphQL.serialize MySchema.new, '{ __schema { types { name fields { name } } } }', operation_name: nil, variables: {}
161
266
  ```
267
+
268
+ ## License
269
+
270
+ [MIT](LICENSE.txt)
@@ -12,3 +12,4 @@ gem "logger"
12
12
  gem "concurrent-ruby", "1.3.4"
13
13
  gem "sqlite3", "~> 1.4"
14
14
  gem "activerecord", "~> 6.0"
15
+ gem "benchmark"
@@ -4,7 +4,25 @@ require 'set'
4
4
 
5
5
  class ArSerializer::Field
6
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
7
+ def initialize(
8
+ klass,
9
+ name,
10
+ includes: nil,
11
+ preloaders: [],
12
+ data_block:,
13
+ only: nil,
14
+ except: nil,
15
+ private: false,
16
+ scoped_access: nil,
17
+ permission: nil,
18
+ fallback: nil,
19
+ order_column: nil,
20
+ orderable: nil,
21
+ type: nil,
22
+ ts_type: nil,
23
+ params_type: nil,
24
+ ts_params_type: nil
25
+ )
8
26
  @klass = klass
9
27
  @name = name
10
28
  @includes = includes
@@ -18,8 +36,8 @@ class ArSerializer::Field
18
36
  @data_block = data_block
19
37
  @order_column = order_column
20
38
  @orderable = orderable
21
- @type = type
22
- @params_type = params_type
39
+ @type = ts_type ? ArSerializer::TSType.new(ts_type) : type
40
+ @params_type = ts_params_type ? ArSerializer::TSType.new(ts_params_type) : params_type
23
41
  end
24
42
 
25
43
  def orderable?
@@ -50,8 +68,8 @@ class ArSerializer::Field
50
68
  splat.call type
51
69
  end
52
70
 
53
- def arguments
54
- return @params_type.is_a?(Proc) ? @params_type.call : @params_type if @params_type
71
+ def arguments_type
72
+ return ArSerializer::GraphQL::TypeClass.from(@params_type.is_a?(Proc) ? @params_type.call : @params_type) if @params_type
55
73
  @preloaders.size
56
74
  @data_block.parameters
57
75
  parameters_list = [@data_block.parameters.drop(@preloaders.size + 1)]
@@ -79,8 +97,8 @@ class ArSerializer::Field
79
97
  end
80
98
  end
81
99
  end
82
- return :any if any && arguments.empty?
83
- arguments.map do |key, req|
100
+ return ArSerializer::GraphQL::TypeClass.from(:any) if any && arguments.empty?
101
+ hash_args = arguments.to_h do |key, req|
84
102
  camelcase = key.to_s.camelcase :lower
85
103
  type = (
86
104
  case key
@@ -94,7 +112,8 @@ class ArSerializer::Field
94
112
  end
95
113
  )
96
114
  [req ? camelcase : "#{camelcase}?", type]
97
- end.to_h
115
+ end
116
+ ArSerializer::GraphQL::TypeClass.from(hash_args)
98
117
  end
99
118
 
100
119
  def validate_attributes(attributes)
@@ -145,7 +164,26 @@ class ArSerializer::Field
145
164
  }[attr_type.type]
146
165
  end
147
166
 
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)
167
+ def self.create(
168
+ klass,
169
+ name,
170
+ ts_type: nil,
171
+ type: nil,
172
+ ts_params_type: nil,
173
+ params_type: nil,
174
+ count_of: nil,
175
+ includes: nil,
176
+ preload: nil,
177
+ only: nil,
178
+ except: nil,
179
+ private: nil,
180
+ scoped_access: nil,
181
+ permission: nil,
182
+ fallback: nil,
183
+ order_column: nil,
184
+ orderable: nil,
185
+ &data_block
186
+ )
149
187
  name = name.to_s
150
188
  if count_of
151
189
  if includes || preload || data_block || only || except || order_column || orderable || scoped_access != nil || fallback
@@ -153,6 +191,10 @@ class ArSerializer::Field
153
191
  end
154
192
  return count_field klass, name, count_of, permission: permission
155
193
  end
194
+
195
+ type = ArSerializer::TSType.new(ts_type) if ts_type
196
+ params_type = ArSerializer::TSType.new(ts_params_type) if ts_params_type
197
+
156
198
  association = klass.reflect_on_association name.underscore if klass.respond_to? :reflect_on_association
157
199
  if association
158
200
  if association.collection?
@@ -5,7 +5,7 @@ module ArSerializer::GraphQL
5
5
  def initialize(name, type)
6
6
  @optional = name.to_s.end_with? '?' # TODO: refactor
7
7
  @name = name.to_s.delete '?'
8
- @type = TypeClass.from type
8
+ @type = type
9
9
  end
10
10
  serializer_field :name
11
11
  serializer_field :type, except: :fields
@@ -22,8 +22,10 @@ module ArSerializer::GraphQL
22
22
  end
23
23
 
24
24
  def args
25
- return [] if field.arguments == :any
26
- field.arguments.map do |key, type|
25
+ arguments = field.arguments_type
26
+ return [] unless arguments.is_a?(HashTypeClass)
27
+
28
+ arguments.type.map do |key, type|
27
29
  ArgClass.new key, type
28
30
  end
29
31
  end
@@ -33,24 +35,26 @@ module ArSerializer::GraphQL
33
35
  end
34
36
 
35
37
  def collect_types(types)
36
- types[:any] = true if field.arguments == :any
37
- args.each { |arg| arg.type.collect_types types }
38
+ field.arguments_type.collect_types types
38
39
  type.collect_types types
39
40
  end
40
41
 
41
42
  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))
43
+ arguments_type = field.arguments_type
44
+ case arguments_type
45
+ when TSTypeClass
46
+ true
47
+ when HashTypeClass
48
+ arguments_type.type.any? do |k, v|
49
+ !k.end_with?('?') && !v.is_a?(OptionalTypeClass)
50
+ end
51
+ else
52
+ false
45
53
  end
46
54
  end
47
55
 
48
56
  def args_ts_type
49
- return 'any' if field.arguments == :any
50
- arg_types = field.arguments.map do |key, type|
51
- "#{key}: #{TypeClass.from(type).ts_type}"
52
- end
53
- arg_types.empty? ? 'Record<string, never>' : "{ #{arg_types.join '; '} }"
57
+ field.arguments_type.ts_type
54
58
  end
55
59
 
56
60
  serializer_field :name, :args
@@ -92,46 +96,16 @@ module ArSerializer::GraphQL
92
96
 
93
97
  class TypeClass
94
98
  include ::ArSerializer::Serializable
95
- attr_reader :type, :only, :except
96
- def initialize(type, only = nil, except = nil)
99
+ attr_reader :type
100
+ def initialize(type)
97
101
  @type = type
98
- @only = only
99
- @except = except
100
- validate!
101
102
  end
102
103
 
103
104
  class InvalidType < StandardError; end
104
105
 
105
- def validate!
106
- valid_symbols = %i[number int float string boolean any unknown]
107
- invalids = []
108
- recursive_validate = lambda do |t|
109
- case t
110
- when Array
111
- t.each { |v| recursive_validate.call v }
112
- when Hash
113
- t.each_value { |v| recursive_validate.call v }
114
- when String, Numeric, true, false, nil
115
- return
116
- when Class
117
- invalids << t unless t.ancestors.include? ArSerializer::Serializable
118
- when Symbol
119
- invalids << t unless valid_symbols.include? t.to_s.gsub(/\?$/, '').to_sym
120
- else
121
- invalids << t
122
- end
123
- end
124
- recursive_validate.call type
125
- return if invalids.empty?
126
- message = "Valid types are String, Numeric, Hash, Array, ArSerializer::Serializable, true, false, nil and Symbol#{valid_symbols}"
127
- raise InvalidType, "Invalid type: #{invalids.map(&:inspect).join(', ')}. #{message}"
128
- end
129
-
130
106
  def collect_types(types); end
131
107
 
132
- def description
133
- ts_type
134
- end
108
+ def description = ts_type
135
109
 
136
110
  def name; end
137
111
 
@@ -139,8 +113,6 @@ module ArSerializer::GraphQL
139
113
 
140
114
  def fields; end
141
115
 
142
- def sample; end
143
-
144
116
  def ts_type; end
145
117
 
146
118
  def association_type; end
@@ -154,35 +126,59 @@ module ArSerializer::GraphQL
154
126
 
155
127
  def self.from(type, only = nil, except = nil)
156
128
  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?('?')
129
+ type = [type[0...-1], nil] if type.is_a?(String) && type.end_with?('?') # ??
158
130
  case type
159
131
  when Class
132
+ raise InvalidType, "#{type} must include ArSerializer::Serializable" unless type.ancestors.include? ArSerializer::Serializable
133
+
160
134
  SerializableTypeClass.new type, only, except
161
- when Symbol, String, Numeric, true, false, nil
135
+ when :number, :int, :float, :string, :boolean, :any, :unknown
136
+ ScalarTypeClass.new type
137
+ when String, Numeric, true, false, nil
162
138
  ScalarTypeClass.new type
163
139
  when Array
164
140
  if type.size == 1
165
- ListTypeClass.new type.first, only, except
141
+ ListTypeClass.new from(type.first, only, except)
166
142
  elsif type.size == 2 && type.last.nil?
167
- OptionalTypeClass.new type, only, except
143
+ OptionalTypeClass.new from(type.first, only, except)
168
144
  else
169
- OrTypeClass.new type, only, except
145
+ OrTypeClass.new type.map {|v| from(v, only, except) }
170
146
  end
171
147
  when Hash
172
- HashTypeClass.new type, only, except
148
+ HashTypeClass.new type.transform_values {|v| from(v, only, except) }
149
+ when ArSerializer::TSType
150
+ TSTypeClass.new type.type
151
+ else
152
+ raise InvalidType, "Invalid type: #{type}"
173
153
  end
174
154
  end
175
155
  end
176
156
 
177
- class ScalarTypeClass < TypeClass
157
+ class TSTypeClass < TypeClass
178
158
  def initialize(type)
179
159
  @type = type
180
160
  end
181
161
 
182
- def kind
183
- 'SCALAR'
162
+ def kind = 'SCALAR'
163
+
164
+ def name = :other
165
+
166
+ def collect_types(types)
167
+ types[:other] = true
168
+ end
169
+
170
+ def gql_type = 'SCALAR'
171
+
172
+ def ts_type = @type
173
+ end
174
+
175
+ class ScalarTypeClass < TypeClass
176
+ def initialize(type)
177
+ @type = type
184
178
  end
185
179
 
180
+ def kind = 'SCALAR'
181
+
186
182
  def name
187
183
  case type
188
184
  when String, :string
@@ -206,24 +202,7 @@ module ArSerializer::GraphQL
206
202
  types[name] = true
207
203
  end
208
204
 
209
- def gql_type
210
- type
211
- end
212
-
213
- def sample
214
- case ts_type
215
- when 'number'
216
- 0
217
- when 'string'
218
- ''
219
- when 'boolean'
220
- true
221
- when 'any', 'unknown'
222
- nil
223
- else
224
- type
225
- end
226
- end
205
+ def gql_type = type
227
206
 
228
207
  def ts_type
229
208
  case type
@@ -240,49 +219,53 @@ module ArSerializer::GraphQL
240
219
  end
241
220
 
242
221
  class HashTypeClass < TypeClass
243
- def kind
244
- 'SCALAR'
245
- end
222
+ def kind = 'SCALAR'
246
223
 
247
- def name
248
- :other
249
- end
224
+ def name = :other
250
225
 
251
226
  def collect_types(types)
252
227
  types[:other] = true
253
- type.values.map do |v|
254
- TypeClass.from(v, only, except).collect_types(types)
228
+ type.values.each do |v|
229
+ v.collect_types(types)
255
230
  end
256
231
  end
257
232
 
258
233
  def association_type
259
234
  type.values.each do |v|
260
- t = TypeClass.from(v, only, except).association_type
235
+ t = v.association_type
261
236
  return t if t
262
237
  end
263
238
  nil
264
239
  end
265
240
 
266
- def gql_type
267
- 'OBJECT'
268
- end
269
-
270
- def sample
271
- type.reject { |k| k.to_s.end_with? '?' }.transform_values do |v|
272
- TypeClass.from(v).sample
273
- end
274
- end
241
+ def gql_type = 'OBJECT'
275
242
 
276
243
  def ts_type
244
+ return 'Record<string, never>' if type.empty?
245
+
277
246
  fields = type.map do |key, value|
278
247
  k = key.to_s == '*' ? '[key: string]' : key
279
- "#{k}: #{TypeClass.from(value, only, except).ts_type}"
248
+ "#{k}: #{value.ts_type}"
280
249
  end
281
250
  "{ #{fields.join('; ')} }"
282
251
  end
252
+
253
+ def to_gql_args
254
+ type.map do |key, type|
255
+ ArgClass.new key, type
256
+ end
257
+ end
283
258
  end
284
259
 
285
260
  class SerializableTypeClass < TypeClass
261
+ attr_reader :only, :except
262
+
263
+ def initialize(type, only = nil, except = nil)
264
+ super type
265
+ @only = only
266
+ @except = except
267
+ end
268
+
286
269
  def field_only
287
270
  [*only].map(&:to_s)
288
271
  end
@@ -291,9 +274,7 @@ module ArSerializer::GraphQL
291
274
  [*except].map(&:to_s)
292
275
  end
293
276
 
294
- def kind
295
- 'OBJECT'
296
- end
277
+ def kind = 'OBJECT'
297
278
 
298
279
  def name
299
280
  name_segments = [type.name.delete(':')]
@@ -322,17 +303,11 @@ module ArSerializer::GraphQL
322
303
  fields.each { |field| field.collect_types types }
323
304
  end
324
305
 
325
- def association_type
326
- self
327
- end
306
+ def association_type = self
328
307
 
329
- def gql_type
330
- name
331
- end
308
+ def gql_type = name
332
309
 
333
- def ts_type
334
- "Type#{name}"
335
- end
310
+ def ts_type = "Type#{name}"
336
311
 
337
312
  def eql?(t)
338
313
  self.class == t.class && self.compare_elements == t.compare_elements
@@ -352,101 +327,55 @@ module ArSerializer::GraphQL
352
327
  end
353
328
 
354
329
  class OptionalTypeClass < TypeClass
355
- def kind
356
- of_type.kind
357
- end
330
+ def kind = type.kind
358
331
 
359
- def name
360
- of_type.name
361
- end
332
+ def name = type.name
362
333
 
363
- def of_type
364
- TypeClass.from type.first, only, except
365
- end
334
+ def of_type = type
366
335
 
367
- def association_type
368
- of_type.association_type
369
- end
336
+ def association_type = type.association_type
370
337
 
371
338
  def collect_types(types)
372
- of_type.collect_types types
339
+ type.collect_types types
373
340
  end
374
341
 
375
- def gql_type
376
- of_type.gql_type
377
- end
342
+ def gql_type = type.gql_type
378
343
 
379
- def sample
380
- nil
381
- end
382
-
383
- def ts_type
384
- "(#{of_type.ts_type} | null)"
385
- end
344
+ def ts_type = "(#{type.ts_type} | null)"
386
345
  end
387
346
 
388
347
  class OrTypeClass < TypeClass
389
- def kind
390
- 'OBJECT'
391
- end
348
+ def kind = 'OBJECT'
392
349
 
393
- def name
394
- :other
395
- end
350
+ def name = :other
396
351
 
397
- def of_types
398
- type.map { |t| TypeClass.from t, only, except }
399
- end
352
+ def of_types = type
400
353
 
401
354
  def collect_types(types)
402
355
  types[:other] = true
403
- of_types.map { |t| t.collect_types types }
404
- end
405
-
406
- def gql_type
407
- kind
356
+ type.map { |t| t.collect_types types }
408
357
  end
409
358
 
410
- def sample
411
- of_types.first.sample
412
- end
359
+ def gql_type = kind
413
360
 
414
- def ts_type
415
- '(' + of_types.map(&:ts_type).join(' | ') + ')'
416
- end
361
+ def ts_type = "(#{type.map(&:ts_type).join(' | ')})"
417
362
  end
418
363
 
419
364
  class ListTypeClass < TypeClass
420
- def kind
421
- 'LIST'
422
- end
365
+ def kind = 'LIST'
423
366
 
424
- def name
425
- 'LIST'
426
- end
367
+ def name = 'LIST'
427
368
 
428
- def of_type
429
- TypeClass.from type, only, except
430
- end
369
+ def of_type = type
431
370
 
432
371
  def collect_types(types)
433
- of_type.collect_types types
434
- end
435
-
436
- def association_type
437
- of_type.association_type
372
+ type.collect_types types
438
373
  end
439
374
 
440
- def gql_type
441
- "[#{of_type.gql_type}]"
442
- end
375
+ def association_type = type.association_type
443
376
 
444
- def sample
445
- []
446
- end
377
+ def gql_type = "[#{type.gql_type}]"
447
378
 
448
- def ts_type
449
- "(#{of_type.ts_type} [])"
450
- end
379
+ def ts_type = "(#{type.ts_type} [])"
451
380
  end
452
381
  end
@@ -85,6 +85,7 @@ module ArSerializer::Serializer
85
85
  raise ArgumentError, "No permission field #{permission} for #{klass}" unless permission_field
86
86
  end
87
87
  if permission_field
88
+ ArSerializer.preload_associations models, permission_field.includes if permission_field.includes.present?
88
89
  preloadeds = permission_field.preloaders.map do |p|
89
90
  preloader_values[[p, nil]] ||= preload.call p, nil
90
91
  end
@@ -95,6 +96,7 @@ module ArSerializer::Serializer
95
96
 
96
97
  defaults = klass._serializer_field_info :defaults
97
98
  if defaults
99
+ ArSerializer.preload_associations models, defaults.includes if defaults.includes.present?
98
100
  defaults.preloaders.each do |p|
99
101
  preloader_values[[p, nil]] ||= preload.call p, nil
100
102
  end
@@ -181,7 +183,7 @@ module ArSerializer::Serializer
181
183
  end
182
184
 
183
185
  if defaults
184
- preloadeds = defaults.preloaders.map { |p| preloader_values[[p]] } || []
186
+ preloadeds = defaults.preloaders.map { |p| preloader_values[[p, nil]] } || []
185
187
  models.each do |model|
186
188
  data = model.instance_exec(*preloadeds, context, {}, &defaults.data_block)
187
189
  output_for_model[model].update data
@@ -16,7 +16,7 @@ module ArSerializer::TypeScript
16
16
  field_definitions = type.fields.map do |field|
17
17
  association_type = field.type.association_type
18
18
  query_type = "Type#{association_type.name}Query" if association_type
19
- params_type = field.args_ts_type unless field.args.empty?
19
+ params_type = field.args_ts_type
20
20
  params_required = field.args_required?
21
21
  attrs = []
22
22
  attrs << "query?: #{query_type}" if query_type
@@ -1,3 +1,3 @@
1
1
  module ArSerializer
2
- VERSION = '1.2.2'
2
+ VERSION = '1.2.4'
3
3
  end
data/lib/ar_serializer.rb CHANGED
@@ -4,6 +4,8 @@ require 'ar_serializer/field'
4
4
  require 'active_record'
5
5
 
6
6
  module ArSerializer
7
+ TSType = Data.define(:type)
8
+
7
9
  def self.serialize(model, query, **option)
8
10
  Serializer.serialize(model, query, **option)
9
11
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - tompng
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-09-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -127,7 +126,6 @@ homepage: https://github.com/tompng/ar_serializer
127
126
  licenses:
128
127
  - MIT
129
128
  metadata: {}
130
- post_install_message:
131
129
  rdoc_options: []
132
130
  require_paths:
133
131
  - lib
@@ -142,8 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
140
  - !ruby/object:Gem::Version
143
141
  version: '0'
144
142
  requirements: []
145
- rubygems_version: 3.5.9
146
- signing_key:
143
+ rubygems_version: 4.0.3
147
144
  specification_version: 4
148
145
  summary: ActiveRecord serializer, avoid N+1
149
146
  test_files: []