command_post 0.0.1

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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +28 -0
  5. data/README.md +310 -0
  6. data/Rakefile +1 -0
  7. data/command_post.gemspec +21 -0
  8. data/lib/command_post/command/command.rb +375 -0
  9. data/lib/command_post/command_post.rb +5 -0
  10. data/lib/command_post/db/connection.rb +12 -0
  11. data/lib/command_post/db/postgresql.sql +45 -0
  12. data/lib/command_post/event_sourcing/aggregate.rb +82 -0
  13. data/lib/command_post/event_sourcing/aggregate_event.rb +107 -0
  14. data/lib/command_post/identity/identity.rb +75 -0
  15. data/lib/command_post/identity/sequence_generator.rb +44 -0
  16. data/lib/command_post/persistence/aggregate_pointer.rb +18 -0
  17. data/lib/command_post/persistence/auto_load.rb +70 -0
  18. data/lib/command_post/persistence/data_validation.rb +113 -0
  19. data/lib/command_post/persistence/persistence.rb +157 -0
  20. data/lib/command_post/persistence/schema_validation.rb +137 -0
  21. data/lib/command_post/util/hash_util.rb +23 -0
  22. data/lib/command_post/util/string_util.rb +18 -0
  23. data/lib/command_post/version.rb +3 -0
  24. data/spec/command_post/command/command_spec.rb +0 -0
  25. data/spec/command_post/identity/identity_lookup_value_aggregate_id_spec.rb +89 -0
  26. data/spec/command_post/identity/identity_lookup_value_checksum_spec.rb +108 -0
  27. data/spec/command_post/identity/identity_lookup_value_field_spec.rb +83 -0
  28. data/spec/command_post/persistence/data_validation_spec.rb +74 -0
  29. data/spec/command_post/persistence/nested_remote_spec.rb +0 -0
  30. data/spec/command_post/persistence/schema_validation_spec.rb +269 -0
  31. data/spec/command_post/require.rb +9 -0
  32. data/spec/spec_helper.rb +0 -0
  33. metadata +112 -0
@@ -0,0 +1,375 @@
1
+ require 'pp'
2
+ require File.expand_path(File.dirname(__FILE__) + '/../util/string_util')
3
+ require File.expand_path(File.dirname(__FILE__) + '/../identity/identity')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../event_sourcing/aggregate_event')
5
+
6
+
7
+
8
+ module CommandPost
9
+
10
+ class Command
11
+
12
+ def validate_persistent_fields object, errors
13
+ object.schema_fields.each do |field_name, field_info|
14
+ if field_info[:type].superclass == Persistence
15
+ if object.send(field_name.to_sym).valid? == false
16
+ errors += object.send(field_name.to_sym).data_errors
17
+ else
18
+ errors += validate_persistent_fields(object.send(field_name.to_sym), errors)
19
+ end
20
+ end
21
+ end
22
+ errors
23
+ end
24
+
25
+
26
+ def hashify_persistent_objects_before_save object
27
+ object.schema_fields.each do |field_name, field_info|
28
+ if field_info[:type].superclass == Persistence
29
+ hashify_persistent_objects_before_save (object.send field_name.to_sym)
30
+ hash = object.send(field_name.to_sym).to_h
31
+ method_name = "#{field_name}=".to_sym
32
+ object.send(method_name, hash)
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+
39
+ def self.auto_generate persistent_class
40
+ @@commands ||= Hash.new
41
+ return if @@commands.keys.include? persistent_class
42
+ @@commands[persistent_class] = []
43
+ self.create_field_correction_commands persistent_class
44
+ self.create_aggregate_creation_commands persistent_class
45
+ end
46
+
47
+
48
+
49
+
50
+ def self.create_aggregate_creation_commands persistent_class
51
+
52
+
53
+ modules = persistent_class.to_s.split('::').length - 1
54
+ parts = persistent_class.to_s.split('::')
55
+ if parts.length == 1
56
+ name = "Command#{persistent_class.to_s}Create#{persistent_class.to_s}"
57
+ elsif parts.length == 2
58
+ name = "Command#{parts[1]}Create#{parts[1]}"
59
+ end
60
+ klass = Object::const_set(name.intern, Class::new(Command) do end )
61
+
62
+ if modules == 1
63
+ klass.const_set parts[0], klass
64
+ end
65
+
66
+
67
+ def_init = Array.new
68
+ def_init << "def self.execute params, user_id "
69
+ def_init << ' AggregateEvent.publish self.create_event( params, user_id)'
70
+ def_init << "end "
71
+
72
+
73
+ klass.instance_eval (def_init.join("\n"))
74
+
75
+
76
+
77
+
78
+ def_init = Array.new
79
+
80
+ def_init << 'def self.build_web_submission_form '
81
+ def_init << " "
82
+
83
+ def_init << " hash = Hash.new "
84
+ def_init << " hash['page_header'] = 'Create Person'"
85
+ def_init << " hash['command_class'] = '#{klass.to_s}'"
86
+
87
+
88
+ def_init << "\n"
89
+ def_init << " hash['current_state'] = {} "
90
+
91
+ def_init << " hash['new_state'] = { "
92
+
93
+
94
+ current_state = Array.new
95
+ persistent_class.schema.each do |form_field, form_field_info|
96
+ current_state << " '#{form_field}' => { 'label' => '#{StringUtil.to_label(form_field, persistent_class.upcase?(form_field))}', 'html' => \"<input type='text' id='#{form_field}' name='#{form_field}'/>\" } "
97
+ end
98
+
99
+ def_init << current_state.join(", \n")
100
+
101
+ def_init << ' }'
102
+
103
+ def_init << " hash['command_description'] = 'Submit command to create a new #{persistent_class.to_s}.'"
104
+ def_init << " hash['command_sub_description'] = ''"
105
+
106
+ def_init << ' hash '
107
+
108
+
109
+ def_init << 'end'
110
+
111
+
112
+ klass.instance_eval (def_init.join("\n"))
113
+
114
+
115
+ def_init = Array.new
116
+ def_init << 'def self.validate params'
117
+ def_init << ' []'
118
+ def_init << 'end '
119
+
120
+ klass.instance_eval (def_init.join("\n"))
121
+
122
+
123
+
124
+ fields = persistent_class.schema.keys.collect {|field| ":#{field}" }
125
+
126
+ array_of_fields = fields.join(',')
127
+
128
+ def_init = Array.new
129
+ def_init << 'def self.fields'
130
+ def_init << " [#{array_of_fields}]" # I LEFT OFF HERE - TRYING TO PUT A STRINGIFIED LIST OF SYMBOLS INTO A HASH
131
+ def_init << 'end '
132
+
133
+ klass.instance_eval (def_init.join("\n"))
134
+
135
+
136
+ def_init = Array.new
137
+ def_init << 'def self.aggregate_type'
138
+ def_init << " #{persistent_class.to_s}"
139
+ def_init << 'end '
140
+
141
+ klass.instance_eval (def_init.join("\n"))
142
+
143
+
144
+ def_init = Array.new
145
+ def_init << 'def self.event_description'
146
+ def_init << " Created Person"
147
+ def_init << 'end '
148
+
149
+ klass.instance_eval (def_init.join("\n"))
150
+
151
+ @@commands[persistent_class] << klass
152
+
153
+ end
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+ def self.create_field_correction_commands persistent_class
167
+
168
+ persistent_class.schema.each do |field_name, field_info|
169
+
170
+
171
+ modules = persistent_class.to_s.split('::').length - 1
172
+ parts = persistent_class.to_s.split('::')
173
+
174
+ names = persistent_class.to_s.split('::')
175
+ if modules == 0
176
+ name = "Command#{persistent_class.to_s}Correct#{CommandPost::StringUtil.to_camel_case(field_name,persistent_class.upcase?(field_name))}"
177
+ elsif modules == 1
178
+ name = "Command#{names[1]}Correct#{CommandPost::StringUtil.to_camel_case(field_name,persistent_class.upcase?(field_name))}"
179
+ end
180
+
181
+
182
+ klass = Object::const_set(name, Class::new(Command) do end )
183
+
184
+ if modules == 1
185
+ klass.const_set parts[0], klass
186
+ end
187
+
188
+
189
+
190
+ def_init = Array.new
191
+ def_init << "def self.execute params, user_id "
192
+ def_init << ' AggregateEvent.publish self.create_event( params, user_id)'
193
+ def_init << "end "
194
+
195
+
196
+ klass.instance_eval (def_init.join("\n"))
197
+
198
+
199
+ def_init = Array.new
200
+
201
+ def_init << 'def self.get_web_submission_form aggregate_id'
202
+ def_init << " object = Aggregate.get_by_aggregate_id(#{persistent_class.to_s}, aggregate_id)"
203
+ def_init << " "
204
+
205
+ def_init << " hash = Hash.new "
206
+
207
+
208
+ def_init << " hash['command_class'] = '#{klass.to_s}'"
209
+ def_init << " hash['page_header'] = 'Correct #{StringUtil.to_label(field_name, persistent_class.upcase?(field_name))}'"
210
+ def_init << " hash['command_description'] = 'Command to correct #{field_name}.'"
211
+ def_init << " hash['command_sub_description'] = ''"
212
+ def_init << "\n"
213
+ def_init << " hash['current_state'] = {"
214
+
215
+
216
+
217
+
218
+ current_state = Array.new
219
+ persistent_class.schema.each do |form_field, form_field_info|
220
+ current_state << " '#{StringUtil.to_label(form_field, persistent_class.upcase?(form_field))}' => object.#{form_field}.to_s "
221
+ end
222
+ def_init << current_state.join(", ")
223
+ def_init << " } "
224
+ def_init << " hash['new_state'] = { } "
225
+
226
+
227
+
228
+ def_init << " hash"
229
+ def_init << 'end'
230
+
231
+
232
+
233
+
234
+ klass.instance_eval (def_init.join("\n"))
235
+
236
+
237
+ def_init = Array.new
238
+ def_init << 'def self.validate params'
239
+ def_init << ' []'
240
+ def_init << 'end '
241
+
242
+ klass.instance_eval (def_init.join("\n"))
243
+
244
+
245
+
246
+
247
+
248
+ def_init = Array.new
249
+ def_init << 'def self.fields'
250
+ def_init << " [:#{field_name}]"
251
+ def_init << 'end '
252
+
253
+ klass.instance_eval (def_init.join("\n"))
254
+
255
+
256
+
257
+
258
+ def_init = Array.new
259
+ def_init << 'def self.aggregate_type'
260
+ def_init << " #{persistent_class.to_s}"
261
+ def_init << 'end '
262
+
263
+ klass.instance_eval (def_init.join("\n"))
264
+
265
+
266
+
267
+
268
+
269
+
270
+ def_init = Array.new
271
+ def_init << 'def self.event_description'
272
+ def_init << " 'Corrected #{field_name}.'"
273
+ def_init << 'end '
274
+
275
+ klass.instance_eval (def_init.join("\n"))
276
+
277
+
278
+
279
+
280
+
281
+ @@commands[persistent_class] << klass
282
+
283
+ end
284
+
285
+ end
286
+
287
+
288
+
289
+
290
+
291
+ def self.publish_event command_class, params, user_id
292
+
293
+ event = AggregateEvent.new
294
+ event.aggregate_type = command_class.aggregate_type
295
+ event.event_description
296
+ #if command_class.to_s.start_with?("Command#{command_class.aggregate_type.to_s}Create")
297
+ event.aggregate_id = 1000
298
+ #end
299
+ event.transaction_id = SequenceGenerator.transaction_id
300
+ event.user_id = user_id
301
+ event.event_description = 'Person created'
302
+
303
+ object = command_class.aggregate_type.new
304
+
305
+
306
+
307
+ command_class.fields.each do |field|
308
+ info = command_class.aggregate_type.schema[field.to_s]
309
+ if info[:type] == Date
310
+ dt = Date._parse(params[field])
311
+ date = Date.new(dt[:year], dt[:mon], dt[:mday])
312
+ object.send("#{field.to_s}=".to_sym, date )
313
+ else
314
+ object.send("#{field.to_s}=".to_sym, params[field])
315
+ end
316
+ end
317
+
318
+
319
+ if params[:aggregate_id]
320
+ event.aggregate_id = params[:aggregate_id]
321
+ event.object = object
322
+ else
323
+ event.object = object
324
+ end
325
+
326
+
327
+ event.publish
328
+
329
+
330
+ end
331
+
332
+
333
+
334
+ def self.commands persistent_class
335
+
336
+ @@commands[persistent_class]
337
+
338
+ end
339
+ end
340
+
341
+
342
+
343
+
344
+
345
+ end
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+
363
+
364
+
365
+
366
+
367
+
368
+
369
+
370
+
371
+
372
+
373
+
374
+
375
+
@@ -0,0 +1,5 @@
1
+ require "command_post/version"
2
+
3
+ module CommandPost
4
+
5
+ end
@@ -0,0 +1,12 @@
1
+
2
+ module CommandPost
3
+
4
+ class Connection
5
+
6
+ def self.db_cqrs
7
+ Sequel.connect("postgres://localhost/cqrs?user=postgres&password=#{ENV['password']}")
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,45 @@
1
+ --drop table aggregate_events;
2
+ --drop table aggregates;
3
+
4
+
5
+
6
+ create table aggregate_events (
7
+
8
+ transaction_id bigint primary key,
9
+ aggregate_id bigint,
10
+ aggregate_type varchar(100),
11
+ event_description text,
12
+ content text,
13
+ call_stack text,
14
+ user_id varchar(100) not null,
15
+ transacted timestamp
16
+
17
+ ) ;
18
+
19
+ create index aggregate_id_idx on aggregate_events(aggregate_id,aggregate_type);
20
+
21
+
22
+
23
+
24
+
25
+ create table aggregates (
26
+
27
+ aggregate_id bigint not null primary key,
28
+ aggregate_lookup_value varchar(100) not null,
29
+ aggregate_type varchar(100) not null,
30
+ content text not null
31
+
32
+ ) ;
33
+
34
+ create index aggregate_lookup_idx on aggregates(aggregate_lookup_value);
35
+
36
+
37
+
38
+
39
+
40
+
41
+ CREATE SEQUENCE aggregate START 1;
42
+
43
+ CREATE SEQUENCE transaction START 1;
44
+
45
+ CREATE SEQUENCE misc START 1;
@@ -0,0 +1,82 @@
1
+ require 'pp'
2
+ require 'securerandom'
3
+ require 'json'
4
+ require 'sequel'
5
+
6
+ require_relative '../db/connection'
7
+ require_relative '../identity/sequence_generator'
8
+ require_relative '../util/hash_util'
9
+
10
+ module CommandPost
11
+
12
+
13
+ $DB ||= Connection.db_cqrs
14
+
15
+ class Aggregate
16
+
17
+ @@prep_stmt_insert ||= $DB[:aggregates].prepare(:insert, :insert_aggregate, :aggregate_id => :$aggregate_id, :aggregate_type => :$aggregate_type, :content => :$content, :aggregate_lookup_value => :$aggregate_lookup_value )
18
+ @@prep_stmt_update ||= $DB[:aggregates].prepare(:update, :update_content_aggregate_lookup_value, :aggregate_id => :$aggregate_id, :content => :$content, :aggregate_lookup_value => :$aggregate_lookup_value )
19
+
20
+
21
+
22
+ def self.replace object, aggregate_lookup_value
23
+
24
+
25
+ content = JSON.generate object
26
+ aggregate_id = object[:aggregate_info][:aggregate_id]
27
+ aggregate_type = object[:aggregate_info][:aggregate_type]
28
+ version = object[:aggregate_info][:version].to_i
29
+
30
+
31
+ if (version) == 1
32
+ @@prep_stmt_insert.call(:aggregate_id => aggregate_id, :aggregate_type => aggregate_type.to_s , :content => content, :aggregate_lookup_value => aggregate_lookup_value )
33
+ else
34
+ $DB["UPDATE aggregates set content = ?, aggregate_lookup_value = ? where aggregate_id = ?", content, aggregate_lookup_value, aggregate_id ].update
35
+ end
36
+ end
37
+
38
+
39
+
40
+ def self.get_by_aggregate_id aggregate_type ,aggregate_id
41
+ hash = Hash.new
42
+ $DB.fetch("SELECT * FROM aggregates WHERE aggregate_id = ?", aggregate_id ) do |row|
43
+ hash = JSON.parse(row[:content])
44
+ end
45
+ aggregate_type.load_from_hash( aggregate_type, HashUtil.symbolize_keys(hash))
46
+ end
47
+
48
+
49
+
50
+ def self.where(aggregate_type)
51
+ results = Array.new
52
+ $DB.fetch("SELECT * FROM aggregates WHERE aggregate_type = ?", aggregate_type.to_s) do |row|
53
+ hash = JSON.parse(row[:content])
54
+ results << aggregate_type.load_from_hash( aggregate_type, HashUtil.symbolize_keys(hash))
55
+ end
56
+ results
57
+ end
58
+
59
+
60
+
61
+ def self.exists? aggregate_type, aggregate_lookup_value
62
+ $DB.fetch("SELECT count(*) as cnt FROM aggregates WHERE aggregate_type = ? and aggregate_lookup_value = ? ", aggregate_type.to_s, aggregate_lookup_value) do |rec|
63
+ return rec[:cnt].to_i > 0
64
+ end
65
+ end
66
+
67
+
68
+
69
+ def self.get_aggregate_by_lookup_value aggregate_type, aggregate_lookup_value
70
+ hash = Hash.new
71
+ $DB.fetch("SELECT content FROM aggregates WHERE aggregate_type = ? and aggregate_lookup_value = ? ", aggregate_type.to_s, aggregate_lookup_value) do |rec|
72
+ hash = JSON.parse(rec[:content])
73
+ end
74
+ if hash.nil? || hash == {}
75
+ {}
76
+ else
77
+ aggregate_type.load_from_hash( aggregate_type, HashUtil.symbolize_keys(hash))
78
+ end
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,107 @@
1
+ require_relative './aggregate.rb'
2
+
3
+
4
+ module CommandPost
5
+
6
+ class AggregateEvent
7
+
8
+ attr_accessor :aggregate_type, :aggregate_id, :content, :transaction_id, :transacted, :event_description , :object, :changes, :user_id, :call_stack
9
+
10
+ @@prepared_statement ||= $DB[:aggregate_events].prepare(:insert, :insert_aggregate_event,
11
+ :aggregate_type => :$aggregate_type,
12
+ :aggregate_id => :$aggregate_id,
13
+ :content => :$content,
14
+ :transaction_id => :$transaction_id,
15
+ :transacted => :$transacted,
16
+ :event_description => :$event_description,
17
+ :user_id => :$user_id )
18
+ @@required_by_txn = [ "aggregate_type", "aggregate_id", "event_description", "content", "transaction_id", "transacted" ]
19
+
20
+ def initialize
21
+ @transaction_id = SequenceGenerator.transaction_id
22
+ @transacted = Time.now
23
+ @object = @changes = nil
24
+ end
25
+
26
+ def publish
27
+ if changes
28
+ save_event changes
29
+ elsif object
30
+ @aggregate_lookup_value = object.aggregate_lookup_value
31
+ save_event object.to_h
32
+ else
33
+ raise 'Event has no state to publish.'
34
+ end
35
+ end
36
+
37
+ def self.publish event
38
+ event.publish
39
+ end
40
+
41
+
42
+
43
+
44
+
45
+ def self.get_history_by_aggregate_id aggregate_id
46
+ hash = Hash.new
47
+ cnt = 0
48
+ results = Array.new
49
+ $DB.fetch("SELECT * FROM aggregate_events WHERE aggregate_id = ? order by transacted", aggregate_id ) do |row|
50
+ hash = JSON.parse(row[:content])
51
+ results << "==================================================================================="
52
+ results << "Version: #{cnt += 1} "
53
+ results << "Event Description: #{row[:event_description]} "
54
+ results << "Change Made By: #{row[:user_id]}"
55
+ results << "------ change data ------------"
56
+ results << hash.pretty_inspect
57
+ end
58
+ results
59
+ end
60
+
61
+
62
+
63
+ private
64
+ def save_event change
65
+
66
+ puts "@aggregate_type = #{@aggregate_type} "
67
+ puts "@aggregate_id = #{@aggregate_id} "
68
+ puts "@transaction_id = #{@transaction_id} "
69
+ puts "@transacted = #{@transacted} "
70
+ puts "@event_description = #{@event_description} "
71
+ puts "@user_id = #{@user_id} "
72
+
73
+
74
+ json = JSON.generate(change)
75
+ @@prepared_statement.call(
76
+ :aggregate_type => @aggregate_type.to_s,
77
+ :aggregate_id => @aggregate_id,
78
+ :content => json,
79
+ :transaction_id => @transaction_id,
80
+ :transacted => @transacted,
81
+ :event_description => @event_description,
82
+ :user_id => @user_id
83
+ )
84
+ Aggregate.replace get_current_object, @aggregate_lookup_value
85
+ end
86
+
87
+
88
+
89
+ def get_current_object
90
+
91
+ version = 0
92
+ accumulated_object = Hash.new
93
+ $DB.fetch("SELECT * FROM aggregate_events WHERE aggregate_id = ? order by transacted", @aggregate_id) do |row|
94
+ accumulated_object.merge!(HashUtil.symbolize_keys(JSON.parse(row[:content])))
95
+ aggregate_details = Hash.new
96
+ aggregate_details[:aggregate_type] = row[:aggregate_type]
97
+ aggregate_details[:aggregate_id] = row[:aggregate_id]
98
+ aggregate_details[:version] = "#{version += 1}"
99
+ aggregate_details[:most_recent_transaction] = row[:transaction_id]
100
+ accumulated_object[:aggregate_info] = aggregate_details
101
+ end
102
+
103
+ accumulated_object
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,75 @@
1
+
2
+ module CommandPost
3
+
4
+
5
+ module Identity
6
+
7
+
8
+ def aggregate_lookup_value
9
+ field = (schema_fields[:lookup][:use])
10
+ (self.send field.to_sym).to_s
11
+ end
12
+
13
+ def aggregate_id
14
+
15
+ @data[:aggregate_info][:aggregate_id]
16
+ end
17
+
18
+
19
+ def aggregate_pointer
20
+
21
+ AggregatePointer.new(aggregate_info)
22
+ end
23
+
24
+
25
+ def self.generate_checksum value
26
+ sha = Digest::SHA2.new
27
+ sha.update(value)
28
+ sha.to_s
29
+ end
30
+
31
+
32
+ def checksum
33
+ data = Array.new
34
+ @data.keys.select {|x| x != :aggregate_info}.each {|x| data << @data[x] }
35
+ Identity.generate_checksum(data.join())
36
+ end
37
+
38
+
39
+ def self.select (&block)
40
+ Aggregate.where(self).select
41
+ end
42
+
43
+
44
+ def aggregate_info
45
+ if @data.keys.include? :aggregate_info
46
+ @data[:aggregate_info]
47
+ else
48
+ raise "A request was made for 'AggregateInfo at a time when it was not present in object state."
49
+ end
50
+ end
51
+
52
+
53
+ def self.find aggregate_id
54
+
55
+ Aggregate.get_by_aggregate_id self, aggregate_id
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+