maestrano-connector-rails 0.4.4 → 1.0.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 (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