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.
- 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'
|