maestrano-connector-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/DEVELOPER.md +10 -0
  4. data/Gemfile +18 -0
  5. data/Gemfile.lock +222 -0
  6. data/LICENSE +21 -0
  7. data/README.md +138 -0
  8. data/Rakefile +37 -0
  9. data/VERSION +1 -0
  10. data/app/controllers/maestrano/account/group_users_controller.rb +20 -0
  11. data/app/controllers/maestrano/account/groups_controller.rb +22 -0
  12. data/app/controllers/maestrano/application_controller.rb +7 -0
  13. data/app/controllers/maestrano/auth/saml_controller.rb +31 -0
  14. data/app/controllers/maestrano/sessions_controller.rb +15 -0
  15. data/app/helpers/maestrano/connector/rails/session_helper.rb +17 -0
  16. data/app/jobs/maestrano/connector/rails/synchronization_job.rb +49 -0
  17. data/app/models/maestrano/connector/rails/complex_entity.rb +175 -0
  18. data/app/models/maestrano/connector/rails/concerns/entity.rb +231 -0
  19. data/app/models/maestrano/connector/rails/concerns/external.rb +13 -0
  20. data/app/models/maestrano/connector/rails/connector_logger.rb +9 -0
  21. data/app/models/maestrano/connector/rails/entity.rb +5 -0
  22. data/app/models/maestrano/connector/rails/external.rb +5 -0
  23. data/app/models/maestrano/connector/rails/id_map.rb +6 -0
  24. data/app/models/maestrano/connector/rails/organization.rb +60 -0
  25. data/app/models/maestrano/connector/rails/sub_entity_base.rb +32 -0
  26. data/app/models/maestrano/connector/rails/synchronization.rb +38 -0
  27. data/app/models/maestrano/connector/rails/user.rb +25 -0
  28. data/app/models/maestrano/connector/rails/user_organization_rel.rb +9 -0
  29. data/bin/rails +12 -0
  30. data/config/routes.rb +28 -0
  31. data/db/migrate/20151122162100_create_maestrano_connector_rails_users.rb +16 -0
  32. data/db/migrate/20151122162414_create_maestrano_connector_rails_organizations.rb +21 -0
  33. data/db/migrate/20151122162613_create_maestrano_connector_rails_user_organization_rels.rb +12 -0
  34. data/db/migrate/20151122163325_create_maestrano_connector_rails_synchronizations.rb +13 -0
  35. data/db/migrate/20151122163449_create_maestrano_connector_rails_id_maps.rb +18 -0
  36. data/lib/generators/connector/USAGE +2 -0
  37. data/lib/generators/connector/complex_entity_generator.rb +19 -0
  38. data/lib/generators/connector/install_generator.rb +70 -0
  39. data/lib/generators/connector/templates/admin_controller.rb +48 -0
  40. data/lib/generators/connector/templates/admin_index.html.erb +51 -0
  41. data/lib/generators/connector/templates/complex_entity_example/contact.rb +22 -0
  42. data/lib/generators/connector/templates/complex_entity_example/contact_and_lead.rb +56 -0
  43. data/lib/generators/connector/templates/complex_entity_example/contact_mapper.rb +17 -0
  44. data/lib/generators/connector/templates/complex_entity_example/lead.rb +22 -0
  45. data/lib/generators/connector/templates/complex_entity_example/lead_mapper.rb +15 -0
  46. data/lib/generators/connector/templates/complex_entity_example/person.rb +23 -0
  47. data/lib/generators/connector/templates/contact_and_lead.rb +55 -0
  48. data/lib/generators/connector/templates/entity.rb +48 -0
  49. data/lib/generators/connector/templates/example_entity.rb +26 -0
  50. data/lib/generators/connector/templates/external.rb +19 -0
  51. data/lib/generators/connector/templates/home_controller.rb +17 -0
  52. data/lib/generators/connector/templates/home_index.html.erb +42 -0
  53. data/lib/generators/connector/templates/oauth_controller.rb +45 -0
  54. data/lib/maestrano-connector-rails.rb +1 -0
  55. data/lib/maestrano/connector/rails.rb +13 -0
  56. data/maestrano-connector-rails.gemspec +197 -0
  57. data/maestrano.png +0 -0
  58. data/spec/dummy/README.md +1 -0
  59. data/spec/dummy/Rakefile +6 -0
  60. data/spec/dummy/app/assets/images/.keep +0 -0
  61. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  62. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  63. data/spec/dummy/app/controllers/admin_controller.rb +48 -0
  64. data/spec/dummy/app/controllers/application_controller.rb +8 -0
  65. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  66. data/spec/dummy/app/controllers/home_controller.rb +17 -0
  67. data/spec/dummy/app/controllers/oauth_controller.rb +45 -0
  68. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  69. data/spec/dummy/app/mailers/.keep +0 -0
  70. data/spec/dummy/app/models/.keep +0 -0
  71. data/spec/dummy/app/models/concerns/.keep +0 -0
  72. data/spec/dummy/app/models/entities/example_entitiy.rb +26 -0
  73. data/spec/dummy/app/models/maestrano/connector/rails/entity.rb +10 -0
  74. data/spec/dummy/app/models/maestrano/connector/rails/external.rb +11 -0
  75. data/spec/dummy/app/views/admin/index.html.erb +51 -0
  76. data/spec/dummy/app/views/home/index.html.erb +36 -0
  77. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  78. data/spec/dummy/bin/bundle +3 -0
  79. data/spec/dummy/bin/rails +4 -0
  80. data/spec/dummy/bin/rake +4 -0
  81. data/spec/dummy/bin/setup +29 -0
  82. data/spec/dummy/config.ru +4 -0
  83. data/spec/dummy/config/application.rb +26 -0
  84. data/spec/dummy/config/boot.rb +5 -0
  85. data/spec/dummy/config/database.yml +25 -0
  86. data/spec/dummy/config/environment.rb +5 -0
  87. data/spec/dummy/config/environments/development.rb +41 -0
  88. data/spec/dummy/config/environments/production.rb +79 -0
  89. data/spec/dummy/config/environments/test.rb +42 -0
  90. data/spec/dummy/config/initializers/assets.rb +11 -0
  91. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  92. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  93. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  94. data/spec/dummy/config/initializers/inflections.rb +16 -0
  95. data/spec/dummy/config/initializers/maestrano.rb +135 -0
  96. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  97. data/spec/dummy/config/initializers/session_store.rb +3 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +23 -0
  100. data/spec/dummy/config/routes.rb +9 -0
  101. data/spec/dummy/config/secrets.yml +22 -0
  102. data/spec/dummy/db/development.sqlite3 +0 -0
  103. data/spec/dummy/db/migrate/20151209134445_create_maestrano_connector_rails_users.maestrano_connector_rails.rb +17 -0
  104. data/spec/dummy/db/migrate/20151209134446_create_maestrano_connector_rails_organizations.maestrano_connector_rails.rb +22 -0
  105. data/spec/dummy/db/migrate/20151209134447_create_maestrano_connector_rails_user_organization_rels.maestrano_connector_rails.rb +13 -0
  106. data/spec/dummy/db/migrate/20151209134448_create_maestrano_connector_rails_synchronizations.maestrano_connector_rails.rb +14 -0
  107. data/spec/dummy/db/migrate/20151209134449_create_maestrano_connector_rails_id_maps.maestrano_connector_rails.rb +19 -0
  108. data/spec/dummy/db/schema.rb +83 -0
  109. data/spec/dummy/lib/assets/.keep +0 -0
  110. data/spec/dummy/log/.keep +0 -0
  111. data/spec/dummy/public/404.html +67 -0
  112. data/spec/dummy/public/422.html +67 -0
  113. data/spec/dummy/public/500.html +66 -0
  114. data/spec/dummy/public/favicon.ico +0 -0
  115. data/spec/factories.rb +33 -0
  116. data/spec/jobs/syncrhonization_job_spec.rb +53 -0
  117. data/spec/models/complex_entity_spec.rb +353 -0
  118. data/spec/models/connector_logger_spec.rb +21 -0
  119. data/spec/models/entity_spec.rb +457 -0
  120. data/spec/models/external_spec.rb +15 -0
  121. data/spec/models/id_map_spec.rb +12 -0
  122. data/spec/models/organizaztion_spec.rb +105 -0
  123. data/spec/models/sub_entity_base_spec.rb +51 -0
  124. data/spec/models/synchronization_spec.rb +75 -0
  125. data/spec/models/user_organization_rel_spec.rb +14 -0
  126. data/spec/models/user_spec.rb +15 -0
  127. data/spec/spec_helper.rb +19 -0
  128. metadata +327 -0
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,20 @@
1
+ class Maestrano::Account::GroupUsersController < Maestrano::Rails::WebHookController
2
+
3
+ # DELETE /maestrano/account/groups/cld-1/users/usr-1
4
+ # DELETE /maestrano/account/groups/cld-1/users/usr-1/tenant
5
+ # Remove a user from a group
6
+ def destroy
7
+ # Set the right uid based on Maestrano.param('sso.creation_mode')
8
+ user_uid = Maestrano.mask_user(params[:id], params[:group_id])
9
+ group_uid = params[:group_id]
10
+
11
+ # Get the entities
12
+ user = User.find_by_provider_and_uid_and_tenant('maestrano', user_uid, params[:tenant])
13
+ organization = Organization.find_by_provider_and_uid_and_tenant('maestrano', group_uid, params[:tenant])
14
+
15
+ # Remove the user from the organization
16
+ organization.remove_member(user)
17
+
18
+ render json: {success: true}
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ class Maestrano::Account::GroupsController < Maestrano::Rails::WebHookController
2
+
3
+ # DELETE /maestrano/account/groups/cld-1
4
+ # DELETE /maestrano/account/groups/cld-1/tenant
5
+ # Delete an entire group
6
+ def destroy
7
+ # id
8
+ group_uid = params[:id]
9
+
10
+ # Get entity
11
+ organization = Organization.find_by_provider_and_uid_and_tenant('maestrano', group_uid, params[:tenant])
12
+
13
+ # Delete all relations
14
+ organization.user_company_rels.delete_all
15
+
16
+ # Delete the organization
17
+ organization.destroy
18
+
19
+ # Respond
20
+ render json: {success: true}
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Maestrano
2
+ class ApplicationController < ActionController::Base
3
+ include SessionHelper
4
+
5
+ protect_from_forgery with: :exception
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ class Maestrano::Auth::SamlController < Maestrano::Rails::SamlBaseController
2
+ #== POST '/maestrano/auth/saml/consume'
3
+ # Final phase of the Single Sign-On handshake. Find or create
4
+ # the required resources (user and group) and sign the user
5
+ # in
6
+ #
7
+ # This action is left to you to customize based on your application
8
+ # requirements. Below is presented a potential way of writing
9
+ # the action.
10
+ #
11
+ # Assuming you have enabled maestrano on a user model
12
+ # called 'User' and a group model called 'Organization'
13
+ # the action could be written the following way
14
+ def consume
15
+ params[:tenant] ||= 'default'
16
+ user = Maestrano::Connector::Rails::User.find_or_create_for_maestrano(user_auth_hash, params[:tenant])
17
+ organization = Maestrano::Connector::Rails::Organization.find_or_create_for_maestrano(group_auth_hash, params[:tenant])
18
+ if user && organization
19
+ unless organization.member?(user)
20
+ organization.add_member(user)
21
+ end
22
+
23
+ session[:tenant] = params[:tenant]
24
+ session[:uid] = user.uid
25
+ session[:org_uid] = organization.uid
26
+ session[:"role_#{organization.uid}"] = user_group_rel_hash[:role]
27
+ end
28
+
29
+ redirect_to main_app.root_path
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module Maestrano
2
+ class SessionsController < ApplicationController
3
+ # Logout
4
+ def destroy
5
+ session.delete(:uid)
6
+ session.delete(:"role_#{session[:org_uid]}")
7
+ session.delete(:org_uid)
8
+ session.delete(:tenant)
9
+ session.delete(:current_user_id)
10
+ @current_user = nil
11
+
12
+ redirect_to root_url
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Maestrano::Connector::Rails
2
+ module SessionHelper
3
+
4
+ def is_admin?(user, organization)
5
+ organization.member?(user) && session[:"role_#{organization.uid}"] && ['Admin', 'Super Admin'].include?(session[:"role_#{organization.uid}"])
6
+ end
7
+
8
+ def current_organization
9
+ Organization.find_by(uid: session[:org_uid], tenant: session[:tenant])
10
+ end
11
+
12
+ def current_user
13
+ @current_user ||= User.find_by(uid: session[:uid], tenant: session[:tenant])
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ module Maestrano::Connector::Rails
2
+ class SynchronizationJob < Struct.new(:organization, :opts)
3
+
4
+ # Supported options:
5
+ # * :forced => true synchronization has been triggered manually (for logging purposes only)
6
+ # * :only_entities => [person, tasks_list]
7
+ # * :full_sync => true synchronization is performed without date filtering
8
+ # * :connec_preemption => true|false : preemption is always|never given to connec in case of conflict (if not set, the most recently updated entity is kept)
9
+ def perform
10
+ ConnectorLogger.log('info', organization, "Start synchronization, opts=#{opts}")
11
+ current_synchronization = Synchronization.create_running(organization)
12
+
13
+ begin
14
+ last_synchronization = Synchronization.where(organization_id: organization.id, status: 'SUCCESS', partial: false).order(updated_at: :desc).first
15
+ connec_client = Maestrano::Connec::Client.new(organization.uid)
16
+ external_client = External.get_client(organization)
17
+
18
+ if opts[:only_entities]
19
+ ConnectorLogger.log('info', organization, "Synchronization is partial and will synchronize only #{opts[:only_entities].join(' ')}")
20
+ # The synchronization is marked as partial and will not be considered as the last-synchronization for the next sync
21
+ current_synchronization.set_partial
22
+ opts[:only_entities].each do |entity|
23
+ sync_entity(entity, organization, connec_client, external_client, last_synchronization, opts)
24
+ end
25
+ else
26
+ organization.synchronized_entities.select{|k, v| v}.keys.each do |entity|
27
+ sync_entity(entity.to_s, organization, connec_client, external_client, last_synchronization, opts)
28
+ end
29
+ end
30
+
31
+ ConnectorLogger.log('info', organization, "Finished synchronization, organization=#{organization.uid}, status=success")
32
+ current_synchronization.set_success
33
+ rescue => e
34
+ ConnectorLogger.log('info', organization, "Finished synchronization, organization=#{organization.uid}, status=error, message=#{e.message} backtrace=#{e.backtrace.join("\n\t")}")
35
+ current_synchronization.set_error(e.message)
36
+ end
37
+ end
38
+
39
+ def sync_entity(entity, organization, connec_client, external_client, last_synchronization, opts)
40
+ entity_instance = "Entities::#{entity.titleize.split.join}".constantize.new
41
+
42
+ external_entities = entity_instance.get_external_entities(external_client, last_synchronization, organization, opts)
43
+ connec_entities = entity_instance.get_connec_entities(connec_client, last_synchronization, organization, opts)
44
+ entity_instance.consolidate_and_map_data(connec_entities, external_entities, organization, opts)
45
+ entity_instance.push_entities_to_external(external_client, connec_entities, organization)
46
+ entity_instance.push_entities_to_connec(connec_client, external_entities, organization)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,175 @@
1
+ module Maestrano::Connector::Rails
2
+ class ComplexEntity
3
+
4
+ @@external_name = External.external_name
5
+
6
+ # -------------------------------------------------------------
7
+ # Complex specific methods
8
+ # Those methods needs to be implemented in each complex entity
9
+ # -------------------------------------------------------------
10
+ def connec_entities_names
11
+ raise "Not implemented"
12
+ end
13
+
14
+ def external_entities_names
15
+ raise "Not implemented"
16
+ end
17
+
18
+ # input : {
19
+ # connec_entities_names[0]: [unmapped_connec_entitiy1, unmapped_connec_entitiy2],
20
+ # connec_entities_names[1]: [unmapped_connec_entitiy3, unmapped_connec_entitiy4]
21
+ # }
22
+ # output : {
23
+ # connec_entities_names[0]: {
24
+ # external_entities_names[0]: [unmapped_connec_entitiy1, unmapped_connec_entitiy2]
25
+ # },
26
+ # connec_entities_names[1]: {
27
+ # external_entities_names[0]: [unmapped_connec_entitiy3],
28
+ # external_entities_names[1]: [unmapped_connec_entitiy4]
29
+ # }
30
+ # }
31
+ def connec_model_to_external_model!(connec_hash_of_entities)
32
+ raise "Not implemented"
33
+ end
34
+
35
+ # input : {
36
+ # external_entities_names[0]: [unmapped_external_entity1}, unmapped_external_entity2],
37
+ # external_entities_names[1]: [unmapped_external_entity3}, unmapped_external_entity4]
38
+ # }
39
+ # output : {
40
+ # external_entities_names[0]: {
41
+ # connec_entities_names[0]: [unmapped_external_entity1],
42
+ # connec_entities_names[1]: [unmapped_external_entity2]
43
+ # },
44
+ # external_entities_names[1]: {
45
+ # connec_entities_names[0]: [unmapped_external_entity3, unmapped_external_entity4]
46
+ # }
47
+ # }
48
+ def external_model_to_connec_model!(external_hash_of_entities)
49
+ raise "Not implemented"
50
+ end
51
+
52
+ # -------------------------------------------------------------
53
+ # General methods
54
+ # -------------------------------------------------------------
55
+ def map_to_external_with_idmap(entity, organization, connec_entity_name, external_entity_name, sub_entity_instance)
56
+ idmap = IdMap.find_by(connec_id: entity['id'], connec_entity: connec_entity_name.downcase, external_entity: external_entity_name.downcase, organization_id: organization.id)
57
+
58
+ if idmap && idmap.last_push_to_external && idmap.last_push_to_external > entity['updated_at']
59
+ ConnectorLogger.log('info', organization, "Discard Connec! #{connec_entity_name} : #{entity}")
60
+ nil
61
+ else
62
+ {entity: sub_entity_instance.map_to(external_entity_name, entity, organization), idmap: idmap || IdMap.create(connec_id: entity['id'], connec_entity: connec_entity_name.downcase, external_entity: external_entity_name.downcase, organization_id: organization.id)}
63
+ end
64
+ end
65
+
66
+ # -------------------------------------------------------------
67
+ # Entity equivalent methods
68
+ # -------------------------------------------------------------
69
+ def get_connec_entities(client, last_synchronization, organization, opts={})
70
+ entities = ActiveSupport::HashWithIndifferentAccess.new
71
+
72
+ self.connec_entities_names.each do |connec_entity_name|
73
+ sub_entity_instance = "Entities::SubEntities::#{connec_entity_name.titleize.split.join}".constantize.new
74
+ entities[connec_entity_name] = sub_entity_instance.get_connec_entities(client, last_synchronization, organization, opts)
75
+ end
76
+ entities
77
+ end
78
+
79
+ def get_external_entities(client, last_synchronization, organization, opts={})
80
+ entities = ActiveSupport::HashWithIndifferentAccess.new
81
+
82
+ self.external_entities_names.each do |external_entity_name|
83
+ sub_entity_instance = "Entities::SubEntities::#{external_entity_name.titleize.split.join}".constantize.new
84
+ entities[external_entity_name] = sub_entity_instance.get_external_entities(client, last_synchronization, organization, opts)
85
+ end
86
+ entities
87
+ end
88
+
89
+ def consolidate_and_map_data(connec_entities, external_entities, organization, opts)
90
+ external_model_to_connec_model!(external_entities)
91
+ connec_model_to_external_model!(connec_entities)
92
+
93
+ external_entities.each do |external_entity_name, entities_in_connec_model|
94
+ entities_in_connec_model.each do |connec_entity_name, entities|
95
+ sub_entity_instance = "Entities::SubEntities::#{external_entity_name.titleize.split.join}".constantize.new
96
+
97
+ entities.map!{|entity|
98
+ idmap = IdMap.find_by(external_id: sub_entity_instance.get_id_from_external_entity_hash(entity), external_entity: external_entity_name.downcase, connec_entity: connec_entity_name.downcase, organization_id: organization.id)
99
+
100
+ # No idmap: creating one, nothing else to do
101
+ unless idmap
102
+ next {entity: sub_entity_instance.map_to(connec_entity_name, entity, organization), idmap: IdMap.create(external_id: sub_entity_instance.get_id_from_external_entity_hash(entity), external_entity: external_entity_name.downcase, connec_entity: connec_entity_name.downcase, organization_id: organization.id)}
103
+ end
104
+
105
+ # Entity has not been modified since its last push to connec!
106
+ if idmap.last_push_to_connec && idmap.last_push_to_connec > sub_entity_instance.get_last_update_date_from_external_entity_hash(entity)
107
+ ConnectorLogger.log('info', organization, "Discard #{@@external_name} #{external_entity_name} : #{entity}")
108
+ next nil
109
+ end
110
+
111
+ equivalent_connec_entities = connec_entities[connec_entity_name][external_entity_name] || []
112
+ # Check for conflict with entities from connec!
113
+ if idmap.connec_id && connec_entity = equivalent_connec_entities.detect{|connec_entity| connec_entity['id'] == idmap.connec_id}
114
+ # We keep the most recently updated entity
115
+ if !opts[:connec_preemption].nil?
116
+ keep_external = !opts[:connec_preemption]
117
+ else
118
+ keep_external = connec_entity['updated_at'] < sub_entity_instance.get_last_update_date_from_external_entity_hash(entity)
119
+ end
120
+
121
+ if keep_external
122
+ ConnectorLogger.log('info', organization, "Conflict between #{@@external_name} #{external_entity_name} #{entity} and Connec! #{connec_entity_name} #{connec_entity}. Entity from #{@@external_name} kept")
123
+ equivalent_connec_entities.delete(connec_entity)
124
+ {entity: sub_entity_instance.map_to(connec_entity_name, entity, organization), idmap: idmap}
125
+ else
126
+ ConnectorLogger.log('info', organization, "Conflict between #{@@external_name} #{external_entity_name} #{entity} and Connec! #{connec_entity_name} #{connec_entity}. Entity from Connec! kept")
127
+ nil
128
+ end
129
+
130
+ else
131
+ {entity: sub_entity_instance.map_to(connec_entity_name, entity, organization), idmap: idmap}
132
+ end
133
+ }.compact!
134
+ end
135
+ end
136
+
137
+ connec_entities.each do |connec_entity_name, entities_in_external_model|
138
+ entities_in_external_model.each do |external_entity_name, entities|
139
+ sub_entity_instance = "Entities::SubEntities::#{connec_entity_name.titleize.split.join}".constantize.new
140
+ entities.map!{|entity|
141
+ self.map_to_external_with_idmap(entity, organization, connec_entity_name, external_entity_name, sub_entity_instance)
142
+ }.compact!
143
+ end
144
+ end
145
+ end
146
+
147
+ # input : {
148
+ # external_entities_names[0]: {
149
+ # connec_entities_names[0]: [mapped_external_entity1],
150
+ # connec_entities_names[1]: [mapped_external_entity2]
151
+ # },
152
+ # external_entities_names[1]: {
153
+ # connec_entities_names[0]: [mapped_external_entity3, mapped_external_entity4]
154
+ # }
155
+ # }
156
+ def push_entities_to_connec(connec_client, mapped_external_entities_with_idmaps, organization)
157
+ mapped_external_entities_with_idmaps.each do |external_entity_name, entities_in_connec_model|
158
+ sub_entity_instance = "Entities::SubEntities::#{external_entity_name.titleize.split.join}".constantize.new
159
+ entities_in_connec_model.each do |connec_entity_name, mapped_entities_with_idmaps|
160
+ sub_entity_instance.push_entities_to_connec_to(connec_client, mapped_entities_with_idmaps, connec_entity_name, organization)
161
+ end
162
+ end
163
+ end
164
+
165
+
166
+ def push_entities_to_external(external_client, mapped_connec_entities_with_idmaps, organization)
167
+ mapped_connec_entities_with_idmaps.each do |connec_entity_name, entities_in_external_model|
168
+ sub_entity_instance = "Entities::SubEntities::#{connec_entity_name.titleize.split.join}".constantize.new
169
+ entities_in_external_model.each do |external_entity_name, mapped_entities_with_idmaps|
170
+ sub_entity_instance.push_entities_to_external_to(external_client, mapped_entities_with_idmaps, external_entity_name, organization)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,231 @@
1
+ module Maestrano::Connector::Rails::Concerns::Entity
2
+ extend ActiveSupport::Concern
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
11
+ end
12
+
13
+ @@external_name = Maestrano::Connector::Rails::External.external_name
14
+
15
+ # ----------------------------------------------
16
+ # Mapper methods
17
+ # ----------------------------------------------
18
+ # Map a Connec! entity to the external format
19
+ def map_to_external(entity, organization)
20
+ self.mapper_class.normalize(entity)
21
+ end
22
+
23
+ # Map an external entity to Connec! format
24
+ def map_to_connec(entity, organization)
25
+ self.mapper_class.denormalize(entity)
26
+ end
27
+
28
+ # ----------------------------------------------
29
+ # Connec! methods
30
+ # ----------------------------------------------
31
+ def get_connec_entities(client, last_synchronization, organization, opts={})
32
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Fetching Connec! #{self.connec_entity_name}")
33
+
34
+ entities = []
35
+
36
+ # Fetch first page
37
+ if last_synchronization.blank? || opts[:full_sync]
38
+ response = client.get("/#{self.connec_entity_name.downcase.pluralize}")
39
+ else
40
+ query_param = URI.encode("$filter=updated_at gt '#{last_synchronization.updated_at.iso8601}'")
41
+ response = client.get("/#{self.connec_entity_name.downcase.pluralize}?#{query_param}")
42
+ end
43
+ raise "No data received from Connec! when trying to fetch #{self.connec_entity_name.pluralize}" unless response
44
+
45
+ response_hash = JSON.parse(response.body)
46
+ if response_hash["#{self.connec_entity_name.downcase.pluralize}"]
47
+ entities << response_hash["#{self.connec_entity_name.downcase.pluralize}"]
48
+ else
49
+ raise "Received unrecognized Connec! data when trying to fetch #{self.connec_entity_name.pluralize}"
50
+ end
51
+
52
+ # Fetch subsequent pages
53
+ while response_hash['pagination'] && response_hash['pagination']['next']
54
+ # ugly way to convert https://api-connec/api/v2/group_id/organizations?next_page_params to /organizations?next_page_params
55
+ next_page = response_hash['pagination']['next'].gsub(/^(.*)\/#{self.connec_entity_name.downcase.pluralize}/, self.connec_entity_name.downcase.pluralize)
56
+ response = client.get(next_page)
57
+
58
+ raise "No data received from Connec! when trying to fetch subsequent page of #{self.connec_entity_name.pluralize}" unless response
59
+ response_hash = JSON.parse(response.body)
60
+ if response_hash["#{self.connec_entity_name.downcase.pluralize}"]
61
+ entities << response_hash["#{self.connec_entity_name.downcase.pluralize}"]
62
+ else
63
+ raise "Received unrecognized Connec! data when trying to fetch subsequent page of #{self.connec_entity_name.pluralize}"
64
+ end
65
+ end
66
+
67
+ entities = entities.flatten
68
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Received data: Source=Connec!, Entity=#{self.connec_entity_name}, Data=#{entities}")
69
+ entities
70
+ end
71
+
72
+ def push_entities_to_connec(connec_client, mapped_external_entities_with_idmaps, organization)
73
+ self.push_entities_to_connec_to(connec_client, mapped_external_entities_with_idmaps, self.connec_entity_name, organization)
74
+ end
75
+
76
+ def push_entities_to_connec_to(connec_client, mapped_external_entities_with_idmaps, connec_entity_name, organization)
77
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending #{@@external_name} #{self.external_entity_name.pluralize} to Connec! #{connec_entity_name.pluralize}")
78
+ mapped_external_entities_with_idmaps.each do |mapped_external_entity_with_idmap|
79
+ external_entity = mapped_external_entity_with_idmap[:entity]
80
+ idmap = mapped_external_entity_with_idmap[:idmap]
81
+
82
+ if idmap.connec_id.blank?
83
+ connec_entity = self.create_entity_to_connec(connec_client, external_entity, connec_entity_name, organization)
84
+ idmap.update_attributes(connec_id: connec_entity['id'], connec_entity: connec_entity_name.downcase, last_push_to_connec: Time.now)
85
+ else
86
+ connec_entity = self.update_entity_to_connec(connec_client, external_entity, idmap.connec_id, connec_entity_name, organization)
87
+ idmap.update_attributes(last_push_to_connec: Time.now)
88
+ end
89
+ end
90
+ end
91
+
92
+ def create_entity_to_connec(connec_client, mapped_external_entity, connec_entity_name, organization)
93
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending create #{connec_entity_name}: #{mapped_external_entity} to Connec!")
94
+ response = connec_client.post("/#{connec_entity_name.downcase.pluralize}", { "#{connec_entity_name.downcase.pluralize}".to_sym => mapped_external_entity })
95
+ raise "No response received from Connec! when trying to create a #{self.connec_entity_name}" unless response
96
+ JSON.parse(response.body)["#{connec_entity_name.downcase.pluralize}"]
97
+ end
98
+
99
+ def update_entity_to_connec(connec_client, mapped_external_entity, connec_id, connec_entity_name, organization)
100
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending update #{connec_entity_name}: #{mapped_external_entity} to Connec!")
101
+ response = connec_client.put("/#{connec_entity_name.downcase.pluralize}/#{connec_id}", { "#{connec_entity_name.downcase.pluralize}".to_sym => mapped_external_entity })
102
+ raise "No response received from Connec! when trying to update a #{self.connec_entity_name}" unless response
103
+ end
104
+
105
+ def map_to_external_with_idmap(entity, organization)
106
+ idmap = Maestrano::Connector::Rails::IdMap.find_by(connec_id: entity['id'], connec_entity: self.connec_entity_name.downcase, organization_id: organization.id)
107
+
108
+ if idmap && idmap.last_push_to_external && idmap.last_push_to_external > entity['updated_at']
109
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Discard Connec! #{self.connec_entity_name} : #{entity}")
110
+ nil
111
+ else
112
+ {entity: self.map_to_external(entity, organization), idmap: idmap || Maestrano::Connector::Rails::IdMap.create(connec_id: entity['id'], connec_entity: self.connec_entity_name.downcase, organization_id: organization.id)}
113
+ end
114
+ end
115
+
116
+ # ----------------------------------------------
117
+ # External methods
118
+ # ----------------------------------------------
119
+ def get_external_entities(client, last_synchronization, organization, opts={})
120
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Fetching #{@@external_name} #{self.external_entity_name.pluralize}")
121
+ raise "Not implemented"
122
+ end
123
+
124
+ def push_entities_to_external(external_client, mapped_connec_entities_with_idmaps, organization)
125
+ push_entities_to_external_to(external_client, mapped_connec_entities_with_idmaps, self.external_entity_name, organization)
126
+ end
127
+
128
+ def push_entities_to_external_to(external_client, mapped_connec_entities_with_idmaps, external_entity_name, organization)
129
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending Connec! #{self.connec_entity_name.pluralize} to #{@@external_name} #{external_entity_name.pluralize}")
130
+ mapped_connec_entities_with_idmaps.each do |mapped_connec_entity_with_idmap|
131
+ self.push_entity_to_external(external_client, mapped_connec_entity_with_idmap, external_entity_name, organization)
132
+ end
133
+ end
134
+
135
+ def push_entity_to_external(external_client, mapped_connec_entity_with_idmap, external_entity_name, organization)
136
+ idmap = mapped_connec_entity_with_idmap[:idmap]
137
+ connec_entity = mapped_connec_entity_with_idmap[:entity]
138
+
139
+ if idmap.external_id.blank?
140
+ external_id = self.create_entity_to_external(external_client, connec_entity, external_entity_name, organization)
141
+ idmap.update_attributes(external_id: external_id, external_entity: external_entity_name.downcase, last_push_to_external: Time.now)
142
+ else
143
+ self.update_entity_to_external(external_client, connec_entity, idmap.external_id, external_entity_name, organization)
144
+ idmap.update_attributes(last_push_to_external: Time.now)
145
+ end
146
+ end
147
+
148
+ def create_entity_to_external(client, mapped_connec_entity, external_entity_name, organization)
149
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending create #{external_entity_name}: #{mapped_connec_entity} to #{@@external_name}")
150
+ raise "Not implemented"
151
+ end
152
+
153
+ def update_entity_to_external(client, mapped_connec_entity, external_id, external_entity_name, organization)
154
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Sending update #{external_entity_name} (id=#{external_id}): #{mapped_connec_entity} to #{@@external_name}")
155
+ raise "Not implemented"
156
+ end
157
+
158
+ def get_id_from_external_entity_hash(entity)
159
+ raise "Not implemented"
160
+ end
161
+
162
+ def get_last_update_date_from_external_entity_hash(entity)
163
+ raise "Not implemented"
164
+ end
165
+
166
+ # ----------------------------------------------
167
+ # General methods
168
+ # ----------------------------------------------
169
+ # * Discards entities that do not need to be pushed because they have not been updated since their last push
170
+ # * Discards entities from one of the two source in case of conflict
171
+ # * Maps not discarded entities and associates them with their idmap, or create one if there isn't any
172
+ def consolidate_and_map_data(connec_entities, external_entities, organization, opts={})
173
+ external_entities.map!{|entity|
174
+ idmap = Maestrano::Connector::Rails::IdMap.find_by(external_id: self.get_id_from_external_entity_hash(entity), external_entity: self.external_entity_name.downcase, organization_id: organization.id)
175
+
176
+ # No idmap: creating one, nothing else to do
177
+ unless idmap
178
+ next {entity: self.map_to_connec(entity, organization), idmap: Maestrano::Connector::Rails::IdMap.create(external_id: self.get_id_from_external_entity_hash(entity), external_entity: self.external_entity_name.downcase, organization_id: organization.id)}
179
+ end
180
+
181
+ # Entity has not been modified since its last push to connec!
182
+ if idmap.last_push_to_connec && idmap.last_push_to_connec > self.get_last_update_date_from_external_entity_hash(entity)
183
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Discard #{@@external_name} #{self.external_entity_name} : #{entity}")
184
+ next nil
185
+ end
186
+
187
+ # Check for conflict with entities from connec!
188
+ if idmap.connec_id && connec_entity = connec_entities.detect{|connec_entity| connec_entity['id'] == idmap.connec_id}
189
+ # We keep the most recently updated entity
190
+ if !opts[:connec_preemption].nil?
191
+ keep_external = !opts[:connec_preemption]
192
+ else
193
+ keep_external = connec_entity['updated_at'] < self.get_last_update_date_from_external_entity_hash(entity)
194
+ end
195
+
196
+ if keep_external
197
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Conflict between #{@@external_name} #{self.external_entity_name} #{entity} and Connec! #{self.connec_entity_name} #{connec_entity}. Entity from #{@@external_name} kept")
198
+ connec_entities.delete(connec_entity)
199
+ {entity: self.map_to_connec(entity, organization), idmap: idmap}
200
+ else
201
+ Maestrano::Connector::Rails::ConnectorLogger.log('info', organization, "Conflict between #{@@external_name} #{self.external_entity_name} #{entity} and Connec! #{self.connec_entity_name} #{connec_entity}. Entity from Connec! kept")
202
+ nil
203
+ end
204
+
205
+ else
206
+ {entity: self.map_to_connec(entity, organization), idmap: idmap}
207
+ end
208
+ }.compact!
209
+
210
+ connec_entities.map!{|entity|
211
+ self.map_to_external_with_idmap(entity, organization)
212
+ }.compact!
213
+ end
214
+
215
+
216
+ # ----------------------------------------------
217
+ # Entity specific methods
218
+ # Those methods need to be define in each entity
219
+ # ----------------------------------------------
220
+ def connec_entity_name
221
+ raise "Not implemented"
222
+ end
223
+
224
+ def external_entity_name
225
+ raise "Not implemented"
226
+ end
227
+
228
+ def mapper_class
229
+ raise "Not implemented"
230
+ end
231
+ end