event_sourcery_generators 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +3 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +35 -0
  8. data/Rakefile +6 -0
  9. data/bin/eventsourcery +4 -0
  10. data/event_sourcery_generators.gemspec +27 -0
  11. data/lib/event_sourcery_generators.rb +5 -0
  12. data/lib/event_sourcery_generators/cli.rb +20 -0
  13. data/lib/event_sourcery_generators/generators/command.rb +86 -0
  14. data/lib/event_sourcery_generators/generators/project.rb +70 -0
  15. data/lib/event_sourcery_generators/generators/query.rb +51 -0
  16. data/lib/event_sourcery_generators/generators/reactor.rb +41 -0
  17. data/lib/event_sourcery_generators/generators/templates/command/aggregate.rb.tt +8 -0
  18. data/lib/event_sourcery_generators/generators/templates/command/aggregate/apply_event_method.rb.tt +6 -0
  19. data/lib/event_sourcery_generators/generators/templates/command/aggregate/command_method.rb.tt +11 -0
  20. data/lib/event_sourcery_generators/generators/templates/command/api_endpoint.rb.tt +6 -0
  21. data/lib/event_sourcery_generators/generators/templates/command/command.rb.tt +40 -0
  22. data/lib/event_sourcery_generators/generators/templates/command/event.rb.tt +1 -0
  23. data/lib/event_sourcery_generators/generators/templates/project/app.json.tt +16 -0
  24. data/lib/event_sourcery_generators/generators/templates/project/config.ru.tt +6 -0
  25. data/lib/event_sourcery_generators/generators/templates/project/environment.rb.tt +64 -0
  26. data/lib/event_sourcery_generators/generators/templates/project/gemfile.tt +19 -0
  27. data/lib/event_sourcery_generators/generators/templates/project/procfile.tt +2 -0
  28. data/lib/event_sourcery_generators/generators/templates/project/rakefile.tt +69 -0
  29. data/lib/event_sourcery_generators/generators/templates/project/readme.md.tt +39 -0
  30. data/lib/event_sourcery_generators/generators/templates/project/request_helpers.rb.tt +10 -0
  31. data/lib/event_sourcery_generators/generators/templates/project/script_server.tt +7 -0
  32. data/lib/event_sourcery_generators/generators/templates/project/script_setup.tt +22 -0
  33. data/lib/event_sourcery_generators/generators/templates/project/server.rb.tt +59 -0
  34. data/lib/event_sourcery_generators/generators/templates/project/spec_helper.rb.tt +38 -0
  35. data/lib/event_sourcery_generators/generators/templates/query/api_endpoint.rb.tt +7 -0
  36. data/lib/event_sourcery_generators/generators/templates/query/projector.rb.tt +40 -0
  37. data/lib/event_sourcery_generators/generators/templates/query/projector_process.tt +4 -0
  38. data/lib/event_sourcery_generators/generators/templates/query/query.rb.tt +11 -0
  39. data/lib/event_sourcery_generators/generators/templates/reactor/reactor.rb.tt +42 -0
  40. data/lib/event_sourcery_generators/generators/templates/reactor/reactor_process.tt +4 -0
  41. data/lib/event_sourcery_generators/version.rb +3 -0
  42. metadata +175 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0bd36fd327e879aab367ddb7735f380114ab5e62
4
+ data.tar.gz: ec0daa66a01178d180b03f24763c9c769542991a
5
+ SHA512:
6
+ metadata.gz: af41b94266b367488bc505d558bfff155a0c02b551c85737f5da1515c6eeb0bd6ac629cdb4dfb5b0ce48d1bd9cd6e9ef5369b46a39c8d1da05da2a7170041602
7
+ data.tar.gz: '094c8926a14ffe7beca942bf1816b7352ad491fbcd14f5259862c71bc2390258c06ed18ccb88ff60fdd36185518a27df26a21ae39e18dd70079093aaaa389f60'
@@ -0,0 +1,3 @@
1
+ pkg/
2
+ /.ruby-version
3
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at odindutton@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Envato
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,35 @@
1
+ # EventSourcery Generators
2
+
3
+ An opinionated CLI tool for building event sourced systems with EventSourcery.
4
+
5
+ ## Getting started
6
+
7
+ Generate a new event sourcery application:
8
+
9
+ ```sh
10
+ $ eventsourcery new recipe_book
11
+ ```
12
+
13
+ Generate a new aggregate, command and event:
14
+
15
+ ```sh
16
+ $ eventsourcery generate:command recipe add
17
+ ```
18
+
19
+ Generate a query and projection that subscribes to events:
20
+
21
+ ```sh
22
+ $ eventsourcery generate:query active_recipes recipe_added
23
+ ```
24
+
25
+ Generate a reactor that subscribes to events:
26
+
27
+ ```sh
28
+ $ eventsourcery generate:reactor recipe_publisher recipe_added recipe_deleted
29
+ ```
30
+
31
+ ## References
32
+
33
+ For more information, check out the `event_sourcery` repository here:
34
+ <https://github.com/envato/event_sourcery>
35
+
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'event_sourcery_generators'
4
+ EventSourceryGenerators::CLI.start
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'event_sourcery_generators/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'event_sourcery_generators'
8
+ spec.version = EventSourceryGenerators::VERSION
9
+ spec.authors = ['Sebastian von Conrad', 'Pablo Lee', 'Chun-wei Kuo', 'Giancarlo Salamanca']
10
+ spec.email = ['sebastian.von.conrad@envato.com', 'pablo.lee@envato.com', 'chun-wei.kuo@envato.com', 'giancarlo.salamanca@envato.com']
11
+
12
+ spec.summary = 'Generators for EventSourcery'
13
+ spec.description = 'An opinionated CLI tool for building event-sourced Ruby services with EventSourcery'
14
+ spec.homepage = 'https://github.com/envato/event_sourcery_generators'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'bin'
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'thor', '~> 0.19'
22
+ spec.add_dependency 'activesupport', '~> 5.1'
23
+ spec.add_dependency 'verbs', '~> 2.1'
24
+ spec.add_development_dependency 'bundler', '~> 1.12'
25
+ spec.add_development_dependency 'rake', '~> 11.2'
26
+ spec.add_development_dependency 'rspec', '~> 3.6'
27
+ end
@@ -0,0 +1,5 @@
1
+ require 'event_sourcery_generators/cli'
2
+ require 'event_sourcery_generators/version'
3
+
4
+ module EventSourceryGenerators
5
+ end
@@ -0,0 +1,20 @@
1
+ require 'thor'
2
+ require 'active_support/inflector'
3
+ require 'verbs'
4
+
5
+ require 'event_sourcery_generators/generators/project'
6
+ require 'event_sourcery_generators/generators/command'
7
+ require 'event_sourcery_generators/generators/query'
8
+ require 'event_sourcery_generators/generators/reactor'
9
+
10
+ module EventSourceryGenerators
11
+ class CLI < Thor
12
+ # Creating projects
13
+ register(Generators::Project, 'new', 'new [PROJECT NAME]', 'Creates a new EventSourcery project')
14
+
15
+ # Creating components inside a project
16
+ register(Generators::Command, 'generate:command', 'generate:command [AGGREGATE] [COMMAND]', 'Generates a new COMMAND for AGGREGATE')
17
+ register(Generators::Query, 'generate:query', 'generate:query [NAME] [EVENT1 EVENT2 ...]', 'Generates a new query with the name NAME and an optional list of EVENTs to subscribe to')
18
+ register(Generators::Reactor, 'generate:reactor', 'generate:reactor NAME [EVENT1 EVENT2 ...]', 'Generates a new Reactor with the name NAME and an optional list of EVENTs to subscribe to')
19
+ end
20
+ end
@@ -0,0 +1,86 @@
1
+ module EventSourceryGenerators
2
+ module Generators
3
+ class Command < Thor::Group
4
+ include Thor::Actions
5
+
6
+ argument :aggregate
7
+ argument :command
8
+
9
+ def self.source_root
10
+ File.join(File.dirname(__FILE__), 'templates/command')
11
+ end
12
+
13
+ def create_or_inject_into_aggregate_file
14
+ aggregate_file = "app/aggregates/#{aggregate_name}.rb"
15
+
16
+ @command_method = erb_file('aggregate/command_method.rb.tt')
17
+ @apply_event_method = erb_file('aggregate/apply_event_method.rb.tt')
18
+
19
+ if File.exist?(aggregate_file)
20
+ insert_into_file(aggregate_file, @command_method, after: "include EventSourcery::AggregateRoot\n")
21
+ insert_into_file(aggregate_file, @apply_event_method, after: "include EventSourcery::AggregateRoot\n")
22
+ else
23
+ template('aggregate.rb.tt', aggregate_file)
24
+ end
25
+ end
26
+
27
+ def create_command_file
28
+ template('command.rb.tt', "app/commands/#{aggregate}/#{command}.rb")
29
+ end
30
+
31
+ def create_event
32
+ event_file = "app/events/#{event_name}.rb"
33
+
34
+ unless File.exist?(event_file)
35
+ template('event.rb.tt', event_file)
36
+ end
37
+ end
38
+
39
+ def inject_command_to_api
40
+ insert_into_file('app/web/server.rb', after: "< Sinatra::Base\n") do
41
+ erb_file('api_endpoint.rb.tt')
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def aggregate_name
48
+ @aggregate_name ||= aggregate.underscore
49
+ end
50
+
51
+ def aggregate_class_name
52
+ @aggregate_class_name ||= aggregate_name.camelize
53
+ end
54
+
55
+ def command_name
56
+ @command_name ||= command.underscore
57
+ end
58
+
59
+ def command_class_name
60
+ @command_class_name ||= command_name.camelize
61
+ end
62
+
63
+ def event_name
64
+ past_participle = Verbs::Conjugator.send(:past_participle, command.downcase.to_sym).to_s
65
+ [ aggregate, past_participle ].map(&:underscore).join('_')
66
+ end
67
+
68
+ def event_class_name
69
+ @event_class_name ||= event_name.underscore.camelize
70
+ end
71
+
72
+ def erb_file(file)
73
+ path = File.join(self.class.source_root, file)
74
+ ERB.new(::File.binread(path), nil, "-", "@output_buffer").result(binding)
75
+ end
76
+
77
+ def project_name
78
+ @project_name ||= File.split(Dir.pwd).last
79
+ end
80
+
81
+ def project_class_name
82
+ @project_class_name ||= project_name.underscore.camelize
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,70 @@
1
+ module EventSourceryGenerators
2
+ module Generators
3
+ class Project < Thor::Group
4
+ include Thor::Actions
5
+
6
+ argument :project_name
7
+
8
+ class_options skip_tests: false, skip_setup: false
9
+
10
+ def self.source_root
11
+ File.join(File.dirname(__FILE__), 'templates', 'project')
12
+ end
13
+
14
+ def setup_ruby_project
15
+ template('gemfile.tt', "#{project_name}/Gemfile")
16
+ template('rakefile.tt', "#{project_name}/Rakefile")
17
+ end
18
+
19
+ def add_readme
20
+ template('readme.md.tt', "#{project_name}/README.md")
21
+ end
22
+
23
+ def setup_app
24
+ template('server.rb.tt', "#{project_name}/app/web/server.rb")
25
+
26
+ %w{aggregates commands events projections reactors}.each do |directory|
27
+ create_file("#{project_name}/app/#{directory}/.gitkeep")
28
+ end
29
+ end
30
+
31
+ def setup_environment
32
+ template('environment.rb.tt', "#{project_name}/config/environment.rb")
33
+ end
34
+
35
+ def setup_scripts
36
+ %w{server setup}.each do |script_name|
37
+ template("script_#{script_name}.tt", "#{project_name}/script/#{script_name}")
38
+ chmod("#{project_name}/script/#{script_name}", 0755)
39
+ end
40
+ end
41
+
42
+ def setup_rspec
43
+ return if options[:skip_tests]
44
+
45
+ template('spec_helper.rb.tt', "#{project_name}/spec/spec_helper.rb")
46
+ template('request_helpers.rb.tt', "#{project_name}/spec/support/request_helpers.rb")
47
+ end
48
+
49
+ def setup_processes_infrastructure
50
+ template('Procfile.tt', "#{project_name}/Procfile")
51
+ template('config.ru.tt', "#{project_name}/config.ru")
52
+ template('app.json.tt', "#{project_name}/app.json")
53
+ end
54
+
55
+ def run_setup_script
56
+ return if options[:skip_setup]
57
+
58
+ inside(project_name) do
59
+ run('./script/setup')
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def project_class_name
66
+ @project_class_name ||= project_name.underscore.camelize
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,51 @@
1
+ module EventSourceryGenerators
2
+ module Generators
3
+ class Query < Thor::Group
4
+ include Thor::Actions
5
+
6
+ argument :query_name
7
+ argument :event_names, type: :array, default: []
8
+
9
+ def self.source_root
10
+ File.join(File.dirname(__FILE__), 'templates/query')
11
+ end
12
+
13
+ def create_query
14
+ template('query.rb.tt', "app/projections/#{query_name}/query.rb")
15
+ end
16
+
17
+ def create_projector
18
+ template('projector.rb.tt', "app/projections/#{query_name}/projector.rb")
19
+ end
20
+
21
+ def inject_query_to_api
22
+ insert_into_file('app/web/server.rb', after: "< Sinatra::Base\n") do
23
+ erb_file('api_endpoint.rb.tt')
24
+ end
25
+ end
26
+
27
+ def add_projector_to_rakefile
28
+ insert_into_file('Rakefile', erb_file('projector_process.tt'), after: "processors = [\n")
29
+ end
30
+
31
+ private
32
+
33
+ def project_name
34
+ @project_name ||= File.split(Dir.pwd).last
35
+ end
36
+
37
+ def project_class_name
38
+ @project_class_name ||= project_name.underscore.camelize
39
+ end
40
+
41
+ def query_class_name
42
+ @query_class_name ||= query_name.underscore.camelize
43
+ end
44
+
45
+ def erb_file(file)
46
+ path = File.join(self.class.source_root, file)
47
+ ERB.new(::File.binread(path), nil, "-", "@output_buffer").result(binding)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ module EventSourceryGenerators
2
+ module Generators
3
+ class Reactor < Thor::Group
4
+ include Thor::Actions
5
+
6
+ argument :reactor_name
7
+ argument :event_names, type: :array, default: []
8
+
9
+ def self.source_root
10
+ File.join(File.dirname(__FILE__), 'templates/reactor')
11
+ end
12
+
13
+ def create_reactor
14
+ template('reactor.rb.tt', "app/reactors/#{reactor_name}.rb")
15
+ end
16
+
17
+ def add_reactor_to_rakefile
18
+ insert_into_file('Rakefile', erb_file('reactor_process.tt'), after: "processors = [\n")
19
+ end
20
+
21
+ private
22
+
23
+ def project_name
24
+ @project_name ||= File.split(Dir.pwd).last
25
+ end
26
+
27
+ def project_class_name
28
+ @project_class_name ||= project_name.underscore.camelize
29
+ end
30
+
31
+ def reactor_class_name
32
+ @reactor_class_name ||= reactor_name.underscore.camelize
33
+ end
34
+
35
+ def erb_file(file)
36
+ path = File.join(self.class.source_root, file)
37
+ ERB.new(::File.binread(path), nil, "-", "@output_buffer").result(binding)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ module <%= project_class_name %>
2
+ module Aggregates
3
+ class <%= aggregate_class_name %>
4
+ include EventSourcery::AggregateRoot
5
+ <%= @apply_event_method %><%= @command_method -%>
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+
2
+ apply <%= event_class_name %> do |event|
3
+ # Mutate state of aggregate based on event, e.g.
4
+ # @<%= event_name %>_occurred = true
5
+ end
6
+
@@ -0,0 +1,11 @@
1
+ def <%= command_name %>(payload)
2
+ # Perform any relevant contextual validations on aggregate
3
+
4
+ # Apply the event without persistence
5
+ apply_event(
6
+ <%= event_class_name %>,
7
+ aggregate_id: id,
8
+ body: payload,
9
+ )
10
+ end
11
+
@@ -0,0 +1,6 @@
1
+ post '/<%= aggregate.pluralize %>/:aggregate_id/<%= command %>' do
2
+ command = Commands::<%= aggregate_class_name %>::<%= command_class_name %>::Command.new(json_params)
3
+ Commands::<%= aggregate_class_name %>::<%= command_class_name %>::CommandHandler.new.handle(command)
4
+ status 201
5
+ end
6
+
@@ -0,0 +1,40 @@
1
+ require 'app/aggregates/<%= aggregate_name %>'
2
+
3
+ module <%= project_class_name %>
4
+ module Commands
5
+ module <%= aggregate_class_name %>
6
+ module <%= command_class_name %>
7
+ class Command
8
+ attr_reader :aggregate_id, :payload
9
+
10
+ def initialize(params)
11
+ @aggregate_id = params.delete(:aggregate_id)
12
+ @payload = params # Select the parameters you want to allow
13
+ end
14
+
15
+ def validate
16
+ # Add validation here
17
+ end
18
+ end
19
+
20
+ class CommandHandler
21
+ def initialize(repository: <%= project_class_name %>.repository)
22
+ @repository = repository
23
+ end
24
+
25
+ def handle(command)
26
+ command.validate
27
+
28
+ aggregate = repository.load(Aggregates::<%= aggregate_class_name %>, command.aggregate_id)
29
+ aggregate.<%= command_name %>(command.payload)
30
+ repository.save(aggregate)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :repository
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1 @@
1
+ <%= event_class_name %> = Class.new(EventSourcery::Event)
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "<%= project_class_name %>",
3
+ "description": "<%= project_class_name %>, an EventSourcery app",
4
+ "keywords": ["<%= project_name %>", "ruby", "event_sourcery", "cqrs"],
5
+ "formation": {
6
+ "web": {
7
+ "quantity": 1
8
+ },
9
+ "processors": {
10
+ "quantity": 1
11
+ }
12
+ },
13
+ "scripts": {
14
+ "postdeploy": "bundle exec rake db:migrate"
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH << '.'
2
+
3
+ require 'config/environment'
4
+ require 'app/web/server'
5
+
6
+ run <%= project_class_name %>::Server.new
@@ -0,0 +1,64 @@
1
+ require 'event_sourcery'
2
+ require 'event_sourcery/postgres'
3
+
4
+ Dir.glob(__dir__ + '/../app/events/*.rb').each { |f| require f }
5
+ Dir.glob(__dir__ + '/../app/reactors/*.rb').each { |f| require f }
6
+ Dir.glob(__dir__ + '/../app/projections/**/projector.rb').each { |f| require f }
7
+
8
+ module <%= project_class_name %>
9
+ class Config
10
+ attr_accessor :database_url
11
+ end
12
+
13
+ def self.config
14
+ @config ||= Config.new
15
+ end
16
+
17
+ def self.configure
18
+ yield config
19
+ end
20
+
21
+ def self.environment
22
+ ENV.fetch('RACK_ENV', 'development')
23
+ end
24
+
25
+ def self.event_store
26
+ EventSourcery::Postgres.config.event_store
27
+ end
28
+
29
+ def self.event_source
30
+ EventSourcery::Postgres.config.event_store
31
+ end
32
+
33
+ def self.tracker
34
+ EventSourcery::Postgres.config.event_tracker
35
+ end
36
+
37
+ def self.event_sink
38
+ EventSourcery::Postgres.config.event_sink
39
+ end
40
+
41
+ def self.projections_database
42
+ EventSourcery::Postgres.config.projections_database
43
+ end
44
+
45
+ def self.repository
46
+ @repository ||= EventSourcery::Repository.new(
47
+ event_source: event_source,
48
+ event_sink: event_sink
49
+ )
50
+ end
51
+ end
52
+
53
+ <%= project_class_name %>.configure do |config|
54
+ config.database_url = ENV['DATABASE_URL'] || "postgres://127.0.0.1:5432/<%= project_name.underscore %>_#{<%= project_class_name %>.environment}"
55
+ end
56
+
57
+ EventSourcery::Postgres.configure do |config|
58
+ database = Sequel.connect(<%= project_class_name %>.config.database_url)
59
+
60
+ # NOTE: Often we choose to split our events and projections into separate
61
+ # databases. For the purposes of this example we'll use one.
62
+ config.event_store_database = database
63
+ config.projections_database = database
64
+ end
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'event_sourcery', git: 'https://github.com/envato/event_sourcery.git'
4
+ gem 'event_sourcery-postgres', git: 'https://github.com/envato/event_sourcery-postgres.git'
5
+
6
+ gem 'rake'
7
+ gem 'sinatra'
8
+ # NOTE: pg is an implicit dependency of event_sourcery-postgres but we need to
9
+ # lock to an older version for deprecation warnings.
10
+ gem 'pg', '0.20.0'
11
+
12
+ group :development, :test do
13
+ gem 'pry'
14
+ gem 'rspec'
15
+ gem 'rack-test'
16
+ gem 'database_cleaner'
17
+ gem 'better_errors'
18
+ gem 'shotgun'
19
+ end
@@ -0,0 +1,2 @@
1
+ web: ./script/server
2
+ processors: bundle exec rake run_processors
@@ -0,0 +1,69 @@
1
+ $LOAD_PATH.unshift '.'
2
+
3
+ task :environment do
4
+ require 'config/environment'
5
+ end
6
+
7
+ desc 'Run Event Stream Processors'
8
+ task run_processors: :environment do
9
+ puts 'Starting Event Stream processors'
10
+
11
+ event_source = <%= project_class_name %>.event_source
12
+ tracker = <%= project_class_name %>.tracker
13
+ db_connection = <%= project_class_name %>.projections_database
14
+
15
+ # Need to disconnect before starting the processors
16
+ # to ensure each forked process has its own connection
17
+ db_connection.disconnect
18
+
19
+ # Show our ESP logs in foreman immediately
20
+ $stdout.sync = true
21
+
22
+ processors = [
23
+ # Add your processors here, like so:
24
+ #
25
+ # EventSourceryTodoApp::Projections::CompletedTodos::Projector.new(
26
+ # tracker: tracker,
27
+ # db_connection: db_connection,
28
+ # ),
29
+ ]
30
+
31
+ EventSourcery::EventProcessing::ESPRunner.new(
32
+ event_processors: processors,
33
+ event_source: event_source,
34
+ ).start!
35
+ end
36
+
37
+ namespace :db do
38
+ desc 'Create database'
39
+ task create: :environment do
40
+ url = <%= project_class_name %>.config.database_url
41
+ database_name = File.basename(url)
42
+ database = Sequel.connect URI.join(url, '/template1').to_s
43
+ begin
44
+ database.run("CREATE DATABASE #{database_name}")
45
+ rescue StandardError => e
46
+ puts "Could not create database '#{database_name}': #{e.class.name} #{e.message}"
47
+ end
48
+ database.disconnect
49
+ end
50
+
51
+ desc 'Drop database'
52
+ task drop: :environment do
53
+ url = <%= project_class_name %>.config.database_url
54
+ database_name = File.basename(url)
55
+ database = Sequel.connect URI.join(url, '/template1').to_s
56
+ database.run("DROP DATABASE IF EXISTS #{database_name}")
57
+ database.disconnect
58
+ end
59
+
60
+ desc 'Migrate database'
61
+ task migrate: :environment do
62
+ database = EventSourcery::Postgres.config.event_store_database
63
+ begin
64
+ EventSourcery::Postgres::Schema.create_event_store(db: database)
65
+ rescue StandardError => e
66
+ puts "Could not create event store: #{e.class.name} #{e.message}"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,39 @@
1
+ # <%= project_class_name %>
2
+
3
+ [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
4
+
5
+ ## Get started
6
+
7
+ Ensure you have Postgres and Ruby 2.3 or higher installed, then run the setup script:
8
+
9
+ ```sh
10
+ $ ./script/setup
11
+ ```
12
+
13
+ ## Using the Application
14
+
15
+ Start the web server and processors (reactors and projectors):
16
+
17
+ ```sh
18
+ $ foreman start
19
+ ```
20
+
21
+ ## Adding features
22
+
23
+ Generate a new aggregate, command and event:
24
+
25
+ ```sh
26
+ $ eventsourcery generate:command recipe add
27
+ ```
28
+
29
+ Generate a query and projection that subscribes to events:
30
+
31
+ ```sh
32
+ $ eventsourcery generate:query active_recipes recipe_added
33
+ ```
34
+
35
+ Generate a reactor that subscribes to events:
36
+
37
+ ```sh
38
+ $ eventsourcery generate:reactor recipe_publisher recipe_added recipe_deleted
39
+ ```
@@ -0,0 +1,10 @@
1
+ module RequestHelpers
2
+ def app
3
+ @@app ||= <%= project_class_name %>::Server
4
+ end
5
+
6
+ def last_event(aggregate_id)
7
+ <%= project_class_name %>.event_store
8
+ .get_events_for_aggregate_id(aggregate_id).last
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ if [ $RACK_ENV == production ]; then
4
+ bundle exec rackup -p $PORT
5
+ else
6
+ bundle exec shotgun -p 3000
7
+ fi
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+
3
+ echo
4
+ echo "--- Installing bundled gems"
5
+ echo
6
+
7
+ bundle install
8
+
9
+ if ! type -t "foreman" > /dev/null; then
10
+ echo
11
+ echo "--- Installing foreman"
12
+ echo
13
+
14
+ gem install foreman
15
+ fi
16
+
17
+ echo
18
+ echo "--- Creating and migrating databases"
19
+ echo
20
+
21
+ bundle exec rake db:create db:migrate
22
+ RACK_ENV=test bundle exec rake db:create db:migrate
@@ -0,0 +1,59 @@
1
+ require 'sinatra'
2
+
3
+ Dir.glob(__dir__ + '/../commands/**/*.rb').each { |f| require f }
4
+ Dir.glob(__dir__ + '/../projections/**/query.rb').each { |f| require f }
5
+
6
+ module <%= project_class_name %>
7
+ class Server < Sinatra::Base
8
+ BadRequest = Class.new(StandardError)
9
+ UnprocessableEntity = Class.new(StandardError)
10
+
11
+ # Ensure our error handlers are triggered in development
12
+ set :show_exceptions, :after_handler
13
+
14
+ configure :development do
15
+ require 'better_errors'
16
+ use BetterErrors::Middleware
17
+ BetterErrors.application_root = __dir__
18
+ end
19
+
20
+ error UnprocessableEntity do |error|
21
+ body "Unprocessable Entity: #{error.message}"
22
+ status 422
23
+ end
24
+
25
+ error BadRequest do |error|
26
+ body "Bad Request: #{error.message}"
27
+ status 400
28
+ end
29
+
30
+ before do
31
+ content_type :json
32
+ end
33
+
34
+ def json_params
35
+ # Coerce this into a symbolised Hash so Sinatra data
36
+ # structures don't leak into the command layer
37
+
38
+ request_body = request.body.read
39
+ unless request_body.empty?
40
+ params.merge!(JSON.parse(request_body))
41
+ end
42
+
43
+ Hash[
44
+ params.map{ |k, v| [k.to_sym, v] }
45
+ ]
46
+ end
47
+
48
+ # Add your API routes here!
49
+ #
50
+ # eg.
51
+ # get '/todos' do
52
+ # ...
53
+ # end
54
+ #
55
+ # post '/todo/:id' do
56
+ # ...
57
+ # end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'rack/test'
4
+ require 'securerandom'
5
+ require 'database_cleaner'
6
+
7
+ $LOAD_PATH << '.'
8
+
9
+ require 'app/web/server'
10
+ require 'config/environment'
11
+ require 'spec/support/request_helpers'
12
+
13
+ RSpec.configure do |config|
14
+ config.include(Rack::Test::Methods, type: :request)
15
+ config.include(RequestHelpers, type: :request)
16
+
17
+ config.expect_with :rspec do |expectations|
18
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
19
+ end
20
+
21
+ config.mock_with :rspec do |mocks|
22
+ mocks.verify_partial_doubles = true
23
+ end
24
+
25
+ config.shared_context_metadata_behavior = :apply_to_host_groups
26
+ config.disable_monkey_patching!
27
+ config.order = :random
28
+ Kernel.srand config.seed
29
+
30
+ EventSourcery.configure do |config|
31
+ config.logger = Logger.new(nil)
32
+ end
33
+
34
+ config.before do
35
+ DatabaseCleaner.strategy = :truncation
36
+ DatabaseCleaner.clean
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ get '/<%= query_name %>' do
2
+ body JSON.pretty_generate(
3
+ <%= project_class_name %>::Projections::<%= query_class_name %>::Query.handle
4
+ )
5
+ status 200
6
+ end
7
+
@@ -0,0 +1,40 @@
1
+ module <%= project_class_name %>
2
+ module Projections
3
+ module <%= query_class_name %>
4
+ class Projector
5
+ include EventSourcery::Postgres::Projector
6
+
7
+ projector_name :<%= query_name %>
8
+
9
+ table :query_<%= query_name %> do
10
+ # Add your projection table columns here, eg:
11
+ # column :todo_id, 'UUID NOT NULL'
12
+ # column :title, :text
13
+ # column :due_date, DateTime
14
+ end
15
+ <%- if event_names.empty? -%>
16
+
17
+ # project TodoAdded do |event|
18
+ # Modify your projection table here, eg:
19
+ # table.insert(
20
+ # todo_id: event.aggregate_id,
21
+ # title: event.body['title'],
22
+ # due_date: event.body['due_date'],
23
+ # )
24
+ # end
25
+ <%- else -%>
26
+ <% event_names.each do |event_name| %>
27
+ project <%= event_name.underscore.camelize %> do |event|
28
+ # Modify your projection table here, eg:
29
+ # table.insert(
30
+ # todo_id: event.aggregate_id,
31
+ # title: event.body['title'],
32
+ # due_date: event.body['due_date'],
33
+ # )
34
+ end
35
+ <%- end -%>
36
+ <%- end -%>
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ <%= project_class_name %>::Projections::<%= query_class_name %>::Projector.new(
2
+ tracker: tracker,
3
+ db_connection: db_connection,
4
+ ),
@@ -0,0 +1,11 @@
1
+ module <%= project_class_name %>
2
+ module Projections
3
+ module <%= query_class_name %>
4
+ class Query
5
+ def self.handle
6
+ <%= project_class_name %>.projections_database[:query_<%= query_name %>].all
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ module <%= project_class_name %>
2
+ module Reactors
3
+ class <%= reactor_class_name %>
4
+ include EventSourcery::Postgres::Reactor
5
+
6
+ processor_name :<%= reactor_name.underscore %>
7
+
8
+ # emits_events ExampleEvent, AnotherEvent
9
+
10
+ # table :reactor_<%= reactor_name.underscore %> do
11
+ # column :todo_id, :uuid, primary_key: true
12
+ # column :title, :text
13
+ # end
14
+
15
+ <%- if event_names.empty? -%>
16
+ # process TodoAdded do |event|
17
+ # table.insert(
18
+ # todo_id: event.aggregate_id,
19
+ # title: event.body['title'],
20
+ # )
21
+ # end
22
+
23
+ # process TodoCompleted do |event|
24
+ # todo = table.where(todo_id: event.aggregate_id).first
25
+ #
26
+ # emit_event(
27
+ # StakeholderNotifiedOfTodoCompletion.new(
28
+ # aggregate_id: event.aggregate_id,
29
+ # body: { title: todo[:title] },
30
+ # )
31
+ # )
32
+ # end
33
+ <%- else -%>
34
+ <% event_names.each do |event_name| %>
35
+ process <%= event_name.underscore.camelize %> do |event|
36
+ # Update scratch tables, emit events, ...
37
+ end
38
+ <%- end -%>
39
+ <%- end -%>
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,4 @@
1
+ <%= project_class_name %>::Reactors::<%= reactor_class_name %>.new(
2
+ tracker: tracker,
3
+ db_connection: db_connection,
4
+ ),
@@ -0,0 +1,3 @@
1
+ module EventSourceryGenerators
2
+ VERSION = '0.2.0'
3
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: event_sourcery_generators
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Sebastian von Conrad
8
+ - Pablo Lee
9
+ - Chun-wei Kuo
10
+ - Giancarlo Salamanca
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2017-06-16 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: thor
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - "~>"
21
+ - !ruby/object:Gem::Version
22
+ version: '0.19'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.19'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - "~>"
35
+ - !ruby/object:Gem::Version
36
+ version: '5.1'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '5.1'
44
+ - !ruby/object:Gem::Dependency
45
+ name: verbs
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - "~>"
49
+ - !ruby/object:Gem::Version
50
+ version: '2.1'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '2.1'
58
+ - !ruby/object:Gem::Dependency
59
+ name: bundler
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - "~>"
63
+ - !ruby/object:Gem::Version
64
+ version: '1.12'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '1.12'
72
+ - !ruby/object:Gem::Dependency
73
+ name: rake
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '11.2'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '11.2'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rspec
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '3.6'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '3.6'
100
+ description: An opinionated CLI tool for building event-sourced Ruby services with
101
+ EventSourcery
102
+ email:
103
+ - sebastian.von.conrad@envato.com
104
+ - pablo.lee@envato.com
105
+ - chun-wei.kuo@envato.com
106
+ - giancarlo.salamanca@envato.com
107
+ executables:
108
+ - eventsourcery
109
+ extensions: []
110
+ extra_rdoc_files: []
111
+ files:
112
+ - ".gitignore"
113
+ - ".rspec"
114
+ - CODE_OF_CONDUCT.md
115
+ - Gemfile
116
+ - LICENSE.txt
117
+ - README.md
118
+ - Rakefile
119
+ - bin/eventsourcery
120
+ - event_sourcery_generators.gemspec
121
+ - lib/event_sourcery_generators.rb
122
+ - lib/event_sourcery_generators/cli.rb
123
+ - lib/event_sourcery_generators/generators/command.rb
124
+ - lib/event_sourcery_generators/generators/project.rb
125
+ - lib/event_sourcery_generators/generators/query.rb
126
+ - lib/event_sourcery_generators/generators/reactor.rb
127
+ - lib/event_sourcery_generators/generators/templates/command/aggregate.rb.tt
128
+ - lib/event_sourcery_generators/generators/templates/command/aggregate/apply_event_method.rb.tt
129
+ - lib/event_sourcery_generators/generators/templates/command/aggregate/command_method.rb.tt
130
+ - lib/event_sourcery_generators/generators/templates/command/api_endpoint.rb.tt
131
+ - lib/event_sourcery_generators/generators/templates/command/command.rb.tt
132
+ - lib/event_sourcery_generators/generators/templates/command/event.rb.tt
133
+ - lib/event_sourcery_generators/generators/templates/project/app.json.tt
134
+ - lib/event_sourcery_generators/generators/templates/project/config.ru.tt
135
+ - lib/event_sourcery_generators/generators/templates/project/environment.rb.tt
136
+ - lib/event_sourcery_generators/generators/templates/project/gemfile.tt
137
+ - lib/event_sourcery_generators/generators/templates/project/procfile.tt
138
+ - lib/event_sourcery_generators/generators/templates/project/rakefile.tt
139
+ - lib/event_sourcery_generators/generators/templates/project/readme.md.tt
140
+ - lib/event_sourcery_generators/generators/templates/project/request_helpers.rb.tt
141
+ - lib/event_sourcery_generators/generators/templates/project/script_server.tt
142
+ - lib/event_sourcery_generators/generators/templates/project/script_setup.tt
143
+ - lib/event_sourcery_generators/generators/templates/project/server.rb.tt
144
+ - lib/event_sourcery_generators/generators/templates/project/spec_helper.rb.tt
145
+ - lib/event_sourcery_generators/generators/templates/query/api_endpoint.rb.tt
146
+ - lib/event_sourcery_generators/generators/templates/query/projector.rb.tt
147
+ - lib/event_sourcery_generators/generators/templates/query/projector_process.tt
148
+ - lib/event_sourcery_generators/generators/templates/query/query.rb.tt
149
+ - lib/event_sourcery_generators/generators/templates/reactor/reactor.rb.tt
150
+ - lib/event_sourcery_generators/generators/templates/reactor/reactor_process.tt
151
+ - lib/event_sourcery_generators/version.rb
152
+ homepage: https://github.com/envato/event_sourcery_generators
153
+ licenses: []
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 2.6.11
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: Generators for EventSourcery
175
+ test_files: []