eventosaurus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'