safrano 0.3.2 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '048c3efb69194f00a6ed5593c23300ae7b9d46f7d98a546d443bd5d9c5d2c6c8'
4
- data.tar.gz: 825698055233240072faee6e038e531884e23d938fc1ed9708e98d27cf0db3c6
3
+ metadata.gz: 6397b8b41d6241576f613abfa0e36c8f6973fa96542b60d388b15954aaa3a92b
4
+ data.tar.gz: 3f36fe0d18bd9659d4e3ce27f501ef1540e82f24a26418c28760aae869de85d9
5
5
  SHA512:
6
- metadata.gz: d2697db38029b2701a418a620b74e7b8080d368cd34461073de46065d7057cb3b852cc62795cabb1bbbad82d10607bff96b899beb483a3c0f03e028cf8cb8a31
7
- data.tar.gz: 12a8674563e67e2f3c4e475d5867f8965e0095a0ff0afea80079f6e7e59c56921abc506d33b07ca483f5f2c16024176a6e3679db26b09e979a9f9fb6fac8d755
6
+ metadata.gz: f6f666fb8e2fb136fe7ca0c215e19c24648d9a1a4494c3cdb1a53f45561cf827c2670be56bcf74005828b3a9ebf78e2efc63e8b89db35fefb9f70d4c730e7ebb
7
+ data.tar.gz: 2cdfe6b8cb732184dacb256e5c114361f41da91f5a0f821c2f61a5b7ae541b0aa8f5ad45b23598c0d5301311c193f9f6e51d6e8b8def7eac78a54176f92cfd98
@@ -1,5 +1,5 @@
1
1
  require 'json'
2
- require_relative '../safrano_core.rb'
2
+ require_relative '../safrano/core.rb'
3
3
  require_relative './entity.rb'
4
4
 
5
5
  module OData
@@ -1,5 +1,7 @@
1
- require 'rack_app.rb'
2
- require 'safrano_core.rb'
1
+ require_relative '../safrano/rack_app.rb'
2
+ require_relative '../safrano/core.rb'
3
+ require 'rack/body_proxy'
4
+ require_relative './common_logger.rb'
3
5
 
4
6
  module OData
5
7
  # Support for OData multipart $batch Requests
@@ -38,7 +40,7 @@ module OData
38
40
  def batch_call(part_req)
39
41
  env = batch_env(part_req)
40
42
  env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
41
-
43
+ began_at = Rack::Utils.clock_time
42
44
  @request = OData::Request.new(env)
43
45
  @response = OData::Response.new
44
46
 
@@ -51,7 +53,16 @@ module OData
51
53
  before
52
54
  dispatch
53
55
 
54
- @response.finish
56
+ status, header, body = @response.finish
57
+ # Logging of sub-requests with ODataCommonLogger.
58
+ # A bit hacky but working
59
+ # TODO: test ?
60
+ if (logga = @full_req.env['safrano.logger_mw'])
61
+ logga.batch_log(env, status, header, began_at)
62
+ # TODO: check why/if we need Rack::Utils::HeaderHash.new(header)
63
+ # and Rack::BodyProxy.new(body) ?
64
+ end
65
+ [status, header, body]
55
66
  end
56
67
 
57
68
  # shamelessely copied from Rack::TEST:Session
@@ -60,7 +71,7 @@ module OData
60
71
 
61
72
  headers.each do |name, value|
62
73
  env_key = name.upcase.tr('-', '_')
63
- env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
74
+ env_key = "HTTP_#{env_key}" unless env_key == 'CONTENT_TYPE'
64
75
  converted_headers[env_key] = value
65
76
  end
66
77
 
@@ -71,7 +82,10 @@ module OData
71
82
  @env = ::Rack::MockRequest.env_for(mime_req.uri,
72
83
  method: mime_req.http_method,
73
84
  input: mime_req.content)
85
+ # Logging of sub-requests
86
+ @env[Rack::RACK_ERRORS] = @full_req.env[Rack::RACK_ERRORS]
74
87
  @env.merge! headers_for_env(mime_req.hd)
88
+
75
89
  @env
76
90
  end
77
91
  end
@@ -90,11 +104,11 @@ module OData
90
104
  # jaune d'oeuf
91
105
  class DisabledHandler < HandlerBase
92
106
  def odata_post(_req)
93
- [404, {}, '$batch is not enabled ']
107
+ [404, EMPTY_HASH, '$batch is not enabled ']
94
108
  end
95
109
 
96
110
  def odata_get(_req)
97
- [404, {}, '$batch is not enabled ']
111
+ [404, EMPTY_HASH, '$batch is not enabled ']
98
112
  end
99
113
  end
100
114
  # battre le tout
@@ -112,7 +126,7 @@ module OData
112
126
  def odata_post(req)
113
127
  @request = req
114
128
 
115
- if @request.media_type == 'multipart/mixed'
129
+ if @request.media_type == OData::MP_MIXED
116
130
 
117
131
  batcha = @request.create_batch_app
118
132
  @mult_request = @request.parse_multipart
@@ -124,12 +138,12 @@ module OData
124
138
 
125
139
  [202, resp_hdrs, @mult_response.body[0]]
126
140
  else
127
- [415, {}, 'Unsupported Media Type']
141
+ [415, EMPTY_HASH, 'Unsupported Media Type']
128
142
  end
129
143
  end
130
144
 
131
145
  def odata_get(_req)
132
- [405, {}, 'You cant GET $batch, POST it ']
146
+ [405, EMPTY_HASH, 'You cant GET $batch, POST it ']
133
147
  end
134
148
  end
135
149
  end
@@ -4,11 +4,14 @@
4
4
 
5
5
  require 'json'
6
6
  require 'rexml/document'
7
- require 'safrano_core.rb'
8
- require 'odata/error.rb'
9
- require 'odata/collection_filter.rb'
10
- require 'odata/collection_order.rb'
11
- require 'odata/url_parameters.rb'
7
+ require_relative '../safrano/core.rb'
8
+ require_relative 'error.rb'
9
+ require_relative 'collection_filter.rb'
10
+ require_relative 'collection_order.rb'
11
+ require_relative 'expand.rb'
12
+ require_relative 'select.rb'
13
+ require_relative 'url_parameters.rb'
14
+ require_relative 'collection_media.rb'
12
15
 
13
16
  # small helper method
14
17
  # http://stackoverflow.com/
@@ -42,25 +45,32 @@ module OData
42
45
  # we will add this to our Model classes with "extend" --> self is the Class
43
46
  module EntityClassBase
44
47
  SINGLE_PK_URL_REGEXP = /\A\(\s*'?([\w\s]+)'?\s*\)(.*)/.freeze
45
- ONLY_INTEGER_RGX = /\A[+-]?\d+\z/
48
+ ONLY_INTEGER_RGX = /\A[+-]?\d+\z/.freeze
46
49
 
47
50
  attr_reader :nav_collection_url_regexp
48
51
  attr_reader :nav_entity_url_regexp
49
52
  attr_reader :entity_id_url_regexp
50
- attr_reader :attrib_paths_url_regexp
51
53
  attr_reader :nav_collection_attribs
52
54
  attr_reader :nav_entity_attribs
53
55
  attr_reader :data_fields
54
56
  attr_reader :inlinecount
57
+ attr_reader :default_template
58
+
59
+ # Sequel associations pointing to this model. Sequel provides association
60
+ # reflection information on the "from" side. But in some cases
61
+ # we will need the reverted way
62
+ # finally not needed and not used yet
63
+ # attr_accessor :assocs_to
64
+
65
+ # set to parent entity in case the collection is a nav.collection
66
+ # nil otherwise
67
+ attr_reader :nav_parent
55
68
 
56
69
  attr_accessor :namespace
57
70
 
58
71
  # dataset
59
72
  attr_accessor :cx
60
73
 
61
- # array of the objects --> dataset.to_a
62
- attr_accessor :ax
63
-
64
74
  # url params
65
75
  attr_reader :params
66
76
 
@@ -75,13 +85,26 @@ module OData
75
85
  attr_accessor :deferred_iblock
76
86
 
77
87
  # convention: entityType is the Ruby Model class --> name is just to_s
88
+ # Warning: for handling Navigation relations, we use anonymous collection classes
89
+ # dynamically subtyped from a Model class, and in such an anonymous class
90
+ # the class-name is not the OData Type. In these subclass we redefine "type_name"
91
+ # thus when we need the Odata type name, we shall use this method instead of just the collection class name
78
92
  def type_name
79
93
  to_s
80
94
  end
81
95
 
82
- # convention: default for entity_set_name is the model table name
96
+ # convention: default for entity_set_name is the type name
83
97
  def entity_set_name
84
- @entity_set_name = (@entity_set_name || table_name.to_s)
98
+ @entity_set_name = (@entity_set_name || type_name)
99
+ end
100
+
101
+ def reset
102
+ # TODO: automatically reset all attributes?
103
+ @deferred_iblock = nil
104
+ @entity_set_name = nil
105
+ @uparms = nil
106
+ @params = nil
107
+ @cx = nil
85
108
  end
86
109
 
87
110
  def execute_deferred_iblock
@@ -89,19 +112,36 @@ module OData
89
112
  end
90
113
 
91
114
  # Factory json-> Model Object instance
92
- def new_from_hson_h(hash, in_changeset: false)
115
+ def new_from_hson_h(hash)
93
116
  enty = new
94
- hash.delete('__metadata')
95
- # DONE: move this somewhere else where it's evaluated only once at setup
96
- # data_fields = db_schema.map do |col, cattr|
97
- # cattr[:primary_key] ? nil : col
98
- # end.select { |col| col }
99
117
  enty.set_fields(hash, @data_fields, missing: :skip)
100
- # in-changeset requests get their own transaction
101
- enty.save(transaction: !in_changeset)
102
118
  enty
103
119
  end
104
120
 
121
+ CREATE_AND_SAVE_ENTY_AND_REL = lambda do |new_entity, assoc, parent|
122
+ # in-changeset requests get their own transaction
123
+ case assoc[:type]
124
+ when :one_to_many, :one_to_one
125
+ OData.create_nav_relation(new_entity, assoc, parent)
126
+ new_entity.save(transaction: false)
127
+ when :many_to_one
128
+ new_entity.save(transaction: false)
129
+ OData.create_nav_relation(new_entity, assoc, parent)
130
+ parent.save(transaction: false)
131
+ # else # not supported
132
+ end
133
+ end
134
+ def odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
135
+ if req.in_changeset
136
+ # in-changeset requests get their own transaction
137
+ CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
138
+ else
139
+ db.transaction do
140
+ CREATE_AND_SAVE_ENTY_AND_REL.call(new_entity, assoc, parent)
141
+ end
142
+ end
143
+ end
144
+
105
145
  def odata_get_inlinecount_w_sequel
106
146
  return unless (icp = @params['$inlinecount'])
107
147
 
@@ -114,10 +154,6 @@ module OData
114
154
  end
115
155
  end
116
156
 
117
- def navigated_coll
118
- false
119
- end
120
-
121
157
  def attrib_path_valid?(path)
122
158
  @attribute_path_list.include? path
123
159
  end
@@ -160,9 +196,7 @@ module OData
160
196
  def check_u_p_inlinecount
161
197
  return unless (icp = @params['$inlinecount'])
162
198
 
163
- unless (icp == 'allpages') || (icp == 'none')
164
- return BadRequestInlineCountParamError
165
- end
199
+ return BadRequestInlineCountParamError unless (icp == 'allpages') || (icp == 'none')
166
200
 
167
201
  nil
168
202
  end
@@ -175,26 +209,25 @@ module OData
175
209
  @uparms.check_order
176
210
  end
177
211
 
212
+ def check_u_p_expand
213
+ @uparms.check_expand
214
+ end
215
+
178
216
  def build_attribute_path_list
179
217
  @attribute_path_list = attribute_path_list
180
- @attrib_paths_url_regexp = @attribute_path_list.join('|')
181
218
  end
182
219
 
183
220
  def attribute_path_list(nodes = Set.new)
184
221
  # break circles
185
- return [] if nodes.include?(entity_set_name)
222
+ return EMPTY_ARRAY if nodes.include?(entity_set_name)
186
223
 
187
224
  ret = @columns.map(&:to_s)
188
225
  nodes.add entity_set_name
189
- if @nav_entity_attribs
190
- @nav_entity_attribs.each do |a, k|
191
- ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
192
- end
226
+ @nav_entity_attribs&.each do |a, k|
227
+ ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
193
228
  end
194
- if @nav_collection_attribs
195
- @nav_collection_attribs.each do |a, k|
196
- ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
197
- end
229
+ @nav_collection_attribs&.each do |a, k|
230
+ ret.concat(k.attribute_path_list(nodes).map { |kc| "#{a}/#{kc}" })
198
231
  end
199
232
  ret
200
233
  end
@@ -203,20 +236,18 @@ module OData
203
236
  return nil unless @params
204
237
 
205
238
  check_u_p_top || check_u_p_skip || check_u_p_orderby ||
206
- check_u_p_filter || check_u_p_inlinecount
239
+ check_u_p_filter || check_u_p_expand || check_u_p_inlinecount
207
240
  end
208
241
 
209
242
  def initialize_dataset
210
243
  @cx = self
211
- @ax = nil
212
- @cx = navigated_dataset if @cx.navigated_coll
244
+ @cx = navigated_dataset if @cx.nav_parent
213
245
  @model = if @cx.respond_to? :model
214
246
  @cx.model
215
247
  else
216
248
  @cx
217
249
  end
218
- @jh = @model.join_by_paths_helper
219
- @uparms = UrlParameters.new(@jh, @params)
250
+ @uparms = UrlParameters4Coll.new(@model, @params)
220
251
  end
221
252
 
222
253
  # finally return the requested output according to format, options etc
@@ -227,9 +258,9 @@ module OData
227
258
  [200, CT_TEXT, @cx.count.to_s]
228
259
  elsif req.accept?(APPJSON)
229
260
  if req.walker.do_links
230
- [200, CT_JSON, to_odata_links_json(service: req.service)]
261
+ [200, CT_JSON, [to_odata_links_json(service: req.service)]]
231
262
  else
232
- [200, CT_JSON, to_odata_json(service: req.service)]
263
+ [200, CT_JSON, [to_odata_json(service: req.service)]]
233
264
  end
234
265
  else # TODO: other formats
235
266
  406
@@ -250,9 +281,17 @@ module OData
250
281
  end
251
282
  end
252
283
 
284
+ def odata_post(req)
285
+ odata_create_entity_and_relation(req, @navattr_reflection, @nav_parent)
286
+ end
287
+
253
288
  # add metadata xml to the passed REXML schema object
254
289
  def add_metadata_rexml(schema)
255
- enty = schema.add_element('EntityType', 'Name' => to_s)
290
+ enty = if @media_handler
291
+ schema.add_element('EntityType', 'Name' => to_s, 'HasStream' => 'true')
292
+ else
293
+ schema.add_element('EntityType', 'Name' => to_s)
294
+ end
256
295
  # with their properties
257
296
  db_schema.each do |pnam, prop|
258
297
  if prop[:primary_key] == true
@@ -268,67 +307,125 @@ module OData
268
307
  end
269
308
 
270
309
  # metadata REXML data for a single Nav attribute
271
- def metadata_nav_rexml_attribs(assoc, cmap, relman, xnamespace)
310
+ def metadata_nav_rexml_attribs(assoc, to_klass, relman, xnamespace)
272
311
  from = type_name
273
- to = cmap[assoc.to_s].type_name
312
+ to = to_klass.type_name
274
313
  relman.get_metadata_xml_attribs(from,
275
314
  to,
276
- association_reflection(assoc)[:type],
277
- xnamespace)
315
+ association_reflection(assoc.to_sym)[:type],
316
+ xnamespace,
317
+ assoc)
278
318
  end
279
319
 
280
320
  # and their Nav attributes == Sequel Model association
281
- def add_metadata_navs_rexml(schema_enty, cmap, relman, xnamespace)
282
- associations.each do |assoc|
283
- # associated objects need to be in the map...
284
- next unless cmap[assoc.to_s]
285
-
286
- nattrs = metadata_nav_rexml_attribs(assoc, cmap, relman, xnamespace)
321
+ def add_metadata_navs_rexml(schema_enty, relman, xnamespace)
322
+ @nav_entity_attribs&.each do |ne, klass|
323
+ nattr = metadata_nav_rexml_attribs(ne,
324
+ klass,
325
+ relman,
326
+ xnamespace)
327
+ schema_enty.add_element('NavigationProperty', nattr)
328
+ end
287
329
 
288
- schema_enty.add_element('NavigationProperty', nattrs)
330
+ @nav_collection_attribs&.each do |nc, klass|
331
+ nattr = metadata_nav_rexml_attribs(nc,
332
+ klass,
333
+ relman,
334
+ xnamespace)
335
+ schema_enty.add_element('NavigationProperty', nattr)
289
336
  end
290
337
  end
291
338
 
339
+ D = 'd'.freeze
340
+ DJ_OPEN = '{"d":'.freeze
341
+ DJ_CLOSE = '}'.freeze
292
342
  def to_odata_links_json(service:)
293
- { 'd' => service.get_coll_odata_links_h(array: get_a,
343
+ innerj = service.get_coll_odata_links_h(array: get_a,
294
344
  uribase: @uribase,
295
- icount: @inlinecount) }.to_json
345
+ icount: @inlinecount).to_json
346
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
296
347
  end
297
348
 
298
- def to_odata_json(service:)
299
- { 'd' => service.get_coll_odata_h(array: get_a,
300
- expand: @params['$expand'],
301
- uribase: @uribase,
302
- icount: @inlinecount) }.to_json
349
+ # def output_template(expand: nil, select: nil)
350
+ def output_template(uparms)
351
+ # output_template_deep(expand_list: expand_list, select: select)
352
+ output_template_deep(expand_list: uparms.expand.template, select: uparms.select)
303
353
  end
304
354
 
305
- def get_a
306
- @ax.nil? ? @cx.to_a : @ax
307
- end
355
+ # Recursive
356
+ def output_template_deep(expand_list:, select: OData::SelectBase::ALL)
357
+ return default_template if expand_list.empty? && select.all_props?
308
358
 
309
- def odata_post(req)
310
- # TODO: this is for v2 only...
311
- on_error = (proc { raise Sequel::Rollback } if req.in_changeset)
359
+ template = {}
360
+ expand_e = {}
361
+ expand_c = {}
362
+ deferr = []
312
363
 
313
- req.with_parsed_data(on_error: on_error) do |data|
314
- data.delete('__metadata')
315
- # validate payload column names
316
- if (invalid = invalid_hash_data?(data))
317
- on_error.call if on_error
318
- return [422, {}, ['Invalid attribute name: ', invalid.to_s]]
364
+ # 1. handle non-navigation properties, only consider $select
365
+ # 2. handle navigations properties, need to check $select and $expand
366
+ if select.all_props?
367
+ template[:all_values] = EMPTYH
368
+ # include all nav attributes -->
369
+ @nav_entity_attribs&.each do |attr, klass|
370
+ if expand_list.key?(attr)
371
+ expand_e[attr] = klass.output_template_deep(expand_list: expand_list[attr])
372
+ else
373
+ deferr << attr
374
+ end
319
375
  end
320
376
 
321
- if req.accept?(APPJSON)
322
-
323
- new_entity = new_from_hson_h(data, in_changeset: req.in_changeset)
324
- req.register_content_id_ref(new_entity)
325
- new_entity.copy_request_infos(req)
377
+ @nav_collection_attribs&.each do |attr, klass|
378
+ if expand_list.key?(attr)
379
+ expand_c[attr] = klass.output_template_deep(expand_list: expand_list[attr])
380
+ else
381
+ deferr << attr
382
+ end
383
+ end
326
384
 
327
- [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
328
- else # TODO: other formats
329
- 415
385
+ else
386
+ template[:selected_vals] = @columns.map(&:to_s) & select.props
387
+ # include only selected nav attribs-->need additional intersection step
388
+ if @nav_entity_attribs
389
+ selected_nav_e = @nav_entity_attribs.keys & select.props
390
+
391
+ selected_nav_e&.each do |attr|
392
+ if expand_list.key?(attr)
393
+ klass = @nav_entity_attribs[attr]
394
+ expand_e[attr] = klass.output_template_deep(expand_list: expand_list[attr])
395
+ else
396
+ deferr << attr
397
+ end
398
+ end
399
+ end
400
+ if @nav_collection_attribs
401
+ selected_nav_c = @nav_collection_attribs.keys & select.props
402
+ selected_nav_c&.each do |attr|
403
+ if expand_list.key?(attr)
404
+ klass = @nav_collection_attribs[attr]
405
+ expand_c[attr] = klass.output_template_deep(expand_list: expand_list[attr])
406
+ else
407
+ deferr << attr
408
+ end
409
+ end
330
410
  end
331
411
  end
412
+ template[:expand_e] = expand_e if expand_e
413
+ template[:expand_c] = expand_c if expand_c
414
+ template[:deferr] = deferr if deferr
415
+ template
416
+ end
417
+
418
+ def to_odata_json(service:)
419
+ template = output_template(@uparms)
420
+ innerj = service.get_coll_odata_h(array: get_a,
421
+ template: template,
422
+ uribase: @uribase,
423
+ icount: @inlinecount).to_json
424
+ "#{DJ_OPEN}#{innerj}#{DJ_CLOSE}"
425
+ end
426
+
427
+ def get_a
428
+ @cx.all
332
429
  end
333
430
 
334
431
  # this functionally similar to the Sequel Rels (many_to_one etc)
@@ -340,9 +437,8 @@ module OData
340
437
  assoc = all_association_reflections.find do |a|
341
438
  a[:name] == assoc_symb && a[:model] == self
342
439
  end
343
- unless assoc
344
- raise OData::API::ModelAssociationNameError.new(self, assoc_symb)
345
- end
440
+
441
+ raise OData::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
346
442
 
347
443
  attr_class = assoc[:class_name].constantize
348
444
  lattr_name_str = (attr_name_str || assoc_symb.to_s)
@@ -357,9 +453,8 @@ module OData
357
453
  assoc = all_association_reflections.find do |a|
358
454
  a[:name] == assoc_symb && a[:model] == self
359
455
  end
360
- unless assoc
361
- raise OData::API::ModelAssociationNameError.new(self, assoc_symb)
362
- end
456
+
457
+ raise OData::API::ModelAssociationNameError.new(self, assoc_symb) unless assoc
363
458
 
364
459
  attr_class = assoc[:class_name].constantize
365
460
  lattr_name_str = (attr_name_str || assoc_symb.to_s)
@@ -367,10 +462,30 @@ module OData
367
462
  @nav_entity_url_regexp = @nav_entity_attribs.keys.join('|')
368
463
  end
369
464
 
465
+ EMPTYH = {}.freeze
466
+
467
+ def build_default_template
468
+ template = { all_values: EMPTYH }
469
+ if @nav_entity_attribs || @nav_collection_attribs
470
+ template[:deferr] = (@nav_entity_attribs&.keys || []) + (@nav_collection_attribs&.keys || EMPTY_ARRAY)
471
+ end
472
+ template
473
+ end
370
474
  # old names...
371
475
  # alias_method :add_nav_prop_collection, :addNavCollectionAttrib
372
476
  # alias_method :add_nav_prop_single, :addNavEntityAttrib
373
477
 
478
+ def finalize_publishing
479
+ # finalize media handler
480
+ @media_handler.register(self) if @media_handler
481
+
482
+ # build default output template structure
483
+ @default_template = build_default_template
484
+
485
+ # and finally build the path list
486
+ build_attribute_path_list
487
+ end
488
+
374
489
  def prepare_pk
375
490
  if primary_key.is_a? Array
376
491
  @pk_names = []
@@ -419,11 +534,7 @@ module OData
419
534
  def check_odata_val_type(val, type)
420
535
  case type
421
536
  when :integer
422
- if (val =~ ONLY_INTEGER_RGX)
423
- [true, Integer(val)]
424
- else
425
- [false, val]
426
- end
537
+ val =~ ONLY_INTEGER_RGX ? [true, Integer(val)] : [false, val]
427
538
  when :string
428
539
  [true, val]
429
540
  else
@@ -471,6 +582,7 @@ module OData
471
582
  end
472
583
  include Transitions
473
584
  end
585
+
474
586
  # special handling for composite key
475
587
  module EntityClassMultiPK
476
588
  include EntityClassBase
@@ -483,7 +595,7 @@ module OData
483
595
  md.shift # remove first element which is the whole match
484
596
  mdc = []
485
597
  error = false
486
- primary_key.each_with_index { |pk, i|
598
+ primary_key.each_with_index do |pk, i|
487
599
  ck, casted = check_odata_val_type(md[i], db_schema[pk][:type])
488
600
  if ck
489
601
  mdc << casted
@@ -491,7 +603,7 @@ module OData
491
603
  error = true
492
604
  break
493
605
  end
494
- }
606
+ end
495
607
  if error
496
608
  [false, md]
497
609
  else
@@ -508,4 +620,38 @@ module OData
508
620
  check_odata_val_type(id, db_schema[primary_key][:type])
509
621
  end
510
622
  end
623
+
624
+ # normal handling for non-media entity
625
+ module EntityClassNonMedia
626
+ # POST for non-media entity collection -->
627
+ # 1. Create and add entity from payload
628
+ # 2. Create relationship if needed
629
+ def odata_create_entity_and_relation(req, assoc, parent)
630
+ # TODO: this is for v2 only...
631
+ req.with_parsed_data do |data|
632
+ data.delete('__metadata')
633
+ # validate payload column names
634
+ if (invalid = invalid_hash_data?(data))
635
+ ::OData::Request::ON_CGST_ERROR.call(req)
636
+ return [422, EMPTY_HASH, ['Invalid attribute name: ', invalid.to_s]]
637
+ end
638
+
639
+ if req.accept?(APPJSON)
640
+ new_entity = new_from_hson_h(data)
641
+ if parent
642
+ odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
643
+ else
644
+ # in-changeset requests get their own transaction
645
+ new_entity.save(transaction: !req.in_changeset)
646
+ end
647
+ req.register_content_id_ref(new_entity)
648
+ new_entity.copy_request_infos(req)
649
+
650
+ [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
651
+ else # TODO: other formats
652
+ 415
653
+ end
654
+ end
655
+ end
656
+ end
511
657
  end