graphiform 0.4.1 → 0.5.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.
- checksums.yaml +4 -4
- data/lib/graphiform/core.rb +110 -27
- data/lib/graphiform/fields.rb +69 -67
- data/lib/graphiform/helpers.rb +71 -28
- data/lib/graphiform/preloader_source.rb +61 -0
- data/lib/graphiform/scope_composer.rb +35 -0
- data/lib/graphiform/skeleton.rb +1 -0
- data/lib/graphiform/version.rb +1 -1
- data/lib/graphiform.rb +2 -0
- metadata +60 -17
- data/lib/graphiform/association_source.rb +0 -58
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68d73b56aab3ca718e3b8c8521ef9e03bc4872913a8d6fe2c5b05498611495ae
|
|
4
|
+
data.tar.gz: f605a09353957a99960460be3c071b2f9d3558e6df61edf5b29f367c7517a117
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 255499f8b9e7742cfbaf9f9cc1e7fe5df3002887e35dc527326dde57288678a1d8bf88923d1a6e6e5a6607b333badf78a90cc7ebc364c99c27365ed74bdaefa3
|
|
7
|
+
data.tar.gz: e793c187d70413324c6c59b557dea0f7d46e346d688ee33681f5c9f4b690e0d14568861b61fb969916ee9596062e65a9a5fd2847442ee1104ab303ab0b103abb
|
data/lib/graphiform/core.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require 'active_support/concern'
|
|
4
4
|
|
|
5
5
|
require 'graphiform/helpers'
|
|
6
|
-
require 'graphiform/
|
|
6
|
+
require 'graphiform/preloader_source'
|
|
7
|
+
require 'graphiform/scope_composer'
|
|
7
8
|
|
|
8
9
|
module Graphiform
|
|
9
10
|
module Core
|
|
@@ -43,49 +44,87 @@ module Graphiform
|
|
|
43
44
|
def graphql_filter
|
|
44
45
|
unless defined? @filter
|
|
45
46
|
local_demodulized_name = demodulized_name
|
|
47
|
+
model_class = self
|
|
46
48
|
@filter = Helpers.get_const_or_create(local_demodulized_name, ::Inputs::Filters) do
|
|
47
49
|
Class.new(::Inputs::Filters::BaseFilter) do
|
|
48
50
|
graphql_name "#{local_demodulized_name}Filter"
|
|
51
|
+
|
|
52
|
+
define_singleton_method(:arguments) do |context = nil|
|
|
53
|
+
model_class.send(:flush_pending_filters!)
|
|
54
|
+
super(context)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
define_singleton_method(:own_arguments) do
|
|
58
|
+
model_class.send(:flush_pending_filters!)
|
|
59
|
+
super()
|
|
60
|
+
end
|
|
49
61
|
end
|
|
50
62
|
end
|
|
51
63
|
@filter.class_eval do
|
|
52
64
|
argument_class Graphiform.configuration[:argument_class] if Graphiform.configuration[:argument_class].present?
|
|
53
|
-
argument 'OR', [self], required: false
|
|
54
|
-
argument 'AND', [self], required: false
|
|
55
65
|
end
|
|
66
|
+
Helpers.add_unless_exists(@filter, 'OR') { @filter.class_eval { argument 'OR', [self], required: false } }
|
|
67
|
+
Helpers.add_unless_exists(@filter, 'AND') { @filter.class_eval { argument 'AND', [self], required: false } }
|
|
56
68
|
end
|
|
57
69
|
|
|
70
|
+
flush_pending_filters!
|
|
58
71
|
@filter
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
def graphql_sort
|
|
62
75
|
unless defined? @graphql_sort
|
|
63
76
|
local_demodulized_name = demodulized_name
|
|
77
|
+
model_class = self
|
|
64
78
|
@graphql_sort = Helpers.get_const_or_create(local_demodulized_name, ::Inputs::Sorts) do
|
|
65
79
|
Class.new(::Inputs::Sorts::BaseSort) do
|
|
66
80
|
graphql_name "#{local_demodulized_name}Sort"
|
|
81
|
+
|
|
82
|
+
define_singleton_method(:arguments) do |context = nil|
|
|
83
|
+
model_class.send(:flush_pending_sorts!)
|
|
84
|
+
super(context)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
define_singleton_method(:own_arguments) do
|
|
88
|
+
model_class.send(:flush_pending_sorts!)
|
|
89
|
+
super()
|
|
90
|
+
end
|
|
67
91
|
end
|
|
68
92
|
end
|
|
69
93
|
@graphql_sort.class_eval do
|
|
70
94
|
argument_class Graphiform.configuration[:argument_class] if Graphiform.configuration[:argument_class].present?
|
|
71
95
|
end
|
|
72
96
|
end
|
|
97
|
+
|
|
98
|
+
flush_pending_sorts!
|
|
73
99
|
@graphql_sort
|
|
74
100
|
end
|
|
75
101
|
|
|
76
102
|
def graphql_grouping
|
|
77
103
|
unless defined? @graphql_grouping
|
|
78
104
|
local_demodulized_name = demodulized_name
|
|
105
|
+
model_class = self
|
|
79
106
|
@graphql_grouping = Helpers.get_const_or_create(local_demodulized_name, ::Inputs::Groupings) do
|
|
80
107
|
Class.new(::Inputs::Groupings::BaseGrouping) do
|
|
81
108
|
graphql_name "#{local_demodulized_name}Grouping"
|
|
82
109
|
argument_class Graphiform.configuration[:argument_class] if Graphiform.configuration[:argument_class].present?
|
|
110
|
+
|
|
111
|
+
define_singleton_method(:arguments) do |context = nil|
|
|
112
|
+
model_class.send(:flush_pending_groupings!)
|
|
113
|
+
super(context)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
define_singleton_method(:own_arguments) do
|
|
117
|
+
model_class.send(:flush_pending_groupings!)
|
|
118
|
+
super()
|
|
119
|
+
end
|
|
83
120
|
end
|
|
84
121
|
end
|
|
85
122
|
@graphql_grouping.class_eval do
|
|
86
123
|
argument_class Graphiform.configuration[:argument_class] if Graphiform.configuration[:argument_class].present?
|
|
87
124
|
end
|
|
88
125
|
end
|
|
126
|
+
|
|
127
|
+
flush_pending_groupings!
|
|
89
128
|
@graphql_grouping
|
|
90
129
|
end
|
|
91
130
|
|
|
@@ -137,11 +176,7 @@ module Graphiform
|
|
|
137
176
|
end
|
|
138
177
|
|
|
139
178
|
def apply_built_ins(where: nil, sort: nil, group: nil, **)
|
|
140
|
-
@value = @value
|
|
141
|
-
@value = @value.apply_sorts(sort.to_h) if sort.present? && @value.respond_to?(:apply_sorts)
|
|
142
|
-
@value = @value.apply_groupings(group.to_h) if group.present? && @value.respond_to?(:apply_groupings)
|
|
143
|
-
|
|
144
|
-
@value
|
|
179
|
+
@value = Graphiform::ScopeComposer.compose(@value, where: where, sort: sort, group: group)
|
|
145
180
|
end
|
|
146
181
|
|
|
147
182
|
# Default resolver - meant to be overridden
|
|
@@ -164,8 +199,8 @@ module Graphiform
|
|
|
164
199
|
end
|
|
165
200
|
|
|
166
201
|
argument :where, local_graphql_filter, required: false
|
|
167
|
-
argument :sort, local_graphql_sort, required: false unless local_graphql_sort.
|
|
168
|
-
argument :group, local_graphql_grouping, required: false unless local_graphql_grouping.
|
|
202
|
+
argument :sort, local_graphql_sort, required: false unless local_graphql_sort.own_arguments.empty?
|
|
203
|
+
argument :group, local_graphql_grouping, required: false unless local_graphql_grouping.own_arguments.empty?
|
|
169
204
|
end
|
|
170
205
|
end
|
|
171
206
|
|
|
@@ -201,7 +236,7 @@ module Graphiform
|
|
|
201
236
|
end
|
|
202
237
|
end
|
|
203
238
|
|
|
204
|
-
def graphql_create_resolver(method_name, resolver_type = graphql_type, read_prepare: nil, read_resolve: nil, null: true, skip_dataloader: false,
|
|
239
|
+
def graphql_create_resolver(method_name, resolver_type = graphql_type, read_prepare: nil, read_resolve: nil, null: true, skip_dataloader: false, **)
|
|
205
240
|
Class.new(graphql_base_resolver) do
|
|
206
241
|
type resolver_type, null: null
|
|
207
242
|
|
|
@@ -212,7 +247,7 @@ module Graphiform
|
|
|
212
247
|
|
|
213
248
|
skip_dataloader ||=
|
|
214
249
|
!association_def ||
|
|
215
|
-
!Helpers.dataloader_support?(dataloader, association_def
|
|
250
|
+
!Helpers.dataloader_support?(dataloader, association_def) ||
|
|
216
251
|
read_resolve ||
|
|
217
252
|
read_prepare ||
|
|
218
253
|
args[:group]
|
|
@@ -226,35 +261,33 @@ module Graphiform
|
|
|
226
261
|
else
|
|
227
262
|
dataloader
|
|
228
263
|
.with(
|
|
229
|
-
|
|
230
|
-
association_def.
|
|
231
|
-
association_def.
|
|
264
|
+
Graphiform::PreloaderSource,
|
|
265
|
+
association_def.name,
|
|
266
|
+
klass: association_def.klass,
|
|
232
267
|
scope: association_def.scope,
|
|
233
268
|
where: args[:where],
|
|
234
269
|
sort: args[:sort],
|
|
235
|
-
multi: true,
|
|
236
|
-
case_sensitive: case_sensitive,
|
|
237
|
-
)
|
|
238
|
-
.load(
|
|
239
|
-
@value.public_send(association_def.join_foreign_key)
|
|
240
270
|
)
|
|
271
|
+
.load(@value)
|
|
241
272
|
end
|
|
242
273
|
end
|
|
243
274
|
end
|
|
244
275
|
end
|
|
245
276
|
|
|
246
|
-
def graphql_create_association_resolver(association_def, resolver_type, null: true, skip_dataloader: false,
|
|
277
|
+
def graphql_create_association_resolver(association_def, resolver_type, null: true, skip_dataloader: false, **)
|
|
247
278
|
Class.new(::Resolvers::BaseResolver) do
|
|
248
279
|
type resolver_type, null: null
|
|
249
280
|
|
|
250
281
|
define_method :resolve do |*|
|
|
251
|
-
|
|
252
|
-
skip_dataloader ||= !Helpers.dataloader_support?(dataloader, association_def, association_def.join_foreign_key)
|
|
253
|
-
|
|
282
|
+
skip_dataloader ||= !Helpers.dataloader_support?(dataloader, association_def)
|
|
254
283
|
return object.public_send(association_def.name) if skip_dataloader
|
|
255
284
|
|
|
256
|
-
|
|
257
|
-
|
|
285
|
+
dataloader.with(
|
|
286
|
+
Graphiform::PreloaderSource,
|
|
287
|
+
association_def.name,
|
|
288
|
+
klass: association_def.klass,
|
|
289
|
+
scope: association_def.scope
|
|
290
|
+
).load(object)
|
|
258
291
|
end
|
|
259
292
|
end
|
|
260
293
|
end
|
|
@@ -273,6 +306,56 @@ module Graphiform
|
|
|
273
306
|
end
|
|
274
307
|
end
|
|
275
308
|
|
|
309
|
+
# --- Pending queues for lazy filter/sort/grouping wiring -------------
|
|
310
|
+
#
|
|
311
|
+
# graphql_readable_field pushes entries here instead of immediately
|
|
312
|
+
# invoking add_scope_def_to_filter / graphql_field_to_sort /
|
|
313
|
+
# graphql_field_to_grouping. The queues are drained the first time
|
|
314
|
+
# the corresponding builder is accessed.
|
|
315
|
+
|
|
316
|
+
def graphiform_pending_filters
|
|
317
|
+
@graphiform_pending_filters ||= []
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def graphiform_pending_sorts
|
|
321
|
+
@graphiform_pending_sorts ||= []
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def graphiform_pending_groupings
|
|
325
|
+
@graphiform_pending_groupings ||= []
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def flush_pending_filters!
|
|
329
|
+
return if @graphiform_pending_filters.nil? || @graphiform_pending_filters.empty?
|
|
330
|
+
|
|
331
|
+
pending = @graphiform_pending_filters
|
|
332
|
+
@graphiform_pending_filters = []
|
|
333
|
+
pending.each do |(name, identifier, options)|
|
|
334
|
+
graphql_add_scopes_to_filter(name, identifier, **options)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def flush_pending_sorts!
|
|
339
|
+
return if @graphiform_pending_sorts.nil? || @graphiform_pending_sorts.empty?
|
|
340
|
+
|
|
341
|
+
pending = @graphiform_pending_sorts
|
|
342
|
+
@graphiform_pending_sorts = []
|
|
343
|
+
pending.each do |(name, identifier, options)|
|
|
344
|
+
graphql_field_to_sort(name, identifier, **options)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def flush_pending_groupings!
|
|
349
|
+
return if @graphiform_pending_groupings.nil? || @graphiform_pending_groupings.empty?
|
|
350
|
+
|
|
351
|
+
pending = @graphiform_pending_groupings
|
|
352
|
+
@graphiform_pending_groupings = []
|
|
353
|
+
pending.each do |(name, identifier, options)|
|
|
354
|
+
graphql_field_to_grouping(name, identifier, **options)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
# ----------------------------------------------------------------------
|
|
358
|
+
|
|
276
359
|
private
|
|
277
360
|
|
|
278
361
|
def demodulized_name
|
|
@@ -280,4 +363,4 @@ module Graphiform
|
|
|
280
363
|
end
|
|
281
364
|
end
|
|
282
365
|
end
|
|
283
|
-
end
|
|
366
|
+
end
|
data/lib/graphiform/fields.rb
CHANGED
|
@@ -22,9 +22,11 @@ module Graphiform
|
|
|
22
22
|
graphql_add_association_field(name, association_def, read_prepare: read_prepare, null: null, as: as, **options) if association_def.present?
|
|
23
23
|
graphql_add_method_field(name, read_prepare: read_prepare, null: null, as: as, **options) unless column_def.present? || association_def.present?
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
# Defer filter/sort/grouping wiring — flushed on first access to
|
|
26
|
+
# graphql_filter / graphql_sort / graphql_grouping.
|
|
27
|
+
graphiform_pending_filters << [name, identifier, options]
|
|
28
|
+
graphiform_pending_sorts << [name, identifier, options]
|
|
29
|
+
graphiform_pending_groupings << [name, identifier, options]
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def graphql_writable_field(
|
|
@@ -34,7 +36,6 @@ module Graphiform
|
|
|
34
36
|
write_prepare: nil,
|
|
35
37
|
prepare: nil,
|
|
36
38
|
description: nil,
|
|
37
|
-
default_value: ::GraphQL::Schema::Argument::NO_DEFAULT,
|
|
38
39
|
as: nil,
|
|
39
40
|
**args
|
|
40
41
|
)
|
|
@@ -49,19 +50,19 @@ module Graphiform
|
|
|
49
50
|
|
|
50
51
|
prepare = write_prepare || prepare
|
|
51
52
|
|
|
52
|
-
graphql_input
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
53
|
+
Helpers.add_unless_exists(graphql_input, argument_name) do
|
|
54
|
+
graphql_input.class_eval do
|
|
55
|
+
argument(
|
|
56
|
+
argument_name,
|
|
57
|
+
argument_type,
|
|
58
|
+
required: required,
|
|
59
|
+
prepare: prepare,
|
|
60
|
+
description: description,
|
|
61
|
+
as: as,
|
|
62
|
+
**args
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def graphql_field(
|
|
@@ -152,16 +153,17 @@ module Graphiform
|
|
|
152
153
|
argument_name = "#{argument_prefix}#{argument_attribute}#{argument_suffix}".underscore
|
|
153
154
|
scope_name = scope_def.name
|
|
154
155
|
|
|
155
|
-
graphql_filter
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
Helpers.add_unless_exists(graphql_filter, argument_name) do
|
|
157
|
+
graphql_filter.class_eval do
|
|
158
|
+
argument(
|
|
159
|
+
argument_name,
|
|
160
|
+
argument_type,
|
|
161
|
+
required: false,
|
|
162
|
+
as: scope_name,
|
|
163
|
+
**options
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
def graphql_field_to_sort(name, as, **options)
|
|
@@ -175,16 +177,17 @@ module Graphiform
|
|
|
175
177
|
|
|
176
178
|
local_graphql_sort = graphql_sort
|
|
177
179
|
|
|
178
|
-
local_graphql_sort
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
Helpers.add_unless_exists(local_graphql_sort, name) do
|
|
181
|
+
local_graphql_sort.class_eval do
|
|
182
|
+
argument(
|
|
183
|
+
name,
|
|
184
|
+
type,
|
|
185
|
+
required: false,
|
|
186
|
+
as: as,
|
|
187
|
+
**options
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
188
191
|
end
|
|
189
192
|
|
|
190
193
|
def graphql_field_to_grouping(name, as, **options)
|
|
@@ -198,16 +201,17 @@ module Graphiform
|
|
|
198
201
|
|
|
199
202
|
local_graphql_grouping = graphql_grouping
|
|
200
203
|
|
|
201
|
-
local_graphql_grouping
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
Helpers.add_unless_exists(local_graphql_grouping, name) do
|
|
205
|
+
local_graphql_grouping.class_eval do
|
|
206
|
+
argument(
|
|
207
|
+
name,
|
|
208
|
+
type,
|
|
209
|
+
required: false,
|
|
210
|
+
as: as,
|
|
211
|
+
**options
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
211
215
|
end
|
|
212
216
|
|
|
213
217
|
def graphql_add_field_to_type(
|
|
@@ -239,21 +243,23 @@ module Graphiform
|
|
|
239
243
|
field_options[:null] = null
|
|
240
244
|
end
|
|
241
245
|
|
|
242
|
-
graphql_type
|
|
243
|
-
|
|
246
|
+
Helpers.add_unless_exists(graphql_type, field_name) do
|
|
247
|
+
graphql_type.class_eval do
|
|
248
|
+
added_field = field(field_name, **field_options, **options)
|
|
244
249
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
if read_prepare || read_resolve
|
|
251
|
+
define_method(
|
|
252
|
+
added_field.method_sym,
|
|
253
|
+
lambda do
|
|
254
|
+
value = read_resolve ? instance_exec(object, context, &read_resolve) : object.public_send(added_field.method_sym)
|
|
255
|
+
value = instance_exec(value, context, &read_prepare) if read_prepare
|
|
251
256
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
value
|
|
258
|
+
end
|
|
259
|
+
)
|
|
260
|
+
end
|
|
255
261
|
end
|
|
256
|
-
end
|
|
262
|
+
end
|
|
257
263
|
end
|
|
258
264
|
|
|
259
265
|
def graphql_add_column_field(field_name, column_def, type: nil, null: nil, as: nil, **options)
|
|
@@ -277,7 +283,6 @@ module Graphiform
|
|
|
277
283
|
read_prepare: nil,
|
|
278
284
|
read_resolve: nil,
|
|
279
285
|
skip_dataloader: false,
|
|
280
|
-
case_sensitive: Graphiform.configuration[:case_sensitive],
|
|
281
286
|
**options
|
|
282
287
|
)
|
|
283
288
|
unless association_def.klass.respond_to?(:graphql_type)
|
|
@@ -304,8 +309,7 @@ module Graphiform
|
|
|
304
309
|
read_prepare: read_prepare,
|
|
305
310
|
read_resolve: read_resolve,
|
|
306
311
|
null: false,
|
|
307
|
-
skip_dataloader: true
|
|
308
|
-
case_sensitive: case_sensitive
|
|
312
|
+
skip_dataloader: true
|
|
309
313
|
),
|
|
310
314
|
false,
|
|
311
315
|
**options
|
|
@@ -321,15 +325,13 @@ module Graphiform
|
|
|
321
325
|
read_prepare: read_prepare,
|
|
322
326
|
read_resolve: read_resolve,
|
|
323
327
|
null: false,
|
|
324
|
-
skip_dataloader: skip_dataloader
|
|
325
|
-
case_sensitive: case_sensitive
|
|
328
|
+
skip_dataloader: skip_dataloader
|
|
326
329
|
)
|
|
327
330
|
else
|
|
328
331
|
klass.graphql_create_association_resolver(
|
|
329
332
|
association_def,
|
|
330
333
|
klass.graphql_type,
|
|
331
|
-
skip_dataloader: skip_dataloader
|
|
332
|
-
case_sensitive: case_sensitive
|
|
334
|
+
skip_dataloader: skip_dataloader
|
|
333
335
|
)
|
|
334
336
|
end
|
|
335
337
|
)
|
|
@@ -354,4 +356,4 @@ module Graphiform
|
|
|
354
356
|
end
|
|
355
357
|
end
|
|
356
358
|
end
|
|
357
|
-
end
|
|
359
|
+
end
|
data/lib/graphiform/helpers.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module Graphiform
|
|
4
6
|
module Helpers
|
|
5
7
|
def self.logger
|
|
@@ -9,6 +11,62 @@ module Graphiform
|
|
|
9
11
|
@logger
|
|
10
12
|
end
|
|
11
13
|
|
|
14
|
+
# --- Name normalization & per-class registries -------------------------
|
|
15
|
+
#
|
|
16
|
+
# Replaces the O(n) `arguments.keys.any? { equal_graphql_names?(...) }`
|
|
17
|
+
# scan (and its repeated string allocations) with an O(1) Set lookup.
|
|
18
|
+
|
|
19
|
+
NAME_NORMALIZE_CACHE = {}
|
|
20
|
+
NAME_NORMALIZE_MUTEX = Mutex.new
|
|
21
|
+
|
|
22
|
+
# Canonicalize a name the same way graphql-ruby presents it externally,
|
|
23
|
+
# so `:my_field`, `"my_field"`, `"myField"`, `"MyField"` all collide.
|
|
24
|
+
def self.normalize_graphql_name(name)
|
|
25
|
+
key = name.is_a?(Symbol) ? name : name.to_s
|
|
26
|
+
cached = NAME_NORMALIZE_CACHE[key]
|
|
27
|
+
return cached if cached
|
|
28
|
+
|
|
29
|
+
NAME_NORMALIZE_MUTEX.synchronize do
|
|
30
|
+
NAME_NORMALIZE_CACHE[key] ||= key.to_s.camelize(:lower).freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fetch (and lazily seed) the registered-names Set for a generated
|
|
35
|
+
# GraphQL class. Seeding from existing `arguments` / `fields` makes this
|
|
36
|
+
# safe even when classes are pre-populated (e.g. `OR`/`AND` on filters,
|
|
37
|
+
# or manual user-defined args).
|
|
38
|
+
def self.tracked_names(klass)
|
|
39
|
+
set = klass.instance_variable_get(:@graphiform_names)
|
|
40
|
+
return set if set
|
|
41
|
+
|
|
42
|
+
set = Set.new
|
|
43
|
+
|
|
44
|
+
if klass.respond_to?(:own_arguments)
|
|
45
|
+
own_args = klass.instance_variable_get(:@own_arguments) || {}
|
|
46
|
+
set.merge(own_args.each_key.map { |k| normalize_graphql_name(k) })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if klass.respond_to?(:fields)
|
|
50
|
+
own_fields = klass.instance_variable_get(:@own_fields) || {}
|
|
51
|
+
set.merge(own_fields.each_key.map { |k| normalize_graphql_name(k) })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
klass.instance_variable_set(:@graphiform_names, set)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Guard helper: yield (which should add the field/argument) only if the
|
|
58
|
+
# name isn't already present. Returns true when the block ran.
|
|
59
|
+
def self.add_unless_exists(klass, name)
|
|
60
|
+
normalized = normalize_graphql_name(name)
|
|
61
|
+
set = tracked_names(klass)
|
|
62
|
+
return false if set.include?(normalized)
|
|
63
|
+
|
|
64
|
+
yield
|
|
65
|
+
set << normalized
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
# -----------------------------------------------------------------------
|
|
69
|
+
|
|
12
70
|
def self.graphql_type(active_record_type)
|
|
13
71
|
is_array = active_record_type.is_a? Array
|
|
14
72
|
active_record_type = is_array ? active_record_type[0] : active_record_type
|
|
@@ -28,24 +86,13 @@ module Graphiform
|
|
|
28
86
|
end
|
|
29
87
|
|
|
30
88
|
def self.get_const_or_create(const, mod = Object)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Object.const_get(new_full_const_name)
|
|
34
|
-
rescue NameError => e
|
|
35
|
-
unless full_const_name(e.missing_name) == new_full_const_name.to_s
|
|
36
|
-
logger.warn "Failed to load #{e.missing_name} when loading constant #{new_full_const_name}"
|
|
37
|
-
return Object.const_get(new_full_const_name)
|
|
38
|
-
end
|
|
39
|
-
|
|
89
|
+
return mod.const_get(const) if mod.const_defined?(const, false)
|
|
90
|
+
|
|
40
91
|
val = yield
|
|
41
92
|
mod.const_set(const, val)
|
|
42
93
|
val
|
|
43
94
|
end
|
|
44
95
|
|
|
45
|
-
def self.equal_graphql_names?(key, name)
|
|
46
|
-
key.downcase == name.to_s.camelize.downcase || key.downcase == name.to_s.downcase
|
|
47
|
-
end
|
|
48
|
-
|
|
49
96
|
def self.full_const_name(name)
|
|
50
97
|
name = "Object#{name}" if name.starts_with?('::')
|
|
51
98
|
name = "Object::#{name}" unless name.starts_with?('Object::')
|
|
@@ -54,25 +101,21 @@ module Graphiform
|
|
|
54
101
|
end
|
|
55
102
|
|
|
56
103
|
def self.association_arguments_valid?(association_def, method)
|
|
57
|
-
association_def.present?
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
return false unless association_def.present?
|
|
105
|
+
return false unless association_def.klass.respond_to?(method)
|
|
106
|
+
|
|
107
|
+
target = association_def.klass.send(method)
|
|
108
|
+
return false unless target.respond_to?(:arguments)
|
|
109
|
+
|
|
110
|
+
own_args = target.instance_variable_get(:@own_arguments) || {}
|
|
111
|
+
!own_args.empty?
|
|
61
112
|
end
|
|
62
113
|
|
|
63
|
-
def self.dataloader_support?(dataloader, association_def
|
|
64
|
-
|
|
65
|
-
association_def.present? &&
|
|
114
|
+
def self.dataloader_support?(dataloader, association_def)
|
|
115
|
+
association_def.present? &&
|
|
66
116
|
!association_def.polymorphic? &&
|
|
67
|
-
!association_def.through_reflection? &&
|
|
68
117
|
!association_def.inverse_of&.polymorphic? &&
|
|
69
|
-
(
|
|
70
|
-
!association_def.scope ||
|
|
71
|
-
association_def.scope.arity.zero?
|
|
72
|
-
) &&
|
|
73
|
-
!keys.is_a?(Array) &&
|
|
74
118
|
!dataloader.is_a?(GraphQL::Dataloader::NullDataloader)
|
|
75
|
-
)
|
|
76
119
|
end
|
|
77
120
|
end
|
|
78
|
-
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'graphql'
|
|
3
|
+
require 'graphql/dataloader'
|
|
4
|
+
require 'graphql/dataloader/source'
|
|
5
|
+
require 'graphql/dataloader/active_record_association_source'
|
|
6
|
+
require 'graphiform/scope_composer'
|
|
7
|
+
|
|
8
|
+
module Graphiform
|
|
9
|
+
# Loads ActiveRecord associations through the dataloader, with optional
|
|
10
|
+
# Scopiform-style `where`/`sort`/`includes`/`scope` composition applied to
|
|
11
|
+
# the relation handed to ActiveRecord::Associations::Preloader.
|
|
12
|
+
#
|
|
13
|
+
# Inherits batching semantics (including `scope.to_sql`-based batch keys),
|
|
14
|
+
# `assoc.loaded?` short-circuiting, polymorphic handling, and cross-source
|
|
15
|
+
# cache reuse from GraphQL::Dataloader::ActiveRecordAssociationSource.
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
#
|
|
19
|
+
# dataloader.with(
|
|
20
|
+
# Graphiform::PreloaderSource,
|
|
21
|
+
# :posts,
|
|
22
|
+
# klass: User.reflect_on_association(:posts).klass,
|
|
23
|
+
# scope: association_reflection.scope,
|
|
24
|
+
# where: args[:where],
|
|
25
|
+
# sort: args[:sort],
|
|
26
|
+
# ).load(user_record)
|
|
27
|
+
class PreloaderSource < ::GraphQL::Dataloader::ActiveRecordAssociationSource
|
|
28
|
+
# @param association_name [Symbol] the association on the parent record(s)
|
|
29
|
+
# @param klass [Class, nil] target model class used to build the
|
|
30
|
+
# composed scope. Required when any of `scope`/`where`/`sort`/`includes`
|
|
31
|
+
# is given; may be nil for bare preloads.
|
|
32
|
+
# @param scope [Proc, nil]
|
|
33
|
+
# @param where [Hash, nil]
|
|
34
|
+
# @param sort [Hash, nil]
|
|
35
|
+
# @param includes [Symbol, Array, Hash, nil]
|
|
36
|
+
def initialize(association_name, klass: nil, scope: nil, where: nil, sort: nil, includes: nil)
|
|
37
|
+
effective_scope = build_scope(klass, scope: scope, where: where, sort: sort, includes: includes)
|
|
38
|
+
super(association_name, effective_scope)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Mirror initialize's scope composition so the batch key (which upstream
|
|
42
|
+
# derives from the scope via `scope.to_sql`) is consistent across loads
|
|
43
|
+
# that should batch together.
|
|
44
|
+
def self.batch_key_for(association_name, klass: nil, scope: nil, where: nil, sort: nil, includes: nil)
|
|
45
|
+
effective_scope = build_scope(klass, scope: scope, where: where, sort: sort, includes: includes)
|
|
46
|
+
super(association_name, effective_scope)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.build_scope(klass, scope: nil, where: nil, sort: nil, includes: nil)
|
|
50
|
+
return nil unless klass
|
|
51
|
+
return nil unless ScopeComposer.customized?(scope: scope, where: where, sort: sort, includes: includes)
|
|
52
|
+
|
|
53
|
+
ScopeComposer.compose(klass.all, scope: scope, where: where, sort: sort, includes: includes)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Instance-level helper kept for symmetry / overrideability.
|
|
57
|
+
def build_scope(klass, **kwargs)
|
|
58
|
+
self.class.build_scope(klass, **kwargs)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Graphiform
|
|
4
|
+
# Builds an ActiveRecord relation by applying Graphiform/Scopiform-style
|
|
5
|
+
# customizations on top of a base relation.
|
|
6
|
+
#
|
|
7
|
+
# Used by both PreloaderSource (to compose the `scope:` handed to
|
|
8
|
+
# ActiveRecord::Associations::Preloader) and Core#apply_built_ins (to apply
|
|
9
|
+
# the same chain to top-level resolver relations).
|
|
10
|
+
module ScopeComposer
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# @param relation [ActiveRecord::Relation] starting relation (e.g. `Model.all`)
|
|
14
|
+
# @param scope [Proc, nil] optional scope block, `instance_exec`'d on the relation
|
|
15
|
+
# @param where [Hash, nil] Scopiform filter hash, applied via `apply_filters`
|
|
16
|
+
# @param sort [Hash, nil] Scopiform sort hash, applied via `apply_sorts`
|
|
17
|
+
# @param includes [Symbol, Array, Hash, nil] eager-load spec passed to `includes`
|
|
18
|
+
# @return [ActiveRecord::Relation]
|
|
19
|
+
def compose(relation, scope: nil, where: nil, sort: nil, group: nil, includes: nil)
|
|
20
|
+
relation = relation.merge(relation.instance_exec(&scope)) if scope.respond_to?(:call)
|
|
21
|
+
relation = relation.includes(includes) if includes.present? && relation.respond_to?(:includes)
|
|
22
|
+
relation = relation.apply_filters(where.to_h) if where.present? && relation.respond_to?(:apply_filters)
|
|
23
|
+
relation = relation.apply_sorts(sort.to_h) if sort.present? && relation.respond_to?(:apply_sorts)
|
|
24
|
+
relation = relation.apply_groupings(group.to_h) if group.present? && relation.respond_to?(:apply_groupings)
|
|
25
|
+
relation
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# True when any customization would actually modify the base relation.
|
|
29
|
+
# Used by PreloaderSource to decide whether to hand a scope to upstream
|
|
30
|
+
# (which disables result caching) or pass nil (which preserves it).
|
|
31
|
+
def customized?(scope: nil, where: nil, sort: nil, group: nil, includes: nil)
|
|
32
|
+
scope.respond_to?(:call) || where.present? || sort.present? || group.present? || includes.present?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/graphiform/skeleton.rb
CHANGED
data/lib/graphiform/version.rb
CHANGED
data/lib/graphiform.rb
CHANGED
|
@@ -6,6 +6,8 @@ require 'graphiform/active_record_helpers'
|
|
|
6
6
|
require 'graphiform/core'
|
|
7
7
|
require 'graphiform/fields'
|
|
8
8
|
require 'graphiform/sort_enum'
|
|
9
|
+
require 'graphiform/scope_composer'
|
|
10
|
+
require 'graphiform/preloader_source'
|
|
9
11
|
class GraphiformConfigurationError < StandardError; end
|
|
10
12
|
|
|
11
13
|
module Graphiform
|
metadata
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphiform
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- jayce.pulsipher
|
|
8
|
-
|
|
8
|
+
- zack.denkers
|
|
9
|
+
autorequire:
|
|
9
10
|
bindir: bin
|
|
10
11
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
+
date: 2026-05-19 00:00:00.000000000 Z
|
|
12
13
|
dependencies:
|
|
13
14
|
- !ruby/object:Gem::Dependency
|
|
14
15
|
name: activerecord
|
|
@@ -16,42 +17,54 @@ dependencies:
|
|
|
16
17
|
requirements:
|
|
17
18
|
- - ">="
|
|
18
19
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
20
|
+
version: '7.1'
|
|
21
|
+
- - "<"
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: '8.2'
|
|
20
24
|
type: :runtime
|
|
21
25
|
prerelease: false
|
|
22
26
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
27
|
requirements:
|
|
24
28
|
- - ">="
|
|
25
29
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
30
|
+
version: '7.1'
|
|
31
|
+
- - "<"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '8.2'
|
|
27
34
|
- !ruby/object:Gem::Dependency
|
|
28
35
|
name: graphql
|
|
29
36
|
requirement: !ruby/object:Gem::Requirement
|
|
30
37
|
requirements:
|
|
31
|
-
- - "
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 2.5.4
|
|
41
|
+
- - "<"
|
|
32
42
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
43
|
+
version: '2.7'
|
|
34
44
|
type: :runtime
|
|
35
45
|
prerelease: false
|
|
36
46
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
47
|
requirements:
|
|
38
|
-
- - "
|
|
48
|
+
- - ">="
|
|
39
49
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
50
|
+
version: 2.5.4
|
|
51
|
+
- - "<"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.7'
|
|
41
54
|
- !ruby/object:Gem::Dependency
|
|
42
55
|
name: scopiform
|
|
43
56
|
requirement: !ruby/object:Gem::Requirement
|
|
44
57
|
requirements:
|
|
45
58
|
- - ">="
|
|
46
59
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.
|
|
60
|
+
version: 0.4.1
|
|
48
61
|
type: :runtime
|
|
49
62
|
prerelease: false
|
|
50
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
64
|
requirements:
|
|
52
65
|
- - ">="
|
|
53
66
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.
|
|
67
|
+
version: 0.4.1
|
|
55
68
|
- !ruby/object:Gem::Dependency
|
|
56
69
|
name: appraisal
|
|
57
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +79,34 @@ dependencies:
|
|
|
66
79
|
- - ">="
|
|
67
80
|
- !ruby/object:Gem::Version
|
|
68
81
|
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '5.20'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '5.20'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rake
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
69
110
|
- !ruby/object:Gem::Dependency
|
|
70
111
|
name: spy
|
|
71
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,9 +149,10 @@ dependencies:
|
|
|
108
149
|
- - ">="
|
|
109
150
|
- !ruby/object:Gem::Version
|
|
110
151
|
version: '0'
|
|
111
|
-
description:
|
|
152
|
+
description:
|
|
112
153
|
email:
|
|
113
154
|
- jayce.pulsipher@3-form.com
|
|
155
|
+
- zack.denkers@3-form.com
|
|
114
156
|
executables: []
|
|
115
157
|
extensions: []
|
|
116
158
|
extra_rdoc_files: []
|
|
@@ -120,10 +162,11 @@ files:
|
|
|
120
162
|
- Rakefile
|
|
121
163
|
- lib/graphiform.rb
|
|
122
164
|
- lib/graphiform/active_record_helpers.rb
|
|
123
|
-
- lib/graphiform/association_source.rb
|
|
124
165
|
- lib/graphiform/core.rb
|
|
125
166
|
- lib/graphiform/fields.rb
|
|
126
167
|
- lib/graphiform/helpers.rb
|
|
168
|
+
- lib/graphiform/preloader_source.rb
|
|
169
|
+
- lib/graphiform/scope_composer.rb
|
|
127
170
|
- lib/graphiform/skeleton.rb
|
|
128
171
|
- lib/graphiform/sort_enum.rb
|
|
129
172
|
- lib/graphiform/version.rb
|
|
@@ -132,7 +175,7 @@ homepage: https://github.com/3-form/graphiform
|
|
|
132
175
|
licenses:
|
|
133
176
|
- MIT
|
|
134
177
|
metadata: {}
|
|
135
|
-
post_install_message:
|
|
178
|
+
post_install_message:
|
|
136
179
|
rdoc_options: []
|
|
137
180
|
require_paths:
|
|
138
181
|
- lib
|
|
@@ -140,15 +183,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
140
183
|
requirements:
|
|
141
184
|
- - ">="
|
|
142
185
|
- !ruby/object:Gem::Version
|
|
143
|
-
version: '
|
|
186
|
+
version: '3.1'
|
|
144
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
188
|
requirements:
|
|
146
189
|
- - ">="
|
|
147
190
|
- !ruby/object:Gem::Version
|
|
148
191
|
version: '0'
|
|
149
192
|
requirements: []
|
|
150
|
-
rubygems_version: 3.3.
|
|
151
|
-
signing_key:
|
|
193
|
+
rubygems_version: 3.3.27
|
|
194
|
+
signing_key:
|
|
152
195
|
specification_version: 4
|
|
153
196
|
summary: Generate GraphQL types, inputs, resolvers, queries, and connections based
|
|
154
197
|
off whitelisted column and association definitions
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Graphiform
|
|
4
|
-
class AssociationSource < GraphQL::Dataloader::Source
|
|
5
|
-
def initialize(model, attribute, **options)
|
|
6
|
-
super()
|
|
7
|
-
|
|
8
|
-
@model = model
|
|
9
|
-
@attribute = attribute
|
|
10
|
-
@options = options
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def fetch(values)
|
|
14
|
-
normalized_values = normalize_values(values)
|
|
15
|
-
records = query(normalized_values.uniq).to_a
|
|
16
|
-
results(normalized_values, records)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def query(values)
|
|
20
|
-
query = @model
|
|
21
|
-
query = query.merge(@model.instance_exec(&@options[:scope])) if @options[:scope].present?
|
|
22
|
-
query = query.where(@attribute => values)
|
|
23
|
-
|
|
24
|
-
query = query.includes(@options[:includes]) if @options[:includes].present? && query.respond_to?(:includes)
|
|
25
|
-
query = query.apply_filters(@options[:where].to_h) if @options[:where].present? && query.respond_to?(:apply_filters)
|
|
26
|
-
query = query.apply_sorts(@options[:sort].to_h) if @options[:sort].present? && query.respond_to?(:apply_sorts)
|
|
27
|
-
|
|
28
|
-
query
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def normalize_value(value)
|
|
32
|
-
value = value.downcase if !@options[:case_sensitive] && value.is_a?(String)
|
|
33
|
-
|
|
34
|
-
value
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def normalize_values(values)
|
|
38
|
-
type_for_attribute = @model.type_for_attribute(@attribute) if @model.respond_to?(:type_for_attribute)
|
|
39
|
-
values.map do |value|
|
|
40
|
-
value = type_for_attribute.cast(value) if type_for_attribute.present?
|
|
41
|
-
normalize_value(value)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def results(values, records)
|
|
46
|
-
record_attributes = records.map { |record| normalize_value(record[@attribute]) }
|
|
47
|
-
values.map do |value|
|
|
48
|
-
if @options[:multi]
|
|
49
|
-
indexes = record_attributes.each_index.select { |index| record_attributes[index] == value }
|
|
50
|
-
indexes.map { |index| index && records[index] }
|
|
51
|
-
else
|
|
52
|
-
index = record_attributes.index(value)
|
|
53
|
-
index && records[index]
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|