graphiti 1.2.16 → 1.3.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +96 -0
  3. data/.standard.yml +4 -4
  4. data/Appraisals +23 -17
  5. data/CHANGELOG.md +7 -1
  6. data/Guardfile +5 -5
  7. data/deprecated_generators/graphiti/generator_mixin.rb +1 -0
  8. data/deprecated_generators/graphiti/resource_generator.rb +1 -1
  9. data/gemfiles/{rails_5.gemfile → rails_5_2.gemfile} +2 -2
  10. data/gemfiles/{rails_5_graphiti_rails.gemfile → rails_5_2_graphiti_rails.gemfile} +3 -4
  11. data/gemfiles/rails_6.gemfile +1 -1
  12. data/gemfiles/rails_6_graphiti_rails.gemfile +2 -3
  13. data/gemfiles/{rails_4.gemfile → rails_7.gemfile} +2 -2
  14. data/gemfiles/rails_7_graphiti_rails.gemfile +19 -0
  15. data/graphiti.gemspec +16 -16
  16. data/lib/graphiti/adapters/abstract.rb +20 -5
  17. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +1 -1
  18. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +1 -1
  19. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +1 -1
  20. data/lib/graphiti/adapters/active_record/{inferrence.rb → inference.rb} +2 -2
  21. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +19 -0
  22. data/lib/graphiti/adapters/active_record.rb +119 -74
  23. data/lib/graphiti/adapters/graphiti_api.rb +1 -1
  24. data/lib/graphiti/adapters/null.rb +1 -1
  25. data/lib/graphiti/adapters/persistence/associations.rb +78 -0
  26. data/lib/graphiti/configuration.rb +3 -1
  27. data/lib/graphiti/debugger.rb +12 -8
  28. data/lib/graphiti/delegates/pagination.rb +47 -13
  29. data/lib/graphiti/deserializer.rb +3 -3
  30. data/lib/graphiti/errors.rb +109 -15
  31. data/lib/graphiti/extensions/extra_attribute.rb +4 -4
  32. data/lib/graphiti/extensions/temp_id.rb +1 -1
  33. data/lib/graphiti/filter_operators.rb +0 -1
  34. data/lib/graphiti/hash_renderer.rb +198 -21
  35. data/lib/graphiti/query.rb +105 -73
  36. data/lib/graphiti/railtie.rb +5 -5
  37. data/lib/graphiti/renderer.rb +19 -1
  38. data/lib/graphiti/request_validator.rb +10 -10
  39. data/lib/graphiti/request_validators/update_validator.rb +4 -5
  40. data/lib/graphiti/request_validators/validator.rb +38 -24
  41. data/lib/graphiti/resource/configuration.rb +35 -7
  42. data/lib/graphiti/resource/dsl.rb +34 -8
  43. data/lib/graphiti/resource/interface.rb +13 -3
  44. data/lib/graphiti/resource/links.rb +3 -3
  45. data/lib/graphiti/resource/persistence.rb +2 -1
  46. data/lib/graphiti/resource/polymorphism.rb +8 -2
  47. data/lib/graphiti/resource/remote.rb +2 -2
  48. data/lib/graphiti/resource/sideloading.rb +4 -4
  49. data/lib/graphiti/resource.rb +12 -1
  50. data/lib/graphiti/resource_proxy.rb +23 -3
  51. data/lib/graphiti/runner.rb +5 -5
  52. data/lib/graphiti/schema.rb +36 -11
  53. data/lib/graphiti/schema_diff.rb +44 -4
  54. data/lib/graphiti/scope.rb +8 -10
  55. data/lib/graphiti/scoping/base.rb +3 -3
  56. data/lib/graphiti/scoping/filter.rb +36 -15
  57. data/lib/graphiti/scoping/filter_group_validator.rb +78 -0
  58. data/lib/graphiti/scoping/paginate.rb +47 -3
  59. data/lib/graphiti/scoping/sort.rb +5 -7
  60. data/lib/graphiti/serializer.rb +49 -7
  61. data/lib/graphiti/sideload/belongs_to.rb +1 -1
  62. data/lib/graphiti/sideload/has_many.rb +19 -1
  63. data/lib/graphiti/sideload/many_to_many.rb +11 -4
  64. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +3 -4
  65. data/lib/graphiti/sideload.rb +47 -23
  66. data/lib/graphiti/stats/dsl.rb +0 -1
  67. data/lib/graphiti/stats/payload.rb +12 -9
  68. data/lib/graphiti/types.rb +15 -15
  69. data/lib/graphiti/util/attribute_check.rb +1 -1
  70. data/lib/graphiti/util/class.rb +6 -0
  71. data/lib/graphiti/util/link.rb +10 -2
  72. data/lib/graphiti/util/persistence.rb +21 -78
  73. data/lib/graphiti/util/relationship_payload.rb +4 -4
  74. data/lib/graphiti/util/remote_params.rb +9 -4
  75. data/lib/graphiti/util/remote_serializer.rb +1 -0
  76. data/lib/graphiti/util/serializer_attributes.rb +41 -11
  77. data/lib/graphiti/util/simple_errors.rb +4 -4
  78. data/lib/graphiti/util/transaction_hooks_recorder.rb +1 -1
  79. data/lib/graphiti/version.rb +1 -1
  80. data/lib/graphiti.rb +6 -3
  81. metadata +46 -37
  82. data/.travis.yml +0 -59
@@ -2,6 +2,31 @@ module Graphiti
2
2
  module Errors
3
3
  class Base < StandardError; end
4
4
 
5
+ class UnreadableAttribute < Base
6
+ def initialize(resource_class, name)
7
+ @resource_class = resource_class
8
+ @name = name
9
+ end
10
+
11
+ def message
12
+ "#{@resource_class}: Requested field #{@name}, but not authorized to read it"
13
+ end
14
+ end
15
+
16
+ class NullRelation
17
+ attr_accessor :id, :errors, :pointer
18
+
19
+ def initialize(id, pointer)
20
+ @id = id
21
+ @pointer = pointer
22
+ @errors = Graphiti::Util::SimpleErrors.new(self)
23
+ end
24
+
25
+ def self.human_attribute_name(attr, options = {})
26
+ attr
27
+ end
28
+ end
29
+
5
30
  class AdapterNotImplemented < Base
6
31
  def initialize(adapter, attribute, method)
7
32
  @adapter = adapter
@@ -10,7 +35,7 @@ module Graphiti
10
35
  end
11
36
 
12
37
  def message
13
- <<-MSG
38
+ <<~MSG
14
39
  The adapter #{@adapter.class} does not implement method '#{@method}', which was requested for attribute '#{@attribute}'. Add this method to your adapter to support this filter operator.
15
40
  MSG
16
41
  end
@@ -24,7 +49,7 @@ module Graphiti
24
49
  end
25
50
 
26
51
  def message
27
- <<-MSG
52
+ <<~MSG
28
53
  #{@parent_resource_class} sideload :#{@name} - #{@message}
29
54
  MSG
30
55
  end
@@ -53,7 +78,7 @@ module Graphiti
53
78
  end
54
79
 
55
80
  def message
56
- <<-MSG
81
+ <<~MSG
57
82
  #{@resource_class}: Tried to pass block to .#{@method_name}, which only accepts a method name.
58
83
  MSG
59
84
  end
@@ -65,7 +90,7 @@ module Graphiti
65
90
  end
66
91
 
67
92
  def message
68
- <<-MSG
93
+ <<~MSG
69
94
  #{@resource_class}: Tried to perform write operation. Writes are not supported for remote resources - hit the endpoint directly.
70
95
  MSG
71
96
  end
@@ -80,7 +105,7 @@ module Graphiti
80
105
  end
81
106
 
82
107
  def message
83
- <<-MSG
108
+ <<~MSG
84
109
  #{@resource.class}: Tried to filter #{@filter_name.inspect} on operator #{@operator.inspect}, but not supported! Supported operators are #{@supported}.
85
110
  MSG
86
111
  end
@@ -93,7 +118,7 @@ module Graphiti
93
118
  end
94
119
 
95
120
  def message
96
- <<-MSG
121
+ <<~MSG
97
122
  #{@sideload.parent_resource.class.name}: tried to sideload #{@sideload.name.inspect}, but more than one #{@sideload.parent_resource.model.name} was passed!
98
123
 
99
124
  This is because you marked the sideload #{@sideload.name.inspect} with single: true
@@ -114,7 +139,7 @@ module Graphiti
114
139
  end
115
140
 
116
141
  def message
117
- <<-MSG
142
+ <<~MSG
118
143
  #{@resource.class.name}: tried to sort on attribute #{@attribute.inspect}, but passed #{@direction.inspect} when only #{@allowlist.inspect} is supported.
119
144
  MSG
120
145
  end
@@ -127,7 +152,7 @@ module Graphiti
127
152
  end
128
153
 
129
154
  def message
130
- <<-MSG
155
+ <<~MSG
131
156
  #{@resource_class.name}: called .on_extra_attribute #{@name.inspect}, but extra attribute #{@name.inspect} does not exist!
132
157
  MSG
133
158
  end
@@ -143,8 +168,13 @@ module Graphiti
143
168
  def message
144
169
  allow = @filter.values[0][:allow]
145
170
  deny = @filter.values[0][:deny]
146
- msg = <<-MSG
147
- #{@resource.class.name}: tried to filter on #{@filter.keys[0].inspect}, but passed invalid value #{@value.inspect}.
171
+ value_string = if @value == "(empty)"
172
+ "empty value"
173
+ else
174
+ "value #{@value.inspect}"
175
+ end
176
+ msg = <<~MSG
177
+ #{@resource.class.name}: tried to filter on #{@filter.keys[0].inspect}, but passed invalid #{value_string}.
148
178
  MSG
149
179
  msg << "\nAllowlist: #{allow.inspect}" if allow
150
180
  msg << "\nDenylist: #{deny.inspect}" if deny
@@ -160,7 +190,7 @@ module Graphiti
160
190
  end
161
191
 
162
192
  def message
163
- <<-MSG
193
+ <<~MSG
164
194
  #{@resource_class.name} You declared an attribute or filter of type "#{@enum_type}" without providing a list of permitted values, which is required.
165
195
 
166
196
  When declaring an attribute:
@@ -184,12 +214,12 @@ module Graphiti
184
214
  end
185
215
 
186
216
  def message
187
- <<-MSG
217
+ <<~MSG
188
218
  #{@resource_class.name}: Cannot link to sideload #{@sideload.name.inspect}!
189
219
 
190
220
  Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
191
221
 
192
- If you do not wish to generate a link, pass link: false or set self.relationship_links_by_default = false.
222
+ If you do not wish to generate a link, pass link: false or set self.autolink = false.
193
223
  MSG
194
224
  end
195
225
  end
@@ -316,14 +346,14 @@ module Graphiti
316
346
  sortable: "sort on",
317
347
  filterable: "filter on",
318
348
  readable: "read",
319
- writable: "write",
349
+ writable: "write"
320
350
  }[@flag]
321
351
  else
322
352
  {
323
353
  sortable: "add sort",
324
354
  filterable: "add filter",
325
355
  readable: "read",
326
- writable: "write",
356
+ writable: "write"
327
357
  }[@flag]
328
358
  end
329
359
  end
@@ -361,6 +391,20 @@ module Graphiti
361
391
  end
362
392
  end
363
393
 
394
+ class UndefinedIDLookup < Base
395
+ def initialize(resource_class)
396
+ @resource_class = resource_class
397
+ end
398
+
399
+ def message
400
+ <<~MSG
401
+ Tried to resolve #{@resource_class} with an :id filter, but the filter was nil.
402
+ This can result in unscoping a query, which can cause incorrect values to be
403
+ returned which may or may not bypass standard access controls.
404
+ MSG
405
+ end
406
+ end
407
+
364
408
  class UnknownAttribute < AttributeError
365
409
  def message
366
410
  "#{super}, but could not find an attribute with that name."
@@ -689,6 +733,12 @@ module Graphiti
689
733
  end
690
734
  end
691
735
 
736
+ class UnsupportedBeforeCursor < Base
737
+ def message
738
+ "Passing in page[before] and page[number] is not supported. Please create an issue if you need it!"
739
+ end
740
+ end
741
+
692
742
  class InvalidInclude < Base
693
743
  def initialize(resource, relationship)
694
744
  @resource = resource
@@ -722,6 +772,21 @@ module Graphiti
722
772
  end
723
773
 
724
774
  class RecordNotFound < Base
775
+ def initialize(resource = nil, id = nil, path = nil)
776
+ @resource = resource
777
+ @id = id
778
+ @path = path
779
+ end
780
+
781
+ def message
782
+ if !@resource.nil? && !@id.nil?
783
+ "The referenced resource '#{@resource}' with id '#{@id}' could not be found.".tap do |msg|
784
+ msg << " Referenced at '#{@path}'" unless @path.nil?
785
+ end
786
+ else
787
+ "Specified Record Not Found"
788
+ end
789
+ end
725
790
  end
726
791
 
727
792
  class RequiredFilter < Base
@@ -757,5 +822,34 @@ module Graphiti
757
822
 
758
823
  class ConflictRequest < InvalidRequest
759
824
  end
825
+
826
+ class FilterGroupInvalidRequirement < Base
827
+ def initialize(resource, valid_required_values)
828
+ @resource = resource
829
+ @valid_required_values = valid_required_values
830
+ end
831
+
832
+ def message
833
+ <<-MSG.gsub(/\s+/, " ").strip
834
+ The filter group required: value on resource #{@resource.class} must be one of the following:
835
+ #{@valid_required_values.join(", ")}
836
+ MSG
837
+ end
838
+ end
839
+
840
+ class FilterGroupMissingRequiredFilters < Base
841
+ def initialize(resource, filter_names, required)
842
+ @resource = resource
843
+ @filter_names = filter_names
844
+ @required_label = required == :all ? "All" : "One"
845
+ end
846
+
847
+ def message
848
+ <<-MSG.gsub(/\s+/, " ").strip
849
+ #{@required_label} of the following filters must be provided on resource #{@resource.type}:
850
+ #{@filter_names.join(", ")}
851
+ MSG
852
+ end
853
+ end
760
854
  end
761
855
  end
@@ -43,12 +43,12 @@ module Graphiti
43
43
  def extra_attribute(name, options = {}, &blk)
44
44
  allow_field = proc {
45
45
  if options[:if]
46
- next false unless instance_eval(&options[:if])
46
+ next false unless instance_exec(&options[:if])
47
47
  end
48
48
 
49
- @extra_fields &&
50
- @extra_fields[@_type] &&
51
- @extra_fields[@_type].include?(name)
49
+ next false unless @extra_fields
50
+
51
+ @extra_fields[@_type]&.include?(name) || @extra_fields[@resource&.type]&.include?(name)
52
52
  }
53
53
 
54
54
  attribute name, if: allow_field, &blk
@@ -13,7 +13,7 @@ module Graphiti
13
13
  # Common interface for jsonapi-rb extensions
14
14
  def as_jsonapi(*)
15
15
  super.tap do |hash|
16
- if (temp_id = @object.instance_variable_get(:'@_jsonapi_temp_id'))
16
+ if (temp_id = @object.instance_variable_get(:@_jsonapi_temp_id))
17
17
  hash[:'temp-id'] = temp_id
18
18
  end
19
19
  end
@@ -17,7 +17,6 @@ module Graphiti
17
17
  end
18
18
  end
19
19
 
20
- # rubocop: disable Style/MethodMissingSuper
21
20
  def method_missing(name, *args, &blk)
22
21
  @procs[name] = blk
23
22
  end
@@ -1,55 +1,232 @@
1
1
  module Graphiti
2
2
  module SerializableHash
3
- def to_hash(fields: nil, include: {})
3
+ def to_hash(fields: nil, include: {}, name_chain: [], graphql: false)
4
4
  {}.tap do |hash|
5
- fields_list = fields[jsonapi_type] if fields
5
+ if fields
6
+ fields_list = nil
7
+
8
+ # Dot syntax wins over jsonapi type
9
+ if name_chain.length > 0
10
+ fields_list = fields[name_chain.join(".").to_sym]
11
+ end
12
+
13
+ if fields_list.nil?
14
+ fields_list = fields[jsonapi_type]
15
+ end
16
+ end
17
+
18
+ # polymorphic resources - merge the PARENT type
19
+ if polymorphic_subclass?
20
+ if fields[@resource.type]
21
+ fields_list ||= []
22
+ fields_list |= fields[@resource.type]
23
+ end
24
+
25
+ if fields[jsonapi_type]
26
+ fields_list ||= []
27
+ fields_list |= fields[jsonapi_type]
28
+ end
29
+ end
30
+
6
31
  attrs = requested_attributes(fields_list).each_with_object({}) { |(k, v), h|
7
- h[k] = instance_eval(&v)
32
+ name = graphql ? k.to_s.camelize(:lower).to_sym : k
33
+ h[name] = instance_eval(&v)
34
+ }
35
+
36
+ # The main logic here is just !!include[k]
37
+ # But we also have special on__<type>--<name> includes
38
+ # Where we only include when matching the polymorphic type
39
+ rels = @_relationships.select { |k, v|
40
+ if include[k]
41
+ true
42
+ else
43
+ included = false
44
+ include.keys.each do |key|
45
+ split = key.to_s.split(/^on__/)
46
+ if split.length > 1
47
+ requested_type, key = split[1].split("--")
48
+ if requested_type.to_sym == jsonapi_type
49
+ included = k == key.to_sym
50
+ break
51
+ end
52
+ end
53
+ end
54
+ included
55
+ end
8
56
  }
9
- rels = @_relationships.select { |k, v| !!include[k] }
57
+
10
58
  rels.each_with_object({}) do |(k, v), h|
59
+ nested_include = include[k]
60
+
61
+ # This logic only fires if it's a special on__<type>--<name> include
62
+ unless include.has_key?(k)
63
+ include.keys.each do |include_key|
64
+ if k == include_key.to_s.split("--")[1].to_sym
65
+ nested_include = include[include_key]
66
+ break
67
+ end
68
+ end
69
+ end
70
+
11
71
  serializers = v.send(:resources)
12
- attrs[k] = if serializers.is_a?(Array)
13
- serializers.map do |rr| # use private method to avoid array casting
14
- rr.to_hash(fields: fields, include: include[k])
72
+ name = graphql ? k.to_s.camelize(:lower) : k
73
+ name_chain = name_chain.dup
74
+ name_chain << k unless name_chain.last == k
75
+
76
+ unless remote_resource? && serializers.nil?
77
+ payload = if serializers.is_a?(Array)
78
+ data = serializers.map { |rr|
79
+ rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
80
+ }
81
+ graphql ? {nodes: data} : data
82
+ elsif serializers.nil?
83
+ if @resource.class.respond_to?(:sideload)
84
+ if @resource.class.sideload(k).type.to_s.include?("_many")
85
+ graphql ? {nodes: []} : []
86
+ end
87
+ end
88
+ else
89
+ serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
15
90
  end
16
- elsif serializers.nil?
17
- nil
18
- else
19
- serializers.to_hash(fields: fields, include: include[k])
91
+
92
+ attrs[name.to_sym] = payload
20
93
  end
21
94
  end
22
95
 
23
- hash[:id] = jsonapi_id
96
+ if !graphql || (fields_list || []).include?(:id)
97
+ hash[:id] = jsonapi_id
98
+ end
99
+
100
+ if (fields_list || []).include?(:_type)
101
+ hash[:_type] = jsonapi_type.to_s
102
+ end
103
+
104
+ if (fields_list || []).include?(:_cursor)
105
+ hash[:_cursor] = cursor
106
+ end
107
+
108
+ if (fields_list || []).include?(:__typename)
109
+ resource_class = @resource.class
110
+ if polymorphic_subclass?
111
+ resource_class = @resource.class.resource_for_type(jsonapi_type)
112
+ end
113
+ hash[:__typename] = ::Graphiti::Util::Class
114
+ .graphql_type_name(resource_class.name)
115
+ end
116
+
24
117
  hash.merge!(attrs) if attrs.any?
25
118
  end
26
119
  end
120
+
121
+ def polymorphic_subclass?
122
+ !remote_resource? &&
123
+ @resource.polymorphic? &&
124
+ @resource.type != jsonapi_type
125
+ end
126
+
127
+ # See hack in util/remote_serializer.rb
128
+ def remote_resource?
129
+ @resource == 1
130
+ end
27
131
  end
28
132
 
29
133
  class HashRenderer
30
- def initialize(resource)
134
+ def initialize(resource, graphql: false)
31
135
  @resource = resource
136
+ @graphql = graphql
32
137
  end
33
138
 
34
139
  def render(options)
35
140
  serializers = options[:data]
36
141
  opts = options.slice(:fields, :include)
37
- to_hash(serializers, opts).tap do |hash|
38
- hash.merge!(options.slice(:meta)) unless options[:meta].empty?
142
+ opts[:graphql] = @graphql
143
+ top_level_key = get_top_level_key(@resource, serializers.is_a?(Array))
144
+
145
+ hash = {top_level_key => {}}
146
+ nodes = get_nodes(serializers, opts)
147
+ add_nodes(hash, top_level_key, options, nodes, @graphql)
148
+ add_stats(hash, top_level_key, options, @graphql)
149
+ if @graphql
150
+ add_page_info(hash, serializers, top_level_key, options)
39
151
  end
152
+
153
+ hash
40
154
  end
41
155
 
42
156
  private
43
157
 
44
- def to_hash(serializers, opts)
45
- {}.tap do |hash|
46
- hash[:data] = if serializers.is_a?(Array)
47
- serializers.map do |s|
48
- s.to_hash(opts)
158
+ def get_top_level_key(resource, is_many)
159
+ key = :data
160
+
161
+ if @graphql
162
+ key = @resource.graphql_entrypoint
163
+ key = key.to_s.singularize.to_sym unless is_many
164
+ end
165
+
166
+ key
167
+ end
168
+
169
+ def get_nodes(serializers, opts)
170
+ if serializers.is_a?(Array)
171
+ serializers.each_with_index.map do |s, index|
172
+ s.to_hash(**opts)
173
+ end
174
+ else
175
+ serializers.to_hash(**opts)
176
+ end
177
+ end
178
+
179
+ def add_nodes(hash, top_level_key, opts, nodes, graphql)
180
+ payload = nodes
181
+ if graphql && nodes.is_a?(Array)
182
+ payload = {nodes: nodes}
183
+ end
184
+
185
+ # Don't render nodes if we only requested stats
186
+ unless graphql && opts[:fields].values == [[:stats]]
187
+ hash[top_level_key] = payload
188
+ end
189
+ end
190
+
191
+ def add_stats(hash, top_level_key, options, graphql)
192
+ if options[:meta] && !options[:meta].empty?
193
+ if @graphql
194
+ if (stats = options[:meta][:stats])
195
+ camelized = {}
196
+ stats.each_pair do |key, value|
197
+ camelized[key.to_s.camelize(:lower).to_sym] = value
198
+ end
199
+ hash[top_level_key][:stats] = camelized
49
200
  end
50
201
  else
51
- serializers.to_hash(opts)
202
+ hash.merge!(options.slice(:meta))
203
+ end
204
+ end
205
+ end
206
+
207
+ # NB - this is only for top-level right now
208
+ # The casing here is GQL-specific, we can update later if needed.
209
+ def add_page_info(hash, serializers, top_level_key, options)
210
+ if (fields = options[:fields].try(:[], :page_info))
211
+ info = {}
212
+
213
+ if fields.include?(:has_next_page)
214
+ info[:hasNextPage] = options[:proxy].pagination.has_next_page?
215
+ end
216
+
217
+ if fields.include?(:has_previous_page)
218
+ info[:hasPreviousPage] = options[:proxy].pagination.has_previous_page?
219
+ end
220
+
221
+ if fields.include?(:start_cursor)
222
+ info[:startCursor] = serializers.first.try(:cursor)
223
+ end
224
+
225
+ if fields.include?(:end_cursor)
226
+ info[:endCursor] = serializers.last.try(:cursor)
52
227
  end
228
+
229
+ hash[top_level_key][:pageInfo] = info
53
230
  end
54
231
  end
55
232
  end