eventosaurus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +307 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +12 -0
- data/circle.yml +6 -0
- data/eventosaurus.gemspec +39 -0
- data/lib/eventosaurus/configuration.rb +82 -0
- data/lib/eventosaurus/models/query.rb +51 -0
- data/lib/eventosaurus/models/table.rb +97 -0
- data/lib/eventosaurus/persistors/sidekiq.rb +12 -0
- data/lib/eventosaurus/persistors/synchronous.rb +16 -0
- data/lib/eventosaurus/query_builder.rb +67 -0
- data/lib/eventosaurus/railtie.rb +12 -0
- data/lib/eventosaurus/services/table_manager_service.rb +76 -0
- data/lib/eventosaurus/storable.rb +125 -0
- data/lib/eventosaurus/tasks/eventosaurus.rake +31 -0
- data/lib/eventosaurus/version.rb +7 -0
- data/lib/eventosaurus/workers/sidekiq.rb +20 -0
- data/lib/eventosaurus.rb +10 -0
- metadata +214 -0
@@ -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,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,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,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
|
data/lib/eventosaurus.rb
ADDED
@@ -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'
|