ar_serializer 1.2.3 → 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: 82a536d1726d005b8fe7606ba04dd252bd169a88686d67b6eb6fdb5f4f2e791d
4
- data.tar.gz: fe03fd5cc99e41f4b9002fe43351f619e65f8a692049bd0af959432fe301e1d8
3
+ metadata.gz: ef5e6277909a8e2573068c3c8c2e19275c8763c9395f610cb0e8aebbce566eee
4
+ data.tar.gz: ad0b8f4f8961dcf576c402071f79b312e93c0a4da958bab46b420adb70ea5416
5
5
  SHA512:
6
- metadata.gz: 86729d4da8d17051e42dcab42dec965163ed478e738459eed8633942f29ff5262e7691b726d785db4fdcbc2781c286d93d4972b400fa8bbfaf62d4dc8e196b30
7
- data.tar.gz: 13d2d9d8c85004a4c02cb0988797395a1d2819535c03d8cb753ee0a2882177ea8a8d090cc1902289afc96ca89440a6530a71f840308d8e33ce9983af16203062
6
+ metadata.gz: 285562de1dc23efb39552eb7b184fb2004dfdf0a63314d161f5397060ca79c22136140612cde7f3d46bc76f3b649e016671fd15561d9829e4c14f22a42cf7c72
7
+ data.tar.gz: 3081e60e717cefe07b8629934babeacecef06c5ee059702cf8371e3214dd5c6fb7a489dd09cc8f5b31d9f8571ef4b8722c607dcce02145e2a5197bcd8a42c7cf
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)
@@ -7,4 +7,3 @@ gemspec path: ".."
7
7
 
8
8
  gem "sqlite3", "~> 1.4"
9
9
  gem "activerecord", "~> 7.0"
10
- gem "benchmark"
@@ -7,4 +7,3 @@ gemspec path: ".."
7
7
 
8
8
  gem "sqlite3"
9
9
  gem "activerecord", "~> 8.0"
10
- gem "benchmark"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ArSerializer
2
- VERSION = '1.2.3'
2
+ VERSION = '1.2.4'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - tompng