frederick_operations_logger 1.1.2

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8489f6536a285d6114451401fd94d9d4043faf6
4
+ data.tar.gz: 9ad30e94ba02ee33085ca246a200783735b85e73
5
+ SHA512:
6
+ metadata.gz: 5565ba1daa79f65f25323d080a9ffcb9c0de12cc9483622d3516fa859b7e1fa3b6991a59982e4db2828fda42b8e0e23d6602910993dceb5b33b9808d5521446e
7
+ data.tar.gz: 132ca1e9a3ba35ef17ef6cd3a734718de1a7d689a9345088c074e460854b988994d5d3171fac627a08558f9bd72cc2df6f91efee58cb137bbd21bebd9a1d83a2
@@ -0,0 +1,45 @@
1
+ # Frederick Operations Logger
2
+
3
+ [ ![Codeship Status for BookerSoftwareInc/frederick_operations_logger](https://app.codeship.com/projects/8e3097e0-ca7d-0135-871a-72c36ec6d502/status?branch=master)](https://app.codeship.com/projects/261776)
4
+
5
+ Provides a Rails / ActiveRecord helper to automatically log all CRUD operations to the appropriate Kafka
6
+ `operations_log` topic for other services to be notified on resource changes.
7
+
8
+ ## Example Usage
9
+
10
+ ```ruby
11
+ # Gemfile
12
+
13
+ FRED_GITHUB_URL = 'https://8218b10ce7d4e20bd6d96d25bd3358ecafb286d8:x-oauth-basic@github.com/BookerSoftwareInc/'
14
+ gem 'frederick_internal_api', git: "#{FRED_GITHUB_URL}frederick_internal_api_gem.git"
15
+ gem 'frederick_operations_logger', git: "#{FRED_GITHUB_URL}frederick_operations_logger.git"
16
+ ```
17
+
18
+ ```ruby
19
+ class FooModel < ActiveRecord::Base
20
+ include FrederickOperationsLogger::ActiveRecord::Helper
21
+
22
+ # Adds an after_commit callback to log CRUD operations.
23
+ # Both a JSONAPI Resource class and Frederick API client class must be provided
24
+ # to be used by the logger when serializing the resource
25
+ # API Client classes are provided by the frederick_api gem or frederick_internal_api gem
26
+ # You can provide a string (as below) to reference a class if needed to avoid
27
+ # dependecy / load order issues between model and resource classes
28
+ logs_operations_with resource_class: 'Api::V2::FooResource', api_client_class: FrederickAPI::V2::Foo
29
+ end
30
+ ```
31
+
32
+ ## RSpec Test Helper - add this to your model spec
33
+
34
+ ```ruby
35
+ # spec/models/foo_model_spec.rb
36
+ require 'spec_helper'
37
+ require 'frederick_operations_logger/test/shared_examples'
38
+
39
+ describe FooModel do
40
+ include_examples 'logs_operations_with', {
41
+ resource_class: Api::V2::FooResource,
42
+ api_client_class: FrederickAPI::V2::Foo
43
+ }
44
+ end
45
+ ```
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'frederick_internal_api'
4
+ require 'frederick_operations_logger/active_record/helper'
5
+ require 'request_store'
6
+
7
+ # Base gem module
8
+ module FrederickOperationsLogger
9
+ REQUEST_STORE_KEY = 'frederick_operations_logger_context'
10
+ ALLOWED_ACTIVITIES = [
11
+ 're_accept_tos', # when subscribers re-accept TOS from ui dialog
12
+ 'update_communication_preferences', # when consumers update preferences from a ui
13
+ ].freeze
14
+ AUTH_INFO_KEYS = %i[user_id application_id application_name ip_address].freeze
15
+
16
+ # FrederickOperationsLogger.context = { activity: 'foo', auth_info: { user_id: 'some...uuid' } }
17
+ # using applications can set local context without concern for empty context in consumed message
18
+ # TODO: auth_info should be set automatically from frederick_api_auth_gem for REST calls
19
+ # auth_info: {
20
+ # user_id: The user id performing the action (if not performed by a system function),
21
+ # application_id: The Doorkeeper application ID that relates to the access token being used,
22
+ # application_name: The Doorkeeper application name or internal application name performing the action
23
+ # ip_address: The IP address of the authorized entity performing the action (user or application)
24
+ # }
25
+ # Use merge: false to replace an already set context instead of merging new info
26
+ def self.context=(activity: nil, auth_info: nil, extras: nil, merge: true)
27
+ auth_info = auth_info.compact.symbolize_keys if auth_info.present?
28
+ extras = extras.compact.deep_symbolize_keys if extras.present?
29
+
30
+ return if activity.nil? && auth_info.blank? && extras.blank? # safeguard against setting empty context
31
+ validate_context(activity, auth_info)
32
+
33
+ RequestStore.store[REQUEST_STORE_KEY] ||= {}
34
+ RequestStore.store[REQUEST_STORE_KEY][:context] = if merge
35
+ merge_context(
36
+ activity: activity,
37
+ auth_info: auth_info,
38
+ extras: extras
39
+ )
40
+ else
41
+ {
42
+ activity: activity,
43
+ auth_info: auth_info,
44
+ extras: extras
45
+ }.compact
46
+ end
47
+ end
48
+
49
+ def self.merge_context(activity: nil, auth_info: nil, extras: nil)
50
+ previous = self.context
51
+ new_auth_info = if auth_info.present?
52
+ previous[:auth_info].present? ? previous[:auth_info].merge(auth_info) : auth_info
53
+ else
54
+ previous[:auth_info]
55
+ end
56
+ new_extras = if extras.present?
57
+ previous[:extras].present? ? previous[:extras].merge(extras) : extras
58
+ else
59
+ previous[:extras]
60
+ end
61
+
62
+ {
63
+ activity: activity || previous[:activity],
64
+ auth_info: new_auth_info,
65
+ extras: new_extras
66
+ }.compact
67
+ end
68
+
69
+ # FrederickOperationsLogger.context
70
+ def self.context
71
+ RequestStore.store.dig(REQUEST_STORE_KEY, :context) || {}
72
+ end
73
+
74
+ def self.validate_context(activity, auth_info)
75
+ raise "Invalid activity: #{activity}" unless activity.nil? || ALLOWED_ACTIVITIES.include?(activity)
76
+ return unless auth_info.present? && (auth_info.keys - AUTH_INFO_KEYS).any?
77
+
78
+ raise "Invalid auth_info: only #{AUTH_INFO_KEYS.join(', ')} are allowed"
79
+ end
80
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrederickOperationsLogger
4
+ module ActiveRecord
5
+ # Include this mixin and use `logs_operations_for` to add an
6
+ # `after_commit` callback that will log any database activity on this model to Kafka.
7
+ # The topic used is `{resource_name}_operations_log`.
8
+ module Helper
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ # Helper to set up resource operations logging
13
+ # @param resource_class string or class (string allowed to circumvent loading order issues)
14
+ def logs_operations_with(resource_class:, api_client_class:)
15
+ @resource_class = resource_class
16
+ @api_client_class = api_client_class
17
+ after_commit :log_to_operations_log
18
+ end
19
+
20
+ # Get the resource class set in .logs_operations_for
21
+ # Checks base_class as well to work in STI models (subclasses)
22
+ def resource_class
23
+ @resource_class ||= base_class.resource_class
24
+ @resource_class.is_a?(String) ? @resource_class = @resource_class.constantize : @resource_class
25
+ end
26
+
27
+ # Get the api client class set in .logs_operations_for
28
+ # Checks base_class as well to work in STI models (subclasses)
29
+ def api_client_class
30
+ @api_client_class ||= base_class.api_client_class
31
+ @api_client_class.is_a?(String) ? @api_client_class = @api_client_class.constantize : @api_client_class
32
+ end
33
+
34
+ # @return valid attributes for JSONAPI resource i.e. ['id', 'name']
35
+ def resource_attributes
36
+ return @resource_attributes if @resource_attributes
37
+
38
+ attrs = resource_class.instance_variable_get(:@_attributes)
39
+ raise "Resource: #{resource_class} has no attributes!" unless attrs.is_a?(Hash)
40
+
41
+ @resource_attributes = resource_class.instance_variable_get(:@_attributes).stringify_keys.keys
42
+ end
43
+
44
+ # Gets relationships from JSONAPI Resource
45
+ # @return hash keyed on relationship foreign key
46
+ # For example: { 'contact_type_id' => { name: 'contact_type', type: 'contact_types' } }
47
+ def resource_relationships
48
+ @resource_relationships ||= if resource_class.instance_variable_get(:@_relationships).nil?
49
+ {}
50
+ else
51
+ resource_class.instance_variable_get(:@_relationships)
52
+ .each_with_object({}) do |(k, v), o|
53
+ o[v.foreign_key] = { name: k, type: v.type.to_s }
54
+ end.stringify_keys
55
+ end
56
+ end
57
+
58
+ # Additional options to write into kafka message along with json resource
59
+ def async_options
60
+ { context: FrederickOperationsLogger.context }
61
+ end
62
+ end
63
+
64
+ private def attributes_for_resource
65
+ attributes.slice(*included_attributes)
66
+ end
67
+
68
+ private def included_attributes
69
+ # Online log updated attributes and relationships on updates
70
+ return ((self.class.resource_attributes & previous_changes.keys) + %w[location_id id]) if log_action == :update
71
+ self.class.resource_attributes
72
+ end
73
+
74
+ private def add_relationships_for_log!(api_resource)
75
+ self.class.resource_relationships.each do |k, v|
76
+ if attributes[k].present?
77
+ api_resource.relationships.send(:"#{v[:name]}=", data: { type: v[:type], id: attributes[k] })
78
+ end
79
+ end
80
+ end
81
+
82
+ private def log_to_operations_log
83
+ additional_attr = {}
84
+ resource = self.class.api_client_class.new(attributes_for_resource)
85
+ add_relationships_for_log!(resource)
86
+
87
+ case log_action
88
+ when :destroy
89
+ return resource.async_log.destroy(self.class.async_options)
90
+ when :update
91
+ additional_attr = add_previous_changes
92
+ resource.mark_as_persisted!
93
+ end
94
+
95
+ resource.async_log.save(self.class.async_options.merge(additional_attr))
96
+ end
97
+
98
+ # resource: { previous_attributes: { location_group_id: 'foo' } }
99
+ private def add_previous_changes
100
+ previous_changes.each_with_object({}) do |(k, v), h|
101
+ h[k.to_sym] = v.first
102
+ end
103
+ end
104
+
105
+ private def log_action
106
+ if previous_changes.key?('id')
107
+ :create
108
+ elsif destroyed?
109
+ :destroy
110
+ else
111
+ :update
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples 'logs_operations_with' do |meta|
4
+ resource_class = meta[:resource_class]
5
+ api_client_class = meta[:api_client_class]
6
+
7
+ describe 'logs_operations_with' do
8
+ let(:instance) { subject }
9
+
10
+ before do
11
+ allow(instance).to receive(:log_to_operations_log)
12
+ end
13
+
14
+ it 'after_commit' do
15
+ instance.run_callbacks(:commit)
16
+ expect(instance).to have_received(:log_to_operations_log)
17
+ end
18
+
19
+ context 'resource_class' do
20
+ it resource_class.to_s do
21
+ expect(described_class.resource_class).to be resource_class
22
+ end
23
+ end
24
+
25
+ context 'api_client_class' do
26
+ it api_client_class.to_s do
27
+ expect(described_class.api_client_class).to be api_client_class
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrederickOperationsLogger
4
+ # Current gem version
5
+ VERSION = '1.1.2'
6
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: frederick_operations_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Frederick Engineering
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-08-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jsonapi-resources
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.9.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.9.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: request_store
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ description: Provides Rails helper for ActiveRecord Models to log operations for JSON
42
+ API Resources
43
+ email:
44
+ - tech@hirefrederick.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - lib/frederick_operations_logger.rb
51
+ - lib/frederick_operations_logger/active_record/helper.rb
52
+ - lib/frederick_operations_logger/test/shared_examples.rb
53
+ - lib/frederick_operations_logger/version.rb
54
+ homepage: https://github.com/BookerSoftwareInc/frederick_operations_logger
55
+ licenses: []
56
+ metadata: {}
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 2.6.14
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Frederick Internal Operations Logger
77
+ test_files: []