safrano 0.4.2 → 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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core_ext/Dir/iter.rb +18 -0
  3. data/lib/core_ext/Hash/transform.rb +21 -0
  4. data/lib/core_ext/Integer/edm.rb +13 -0
  5. data/lib/core_ext/REXML/Document/output.rb +16 -0
  6. data/lib/core_ext/String/convert.rb +25 -0
  7. data/lib/core_ext/String/edm.rb +13 -0
  8. data/lib/core_ext/dir.rb +3 -0
  9. data/lib/core_ext/hash.rb +3 -0
  10. data/lib/core_ext/integer.rb +3 -0
  11. data/lib/core_ext/rexml.rb +3 -0
  12. data/lib/core_ext/string.rb +5 -0
  13. data/lib/odata/attribute.rb +15 -10
  14. data/lib/odata/batch.rb +9 -7
  15. data/lib/odata/collection.rb +140 -591
  16. data/lib/odata/collection_filter.rb +18 -42
  17. data/lib/odata/collection_media.rb +111 -54
  18. data/lib/odata/collection_order.rb +5 -2
  19. data/lib/odata/common_logger.rb +2 -0
  20. data/lib/odata/complex_type.rb +152 -0
  21. data/lib/odata/edm/primitive_types.rb +184 -0
  22. data/lib/odata/entity.rb +123 -172
  23. data/lib/odata/error.rb +183 -32
  24. data/lib/odata/expand.rb +20 -17
  25. data/lib/odata/filter/base.rb +74 -0
  26. data/lib/odata/filter/error.rb +49 -6
  27. data/lib/odata/filter/parse.rb +41 -25
  28. data/lib/odata/filter/sequel.rb +133 -62
  29. data/lib/odata/filter/sequel_function_adapter.rb +148 -0
  30. data/lib/odata/filter/token.rb +26 -19
  31. data/lib/odata/filter/tree.rb +106 -52
  32. data/lib/odata/function_import.rb +168 -0
  33. data/lib/odata/model_ext.rb +639 -0
  34. data/lib/odata/navigation_attribute.rb +13 -26
  35. data/lib/odata/relations.rb +5 -5
  36. data/lib/odata/select.rb +17 -5
  37. data/lib/odata/transition.rb +71 -0
  38. data/lib/odata/url_parameters.rb +100 -24
  39. data/lib/odata/walker.rb +20 -10
  40. data/lib/safrano.rb +18 -38
  41. data/lib/safrano/contract.rb +143 -0
  42. data/lib/safrano/core.rb +23 -107
  43. data/lib/safrano/core_ext.rb +13 -0
  44. data/lib/safrano/deprecation.rb +73 -0
  45. data/lib/safrano/multipart.rb +29 -33
  46. data/lib/safrano/rack_app.rb +66 -65
  47. data/lib/safrano/{odata_rack_builder.rb → rack_builder.rb} +18 -2
  48. data/lib/safrano/request.rb +96 -45
  49. data/lib/safrano/response.rb +4 -2
  50. data/lib/safrano/sequel_join_by_paths.rb +2 -2
  51. data/lib/safrano/service.rb +240 -130
  52. data/lib/safrano/version.rb +3 -1
  53. data/lib/sequel/plugins/join_by_paths.rb +6 -19
  54. metadata +32 -11
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Safrano
6
+ # Type mapping DB --> Edm
7
+ # TypeMap = {"INTEGER" => "Edm.Int32" , "TEXT" => "Edm.String",
8
+ # "STRING" => "Edm.String"}
9
+ # Todo: complete mapping... this is just for the most common ones
10
+
11
+ # TODO: use Sequel GENERIC_TYPES: -->
12
+ # Constants
13
+ # GENERIC_TYPES = %w'String Integer Float Numeric BigDecimal Date DateTime
14
+ # Time File TrueClass FalseClass'.freeze
15
+ # Classes specifying generic types that Sequel will convert to
16
+ # database-specific types.
17
+ DB_TYPE_STRING_RGX = /\ACHAR\s*\(\d+\)\z/.freeze
18
+
19
+ # used in $metadata
20
+ # cf. Sequel Database column_schema_default_to_ruby_value
21
+ # schema_column_type
22
+ # https://www.odata.org/documentation/odata-version-2-0/overview/
23
+ def self.default_edm_type(ruby_type:)
24
+ case ruby_type
25
+ when :integer
26
+ 'Edm.Int32'
27
+ when :string
28
+ 'Edm.String'
29
+ when :date, :datetime,
30
+ 'Edm.DateTime'
31
+ when :time
32
+ 'Edm.Time'
33
+ when :boolean
34
+ 'Edm.Boolean'
35
+ when :float
36
+ 'Edm.Double'
37
+ when :decimal
38
+ 'Edm.Decimal'
39
+ when :blob
40
+ 'Edm.Binary'
41
+ end
42
+ end
43
+
44
+ # use Edm twice so that we can do include Safrano::Edm and then
45
+ # have Edm::Int32 etc... availabe
46
+ # and we can have Edm::String different from ::String
47
+ module Edm
48
+ module Edm
49
+ module OutputClassMethods
50
+ def type_name
51
+ "Edm.#{name.split('::').last}"
52
+ end
53
+
54
+ def odata_collection(array)
55
+ array
56
+ end
57
+
58
+ def odata_value(instance)
59
+ instance
60
+ end
61
+ end
62
+
63
+ class Null < NilClass
64
+ extend OutputClassMethods
65
+ # nil --> null convertion is done by to_json
66
+ def self.odata_value(instance)
67
+ nil
68
+ end
69
+
70
+ def self.convert_from_urlparam(v)
71
+ return Contract::NOK unless (v == 'null')
72
+
73
+ Contract.valid(nil)
74
+ end
75
+ end
76
+
77
+ # Binary is a String with the BINARY encoding
78
+ class Binary < String
79
+ extend OutputClassMethods
80
+
81
+ def self.convert_from_urlparam(v)
82
+ Contract.valid(v.dup.force_encoding('BINARY'))
83
+ end
84
+ end
85
+
86
+ # an object alwys evaluates to
87
+ # true ([true, anything not false & not nil objs])
88
+ # or false([nil, false])
89
+ class Boolean < Object
90
+ extend OutputClassMethods
91
+ def Boolean.odata_value(instance)
92
+ instance ? true : false
93
+ end
94
+
95
+ def self.odata_collection(array)
96
+ array.map { |v| odata_value(v) }
97
+ end
98
+
99
+ def self.convert_from_urlparam(v)
100
+ return Contract::NOK unless ['true', 'false'].include?(v)
101
+
102
+ Contract.valid(v == 'true')
103
+ end
104
+ end
105
+
106
+ # Bytes are usualy represented as Intger in ruby,
107
+ # eg.String.bytes --> Array of ints
108
+ class Byte < Integer
109
+ extend OutputClassMethods
110
+
111
+ def self.convert_from_urlparam(v)
112
+ return Contract::NOK unless ((bytev = v.to_i) < 256)
113
+
114
+ Contract.valid(bytev)
115
+ end
116
+ end
117
+
118
+ class DateTime < ::DateTime
119
+ extend OutputClassMethods
120
+ def DateTime.odata_value(instance)
121
+ instance.to_datetime
122
+ end
123
+
124
+ def self.odata_collection(array)
125
+ array.map { |v| odata_value(v) }
126
+ end
127
+
128
+ def self.convert_from_urlparam(v)
129
+ begin
130
+ Contract.valid(DateTime.parse(v))
131
+ rescue
132
+ return convertion_error(v)
133
+ end
134
+ end
135
+ end
136
+
137
+ class String < ::String
138
+ extend OutputClassMethods
139
+
140
+ def self.convert_from_urlparam(v)
141
+ Contract.valid(v)
142
+ end
143
+ end
144
+
145
+ class Int32 < Integer
146
+ extend OutputClassMethods
147
+
148
+ def self.convert_from_urlparam(v)
149
+ return Contract::NOK unless (ret = number_or_nil(v))
150
+
151
+ Contract.valid(ret)
152
+ end
153
+ end
154
+
155
+ class Int64 < Integer
156
+ extend OutputClassMethods
157
+
158
+ def self.convert_from_urlparam(v)
159
+ return Contract::NOK unless (ret = number_or_nil(v))
160
+
161
+ Contract.valid(ret)
162
+ end
163
+ end
164
+
165
+ class Double < Float
166
+ extend OutputClassMethods
167
+
168
+ def self.convert_from_urlparam(v)
169
+ begin
170
+ Contract.valid(v.to_f)
171
+ rescue
172
+ return Contract::NOK
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ # include Safrano
181
+
182
+ # x = Edm::String.new('xxx')
183
+
184
+ # pp x
data/lib/odata/entity.rb CHANGED
@@ -1,44 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'rexml/document'
3
5
  require 'safrano.rb'
4
- require 'odata/collection.rb' # required for self.class.entity_type_name ??
6
+ require 'odata/model_ext.rb' # required for self.class.entity_type_name ??
5
7
  require_relative 'navigation_attribute'
6
8
 
7
- module OData
9
+ module Safrano
8
10
  # this will be mixed in the Model classes (subclasses of Sequel Model)
9
11
  module EntityBase
10
12
  attr_reader :params
11
- attr_reader :uribase
12
13
 
13
- include EntityBase::NavigationInfo
14
+ include Safrano::NavigationInfo
14
15
 
15
16
  # methods related to transitions to next state (cf. walker)
16
17
  module Transitions
17
18
  def allowed_transitions
18
- alltr = [
19
- Safrano::TransitionEnd,
20
- Safrano::TransitionCount,
21
- Safrano::TransitionLinks,
22
- Safrano::TransitionValue,
23
- Safrano::Transition.new(self.class.transition_attribute_regexp,
24
- trans: 'transition_attribute')
25
- ]
26
- if (ncurgx = self.class.nav_collection_url_regexp)
27
- alltr <<
28
- Safrano::Transition.new(%r{\A/(#{ncurgx})(.*)\z},
29
- trans: 'transition_nav_collection')
30
-
31
- end
32
- if (neurgx = self.class.nav_entity_url_regexp)
33
- alltr <<
34
- Safrano::Transition.new(%r{\A/(#{neurgx})(.*)\z},
35
- trans: 'transition_nav_entity')
36
- end
37
- alltr
19
+ self.class.entity_allowed_transitions
38
20
  end
39
21
 
40
22
  def transition_end(_match_result)
41
- [nil, :end]
23
+ Safrano::Transition::RESULT_END
42
24
  end
43
25
 
44
26
  def transition_count(_match_result)
@@ -56,8 +38,7 @@ module OData
56
38
 
57
39
  def transition_attribute(match_result)
58
40
  attrib = match_result[1]
59
- # [values[attrib.to_sym], :run]
60
- [OData::Attribute.new(self, attrib), :run]
41
+ [Safrano::Attribute.new(self, attrib), :run]
61
42
  end
62
43
 
63
44
  def transition_nav_collection(match_result)
@@ -69,10 +50,20 @@ module OData
69
50
  attrib = match_result[1]
70
51
  [get_related_entity(attrib), :run]
71
52
  end
53
+
54
+ def transition_invalid_attribute(match_result)
55
+ invalid_attrib = match_result[1]
56
+ [nil, :error, Safrano::ErrorNotFoundSegment.new(invalid_attrib)]
57
+ end
72
58
  end
73
59
 
74
60
  include Transitions
75
61
 
62
+ # for testing only?
63
+ def ==(other)
64
+ ((self.class.type_name == other.class.type_name) and (@values == other.values))
65
+ end
66
+
76
67
  def nav_values
77
68
  @nav_values = {}
78
69
 
@@ -90,26 +81,27 @@ module OData
90
81
  @nav_coll
91
82
  end
92
83
 
93
- def uri(uriba)
94
- "#{uriba}/#{self.class.entity_set_name}(#{pk_uri})"
84
+ def uri
85
+ @odata_pk ||= "(#{pk_uri})"
86
+ "#{self.class.uri}#{@odata_pk}"
95
87
  end
88
+
96
89
  D = 'd'.freeze
97
90
  DJ_OPEN = '{"d":'.freeze
98
91
  DJ_CLOSE = '}'.freeze
99
92
 
100
93
  # Json formatter for a single entity (probably OData V1/V2 like)
101
- def to_odata_json(service:)
102
- template = self.class.output_template(@uparms)
103
- innerj = service.get_entity_odata_h(entity: self,
104
- template: template,
105
- uribase: @uribase).to_json
94
+ def to_odata_json(request:)
95
+ template = self.class.output_template(expand_list: @uparms.expand.template,
96
+ select: @uparms.select)
97
+ innerj = request.service.get_entity_odata_h(entity: self,
98
+ template: template).to_json
106
99
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
107
100
  end
108
101
 
109
102
  # Json formatter for a single entity reached by navigation $links
110
103
  def to_odata_onelink_json(service:)
111
- innerj = service.get_entity_odata_link_h(entity: self,
112
- uribase: @uribase).to_json
104
+ innerj = service.get_entity_odata_link_h(entity: self).to_json
113
105
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
114
106
  end
115
107
 
@@ -120,36 +112,11 @@ module OData
120
112
  selvals
121
113
  end
122
114
 
123
- # needed for proper datetime output
124
- # TODO: design/performance
125
- def casted_values(cols = nil)
126
- vals = case cols
127
- when nil
128
- values_for_odata
129
- else
130
- selected_values_for_odata(cols)
131
- end
132
-
133
- # WARNING; this code is duplicated in attribute.rb
134
- # (and the inverted transformation is in test/client.rb)
135
- # will require a more systematic solution some day
136
-
137
- vals.transform_values! do |v|
138
- case v
139
- when Time
140
- # try to get back the database time zone and value
141
- (v + v.gmt_offset).utc.to_datetime
142
- else
143
- v
144
- end
145
- end
146
- end
147
-
148
- # post paylod expects the new entity in an array
149
- def to_odata_post_json(service:)
150
- innerj = service.get_coll_odata_h(array: [self],
151
- template: self.class.default_template,
152
- uribase: @uribase).to_json
115
+ # some clients wrongly expect post payload with the new entity in an array
116
+ # TODO quirks array mode !
117
+ def to_odata_array_json(request:)
118
+ innerj = request.service.get_coll_odata_h(array: [self],
119
+ template: self.class.default_template).to_json
153
120
  "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
154
121
  end
155
122
 
@@ -159,29 +126,34 @@ module OData
159
126
 
160
127
  def copy_request_infos(req)
161
128
  @params = req.params
162
- @uribase = req.uribase
163
129
  @do_links = req.walker.do_links
164
- @uparms = UrlParameters4Single.new(@params)
130
+ @uparms = UrlParameters4Single.new(self, @params)
165
131
  end
166
132
 
167
- # Finally Process REST verbs...
168
- def odata_get(req)
169
- copy_request_infos(req)
133
+ def odata_get_output(req)
170
134
  if req.walker.media_value
171
135
  odata_media_value_get(req)
172
136
  elsif req.accept?(APPJSON)
137
+ # json is default content type so we dont need to specify it here again
173
138
  if req.walker.do_links
174
- [200, CT_JSON, [to_odata_onelink_json(service: req.service)]]
139
+ [200, EMPTY_HASH, [to_odata_onelink_json(service: req.service)]]
175
140
  else
176
- [200, CT_JSON, [to_odata_json(service: req.service)]]
141
+ [200, EMPTY_HASH, [to_odata_json(request: req)]]
177
142
  end
178
143
  else # TODO: other formats
179
144
  415
180
145
  end
181
146
  end
182
147
 
148
+ # Finally Process REST verbs...
149
+ def odata_get(req)
150
+ copy_request_infos(req)
151
+ @uparms.check_all.tap_valid { return odata_get_output(req) }
152
+ .tap_error { |e| return e.odata_get(req) }
153
+ end
154
+
183
155
  DELETE_REL_AND_ENTY = lambda do |entity, assoc, parent|
184
- OData.remove_nav_relation(assoc, parent)
156
+ Safrano.remove_nav_relation(assoc, parent)
185
157
  entity.destroy(transaction: false)
186
158
  end
187
159
 
@@ -227,7 +199,6 @@ module OData
227
199
  odata_media_value_put(req)
228
200
  elsif req.accept?(APPJSON)
229
201
  data = JSON.parse(req.body.read)
230
- @uribase = req.uribase
231
202
  data.delete('__metadata')
232
203
 
233
204
  if req.in_changeset
@@ -249,14 +220,12 @@ module OData
249
220
 
250
221
  # validate payload column names
251
222
  if (invalid = self.class.invalid_hash_data?(data))
252
- ::OData::Request::ON_CGST_ERROR.call(req)
223
+ ::Safrano::Request::ON_CGST_ERROR.call(req)
253
224
  return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
254
225
  end
255
226
  # TODO: check values/types
256
227
 
257
228
  my_data_fields = self.class.data_fields
258
- @uribase = req.uribase
259
- # if req.accept?('application/json')
260
229
 
261
230
  if req.in_changeset
262
231
  set_fields(data, my_data_fields, missing: :skip)
@@ -269,73 +238,11 @@ module OData
269
238
  end
270
239
  end
271
240
 
272
- # redefinitions of the main methods for a navigated collection
273
- # (eg. all Books of Author[2] is Author[2].Books.all )
274
- module NavigationRedefinitions
275
- def all
276
- @child_method.call
277
- end
278
-
279
- def count
280
- @child_method.call.count
281
- end
282
-
283
- def dataset
284
- @child_dataset_method.call
285
- end
286
-
287
- def navigated_dataset
288
- @child_dataset_method.call
289
- end
290
-
291
- def each
292
- y = @child_method.call
293
- y.each { |enty| yield enty }
294
- end
295
-
296
- # TODO: design... this is not DRY
297
- def slug_field
298
- superclass.slug_field
299
- end
300
-
301
- def type_name
302
- superclass.type_name
303
- end
304
-
305
- def media_handler
306
- superclass.media_handler
307
- end
308
-
309
- def default_template
310
- superclass.default_template
311
- end
312
-
313
- def to_a
314
- y = @child_method.call
315
- y.to_a
316
- end
317
- end
318
- # GetRelated that returns a anonymous Class (ie. representing a collection)
319
- # subtype of the related object Class ( childklass )
241
+ # GetRelated that returns a collection object representing
242
+ # wrapping the related object Class ( childklass )
320
243
  # (...to_many relationship )
321
244
  def get_related(childattrib)
322
- parent = self
323
- childklass = self.class.nav_collection_attribs[childattrib]
324
- Class.new(childklass) do
325
- # this makes use of Sequel's Model relationships; eg this is
326
- # 'Race[12].Edition'
327
- # where Race[12] would be our self and 'Edition' is the
328
- # childattrib(collection)
329
- @child_method = parent.method(childattrib.to_sym)
330
- @child_dataset_method = parent.method("#{childattrib}_dataset".to_sym)
331
- @nav_parent = parent
332
- @navattr_reflection = parent.class.association_reflections[childattrib.to_sym]
333
- prepare_pk
334
- prepare_fields
335
- # Now in this anonymous Class we can refine the "all, count and []
336
- # methods, to take into account the relationship
337
- extend NavigationRedefinitions
338
- end
245
+ Safrano::OData::NavigatedCollection.new(childattrib, self)
339
246
  end
340
247
 
341
248
  # GetRelatedEntity that returns an single related Entity
@@ -350,27 +257,28 @@ module OData
350
257
  # then we return a Nil... wrapper object. This object then
351
258
  # allows to receive a POST operation that would actually create the nav attribute entity
352
259
 
353
- ret = method(childattrib.to_sym).call || OData::NilNavigationAttribute.new
260
+ ret = method(childattrib.to_sym).call || Safrano::NilNavigationAttribute.new
354
261
 
355
262
  ret.set_relation_info(self, childattrib)
356
263
 
357
264
  ret
358
265
  end
359
266
  end
360
- # end of module ODataEntity
267
+
268
+ # end of module SafranoEntity
361
269
  module Entity
362
270
  include EntityBase
363
271
  end
364
272
 
365
273
  module NonMediaEntity
366
274
  # non media entity metadata for json h
367
- def metadata_h(uribase:)
368
- { uri: uri(uribase),
275
+ def metadata_h
276
+ { uri: uri,
369
277
  type: type_name }
370
278
  end
371
279
 
372
280
  def values_for_odata
373
- values.dup
281
+ values
374
282
  end
375
283
 
376
284
  def odata_delete(req)
@@ -378,7 +286,7 @@ module OData
378
286
  # delete
379
287
  begin
380
288
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
381
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
289
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
382
290
  rescue SequelAdapterError => e
383
291
  BadRequestSequelAdapterError.new(e).odata_get(req)
384
292
  end
@@ -398,35 +306,55 @@ module OData
398
306
  end
399
307
  end
400
308
 
309
+ module MappingBeforeOutput
310
+ # needed for proper datetime output
311
+ def casted_values(cols = nil)
312
+ vals = case cols
313
+ when nil
314
+ # we need to dup the model values as we need to change it before passing to_json,
315
+ # but we dont want to interfere with Sequel's owned data
316
+ # (eg because then in worst case it could happen that we write back changed values to DB)
317
+ values_for_odata.dup
318
+ else
319
+ selected_values_for_odata(cols)
320
+ end
321
+ self.class.time_cols.each { |tc| vals[tc] = vals[tc]&.iso8601 if vals.key?(tc) }
322
+ vals
323
+ end
324
+ end
325
+ module NoMappingBeforeOutput
326
+ # current model does not have eg. Time fields--> no special mapping, just to_json is fine
327
+ # --> we can use directly the model.values (values_for_odata) withoud dup'ing it as we dont
328
+ # need to change it, just output as is
329
+ def casted_values(cols = nil)
330
+ case cols
331
+ when nil
332
+ values_for_odata
333
+ else
334
+ selected_values_for_odata(cols)
335
+ end
336
+ end
337
+ end
338
+
401
339
  module MediaEntity
402
340
  # media entity metadata for json h
403
- def metadata_h(uribase:)
404
- { uri: uri(uribase),
341
+ def metadata_h
342
+ { uri: uri,
405
343
  type: type_name,
406
- media_src: media_src(uribase),
407
- edit_media: edit_media(uribase),
344
+ media_src: media_src,
345
+ edit_media: edit_media,
408
346
  content_type: @values[:content_type] }
409
347
  end
410
348
 
411
- def media_src(uribase)
349
+ def media_src
412
350
  version = self.class.media_handler.ressource_version(self)
413
- "#{uri(uribase)}/$value?version=#{version}"
414
- end
415
-
416
- def edit_media(uribase)
417
- "#{uri(uribase)}/$value"
351
+ "#{uri}/$value?version=#{version}"
418
352
  end
419
353
 
420
- # directory where to put/find the media files for this entity-type
421
- def klass_dir
422
- type_name
354
+ def edit_media
355
+ "#{uri}/$value"
423
356
  end
424
357
 
425
- # # this is just ModelKlass/pk as a single string
426
- # def qualified_media_path_id
427
- # "#{self.class}/#{media_path_id}"
428
- # end
429
-
430
358
  def values_for_odata
431
359
  ret = values.dup
432
360
  ret.delete(:content_type)
@@ -443,7 +371,7 @@ module OData
443
371
  # delete the relation(s) to parent(s) (if any) and then entity
444
372
  odata_delete_relation_and_entity(req, @navattr_reflection, @nav_parent)
445
373
  # result
446
- [200, CT_JSON, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
374
+ [200, EMPTY_HASH, [{ 'd' => req.service.get_emptycoll_odata_h }.to_json]]
447
375
  else # TODO: other formats
448
376
  415
449
377
  end
@@ -464,11 +392,14 @@ module OData
464
392
  set_fields(emdata, model.data_fields, missing: :skip)
465
393
  save(transaction: false)
466
394
  else
395
+
467
396
  update_fields(emdata, model.data_fields, missing: :skip)
397
+
468
398
  end
469
399
  model.media_handler.replace_file(data: data,
470
400
  entity: self,
471
401
  filename: filename)
402
+
472
403
  ARY_204_EMPTY_HASH_ARY
473
404
  end
474
405
  end
@@ -494,8 +425,11 @@ module OData
494
425
  module EntityMultiPK
495
426
  include Entity
496
427
  def pk_uri
497
- # pk_hash is provided by Sequel
498
- pk_hash.map { |k, v| "#{k}='#{v}'" }.join(COMMA)
428
+ pku = +''
429
+ self.class.odata_upk_parts.each_with_index { |upart, i|
430
+ pku = "#{pku}#{upart}#{pk[i]}"
431
+ }
432
+ pku
499
433
  end
500
434
 
501
435
  def media_path_id
@@ -506,5 +440,22 @@ module OData
506
440
  pk_hash.values
507
441
  end
508
442
  end
509
- end
510
- # end of Module OData
443
+
444
+ module EntityCreateStandardOutput
445
+ # Json formatter for a create entity POST call / Standard version; return as json object
446
+ def to_odata_create_json(request:)
447
+ # TODO Perf: reduce method call overhead
448
+ # we added this redirection for readability and flexibility
449
+ to_odata_json(request: request)
450
+ end
451
+ end
452
+
453
+ module EntityCreateArrayOutput
454
+ # Json formatter for a create entity POST call Array version
455
+ def to_odata_create_json(request:)
456
+ # TODO Perf: reduce method call overhead
457
+ # we added this redirection for readability and flexibility
458
+ to_odata_array_json(request: request)
459
+ end
460
+ end
461
+ end # end of Module OData