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 +4 -4
- data/README.md +158 -49
- data/gemfiles/Gemfile-rails-7 +0 -1
- data/gemfiles/Gemfile-rails-8 +0 -1
- data/lib/ar_serializer/serializer.rb +3 -1
- data/lib/ar_serializer/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef5e6277909a8e2573068c3c8c2e19275c8763c9395f610cb0e8aebbce566eee
|
|
4
|
+
data.tar.gz: ad0b8f4f8961dcf576c402071f79b312e93c0a4da958bab46b420adb70ea5416
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 285562de1dc23efb39552eb7b184fb2004dfdf0a63314d161f5397060ca79c22136140612cde7f3d46bc76f3b649e016671fd15561d9829e4c14f22a42cf7c72
|
|
7
|
+
data.tar.gz: 3081e60e717cefe07b8629934babeacecef06c5ee059702cf8371e3214dd5c6fb7a489dd09cc8f5b31d9f8571ef4b8722c607dcce02145e2a5197bcd8a42c7cf
|
data/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# ArSerializer
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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 :
|
|
68
|
-
{
|
|
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
|
-
|
|
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
|
-
#
|
|
101
|
+
# When the data block is exactly `do |preloaded| preloaded[id] end`, it can be omitted.
|
|
81
102
|
end
|
|
103
|
+
```
|
|
82
104
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/gemfiles/Gemfile-rails-7
CHANGED
data/gemfiles/Gemfile-rails-8
CHANGED
|
@@ -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
|