eventosaurus 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.
@@ -0,0 +1,51 @@
1
+ module Eventosaurus
2
+ module Models
3
+ class Query
4
+ attr_reader :args, :partition_key_name, :partition_key_type
5
+
6
+ def initialize(definition)
7
+ @partition_key_name = definition[:partition_key].keys[0]
8
+ @partition_key_type = definition[:partition_key].values[0]
9
+ @args = { table_name: definition[:full_table_name] }
10
+ end
11
+
12
+ def run
13
+ Eventosaurus.configuration.dynamodb_client.query(args)
14
+ end
15
+
16
+ def select(attrs)
17
+ @args[:select] = 'SPECIFIC_ATTRIBUTES'
18
+ @args[:attributes_to_get] = attrs
19
+
20
+ self
21
+ end
22
+
23
+ def count
24
+ @args[:select] = 'COUNT'
25
+
26
+ self
27
+ end
28
+
29
+ def by_partition_key(value:, operator:)
30
+ @args[:key_conditions] = {
31
+ partition_key_name => {
32
+ attribute_value_list: [value],
33
+ comparison_operator: operator
34
+ }
35
+ }
36
+
37
+ self
38
+ end
39
+
40
+ def by_local_secondary(name:, value:, operator:)
41
+ @args[:query_filter] ||= {}
42
+ @args[:query_filter][name] = {
43
+ attribute_value_list: [value],
44
+ comparison_operator: operator
45
+ }
46
+
47
+ self
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,97 @@
1
+ module Eventosaurus
2
+ module Models
3
+ class Table
4
+ attr_reader :local_indexes,
5
+ :partition_key_name,
6
+ :partition_key_type,
7
+ :read_capacity_units,
8
+ :table_name,
9
+ :write_capacity_units
10
+
11
+ def initialize(table_name:, partition_key:, local_indexes: {}, args: {})
12
+ @table_name = table_name
13
+ @partition_key_name = partition_key.keys.first
14
+ @partition_key_type = partition_key.values.first.to_s.capitalize
15
+ @local_indexes = local_indexes
16
+ @read_capacity_units = args[:read_capacity_units] || 50
17
+ @write_capacity_units = args[:read_capacity_units] || 100
18
+ end
19
+
20
+ def to_hash
21
+ {
22
+ attribute_definitions: attribute_definitions,
23
+ global_secondary_indexes: global_secondary_indexes,
24
+ key_schema: table_key_schema,
25
+ local_secondary_indexes: local_secondary_indexes,
26
+ provisioned_throughput: provisioned_throughput,
27
+ table_name: table_name
28
+ }
29
+ end
30
+
31
+ private
32
+
33
+ def attribute_definitions
34
+ attrs = local_indexes.merge(partition_key_name => partition_key_type)
35
+ attrs[:event_uuid] = :s
36
+
37
+ attr_defs = []
38
+
39
+ attrs.each_pair do |name, type|
40
+ attr_defs << { attribute_name: name, attribute_type: type.to_s.capitalize }
41
+ end
42
+
43
+ attr_defs
44
+ end
45
+
46
+ def local_secondary_indexes
47
+ secondary_defs = []
48
+
49
+ local_indexes[:created_at] = :s
50
+
51
+ local_indexes.each_pair do |local_index, _|
52
+ secondary_defs << {
53
+ index_name: "Index#{local_index.to_s.camelize}",
54
+ key_schema: local_key_schema(local_index),
55
+ projection: { projection_type: 'KEYS_ONLY' }
56
+ }
57
+ end
58
+
59
+ secondary_defs
60
+ end
61
+
62
+ def local_key_schema(attribute_name)
63
+ [
64
+ { attribute_name: partition_key_name, key_type: 'HASH' },
65
+ { attribute_name: attribute_name, key_type: 'RANGE' }
66
+ ]
67
+ end
68
+
69
+ def global_secondary_indexes
70
+ [event_uuid_global_secondary_index]
71
+ end
72
+
73
+ def event_uuid_global_secondary_index
74
+ {
75
+ index_name: 'event_uuid',
76
+ key_schema: [{ attribute_name: 'event_uuid', key_type: 'HASH' }],
77
+ projection: { projection_type: 'KEYS_ONLY' },
78
+ provisioned_throughput: provisioned_throughput
79
+ }
80
+ end
81
+
82
+ def table_key_schema
83
+ [
84
+ { attribute_name: partition_key_name, key_type: 'HASH' },
85
+ { attribute_name: 'event_uuid', key_type: 'RANGE' }
86
+ ]
87
+ end
88
+
89
+ def provisioned_throughput
90
+ {
91
+ read_capacity_units: read_capacity_units,
92
+ write_capacity_units: write_capacity_units
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,12 @@
1
+ require 'sidekiq'
2
+ require 'eventosaurus/workers/sidekiq'
3
+
4
+ module Eventosaurus
5
+ module Persistors
6
+ class Sidekiq
7
+ def self.persist(klass_name, item)
8
+ Eventosaurus::Workers::Sidekiq.perform_async(klass_name, item)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module Eventosaurus
2
+ module Persistors
3
+ class Synchronous
4
+ def self.persist(klass_name, item)
5
+ klass = klass_name.constantize
6
+ klass.put_item(item)
7
+
8
+ rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
9
+ # This must be a double submit. Log it just in case.
10
+ Eventosaurus.configuration.logger.info(
11
+ "Duplicate UUID sent to DynamoDB. Details: #{item}. Error: #{e}"
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,67 @@
1
+ module Eventosaurus
2
+ module QueryBuilder
3
+ delegate :count, :select, to: :new_query
4
+
5
+ def build_query_methods
6
+ secondary_indexes = definition[:local_indexes] || {}
7
+ build_partition_key_methods
8
+ build_local_secondary_methods(secondary_indexes)
9
+ end
10
+
11
+ def build_partition_key_methods
12
+ method_name = generate_method_name(partition_key_name)
13
+
14
+ build_partition_key_event_class_method(method_name)
15
+ build_partition_key_query_instance_method(method_name)
16
+ end
17
+
18
+ def build_partition_key_event_class_method(method_name)
19
+ singleton_proc = proc do |value, operator = 'EQ'|
20
+ new_query.by_partition_key(value: value, operator: operator)
21
+ end
22
+
23
+ define_singleton_method(method_name, singleton_proc)
24
+ end
25
+
26
+ def build_partition_key_query_instance_method(method_name)
27
+ instance_proc = proc do |value, operator = 'EQ'|
28
+ by_partition_key(value: value, operator: operator)
29
+ end
30
+
31
+ Eventosaurus::Models::Query.send(:define_method, method_name, instance_proc)
32
+ end
33
+
34
+ def build_local_secondary_methods(secondary_indexes)
35
+ secondary_indexes.each_pair do |attr_name, _|
36
+ method_name = generate_method_name(attr_name)
37
+
38
+ build_local_secondary_event_class_method(method_name, attr_name)
39
+ build_local_secondary_query_instance_method(method_name, attr_name)
40
+ end
41
+ end
42
+
43
+ def build_local_secondary_event_class_method(method_name, attr_name)
44
+ singleton_proc = proc do |value, operator = 'EQ'|
45
+ new_query.by_local_secondary(name: attr_name, value: value, operator: operator)
46
+ end
47
+
48
+ define_singleton_method(method_name, singleton_proc)
49
+ end
50
+
51
+ def build_local_secondary_query_instance_method(method_name, attr_name)
52
+ instance_proc = proc do |value, operator = 'EQ'|
53
+ by_local_secondary(name: attr_name, value: value, operator: operator)
54
+ end
55
+
56
+ Eventosaurus::Models::Query.send(:define_method, method_name, instance_proc)
57
+ end
58
+
59
+ def new_query
60
+ Eventosaurus::Models::Query.new(definition)
61
+ end
62
+
63
+ def generate_method_name(attr)
64
+ "by_#{attr}".to_sym
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,12 @@
1
+ require 'eventosaurus'
2
+ require 'rails'
3
+
4
+ module Eventosaurus
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :eventosaurus
7
+
8
+ rake_tasks do
9
+ load "eventosaurus/tasks/eventosaurus.rake"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,76 @@
1
+ module Eventosaurus
2
+ class TableManagerService
3
+ class << self
4
+ def create_tables
5
+ load_events
6
+
7
+ existing_table_names = list_tables
8
+ Eventosaurus.event_table_definitions.each do |definition|
9
+ full_name = full_table_name(definition[:name])
10
+ next if existing_table_names.include?(full_name)
11
+
12
+ new_table = table_from_definition(definition).to_hash
13
+ api_create_table(new_table.to_hash)
14
+ end
15
+ end
16
+
17
+ def list_tables
18
+ prefix = Eventosaurus.configuration.environment_prefix
19
+ api_list_tables.table_names.select { |table_name| table_name.start_with?(prefix) }
20
+ end
21
+
22
+ def drop_tables
23
+ api_drop_tables
24
+ end
25
+
26
+ def describe_tables
27
+ load_events
28
+
29
+ Eventosaurus.event_table_definitions.map do |definition|
30
+ table_from_definition(definition).to_hash
31
+ end
32
+ end
33
+
34
+ def full_table_name(table_name)
35
+ "#{Eventosaurus.configuration.environment_prefix}_#{table_name}"
36
+ end
37
+
38
+ private
39
+
40
+ def table_from_definition(definition)
41
+ full_name = full_table_name(definition[:name])
42
+ partition_key = definition[:partition_key]
43
+ local_indexes = definition[:local_indexes] || {}
44
+
45
+ Eventosaurus::Models::Table.new(
46
+ table_name: full_name,
47
+ partition_key: partition_key,
48
+ local_indexes: local_indexes
49
+ )
50
+ end
51
+
52
+ # Ensure all table definitions are saved in the module variable @event_table_definitions
53
+ def load_events
54
+ Dir["#{Rails.root}/app/models/events/*.rb"].each { |file| require file }
55
+ end
56
+
57
+ def dynamodb_client
58
+ Eventosaurus.configuration.dynamodb_client
59
+ end
60
+
61
+ def api_list_tables
62
+ dynamodb_client.list_tables
63
+ end
64
+
65
+ def api_create_table(table_details)
66
+ dynamodb_client.create_table(table_details)
67
+ end
68
+
69
+ def api_drop_tables
70
+ list_tables.each do |table_name|
71
+ dynamodb_client.delete_table(table_name: table_name)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,125 @@
1
+ module Eventosaurus
2
+ class EventError < StandardError; end
3
+
4
+ @event_table_definitions = []
5
+
6
+ def self.add_table_definition(definition)
7
+ @event_table_definitions << definition
8
+ @event_table_definitions.uniq!
9
+ end
10
+
11
+ def self.event_table_definitions
12
+ @event_table_definitions
13
+ end
14
+
15
+ module Storable
16
+ def self.included(base)
17
+ base.extend ClassMethods
18
+ base.extend Eventosaurus::QueryBuilder
19
+ end
20
+
21
+ module ClassMethods
22
+ attr_accessor :table_name
23
+ attr_accessor :definition
24
+ attr_accessor :partition_key_name
25
+ attr_accessor :partition_key_type
26
+ attr_accessor :composite_primary_key_attrs
27
+
28
+ def store(*args)
29
+ item = details(*args)
30
+ item[:created_at] = Time.now.utc
31
+ item[:event_uuid] = generate_uuid(item)
32
+
33
+ persistor.persist(name, item)
34
+ rescue StandardError => error
35
+ on_error(error)
36
+ end
37
+
38
+ def put_item(item)
39
+ dynamodb_client.put_item(table_name: table_name, item: item, condition_expression: condition_expression)
40
+ end
41
+
42
+ private
43
+
44
+ # override in your event class, if your heart desires
45
+ def on_error(error)
46
+ raise error
47
+ end
48
+
49
+ # Ensure we do not overwrite. This causes SDK to raise
50
+ # if UUID is already found in DynamoDB
51
+ def condition_expression
52
+ "attribute_not_exists(event_uuid)"
53
+ end
54
+
55
+ def generate_uuid_predigest(item)
56
+ unless all_composite_primary_key_attrs_found?(item)
57
+ raise EventError, "missing composite primary key attributes in your item"
58
+ end
59
+
60
+ Array.wrap(composite_primary_key_attrs).each_with_object('') do |key_attr, key|
61
+ key << "#{key_attr}=#{item[key_attr]}:" if item[key_attr].present?
62
+ end
63
+ end
64
+
65
+ def generate_uuid(item)
66
+ created_at_ms = (item[:created_at].to_f * 1000).to_i
67
+
68
+ if composite_primary_key_attrs.present?
69
+ "#{created_at_ms}:#{Digest::SHA256.digest(generate_uuid_predigest(item))}"
70
+ else
71
+ "#{created_at_ms}:#{SecureRandom.uuid}"
72
+ end
73
+ end
74
+
75
+ def all_composite_primary_key_attrs_found?(item)
76
+ (composite_primary_key_attrs - item.keys).blank?
77
+ end
78
+
79
+ def dynamodb_client
80
+ Eventosaurus.configuration.dynamodb_client
81
+ end
82
+
83
+ def persistor
84
+ Eventosaurus.configuration.persistor
85
+ end
86
+
87
+ def table_definition(definition)
88
+ self.definition = definition
89
+
90
+ validate_table_definition
91
+ store_definition_elements
92
+ augment_table_with_defaults
93
+ build_query_methods
94
+
95
+ Eventosaurus.add_table_definition(definition)
96
+ end
97
+
98
+ def composite_primary_key(*cpk_attrs)
99
+ self.composite_primary_key_attrs = Array.wrap(cpk_attrs)
100
+ end
101
+
102
+ def validate_table_definition
103
+ raise EventError, "No partition key specified" unless definition[:partition_key].present?
104
+ raise EventError, "No table name specified" unless definition[:name].present?
105
+ end
106
+
107
+ def store_definition_elements
108
+ self.table_name = Eventosaurus::TableManagerService.full_table_name(definition[:name])
109
+ self.partition_key_name = definition[:partition_key].keys[0]
110
+ self.partition_key_type = definition[:partition_key].values[0]
111
+ end
112
+
113
+ def augment_table_with_defaults
114
+ definition[:full_table_name] = table_name
115
+ definition[:local_indexes] ||= {}
116
+ definition[:local_indexes][:event_uuid] = :s
117
+ definition[:local_indexes][:created_at] = :s
118
+ end
119
+
120
+ def details
121
+ raise NotImplementedError
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,31 @@
1
+ desc 'Create Tables for this environment. Do nothing if table exists'
2
+ namespace :eventosaurus do
3
+ desc 'create dynamodb tables. If tables exist, do nothing'
4
+ task create_tables: :environment do
5
+ Eventosaurus::TableManagerService.create_tables
6
+ puts "tables created."
7
+ end
8
+
9
+ desc 'list dynamodb tables'
10
+ task list_tables: :environment do
11
+ puts "tables: #{ Eventosaurus::TableManagerService.list_tables.join(',') }"
12
+ end
13
+
14
+ desc 'drop dynamodb tables'
15
+ task drop_tables: :environment do
16
+ Eventosaurus::TableManagerService.drop_tables
17
+ puts 'tables destroyed'
18
+ end
19
+
20
+ desc 'describe tables'
21
+ task describe_tables: :environment do
22
+ descriptions = Eventosaurus::TableManagerService.describe_tables
23
+ descriptions.each do |description|
24
+ puts ""
25
+ puts "-----------------------------------"
26
+ puts "TABLE: #{description[:table_name]}"
27
+ puts ""
28
+ pp description
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module Eventosaurus
2
+ MAJOR = '1'.freeze
3
+ MINOR = '0'.freeze
4
+ PATCH = '0'.freeze
5
+
6
+ VERSION = [MAJOR, MINOR, PATCH].join('.')
7
+ end
@@ -0,0 +1,20 @@
1
+ module Eventosaurus
2
+ module Workers
3
+ class Sidekiq
4
+ include ::Sidekiq::Worker
5
+
6
+ sidekiq_options retry: 3
7
+
8
+ def perform(klass_name, item)
9
+ klass = klass_name.constantize
10
+ klass.put_item(item)
11
+
12
+ rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
13
+ # This must be a double submit. Log it just in case.
14
+ Eventosaurus.configuration.logger.info(
15
+ "Duplicate UUID sent to DynamoDB. Details: #{item}. Error: #{e}"
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'eventosaurus/railtie' if defined?(Rails)
4
+ require 'eventosaurus/version'
5
+ require 'eventosaurus/configuration'
6
+ require 'eventosaurus/storable'
7
+ require 'eventosaurus/query_builder'
8
+ require 'eventosaurus/services/table_manager_service'
9
+ require 'eventosaurus/models/table.rb'
10
+ require 'eventosaurus/models/query.rb'