global-registry-bindings 0.0.1

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +126 -0
  4. data/lib/global_registry_bindings.rb +8 -0
  5. data/lib/global_registry_bindings/entity/delete_entity_methods.rb +22 -0
  6. data/lib/global_registry_bindings/entity/entity_methods.rb +62 -0
  7. data/lib/global_registry_bindings/entity/entity_type_methods.rb +54 -0
  8. data/lib/global_registry_bindings/entity/mdm_methods.rb +43 -0
  9. data/lib/global_registry_bindings/entity/push_entity_methods.rb +78 -0
  10. data/lib/global_registry_bindings/entity/push_relationship_methods.rb +81 -0
  11. data/lib/global_registry_bindings/entity/relationship_type_methods.rb +59 -0
  12. data/lib/global_registry_bindings/exceptions.rb +9 -0
  13. data/lib/global_registry_bindings/global_registry_bindings.rb +71 -0
  14. data/lib/global_registry_bindings/options.rb +80 -0
  15. data/lib/global_registry_bindings/options/class_options.rb +58 -0
  16. data/lib/global_registry_bindings/options/instance_options.rb +61 -0
  17. data/lib/global_registry_bindings/railtie.rb +19 -0
  18. data/lib/global_registry_bindings/version.rb +7 -0
  19. data/lib/global_registry_bindings/workers/delete_gr_entity_worker.rb +22 -0
  20. data/lib/global_registry_bindings/workers/pull_mdm_id_worker.rb +30 -0
  21. data/lib/global_registry_bindings/workers/push_gr_entity_worker.rb +21 -0
  22. data/lib/global_registry_bindings/workers/push_relationship_worker.rb +21 -0
  23. data/spec/acceptance/global_registry_bindings_spec.rb +74 -0
  24. data/spec/factories/factories.rb +37 -0
  25. data/spec/fixtures/get_entities_person.json +8 -0
  26. data/spec/fixtures/get_entities_person_mdm.json +13 -0
  27. data/spec/fixtures/get_entities_person_relationship.json +32 -0
  28. data/spec/fixtures/get_entity_types.json +9 -0
  29. data/spec/fixtures/get_entity_types_address.json +59 -0
  30. data/spec/fixtures/get_entity_types_address_partial.json +43 -0
  31. data/spec/fixtures/get_entity_types_fancy_org.json +43 -0
  32. data/spec/fixtures/get_entity_types_fancy_org_partial.json +35 -0
  33. data/spec/fixtures/get_entity_types_person.json +42 -0
  34. data/spec/fixtures/get_entity_types_person_partial.json +34 -0
  35. data/spec/fixtures/get_relationship_types.json +9 -0
  36. data/spec/fixtures/get_relationship_types_person_fancy_org.json +41 -0
  37. data/spec/fixtures/get_relationship_types_person_fancy_org_partial.json +33 -0
  38. data/spec/fixtures/post_entities_fancy_org.json +8 -0
  39. data/spec/fixtures/post_entities_fancy_org_parent.json +8 -0
  40. data/spec/fixtures/post_entities_person.json +8 -0
  41. data/spec/fixtures/post_entity_types_address.json +9 -0
  42. data/spec/fixtures/post_entity_types_fancy_org.json +9 -0
  43. data/spec/fixtures/post_entity_types_person.json +9 -0
  44. data/spec/fixtures/post_relationship_types_person_fancy_org.json +16 -0
  45. data/spec/fixtures/put_entities_address.json +20 -0
  46. data/spec/fixtures/put_entities_person_relationship.json +29 -0
  47. data/spec/fixtures/put_entities_relationship.json +8 -0
  48. data/spec/fixtures/put_relationship_types_fields.json +33 -0
  49. data/spec/internal/app/models/address.rb +11 -0
  50. data/spec/internal/app/models/application_record.rb +5 -0
  51. data/spec/internal/app/models/assignment.rb +10 -0
  52. data/spec/internal/app/models/default.rb +6 -0
  53. data/spec/internal/app/models/namespaced/person.rb +18 -0
  54. data/spec/internal/app/models/organization.rb +12 -0
  55. data/spec/internal/config/database.yml +4 -0
  56. data/spec/internal/config/initializers/global_registry.rb +8 -0
  57. data/spec/internal/config/routes.rb +5 -0
  58. data/spec/internal/db/schema.rb +41 -0
  59. data/spec/internal/log/test.log +35717 -0
  60. data/spec/models/address_spec.rb +228 -0
  61. data/spec/models/assignment_spec.rb +200 -0
  62. data/spec/models/organization_spec.rb +127 -0
  63. data/spec/models/person_spec.rb +247 -0
  64. data/spec/spec_helper.rb +55 -0
  65. data/spec/workers/delete_gr_entity_worker_spec.rb +33 -0
  66. data/spec/workers/pull_mdm_id_worker_spec.rb +71 -0
  67. data/spec/workers/push_gr_entity_worker_spec.rb +27 -0
  68. data/spec/workers/push_relationship_worker_spec.rb +27 -0
  69. metadata +458 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'global_registry'
4
+
5
+ module GlobalRegistry #:nodoc:
6
+ module Bindings #:nodoc:
7
+ module Entity #:nodoc:
8
+ module RelationshipTypeMethods
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ def push_global_registry_relationship_type # rubocop:disable Metrics/MethodLength
13
+ parent_entity_type_id, related_entity_type_id = associated_entity_ids
14
+
15
+ relationship_type = Rails.cache.fetch(relationship_type_cache_key, expires_in: 1.hour) do
16
+ GlobalRegistry::RelationshipType.get(
17
+ 'filters[between]' => "#{parent_entity_type_id},#{related_entity_type_id}"
18
+ )['relationship_types'].detect do |r|
19
+ r['relationship1']['relationship_name'] == global_registry.parent_relationship_name.to_s
20
+ end
21
+ end
22
+
23
+ unless relationship_type
24
+ relationship_type =
25
+ GlobalRegistry::RelationshipType.post(relationship_type: {
26
+ entity_type1_id: parent_entity_type_id,
27
+ entity_type2_id: related_entity_type_id,
28
+ relationship1: global_registry.parent_relationship_name,
29
+ relationship2: global_registry.related_relationship_name
30
+ })['relationship_type']
31
+ end
32
+ push_global_registry_relationship_type_fields(relationship_type)
33
+ relationship_type
34
+ end
35
+
36
+ def associated_entity_ids
37
+ [global_registry.parent_class.send(:push_entity_type)&.dig('id'),
38
+ global_registry.related_class.send(:push_entity_type)&.dig('id')]
39
+ end
40
+
41
+ def push_global_registry_relationship_type_fields(relationship_type)
42
+ existing_fields = relationship_type['fields']&.collect { |f| f['name'].to_sym } || []
43
+ fields = columns_to_push
44
+ .reject { |k, _v| existing_fields.include? k }
45
+ .map { |name, type| { name: name, field_type: type } }
46
+ return if fields.empty?
47
+ GlobalRegistry::RelationshipType.put(relationship_type['id'],
48
+ relationship_type: { fields: fields })
49
+ end
50
+
51
+ def relationship_type_cache_key
52
+ "GlobalRegistry::Bindings::RelationshipType::#{global_registry.parent_type}::" \
53
+ "#{global_registry.related_type}::#{global_registry.parent_relationship_name}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GlobalRegistry #:nodoc:
4
+ module Bindings #:nodoc:
5
+ class RecordMissingGlobalRegistryId < StandardError; end
6
+ class EntityMissingMdmId < StandardError; end
7
+ class RelatedEntityMissingGlobalRegistryId < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext'
4
+ require 'global_registry'
5
+ require 'global_registry_bindings/exceptions'
6
+ require 'global_registry_bindings/options'
7
+ require 'global_registry_bindings/entity/entity_methods'
8
+ require 'global_registry_bindings/entity/entity_type_methods'
9
+ require 'global_registry_bindings/entity/relationship_type_methods'
10
+ require 'global_registry_bindings/entity/push_entity_methods'
11
+ require 'global_registry_bindings/entity/delete_entity_methods'
12
+ require 'global_registry_bindings/entity/mdm_methods'
13
+ require 'global_registry_bindings/entity/push_relationship_methods'
14
+
15
+ module GlobalRegistry #:nodoc:
16
+ module Bindings #:nodoc:
17
+ # Call this in your model to enable and configure Global Registry bindings.
18
+ #
19
+ # Options:
20
+ #
21
+ # * `:id_column`: Column used to track the Global Registry ID for the model instance. Can be a :string or :uuid
22
+ # column. (default: `:global_registry_id`)
23
+ # * `:mdm_id_column`: Column used to enable MDM tracking and set the name of the column. MDM is disabled when this
24
+ # option is nil or empty. (default: `nil`)
25
+ # * `:type`: Global Registry entity type. Default value is underscored name of the model.
26
+ # * `:push_on`: Array of Active Record lifecycle events used to push changes to Global Registry.
27
+ # (default: `[:create, :update, :delete]`)
28
+ # * `:parent_association`: Name of the Active Record parent association. Must be defined before calling
29
+ # global_registry_bindings in order to determine foreign_key field. (default: `nil`)
30
+ # * `:related_association`: Name of the Active Record related association. Setting this option changes the
31
+ # global registry binding from entity to relationship. Active Record association must be defined before calling
32
+ # global_registry_bindings in order to determine the foreign key. `:parent_relationship_name` and
33
+ # `:related_relationship_name` must be set for relationship binding to work. (default: `nil`)
34
+ # * `:parent_relationship_name`: Name of parent relationship role. (default: `nil`)
35
+ # * `:related_relationship_name`: Name of the related relationship role. (default: `nil`)
36
+ # * `:exclude_fields`: Model fields to exclude when pushing to Global Registry. Will additionally include
37
+ # `:mdm_id_column` and `:parent_association` foreign key when defined.
38
+ # (default: `[:id, :created_at, :updated_at, :global_registry_id]`)
39
+ # * `:extra_fields`: Additional fields to send to Global Registry. This should be a hash with name as the key
40
+ # and :type attributes as the value. Ex: `{language: :string}`. Name is a symbol and type is an ActiveRecord
41
+ # column type.
42
+ # * `:mdm_timeout`: Only pull mdm information at most once every `:mdm_timeout`. (default: `1.minute`)
43
+ #
44
+ # @api public
45
+ def global_registry_bindings(options = {})
46
+ global_registry_bindings_parse_options! options
47
+
48
+ include Options
49
+ include Entity::EntityMethods
50
+ if global_registry.push_on.any? { |item| %i[create update].include? item }
51
+ if global_registry.related_association && global_registry.parent_association
52
+ include Entity::RelationshipTypeMethods
53
+ include Entity::PushRelationshipMethods
54
+ else
55
+ include Entity::EntityTypeMethods
56
+ include Entity::PushEntityMethods
57
+ end
58
+ end
59
+
60
+ include Entity::DeleteEntityMethods if global_registry.push_on.include? :delete
61
+ include Entity::MdmMethods if global_registry.mdm_id_column.present?
62
+ end
63
+
64
+ private
65
+
66
+ def global_registry_bindings_parse_options!(options)
67
+ class_attribute :_global_registry_bindings_options_hash
68
+ self._global_registry_bindings_options_hash = GlobalRegistry::Bindings::OptionsParser.new(self).parse(options)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'global_registry_bindings/options/instance_options'
4
+ require 'global_registry_bindings/options/class_options'
5
+
6
+ module GlobalRegistry #:nodoc:
7
+ module Bindings #:nodoc:
8
+ module Options
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ @_global_registry_bindings_class_options ||= GlobalRegistry::Bindings::Options::ClassOptions.new(self)
13
+ end
14
+
15
+ def global_registry
16
+ @_global_registry_bindings_instance_options ||= GlobalRegistry::Bindings::Options::InstanceOptions.new(self)
17
+ end
18
+
19
+ module ClassMethods
20
+ def global_registry
21
+ @_global_registry_bindings_class_options
22
+ end
23
+ end
24
+ end
25
+
26
+ class OptionsParser
27
+ def initialize(model_class)
28
+ @model_class = model_class
29
+ end
30
+
31
+ def defaults
32
+ {
33
+ id_column: :global_registry_id,
34
+ mdm_id_column: nil,
35
+ type: @model_class.name.demodulize.underscore.to_sym,
36
+ push_on: %i[create update delete],
37
+ parent_association: nil,
38
+ related_association: nil,
39
+ parent_relationship_name: nil,
40
+ related_relationship_name: nil,
41
+ exclude_fields: %i[id created_at updated_at],
42
+ extra_fields: {},
43
+ mdm_timeout: 1.minute
44
+ }.freeze
45
+ end
46
+
47
+ def parse(options_hash = {})
48
+ @options = defaults.deep_merge(options_hash) do |key, oldval, newval|
49
+ if key == :exclude_fields
50
+ oldval.concat Array.wrap(newval)
51
+ else
52
+ newval
53
+ end
54
+ end
55
+ update_excludes
56
+ validate_options
57
+ @options
58
+ end
59
+
60
+ private
61
+
62
+ def update_excludes
63
+ @options[:exclude_fields] << @options[:id_column]
64
+ @options[:exclude_fields] << @options[:mdm_id_column] if @options[:mdm_id_column].present?
65
+
66
+ parent_id_column = association_foreign_key @options[:parent_association]
67
+ @options[:exclude_fields] << parent_id_column.to_sym if parent_id_column
68
+
69
+ related_id_column = association_foreign_key @options[:related_association]
70
+ @options[:exclude_fields] << related_id_column.to_sym if related_id_column
71
+ end
72
+
73
+ def validate_options; end
74
+
75
+ def association_foreign_key(name)
76
+ @model_class.reflect_on_all_associations.detect { |a| a.name == name }&.foreign_key if name
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module GlobalRegistry #:nodoc:
6
+ module Bindings #:nodoc:
7
+ module Options
8
+ class ClassOptions
9
+ delegate :id_column,
10
+ :mdm_id_column,
11
+ :mdm_timeout,
12
+ :type,
13
+ :push_on,
14
+ :parent_association,
15
+ :related_association,
16
+ :parent_relationship_name,
17
+ :related_relationship_name,
18
+ :exclude_fields,
19
+ :extra_fields, to: :@options
20
+
21
+ def initialize(model_class)
22
+ @model_class = model_class
23
+ @options = OpenStruct.new model_class._global_registry_bindings_options_hash
24
+ end
25
+
26
+ def parent_class
27
+ return if parent_association.blank?
28
+ @model_class.reflect_on_all_associations
29
+ .detect { |a| a.name == parent_association.to_sym }
30
+ &.klass
31
+ end
32
+
33
+ def related_class
34
+ return if related_association.blank?
35
+ @model_class.reflect_on_all_associations
36
+ .detect { |a| a.name == related_association.to_sym }
37
+ &.klass
38
+ end
39
+
40
+ def parent_is_self?
41
+ parent_association.present? && parent_type == type
42
+ end
43
+
44
+ def parent_type
45
+ parent_class&.global_registry&.type
46
+ end
47
+
48
+ def related_type
49
+ related_class&.global_registry&.type
50
+ end
51
+
52
+ def mdm_worker_class_name
53
+ "Pull#{@model_class.name.tr(':', '')}MdmIdWorker"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GlobalRegistry #:nodoc:
4
+ module Bindings #:nodoc:
5
+ module Options
6
+ class InstanceOptions
7
+ delegate :id_column,
8
+ :mdm_id_column,
9
+ :mdm_timeout,
10
+ :type,
11
+ :push_on,
12
+ :parent_association,
13
+ :related_association,
14
+ :exclude_fields,
15
+ :extra_fields,
16
+ :parent_class,
17
+ :related_class,
18
+ :parent_type,
19
+ :related_type,
20
+ :parent_relationship_name,
21
+ :related_relationship_name,
22
+ :parent_is_self?,
23
+ :mdm_worker_class_name,
24
+ to: :@class_options
25
+
26
+ def initialize(model)
27
+ @model = model
28
+ @class_options = model.class.global_registry
29
+ end
30
+
31
+ def id_value
32
+ @model.send id_column
33
+ end
34
+
35
+ def id_value=(value)
36
+ @model.send "#{id_column}=", value
37
+ end
38
+
39
+ def id_value?
40
+ @model.send "#{id_column}?"
41
+ end
42
+
43
+ def parent
44
+ @model.send(parent_association) if parent_association.present?
45
+ end
46
+
47
+ def related
48
+ @model.send(related_association) if related_association.present?
49
+ end
50
+
51
+ def parent_id_value
52
+ parent&.global_registry&.id_value
53
+ end
54
+
55
+ def related_id_value
56
+ related&.global_registry&.id_value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GlobalRegistry #:nodoc:
4
+ module Bindings #:nodoc:
5
+ class Railtie < Rails::Railtie
6
+ initializer 'global_registry_bindings_railtie.configure_rollbar' do
7
+ if Module.const_defined? :Rollbar
8
+ ::Rollbar.configure do |config|
9
+ config.exception_level_filters.merge!(
10
+ 'GlobalRegistry::Bindings::RecordMissingGlobalRegistryId' => 'ignore',
11
+ 'GlobalRegistry::Bindings::EntityMissingMdmId' => 'ignore',
12
+ 'GlobalRegistry::Bindings::RelatedEntityMissingGlobalRegistryId' => 'ignore'
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GlobalRegistry #:nodoc:
4
+ module Bindings #:nodoc:
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+ require 'sidekiq-unique-jobs'
5
+ require 'global_registry'
6
+
7
+ module GlobalRegistry #:nodoc:
8
+ module Bindings #:nodoc:
9
+ module Workers #:nodoc:
10
+ class DeleteGrEntityWorker
11
+ include Sidekiq::Worker
12
+ sidekiq_options unique: :until_executed
13
+
14
+ def perform(global_registry_id)
15
+ GlobalRegistry::Entity.delete(global_registry_id)
16
+ rescue RestClient::ResourceNotFound # rubocop:disable Lint/HandleExceptions
17
+ # If the record doesn't exist, we don't care
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+ require 'sidekiq-unique-jobs'
5
+
6
+ module GlobalRegistry #:nodoc:
7
+ module Bindings #:nodoc:
8
+ module Workers #:nodoc:
9
+ class PullMdmIdWorker
10
+ include Sidekiq::Worker
11
+
12
+ def perform(model_class, id)
13
+ model_class.find(id).send(:pull_mdm_id_from_global_registry)
14
+ rescue ActiveRecord::RecordNotFound
15
+ # If the record was deleted after the job was created, swallow it
16
+ return
17
+ rescue RestClient::ResourceNotFound
18
+ Rails.logger.info "GR entity for #{self.class.name} #{id} does not exist; will _not_ retry"
19
+ end
20
+ end
21
+
22
+ def self.mdm_worker_class(model_class)
23
+ klass = Class.new(PullMdmIdWorker) do
24
+ sidekiq_options unique: :until_timeout, unique_expiration: model_class.global_registry.mdm_timeout
25
+ end
26
+ const_set model_class.global_registry.mdm_worker_class_name, klass
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+ require 'sidekiq-unique-jobs'
5
+
6
+ module GlobalRegistry #:nodoc:
7
+ module Bindings #:nodoc:
8
+ module Workers #:nodoc:
9
+ class PushGrEntityWorker
10
+ include Sidekiq::Worker
11
+ sidekiq_options unique: :until_and_while_executing
12
+
13
+ def perform(model_class, id)
14
+ model_class.find(id).send(:push_entity_to_global_registry)
15
+ rescue ActiveRecord::RecordNotFound # rubocop:disable Lint/HandleExceptions
16
+ # If the record was deleted after the job was created, swallow it
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end