maestrano-connector-rails 0.4.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/README.md +1 -1
  4. data/VERSION +1 -1
  5. data/app/controllers/maestrano/connec_controller.rb +17 -19
  6. data/app/jobs/maestrano/connector/rails/all_synchronizations_job.rb +1 -1
  7. data/app/jobs/maestrano/connector/rails/push_to_connec_job.rb +11 -11
  8. data/app/jobs/maestrano/connector/rails/synchronization_job.rb +20 -11
  9. data/app/models/maestrano/connector/rails/concerns/complex_entity.rb +66 -74
  10. data/app/models/maestrano/connector/rails/concerns/connec_helper.rb +102 -0
  11. data/app/models/maestrano/connector/rails/concerns/entity.rb +233 -231
  12. data/app/models/maestrano/connector/rails/concerns/external.rb +7 -0
  13. data/app/models/maestrano/connector/rails/concerns/sub_entity_base.rb +14 -43
  14. data/app/models/maestrano/connector/rails/connec_helper.rb +5 -0
  15. data/app/models/maestrano/connector/rails/organization.rb +8 -2
  16. data/db/20160524112054_add_encryption_on_oauth_keys.rb +8 -0
  17. data/lib/generators/connector/install_generator.rb +1 -0
  18. data/lib/generators/connector/templates/complex_entity_example/contact.rb +1 -1
  19. data/lib/generators/connector/templates/complex_entity_example/contact_and_lead.rb +2 -2
  20. data/lib/generators/connector/templates/entity.rb +13 -19
  21. data/lib/generators/connector/templates/example_entity.rb +2 -2
  22. data/lib/generators/connector/templates/example_entity_spec.rb +73 -0
  23. data/lib/generators/connector/templates/external.rb +11 -0
  24. data/maestrano-connector-rails.gemspec +13 -8
  25. data/release_notes.md +81 -0
  26. data/spec/controllers/connec_controller_spec.rb +19 -6
  27. data/spec/dummy/app/models/maestrano/connector/rails/entity.rb +0 -7
  28. data/spec/dummy/app/models/maestrano/connector/rails/external.rb +7 -0
  29. data/spec/dummy/app/views/home/index.html.erb +1 -36
  30. data/spec/factories.rb +3 -0
  31. data/spec/integration/connec_to_external_spec.rb +188 -0
  32. data/spec/integration/external_to_connec_spec.rb +155 -0
  33. data/spec/integration/integration_complex_spec.rb +281 -0
  34. data/spec/integration/singleton_spec.rb +288 -0
  35. data/spec/jobs/all_synchronizations_job_spec.rb +5 -0
  36. data/spec/jobs/push_to_connec_job_spec.rb +3 -6
  37. data/spec/jobs/synchronization_job_spec.rb +29 -17
  38. data/spec/models/complex_entity_spec.rb +257 -412
  39. data/spec/models/connec_helper_spec.rb +143 -0
  40. data/spec/models/entity_spec.rb +420 -348
  41. data/spec/models/external_spec.rb +4 -0
  42. data/spec/models/organization_spec.rb +2 -1
  43. data/spec/models/sub_entity_base_spec.rb +28 -69
  44. data/template/factories.rb +3 -1
  45. data/template/maestrano-connector-template.rb +11 -13
  46. data/template/maestrano.rb +2 -1
  47. data/template/settings/development.yml +4 -2
  48. data/template/settings/production.yml +1 -11
  49. data/template/settings/settings.yml +8 -0
  50. data/template/settings/test.yml +2 -0
  51. data/template/settings/uat.yml +1 -9
  52. metadata +12 -7
  53. data/Gemfile.lock +0 -256
  54. data/realse_notes.md +0 -16
  55. data/spec/dummy/app/views/admin/index.html.erb +0 -51
  56. data/spec/dummy/db/development.sqlite3 +0 -0
  57. data/spec/dummy/db/test.sqlite3 +0 -0
@@ -1,14 +1,14 @@
1
1
  module Maestrano::Connector::Rails::Concerns::Entity
2
2
  extend ActiveSupport::Concern
3
3
 
4
- module ClassMethods
5
- # Return an array of all the entities that the connector can synchronize
6
- # If you add new entities, you need to generate
7
- # a migration to add them to existing organizations
8
- def entities_list
9
- raise "Not implemented"
10
- end
4
+ def initialize(organization, connec_client, external_client, opts={})
5
+ @organization = organization
6
+ @connec_client = connec_client
7
+ @external_client = external_client
8
+ @opts = opts
9
+ end
11
10
 
11
+ module ClassMethods
12
12
  # ----------------------------------------------
13
13
  # IdMap methods
14
14
  # ----------------------------------------------
@@ -19,34 +19,18 @@ module Maestrano::Connector::Rails::Concerns::Entity
19
19
  }
20
20
  end
21
21
 
22
- def find_or_create_idmap(organization_and_id)
23
- Maestrano::Connector::Rails::IdMap.find_or_create_by(names_hash.merge(organization_and_id))
24
- end
25
-
26
22
  # organization_and_id can be either:
27
23
  # * {connec_id: 'id', organization_id: 'id'}
28
24
  # * {external_id: 'id', organization_id: 'id'}
29
25
  # Needs to include either connec_entity or external_entity for complex entities
26
+ def find_or_create_idmap(organization_and_id)
27
+ Maestrano::Connector::Rails::IdMap.find_or_create_by(names_hash.merge(organization_and_id))
28
+ end
30
29
  def find_idmap(organization_and_id)
31
30
  Maestrano::Connector::Rails::IdMap.find_by(names_hash.merge(organization_and_id))
32
31
  end
33
-
34
- def create_idmap_from_external_entity(entity, organization)
35
- h = names_hash.merge({
36
- external_id: id_from_external_entity_hash(entity),
37
- name: object_name_from_external_entity_hash(entity),
38
- organization_id: organization.id
39
- })
40
- Maestrano::Connector::Rails::IdMap.create(h)
41
- end
42
-
43
- def create_idmap_from_connec_entity(entity, organization)
44
- h = names_hash.merge({
45
- connec_id: entity['id'],
46
- name: object_name_from_connec_entity_hash(entity),
47
- organization_id: organization.id
48
- })
49
- Maestrano::Connector::Rails::IdMap.create(h)
32
+ def create_idmap(organization_and_id)
33
+ Maestrano::Connector::Rails::IdMap.create(names_hash.merge(organization_and_id))
50
34
  end
51
35
 
52
36
  # ----------------------------------------------
@@ -114,11 +98,16 @@ module Maestrano::Connector::Rails::Concerns::Entity
114
98
  raise "Not implemented"
115
99
  end
116
100
 
117
- # [{reference_class: Entities::.., connec_field: 'account_id', external_field: 'account/something/id'}]
101
+ # An array of connec fields that are references
118
102
  def references
119
103
  []
120
104
  end
121
105
 
106
+ # An array of fields for smart merging. See connec! documentation
107
+ def connec_matching_fields
108
+ nil
109
+ end
110
+
122
111
  def can_read_connec?
123
112
  can_write_external?
124
113
  end
@@ -135,10 +124,6 @@ module Maestrano::Connector::Rails::Concerns::Entity
135
124
  true
136
125
  end
137
126
 
138
- def can_update_connec?
139
- true
140
- end
141
-
142
127
  def can_update_external?
143
128
  true
144
129
  end
@@ -147,24 +132,19 @@ module Maestrano::Connector::Rails::Concerns::Entity
147
132
  # ----------------------------------------------
148
133
  # Mapper methods
149
134
  # ----------------------------------------------
150
- # Map a Connec! entity to the external format
151
- def map_to_external(entity, organization)
152
- ref_hash = {}
153
- self.class.references.each do |ref|
154
- ref_hash.merge! ref[:external_field].split('/').reverse.inject(self.class.id_from_ref(entity, ref, false, organization)) { |a, n| { n.to_sym => a } }
155
- end
156
-
157
- self.class.mapper_class.normalize(entity).merge(ref_hash)
135
+ # Map a Connec! entity to the external model
136
+ def map_to_external(entity)
137
+ connec_id = entity[:__connec_id]
138
+ mapped_entity = self.class.mapper_class.normalize(entity)
139
+ (connec_id ? mapped_entity.merge(__connec_id: connec_id) : mapped_entity).with_indifferent_access
158
140
  end
159
141
 
160
- # Map an external entity to Connec! format
161
- def map_to_connec(entity, organization)
162
- ref_hash = {}
163
- self.class.references.each do |ref|
164
- ref_hash.merge! ref[:connec_field].split('/').reverse.inject(self.class.id_from_ref(entity, ref, true, organization)) { |a, n| { n.to_sym => a } }
165
- end
166
-
167
- self.class.mapper_class.denormalize(entity).merge(ref_hash)
142
+ # Map an external entity to Connec! model
143
+ def map_to_connec(entity)
144
+ mapped_entity = self.class.mapper_class.denormalize(entity).merge(id: self.class.id_from_external_entity_hash(entity))
145
+ folded_entity = Maestrano::Connector::Rails::ConnecHelper.fold_references(mapped_entity, self.class.references, @organization)
146
+ folded_entity.merge!(opts: (mapped_entity[:opts] || {}).merge(matching_fields: self.class.connec_matching_fields)) if self.class.connec_matching_fields
147
+ folded_entity
168
148
  end
169
149
 
170
150
  # ----------------------------------------------
@@ -174,324 +154,346 @@ module Maestrano::Connector::Rails::Concerns::Entity
174
154
  # * full_sync
175
155
  # * $filter (see Connec! documentation)
176
156
  # * $orderby (see Connec! documentation)
177
- def get_connec_entities(client, last_synchronization, organization, opts={})
178
- return [] unless self.class.can_read_connec?
157
+ def get_connec_entities(last_synchronization)
158
+ return [] if @opts[:skip_connec] || !self.class.can_read_connec?
179
159
 
180
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Fetching Connec! #{self.class.connec_entity_name}")
160
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Fetching Connec! #{self.class.connec_entity_name}")
181
161
 
182
162
  entities = []
183
163
  query_params = {}
184
- query_params[:$orderby] = opts[:$orderby] if opts[:$orderby]
164
+ query_params[:$orderby] = @opts[:$orderby] if @opts[:$orderby]
185
165
 
186
166
  # Fetch first page
187
- if last_synchronization.blank? || opts[:full_sync]
188
- Maestrano::Connector::Rails::ConnectorLogger.log('debug', organization, "entity=#{self.class.connec_entity_name}, fetching all data")
189
- query_params[:$filter] = opts[:$filter] if opts[:$filter]
167
+ page_number = 0
168
+ if last_synchronization.blank? || @opts[:full_sync]
169
+ Maestrano::Connector::Rails::ConnectorLogger.log('debug', @organization, "entity=#{self.class.connec_entity_name}, fetching all data")
170
+ query_params[:$filter] = @opts[:$filter] if @opts[:$filter]
190
171
  else
191
- Maestrano::Connector::Rails::ConnectorLogger.log('debug', organization, "entity=#{self.class.connec_entity_name}, fetching data since #{last_synchronization.updated_at.iso8601}")
192
- filter = "updated_at gt '#{last_synchronization.updated_at.iso8601}'"
193
- filter += " and #{opts[:$filter]}" if opts[:$filter]
194
- query_params[:$filter] = filter
172
+ Maestrano::Connector::Rails::ConnectorLogger.log('debug', @organization, "entity=#{self.class.connec_entity_name}, fetching data since #{last_synchronization.updated_at.iso8601}")
173
+ query_params[:$filter] = "updated_at gt '#{last_synchronization.updated_at.iso8601}'" + (@opts[:$filter] ? " and #{@opts[:$filter]}" : '')
195
174
  end
196
- response = client.get("/#{self.class.normalized_connec_entity_name}?#{query_params.to_query}")
197
- raise "No data received from Connec! when trying to fetch #{self.class.normalized_connec_entity_name}" unless response && !response.body.blank?
198
175
 
199
- response_hash = JSON.parse(response.body)
200
- Maestrano::Connector::Rails::ConnectorLogger.log('debug', organization, "received first page entity=#{self.class.connec_entity_name}, response=#{response.body}")
201
- if response_hash["#{self.class.normalized_connec_entity_name}"]
202
- entities << response_hash["#{self.class.normalized_connec_entity_name}"]
203
- else
204
- raise "Received unrecognized Connec! data when trying to fetch #{self.class.normalized_connec_entity_name}"
205
- end
176
+ uri = "/#{self.class.normalized_connec_entity_name}?#{query_params.to_query}"
177
+ response_hash = fetch_connec(uri, 0)
178
+ entities = response_hash["#{self.class.normalized_connec_entity_name}"]
206
179
 
207
180
  # Fetch subsequent pages
208
181
  while response_hash['pagination'] && response_hash['pagination']['next']
182
+ page_number += 1
209
183
  # ugly way to convert https://api-connec/api/v2/group_id/organizations?next_page_params to /organizations?next_page_params
210
184
  next_page = response_hash['pagination']['next'].gsub(/^(.*)\/#{self.class.normalized_connec_entity_name}/, self.class.normalized_connec_entity_name)
211
- response = client.get(next_page)
212
185
 
213
- raise "No data received from Connec! when trying to fetch subsequent page of #{self.class.connec_entity_name.pluralize}" unless response && !response.body.blank?
214
- Maestrano::Connector::Rails::ConnectorLogger.log('debug', organization, "received next page entity=#{self.class.connec_entity_name}, response=#{response.body}")
215
-
216
- response_hash = JSON.parse(response.body)
217
- if response_hash["#{self.class.normalized_connec_entity_name}"]
218
- entities << response_hash["#{self.class.normalized_connec_entity_name}"]
219
- else
220
- raise "Received unrecognized Connec! data when trying to fetch subsequent page of #{self.class.connec_entity_name.pluralize}"
221
- end
186
+ response_hash = fetch_connec(uri, page_number)
187
+ entities << response_hash["#{self.class.normalized_connec_entity_name}"]
222
188
  end
223
189
 
224
- entities = entities.flatten
225
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Received data: Source=Connec!, Entity=#{self.class.connec_entity_name}, Data=#{entities}")
190
+ entities.flatten!
191
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Received data: Source=Connec!, Entity=#{self.class.connec_entity_name}, Data=#{entities}")
226
192
  entities
227
193
  end
228
194
 
229
- def push_entities_to_connec(connec_client, mapped_external_entities_with_idmaps, organization)
230
- push_entities_to_connec_to(connec_client, mapped_external_entities_with_idmaps, self.class.connec_entity_name, organization)
195
+ def push_entities_to_connec(mapped_external_entities_with_idmaps)
196
+ push_entities_to_connec_to(mapped_external_entities_with_idmaps, self.class.connec_entity_name)
231
197
  end
232
198
 
233
- def push_entities_to_connec_to(connec_client, mapped_external_entities_with_idmaps, connec_entity_name, organization)
199
+ def push_entities_to_connec_to(mapped_external_entities_with_idmaps, connec_entity_name)
234
200
  return unless self.class.can_write_connec?
235
201
 
236
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending #{Maestrano::Connector::Rails::External.external_name} #{self.class.external_entity_name.pluralize} to Connec! #{connec_entity_name.pluralize}")
202
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Sending #{Maestrano::Connector::Rails::External.external_name} #{self.class.external_entity_name.pluralize} to Connec! #{connec_entity_name.pluralize}")
237
203
 
238
- request_per_call = 100
239
- start = 0
240
- while start < mapped_external_entities_with_idmaps.size
241
- # Prepare batch request
242
- batch_entities = mapped_external_entities_with_idmaps.slice(start, request_per_call)
243
- batch_request = {sequential: true, ops: []}
244
- batch_entities.each do |mapped_external_entity_with_idmap|
245
- external_entity = mapped_external_entity_with_idmap[:entity]
246
- idmap = mapped_external_entity_with_idmap[:idmap]
247
- if idmap.connec_id.blank?
248
- batch_request[:ops] << batch_op('post', external_entity, nil, self.class.normalize_connec_entity_name(connec_entity_name), organization)
249
- else
250
- next unless self.class.can_update_connec?
251
- batch_request[:ops] << batch_op('put', external_entity, idmap.connec_id, self.class.normalize_connec_entity_name(connec_entity_name), organization)
252
- end
253
- end
254
-
255
- # Batch call
256
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending batch request to Connec! for #{self.class.normalize_connec_entity_name(connec_entity_name)}. Batch_request_size: #{batch_request[:ops].size}. Call_number: #{(start/request_per_call) + 1}")
257
- response = connec_client.batch(batch_request)
258
- Maestrano::Connector::Rails::ConnectorLogger.log('debug', organization, "Received batch response from Connec! for #{self.class.normalize_connec_entity_name(connec_entity_name)}: #{response}")
259
- raise "No data received from Connec! when trying to send batch request for #{self.class.connec_entity_name.pluralize}" unless response && !response.body.blank?
260
- response = JSON.parse(response.body)
261
-
262
- # Parse barch response
263
- response['results'].each_with_index do |result, index|
264
- if result['status'] == 200
265
- batch_entities[index][:idmap].update_attributes(last_push_to_connec: Time.now, message: nil)
266
- elsif result['status'] == 201
267
- batch_entities[index][:idmap].update_attributes(connec_id: result['body'][self.class.normalize_connec_entity_name(connec_entity_name)]['id'], last_push_to_connec: Time.now, message: nil)
268
- else
269
- Maestrano::Connector::Rails::ConnectorLogger.log('error', organization, "Error while pushing to Connec!: #{result['body']}")
270
- batch_entities[index][:idmap].update_attributes(message: result['body'].truncate(255))
271
- end
272
- end
273
- start += request_per_call
274
- end
204
+ proc = lambda{|mapped_external_entity_with_idmap| batch_op('post', mapped_external_entity_with_idmap[:entity], nil, self.class.normalize_connec_entity_name(connec_entity_name))}
205
+ batch_calls(mapped_external_entities_with_idmaps, proc, connec_entity_name)
275
206
  end
276
207
 
277
- def batch_op(method, mapped_external_entity, id, connec_entity_name, organization)
278
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending #{method.upcase} #{connec_entity_name}: #{mapped_external_entity} to Connec! (Preparing batch request)")
208
+ def batch_op(method, mapped_external_entity, id, connec_entity_name)
209
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Sending #{method.upcase} #{connec_entity_name}: #{mapped_external_entity} to Connec! (Preparing batch request)")
279
210
  {
280
211
  method: method,
281
- url: "/api/v2/#{organization.uid}/#{connec_entity_name}" + (id.nil? ? '' : "/#{id}"),
212
+ url: "/api/v2/#{@organization.uid}/#{connec_entity_name}/#{id}", # id should be nil for POST
282
213
  params: {
283
214
  "#{connec_entity_name}".to_sym => mapped_external_entity
284
215
  }
285
216
  }
286
217
  end
287
218
 
288
- def map_to_external_with_idmap(entity, organization)
289
- idmap = self.class.find_idmap({connec_id: entity['id'], organization_id: organization.id})
290
-
291
- if idmap
292
- return nil if idmap.external_inactive || !idmap.to_external
293
-
294
- if idmap.last_push_to_external && idmap.last_push_to_external > entity['updated_at']
295
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Discard Connec! #{self.class.connec_entity_name} : #{entity}")
296
- nil
297
- else
298
- idmap.update(name: self.class.object_name_from_connec_entity_hash(entity))
299
- {entity: map_to_external(entity, organization), idmap: idmap}
300
- end
301
- else
302
- {entity: map_to_external(entity, organization), idmap: self.class.create_idmap_from_connec_entity(entity, organization)}
303
- end
304
- end
305
-
306
219
  # ----------------------------------------------
307
220
  # External methods
308
221
  # ----------------------------------------------
309
- def get_external_entities(client, last_synchronization, organization, opts={})
310
- return [] unless self.class.can_read_external?
311
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Fetching #{Maestrano::Connector::Rails::External.external_name} #{self.class.external_entity_name.pluralize}")
222
+ def get_external_entities_wrapper(last_synchronization)
223
+ return [] if @opts[:skip_external] || !self.class.can_read_external?
224
+ get_external_entities(last_synchronization)
225
+ end
226
+
227
+ def get_external_entities(last_synchronization)
228
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Fetching #{Maestrano::Connector::Rails::External.external_name} #{self.class.external_entity_name.pluralize}")
312
229
  raise "Not implemented"
313
230
  end
314
231
 
315
- def push_entities_to_external(external_client, mapped_connec_entities_with_idmaps, organization)
316
- push_entities_to_external_to(external_client, mapped_connec_entities_with_idmaps, self.class.external_entity_name, organization)
232
+ def push_entities_to_external(mapped_connec_entities_with_idmaps)
233
+ push_entities_to_external_to(mapped_connec_entities_with_idmaps, self.class.external_entity_name)
317
234
  end
318
235
 
319
- def push_entities_to_external_to(external_client, mapped_connec_entities_with_idmaps, external_entity_name, organization)
236
+ def push_entities_to_external_to(mapped_connec_entities_with_idmaps, external_entity_name)
320
237
  return unless self.class.can_write_external?
321
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending Connec! #{self.class.connec_entity_name.pluralize} to #{Maestrano::Connector::Rails::External.external_name} #{external_entity_name.pluralize}")
322
- mapped_connec_entities_with_idmaps.each do |mapped_connec_entity_with_idmap|
323
- push_entity_to_external(external_client, mapped_connec_entity_with_idmap, external_entity_name, organization)
238
+
239
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Sending Connec! #{self.class.connec_entity_name.pluralize} to #{Maestrano::Connector::Rails::External.external_name} #{external_entity_name.pluralize}")
240
+ ids_to_send_to_connec = mapped_connec_entities_with_idmaps.map{ |mapped_connec_entity_with_idmap|
241
+ push_entity_to_external(mapped_connec_entity_with_idmap, external_entity_name)
242
+ }.compact
243
+
244
+ unless ids_to_send_to_connec.empty?
245
+ # Send the external ids to connec if it was a creation
246
+ proc = lambda{|id| batch_op('put', {id: [Maestrano::Connector::Rails::ConnecHelper.id_hash(id[:external_id], @organization)]}, id[:connec_id], self.class.normalize_connec_entity_name(self.class.connec_entity_name)) }
247
+ batch_calls(ids_to_send_to_connec, proc, self.class.connec_entity_name, true)
324
248
  end
325
249
  end
326
250
 
327
- def push_entity_to_external(external_client, mapped_connec_entity_with_idmap, external_entity_name, organization)
251
+
252
+ def push_entity_to_external(mapped_connec_entity_with_idmap, external_entity_name)
328
253
  idmap = mapped_connec_entity_with_idmap[:idmap]
329
- connec_entity = mapped_connec_entity_with_idmap[:entity]
254
+ mapped_connec_entity = mapped_connec_entity_with_idmap[:entity]
330
255
 
331
256
  begin
257
+ # Create and return id to send to connec!
332
258
  if idmap.external_id.blank?
333
- external_id = create_external_entity(external_client, connec_entity, external_entity_name, organization)
334
- idmap.update_attributes(external_id: external_id, last_push_to_external: Time.now, message: nil)
259
+ connec_id = mapped_connec_entity_with_idmap[:idmap].connec_id
260
+ external_id = create_external_entity(mapped_connec_entity, external_entity_name)
261
+ idmap.update(external_id: external_id, last_push_to_external: Time.now, message: nil)
262
+ return {connec_id: connec_id, external_id: external_id, idmap: idmap}
263
+
264
+ # Update
335
265
  else
336
266
  return unless self.class.can_update_external?
337
- update_external_entity(external_client, connec_entity, idmap.external_id, external_entity_name, organization)
338
- idmap.update_attributes(last_push_to_external: Time.now, message: nil)
267
+ update_external_entity(mapped_connec_entity, idmap.external_id, external_entity_name)
268
+
269
+ # Return the id to send it to connec! if the first push of a singleton
270
+ if self.class.singleton? && idmap.last_push_to_external.nil?
271
+ connec_id = mapped_connec_entity_with_idmap[:idmap].connec_id
272
+ idmap.update(last_push_to_external: Time.now, message: nil)
273
+ return {connec_id: connec_id, external_id: idmap.external_id}
274
+ else
275
+ idmap.update(last_push_to_external: Time.now, message: nil)
276
+ end
277
+
339
278
  end
340
279
  rescue => e
341
280
  # Store External error
342
- Maestrano::Connector::Rails::ConnectorLogger.log('error', organization, "Error while pushing to #{Maestrano::Connector::Rails::External.external_name}: #{e}")
343
- idmap.update_attributes(message: e.message.truncate(255))
281
+ Maestrano::Connector::Rails::ConnectorLogger.log('error', @organization, "Error while pushing to #{Maestrano::Connector::Rails::External.external_name}: #{e}")
282
+ idmap.update(message: e.message.truncate(255))
344
283
  end
284
+ nil
345
285
  end
346
286
 
347
- def create_external_entity(client, mapped_connec_entity, external_entity_name, organization)
348
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending create #{external_entity_name}: #{mapped_connec_entity} to #{Maestrano::Connector::Rails::External.external_name}")
287
+ def create_external_entity(mapped_connec_entity, external_entity_name)
288
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Sending create #{external_entity_name}: #{mapped_connec_entity} to #{Maestrano::Connector::Rails::External.external_name}")
349
289
  raise "Not implemented"
350
290
  end
351
291
 
352
- def update_external_entity(client, mapped_connec_entity, external_id, external_entity_name, organization)
353
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending update #{external_entity_name} (id=#{external_id}): #{mapped_connec_entity} to #{Maestrano::Connector::Rails::External.external_name}")
292
+ def update_external_entity(mapped_connec_entity, external_id, external_entity_name)
293
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Sending update #{external_entity_name} (id=#{external_id}): #{mapped_connec_entity} to #{Maestrano::Connector::Rails::External.external_name}")
354
294
  raise "Not implemented"
355
295
  end
356
296
 
357
297
  # This method is called during the webhook workflow only. It should return the array of filtered entities
358
298
  # The aim is to have the same filtering as with the Connec! filters on API calls in the webhooks
359
- def filter_connec_entities(entities, organization, opts={})
299
+ def filter_connec_entities(entities)
360
300
  entities
361
301
  end
302
+
362
303
  # ----------------------------------------------
363
304
  # General methods
364
305
  # ----------------------------------------------
365
306
  # * Discards entities that do not need to be pushed because they have not been updated since their last push
366
307
  # * Discards entities from one of the two source in case of conflict
367
308
  # * Maps not discarded entities and associates them with their idmap, or create one if there isn't any
368
- # * Return a hash {connec_entities: [], external_entities: []}
369
- def consolidate_and_map_data(connec_entities, external_entities, organization, opts={})
370
- return consolidate_and_map_singleton(connec_entities, external_entities, organization, opts) if self.class.singleton?
309
+ # * Returns a hash {connec_entities: [], external_entities: []}
310
+ def consolidate_and_map_data(connec_entities, external_entities)
311
+ return consolidate_and_map_singleton(connec_entities, external_entities) if self.class.singleton?
312
+
313
+ mapped_connec_entities = consolidate_and_map_connec_entities(connec_entities, external_entities, self.class.references, self.class.external_entity_name)
314
+ mapped_external_entities = consolidate_and_map_external_entities(external_entities, self.class.connec_entity_name)
371
315
 
372
- mapped_external_entities = external_entities.map{|entity|
373
- idmap = self.class.find_idmap({external_id: self.class.id_from_external_entity_hash(entity), organization_id: organization.id})
316
+ return {connec_entities: mapped_connec_entities, external_entities: mapped_external_entities}
317
+ end
318
+
319
+ def consolidate_and_map_connec_entities(connec_entities, external_entities, references, external_entity_name)
320
+ connec_entities.map{|entity|
321
+ entity = Maestrano::Connector::Rails::ConnecHelper.unfold_references(entity, references, @organization)
322
+ next nil unless entity
323
+ connec_id = entity.delete(:__connec_id)
374
324
 
375
- # No idmap: creating one, nothing else to do
376
- unless idmap
377
- next {entity: map_to_connec(entity, organization), idmap: self.class.create_idmap_from_external_entity(entity, organization)}
325
+ if entity['id'].blank?
326
+ idmap = self.class.create_idmap(organization_id: @organization.id, name: self.class.object_name_from_connec_entity_hash(entity), external_entity: external_entity_name.downcase, connec_id: connec_id)
327
+ next map_connec_entity_with_idmap(entity, external_entity_name, idmap)
378
328
  end
379
329
 
330
+ idmap = self.class.find_or_create_idmap(external_id: entity['id'], organization_id: @organization.id, external_entity: external_entity_name.downcase, connec_id: connec_id)
331
+ idmap.update(name: self.class.object_name_from_connec_entity_hash(entity))
332
+
333
+ next nil if idmap.external_inactive || !idmap.to_external || (!@opts[:full_sync] && not_modified_since_last_push_to_external?(idmap, entity))
334
+
335
+ # Check for conflict with entities from external
336
+ solve_conflict(entity, external_entities, external_entity_name, idmap)
337
+ }.compact
338
+ end
339
+
340
+ def consolidate_and_map_external_entities(external_entities, connec_entity_name)
341
+ external_entities.map{|entity|
342
+ entity_id = self.class.id_from_external_entity_hash(entity)
343
+ idmap = self.class.find_or_create_idmap(external_id: entity_id, organization_id: @organization.id, connec_entity: connec_entity_name.downcase)
344
+
380
345
  # Not pushing entity to Connec!
381
346
  next nil unless idmap.to_connec
382
347
 
383
348
  # Not pushing to Connec! and flagging as inactive if inactive in external application
384
349
  inactive = self.class.inactive_from_external_entity_hash?(entity)
385
- idmap.update(external_inactive: inactive)
350
+ idmap.update(external_inactive: inactive, name: self.class.object_name_from_external_entity_hash(entity))
386
351
  next nil if inactive
387
352
 
388
353
  # Entity has not been modified since its last push to connec!
389
- next nil if self.class.not_modified_since_last_push_to_connec?(idmap, entity, self, organization)
354
+ next nil if !@opts[:full_sync] && not_modified_since_last_push_to_connec?(idmap, entity)
390
355
 
391
- idmap.update(name: self.class.object_name_from_external_entity_hash(entity))
392
356
 
393
- # Check for conflict with entities from connec!
394
- self.class.solve_conflict(entity, self, connec_entities, self.class.connec_entity_name, idmap, organization, opts)
357
+ map_external_entity_with_idmap(entity, connec_entity_name, idmap)
395
358
  }.compact
396
-
397
- mapped_connec_entities = connec_entities.map{|entity|
398
- map_to_external_with_idmap(entity, organization)
399
- }.compact
400
-
401
- return {connec_entities: mapped_connec_entities, external_entities: mapped_external_entities}
402
359
  end
403
360
 
404
- def consolidate_and_map_singleton(connec_entities, external_entities, organization, opts={})
361
+ def consolidate_and_map_singleton(connec_entities, external_entities)
405
362
  return {connec_entities: [], external_entities: []} if external_entities.empty? && connec_entities.empty?
406
363
 
407
- idmap = self.class.find_or_create_idmap({organization_id: organization.id})
364
+ idmap = self.class.find_or_create_idmap({organization_id: @organization.id})
365
+ # No to_connec, to_external and inactive consideration here as we don't expect those workflow for singleton
408
366
 
409
367
  if external_entities.empty?
410
368
  keep_external = false
411
369
  elsif connec_entities.empty?
412
370
  keep_external = true
413
- elsif !opts[:connec_preemption].nil?
414
- keep_external = !opts[:connec_preemption]
371
+ elsif @opts.has_key?(:connec_preemption)
372
+ keep_external = !@opts[:connec_preemption]
415
373
  else
416
- keep_external = self.class.is_external_more_recent?(connec_entities.first, external_entities.first, self)
374
+ keep_external = !is_connec_more_recent?(connec_entities.first, external_entities.first)
417
375
  end
376
+
418
377
  if keep_external
419
378
  idmap.update(external_id: self.class.id_from_external_entity_hash(external_entities.first), name: self.class.object_name_from_external_entity_hash(external_entities.first))
420
- return {connec_entities: [], external_entities: [{entity: map_to_connec(external_entities.first, organization), idmap: idmap}]}
379
+ return {connec_entities: [], external_entities: [{entity: map_to_connec(external_entities.first), idmap: idmap}]}
421
380
  else
422
- idmap.update(connec_id: connec_entities.first['id'], name: self.class.object_name_from_connec_entity_hash(connec_entities.first))
423
- return {connec_entities: [{entity: map_to_external(connec_entities.first, organization), idmap: idmap}], external_entities: []}
381
+ entity = Maestrano::Connector::Rails::ConnecHelper.unfold_references(connec_entities.first, self.class.references, @organization)
382
+ idmap.update(name: self.class.object_name_from_connec_entity_hash(entity), connec_id: entity.delete(:__connec_id))
383
+ idmap.update(external_id: self.class.id_from_external_entity_hash(external_entities.first)) unless external_entities.empty?
384
+ return {connec_entities: [{entity: map_to_external(entity), idmap: idmap}], external_entities: []}
424
385
  end
425
386
  end
426
387
 
427
388
  # ----------------------------------------------
428
389
  # After and before sync
429
390
  # ----------------------------------------------
430
- def before_sync(connec_client, external_client, last_synchronization, organization, opts)
391
+ def before_sync(last_synchronization)
431
392
  # Does nothing by default
432
393
  end
433
394
 
434
- def after_sync(connec_client, external_client, last_synchronization, organization, opts)
395
+ def after_sync(last_synchronization)
435
396
  # Does nothing by default
436
397
  end
437
398
 
399
+
438
400
  # ----------------------------------------------
439
401
  # Internal helper methods
440
402
  # ----------------------------------------------
441
- module ClassMethods
442
- def not_modified_since_last_push_to_connec?(idmap, entity, entity_instance, organization)
443
- result = idmap.last_push_to_connec && idmap.last_push_to_connec > entity_instance.class.last_update_date_from_external_entity_hash(entity)
444
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Discard #{Maestrano::Connector::Rails::External::external_name} #{entity_instance.class.external_entity_name} : #{entity}") if result
445
- result
403
+ private
404
+ # array_with_idmap must be an array of hashes with a key idmap
405
+ # proc is a lambda to create a batch_op from an element of the array
406
+ def batch_calls(array_with_idmap, proc, connec_entity_name, id_update_only=false)
407
+ request_per_call = @opts[:request_per_batch_call] || 100
408
+ start = 0
409
+ while start < array_with_idmap.size
410
+ # Prepare batch request
411
+ batch_entities = array_with_idmap.slice(start, request_per_call)
412
+ batch_request = {sequential: true, ops: []}
413
+
414
+ batch_entities.each do |id|
415
+ batch_request[:ops] << proc.call(id)
416
+ end
417
+
418
+ # Batch call
419
+ log_info = id_update_only ? 'with only ids' : ''
420
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Sending batch request to Connec! #{log_info} for #{self.class.normalize_connec_entity_name(connec_entity_name)}. Batch_request_size: #{batch_request[:ops].size}. Call_number: #{(start/request_per_call) + 1}")
421
+ response = @connec_client.batch(batch_request)
422
+ Maestrano::Connector::Rails::ConnectorLogger.log('debug', @organization, "Received batch response from Connec! for #{self.class.normalize_connec_entity_name(connec_entity_name)}: #{response}")
423
+ raise "No data received from Connec! when trying to send batch request #{log_info} for #{self.class.connec_entity_name.pluralize}" unless response && !response.body.blank?
424
+ response = JSON.parse(response.body)
425
+
426
+ # Parse batch response
427
+ response['results'].each_with_index do |result, index|
428
+ if result['status'] == 200
429
+ batch_entities[index][:idmap].update(connec_id: result['body'][self.class.normalize_connec_entity_name(connec_entity_name)]['id'].find{|id| id['provider'] == 'connec'}['id'], last_push_to_connec: Time.now, message: nil) unless id_update_only # id_update_only only apply for 200 as it's doing PUTs
430
+ elsif result['status'] == 201
431
+ batch_entities[index][:idmap].update(connec_id: result['body'][self.class.normalize_connec_entity_name(connec_entity_name)]['id'].find{|id| id['provider'] == 'connec'}['id'], last_push_to_connec: Time.now, message: nil)
432
+ else
433
+ Maestrano::Connector::Rails::ConnectorLogger.log('error', @organization, "Error while pushing to Connec!: #{result['body']}")
434
+ batch_entities[index][:idmap].update(message: result['body'].to_s.truncate(255))
435
+ end
436
+ end
437
+ start += request_per_call
438
+ end
439
+ end
440
+
441
+ def not_modified_since_last_push_to_connec?(idmap, entity)
442
+ not_modified = idmap.last_push_to_connec && idmap.last_push_to_connec > self.class.last_update_date_from_external_entity_hash(entity)
443
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Discard #{Maestrano::Connector::Rails::External::external_name} #{self.class.external_entity_name} : #{entity}") if not_modified
444
+ not_modified
445
+ end
446
+
447
+ def not_modified_since_last_push_to_external?(idmap, entity)
448
+ not_modified = idmap.last_push_to_external && idmap.last_push_to_external > entity['updated_at']
449
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Discard Connec! #{self.class.connec_entity_name} : #{entity}") if not_modified
450
+ not_modified
446
451
  end
447
452
 
448
- def is_external_more_recent?(connec_entity, external_entity, entity_instance)
449
- connec_entity['updated_at'] < entity_instance.class.last_update_date_from_external_entity_hash(external_entity)
453
+ def is_connec_more_recent?(connec_entity, external_entity)
454
+ connec_entity['updated_at'] > self.class.last_update_date_from_external_entity_hash(external_entity)
450
455
  end
451
456
 
452
- def solve_conflict(external_entity, entity_instance, connec_entities, connec_entity_name, idmap, organization, opts)
453
- if idmap.connec_id && connec_entity = connec_entities.detect{|connec_entity| connec_entity['id'] == idmap.connec_id}
457
+ def solve_conflict(connec_entity, external_entities, external_entity_name, idmap)
458
+ if external_entity = external_entities.find{|external_entity| connec_entity['id'] == self.class.id_from_external_entity_hash(external_entity)}
454
459
  # We keep the most recently updated entity
455
- if !opts[:connec_preemption].nil?
456
- keep_external = !opts[:connec_preemption]
460
+ if @opts.has_key?(:connec_preemption)
461
+ keep_connec = @opts[:connec_preemption]
457
462
  else
458
- keep_external = is_external_more_recent?(connec_entity, external_entity, entity_instance)
463
+ keep_connec = is_connec_more_recent?(connec_entity, external_entity)
459
464
  end
460
465
 
461
- if keep_external
462
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Conflict between #{Maestrano::Connector::Rails::External::external_name} #{entity_instance.class.external_entity_name} #{external_entity} and Connec! #{connec_entity_name} #{connec_entity}. Entity from external kept")
463
- connec_entities.delete(connec_entity)
464
- entity_instance.map_external_entity_with_idmap(external_entity, connec_entity_name, idmap, organization)
466
+ if keep_connec
467
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Conflict between #{Maestrano::Connector::Rails::External::external_name} #{external_entity_name} #{external_entity} and Connec! #{self.class.connec_entity_name} #{connec_entity}. Entity from external kept")
468
+ external_entities.delete(external_entity)
469
+ map_connec_entity_with_idmap(connec_entity, external_entity_name, idmap)
465
470
  else
466
- Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Conflict between #{Maestrano::Connector::Rails::External::external_name} #{entity_instance.class.external_entity_name} #{external_entity} and Connec! #{connec_entity_name} #{connec_entity}. Entity from Connec! kept")
471
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', @organization, "Conflict between #{Maestrano::Connector::Rails::External::external_name} #{external_entity_name} #{external_entity} and Connec! #{self.class.connec_entity_name} #{connec_entity}. Entity from Connec! kept")
467
472
  nil
468
473
  end
469
474
 
470
475
  else
471
- entity_instance.map_external_entity_with_idmap(external_entity, connec_entity_name, idmap, organization)
476
+ map_connec_entity_with_idmap(connec_entity, external_entity_name, idmap)
472
477
  end
473
478
  end
474
479
 
475
- def id_from_ref(entity, ref, is_external, organization)
476
- # field can be address/billing/country_id
477
- field = is_external ? ref[:external_field] : ref[:connec_field]
478
- field = field.split('/')
479
- id = entity
480
- field.each do |f|
481
- id &&= id[f]
482
- end
480
+ def map_connec_entity_with_idmap(connec_entity, external_entity_name, idmap)
481
+ {entity: map_to_external(connec_entity), idmap: idmap}
482
+ end
483
483
 
484
- if is_external
485
- idmap = ref[:reference_class].find_idmap({external_id: id, organization_id: organization.id})
486
- idmap && idmap.connec_id
487
- else
488
- idmap = ref[:reference_class].find_idmap({connec_id: id, organization_id: organization.id})
489
- idmap && idmap.external_id
490
- end
484
+ def map_external_entity_with_idmap(external_entity, connec_entity_name, idmap)
485
+ {entity: map_to_connec(external_entity), idmap: idmap}
486
+ end
487
+
488
+ def fetch_connec(uri, page_number)
489
+ response = @connec_client.get(uri)
490
+ raise "No data received from Connec! when trying to fetch page #{page_number} of #{self.class.normalized_connec_entity_name}" unless response && !response.body.blank?
491
+
492
+ response_hash = JSON.parse(response.body)
493
+ Maestrano::Connector::Rails::ConnectorLogger.log('debug', @organization, "received first page entity=#{self.class.connec_entity_name}, response=#{response_hash}")
494
+ raise "Received unrecognized Connec! data when trying to fetch page #{page_number} of #{self.class.normalized_connec_entity_name}: #{response_hash}" unless response_hash["#{self.class.normalized_connec_entity_name}"]
495
+
496
+ response_hash
491
497
  end
492
- end
493
498
 
494
- def map_external_entity_with_idmap(external_entity, connec_entity_name, idmap, organization)
495
- {entity: map_to_connec(external_entity, organization), idmap: idmap}
496
- end
497
499
  end