safrano 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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