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 +4 -4
- data/.github/workflows/test.yml +1 -1
- data/Gemfile +1 -0
- data/README.md +158 -49
- data/gemfiles/Gemfile-rails-6 +1 -0
- data/lib/ar_serializer/field.rb +51 -9
- data/lib/ar_serializer/graphql/types.rb +101 -172
- data/lib/ar_serializer/serializer.rb +3 -1
- data/lib/ar_serializer/type_script.rb +1 -1
- data/lib/ar_serializer/version.rb +1 -1
- data/lib/ar_serializer.rb +2 -0
- metadata +3 -6
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/.github/workflows/test.yml
CHANGED
data/Gemfile
CHANGED
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-6
CHANGED
data/lib/ar_serializer/field.rb
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
96
|
-
def initialize(type
|
|
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
|
|
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
|
|
157
|
+
class TSTypeClass < TypeClass
|
|
178
158
|
def initialize(type)
|
|
179
159
|
@type = type
|
|
180
160
|
end
|
|
181
161
|
|
|
182
|
-
def kind
|
|
183
|
-
|
|
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.
|
|
254
|
-
|
|
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 =
|
|
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}: #{
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
def gql_type
|
|
407
|
-
kind
|
|
356
|
+
type.map { |t| t.collect_types types }
|
|
408
357
|
end
|
|
409
358
|
|
|
410
|
-
def
|
|
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
|
-
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
def association_type
|
|
437
|
-
of_type.association_type
|
|
372
|
+
type.collect_types types
|
|
438
373
|
end
|
|
439
374
|
|
|
440
|
-
def
|
|
441
|
-
"[#{of_type.gql_type}]"
|
|
442
|
-
end
|
|
375
|
+
def association_type = type.association_type
|
|
443
376
|
|
|
444
|
-
def
|
|
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
|
|
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
|
data/lib/ar_serializer.rb
CHANGED
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.
|
|
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:
|
|
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:
|
|
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: []
|