praxis 2.0.pre.10 → 2.0.pre.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -3
  3. data/CHANGELOG.md +9 -0
  4. data/bin/praxis +59 -2
  5. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  6. data/lib/praxis/docs/open_api_generator.rb +1 -1
  7. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +57 -8
  8. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  9. data/lib/praxis/extensions/pagination.rb +5 -32
  10. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  11. data/lib/praxis/mapper/resource.rb +18 -2
  12. data/lib/praxis/mapper/selector_generator.rb +1 -0
  13. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  14. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  15. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  16. data/lib/praxis/tasks/api_docs.rb +4 -1
  17. data/lib/praxis/version.rb +1 -1
  18. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +15 -2
  19. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  20. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  21. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  22. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  23. data/tasks/thor/example.rb +12 -6
  24. data/tasks/thor/model.rb +40 -0
  25. data/tasks/thor/scaffold.rb +117 -0
  26. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  27. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  28. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  29. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  30. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
  31. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  32. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  33. data/tasks/thor/templates/generator/example_app/config/environment.rb +2 -1
  34. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  35. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  36. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  37. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  38. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  39. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  40. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  41. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  42. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  43. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  44. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  45. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  46. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  47. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  48. metadata +14 -2
@@ -1,10 +1,23 @@
1
1
  require 'singleton'
2
2
 
3
+ require 'praxis/extensions/field_selection'
4
+
3
5
  module Praxis
4
6
  module Plugins
5
7
  module MapperPlugin
6
8
  include Praxis::PluginConcern
7
9
 
10
+ # The Mapper plugin is an overarching set of things to include in your application
11
+ # when you want to use the rendring, field_selection, filtering (and potentially pagination) extensions
12
+ # To use the plugin, set it up like any other plugin by registering to the bootloader.
13
+ # Typically you'd do that in environment.rb, inside the `Praxis::Application.configure do |application|` block, by:
14
+ # application.bootloader.use Praxis::Plugins::MapperPlugin
15
+ #
16
+ # The plugin accepts only 1 configuration option thus far, which you can set inside the same block as:
17
+ # application.config.mapper.debug_queries = true
18
+ # when debug_queries is set to true, the system will output information about the expanded fields
19
+ # and associations that the system ihas calculated necessary to pull from the DB, based on the requested
20
+ # API fields, API filters and `property` dependencies defined in the domain models (i.e., resources)
8
21
  class Plugin < Praxis::Plugin
9
22
  include Singleton
10
23
 
@@ -28,17 +41,11 @@ module Praxis
28
41
  extend ActiveSupport::Concern
29
42
 
30
43
  included do
44
+ include Praxis::Extensions::Rendering
31
45
  include Praxis::Extensions::FieldExpansion
32
46
  end
33
47
 
34
- def set_selectors
35
- return unless self.media_type.respond_to?(:domain_model) &&
36
- self.media_type.domain_model < Praxis::Mapper::Resource
37
-
38
- selector_generator.add(self.media_type.domain_model, self.expanded_fields)
39
- end
40
-
41
- def build_query(base_query, type: :active_record) # rubocop:disable Metrics/AbcSize
48
+ def build_query(base_query) # rubocop:disable Metrics/AbcSize
42
49
  domain_model = self.media_type&.domain_model
43
50
  raise "No domain model defined for #{self.name}. Cannot use the attribute filtering helpers without it" unless domain_model
44
51
 
@@ -47,18 +54,20 @@ module Praxis
47
54
  base_query = domain_model.craft_filter_query( base_query , filters: filters )
48
55
  # Handle field and nested field selection
49
56
  base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors)
50
- # handle pagination and ordering
51
- base_query = _craft_pagination_query(query: base_query, type: type) if self.respond_to?(:_pagination)
57
+ # handle pagination and ordering if the pagination extention is included
58
+ base_query = domain_model.craft_pagination_query(base_query, pagination: _pagination) if self.respond_to?(:_pagination)
52
59
 
53
60
  base_query
54
61
  end
55
62
 
56
63
  def selector_generator
57
- @selector_generator ||= Praxis::Mapper::SelectorGenerator.new
58
- end
64
+ return unless self.media_type.respond_to?(:domain_model) &&
65
+ self.media_type.domain_model < Praxis::Mapper::Resource
59
66
 
67
+ @selector_generator ||= \
68
+ Praxis::Mapper::SelectorGenerator.new.add(self.media_type.domain_model, self.expanded_fields)
69
+ end
60
70
  end
61
-
62
71
  end
63
72
  end
64
73
  end
@@ -1,14 +1,45 @@
1
1
  require 'singleton'
2
2
  require 'praxis/extensions/pagination'
3
3
 
4
- # Simple plugin concept
4
+ # The PaginationPlugin can be configured to take advantage of adding pagination and sorting to
5
+ # your DB queries.
6
+ # When combined with the MapperPlugin, there is no extra configuration that needs to be done for
7
+ # the system to appropriately identify the pagination and order parameters in the API, and translate
8
+ # that in to the appropriate queries to fetch.
9
+ #
10
+ # To use this plugin without the MapperPlugin (probably a rare case), one can apply the appropriate
11
+ # clauses onto a query, by directly calling (in the controller) the `craft_pagination_query` method
12
+ # of the domain_model associated to the controller's mediatype.
13
+ # For example, here's how you can manually use this extension in a fictitious users index action:
14
+ # def index
15
+ # base_query = User.all # Start by not excluding any user
16
+ # domain_model = self.media_type.domain_model
17
+ # objs = domain_model.craft_pagination_query(base_query, pagination: _pagination)
18
+ # display(objs)
19
+ # end
20
+ #
21
+ # This plugin accepts configuration about the default behavior of pagination.
22
+ # Any of these configs can individually be overidden when defining each Pagination/Order parameters
23
+ # in any of the Endpoint actions.
24
+ #
5
25
  # Example configuration for this plugin
6
26
  # Praxis::Application.configure do |application|
7
27
  # application.bootloader.use Praxis::Plugins::PaginationPlugin, {
28
+ # # The maximum number of results that a paginated response will ever allow
8
29
  # max_items: 500, # Unlimited by default,
30
+ # # The default page size to use when no `items` is specified
9
31
  # default_page_size: 100,
10
- # disallow_paging_by_default: false,
11
- # # See all available options below
32
+ # # Disallows the use of the page type pagination mode when true (i.e., using 'page=' parameter)
33
+ # disallow_paging_by_default: true, # Default false
34
+ # # Disallows the use of the cursor type pagination mode when true (i.e., using 'by=' or 'from=' parameter)
35
+ # disallow_cursor_by_default: true, # Default false
36
+ # # The default mode params to use
37
+ # paging_default_mode: {by: :uuid}, # Default {by: :uid}
38
+ # # Weather or not to enforce that all requested sort fields are part of the media_type attributes
39
+ # # when false (not enforced) only the first field would be checked
40
+ # sorting: {
41
+ # enforce_all_fields: false # Default true
42
+ # }
12
43
  # end
13
44
  # end
14
45
  #
@@ -39,7 +70,6 @@ module Praxis
39
70
  attribute :paging_default_mode, Hash, default: Praxis::Types::PaginationParams.paging_default_mode
40
71
  attribute :disallow_paging_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_paging_by_default
41
72
  attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
42
- attribute :disallow_cursor_by_default, Attributor::Boolean, default: Praxis::Types::PaginationParams.disallow_cursor_by_default
43
73
  attribute :sorting do
44
74
  attribute :enforce_all_fields, Attributor::Boolean, default: Praxis::Types::OrderingParams.enforce_all_fields
45
75
  end
@@ -21,7 +21,10 @@ namespace :praxis do
21
21
  trap('INT') { s.shutdown }
22
22
  s.start
23
23
  end
24
- `open http://localhost:#{docs_port}/`
24
+ # If there is only 1 version we'll feature it and open the browser onto it
25
+ versions = Dir.children(root)
26
+ featured_version = (versions.size < 2) ? "#{versions.first}/" : ''
27
+ `open http://localhost:#{docs_port}/#{featured_version}`
25
28
  wb.join
26
29
  end
27
30
  desc "Generate and package all OpenApi Docs into a zip, ready for a Web server (like S3...) to present it"
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.10'
2
+ VERSION = '2.0.pre.11'
3
3
  end
@@ -39,7 +39,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
39
39
  instance
40
40
  expect(instance.query).to eq(base_query)
41
41
  expect(instance.model).to eq(base_model)
42
- expect(instance.attr_to_column).to eq(filters_map)
42
+ expect(instance.filters_map).to eq(filters_map)
43
43
  end
44
44
  end
45
45
  context 'generate' do
@@ -57,7 +57,20 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
57
57
  let(:filters_string) { 'category_uuid=deadbeef1' }
58
58
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
59
59
  end
60
- context 'that maps to a different name' do
60
+ context 'same-name filter mapping works' do
61
+ context 'even if ther was not a filter explicitly defined for it' do
62
+ let(:filters_string) { 'category_uuid=deadbeef1' }
63
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
64
+ end
65
+
66
+ context 'but if it is a field that does not exist in the model' do
67
+ let(:filters_string) { 'nonexisting=valuehere' }
68
+ it 'it blows up with the right error' do
69
+ expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
70
+ end
71
+ end
72
+ end
73
+ context 'that maps to a different name' do
61
74
  let(:filters_string) { 'name=Book1'}
62
75
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
63
76
  end
@@ -29,7 +29,7 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
29
29
  :id # We always load the primary keys
30
30
  ]
31
31
  end
32
- let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields) }
32
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields).selectors }
33
33
  let(:debug){ false }
34
34
 
35
35
  subject(:selector) {described_class.new(query: query, selectors: selector_node, debug: debug) }
@@ -64,7 +64,7 @@ describe Praxis::Extensions::FieldSelection::SequelQuerySelector do
64
64
  ]
65
65
  end
66
66
 
67
- let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields) }
67
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields).selectors }
68
68
  subject {described_class.new(query: query, selectors: selector_node, debug: debug) }
69
69
 
70
70
  context 'generate' do
@@ -103,7 +103,7 @@ class ActiveBookResource < ActiveBaseResource
103
103
 
104
104
  filters_mapping(
105
105
  id: :id,
106
- category_uuid: :category_uuid,
106
+ # category_uuid: :category_uuid #NOTE: we do not need to define same-name-mappings if they exist
107
107
  'fake_nested.name': 'simple_name',
108
108
  'name': 'simple_name',
109
109
  'name_is_not': lambda do |spec| # Silly way to use a proc, but good enough for testing
@@ -8,7 +8,7 @@ describe Praxis::Mapper::SelectorGenerator do
8
8
  context '#add' do
9
9
  let(:resource) { SimpleResource }
10
10
  shared_examples 'a proper selector' do
11
- it { expect(generator.add(resource, fields).dump).to be_deep_equal selectors }
11
+ it { expect(generator.add(resource, fields).selectors.dump).to be_deep_equal selectors }
12
12
  end
13
13
 
14
14
  context 'basic combos' do
@@ -33,16 +33,22 @@ module PraxisGen
33
33
  puts
34
34
  puts " cd #{app_name}"
35
35
  puts " bundle"
36
- puts " bundle exec rake db:create db:migrate db:seed # To create/migrate/seed the dev DB"
37
- puts " bundle exec rackup # To start the web server"
36
+ puts " bundle exec rake db:recreate # To create/migrate/seed the dev DB"
37
+ puts " bundle exec rackup # To start the web server"
38
38
  puts
39
39
  puts "From another terminal/app, use curl (or your favorite HTTP client) to retrieve data from the API"
40
40
  puts " For example: "
41
- puts " Get all users without filters or limit, and display only uid, and last_name fields"
42
- puts " curl -H 'X-Api-Version: 1' http://localhost:9292/users?fields=uid,last_name"
41
+ puts " Get all users without filters or limit, and display only id, and first_name fields"
42
+ puts " curl -G -H 'X-Api-Version: 1' http://localhost:9292/users \\"
43
+ puts " --data-urlencode \"fields=id,first_name\""
43
44
  puts
44
- puts " Get the last 5 users, ordered by last_name (descending), and display only uid, and last_name fields"
45
- puts " curl -H 'X-Api-Version: 1' 'http://localhost:9292/users?fields=uid,last_name&order=-last_name&pagination=by%3Dlast_name,items%3D5' "
45
+ puts " Get the last 5 users, with last_names starting with \"L\" ordered by first_name (descending)"
46
+ puts " and display only id, first_name, last_name, and email fields"
47
+ puts " curl -G -H 'X-Api-Version: 1' http://localhost:9292/users \\"
48
+ puts " --data-urlencode \"filters=last_name=L*\" \\"
49
+ puts " --data-urlencode \"pagination=by=first_name,items=5\" \\"
50
+ puts " --data-urlencode \"order=-first_name\" \\"
51
+ puts " --data-urlencode \"fields=id,first_name,last_name,email\""
46
52
  puts " (Note: To list all routes use: bundle exec rake praxis:routes)"
47
53
  puts
48
54
  nil
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PraxisGen
4
+ class Model < Thor
5
+ require 'active_support/inflector'
6
+ include Thor::Actions
7
+
8
+ def self.source_root
9
+ File.dirname(__FILE__) + "/templates/generator/scaffold"
10
+ end
11
+
12
+ desc "gmodel", "Generates a skeleton model file under app/models for ActiveRecord or Sequel."
13
+ argument :model_name, required: true
14
+ option :orm, required: false, default: 'activerecord', enum: ['activerecord','sequel']
15
+ def g
16
+ #self.class.check_name(model_name)
17
+ template_file = \
18
+ if options[:orm] == 'activerecord'
19
+ 'models/active_record.rb'
20
+ else
21
+ 'models/sequel.rb'
22
+ end
23
+ puts "Generating Model for #{model_name}"
24
+ template template_file, "app/models/#{model_name}.rb"
25
+ nil
26
+ end
27
+ # Helper functions (which are available in the ERB contexts)
28
+ no_commands do
29
+ def model_class
30
+ model_name.camelize
31
+ end
32
+ end
33
+
34
+ # TODO: do we want the argument to be camelcase? or snake case?
35
+ def self.check_name(name)
36
+ sanitized = name.downcase.gsub(/[^a-z0-9_]/, '')
37
+ raise "Please use only downcase letters, numbers and underscores for the model" unless sanitized == name
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PraxisGen
4
+ class Scaffold < Thor
5
+ require 'active_support/inflector'
6
+ include Thor::Actions
7
+
8
+ attr_reader :actions_hash
9
+
10
+ def self.source_root
11
+ File.dirname(__FILE__) + "/templates/generator/scaffold"
12
+ end
13
+
14
+ desc "g","Generates an API design and implementation scaffold for managing a collection of <collection_name>"
15
+ argument :collection_name, required: true
16
+ option :version, required: false, default: '1',
17
+ desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
18
+ option :design, type: :boolean, default: true,
19
+ desc: 'Include the Endpoint and MediaType files for the collection'
20
+ option :implementation, type: :boolean, default: true,
21
+ desc: 'Include the Controller and (possibly the) Resource files for the collection (see --no-resource)'
22
+ option :resource, type: :boolean, default: true,
23
+ desc: 'Disable (or enable) the creation of the Resource files when generating implementation'
24
+ option :model, type: :string, enum: ['activerecord','sequel'],
25
+ desc: 'It also generates a model for the given ORM. An empty --model flag will default to activerecord'
26
+ option :actions, type: :string, default: 'crud', enum: ['cr','cru','crud','u','ud','d'],
27
+ desc: 'Specifies the actions to generate for the API. cr=create, u=update, d=delete. Index and show actions are always generated'
28
+ def g
29
+ self.class.check_name(collection_name)
30
+ @actions_hash = self.class.compose_actions_hash(options[:actions])
31
+ env_rb = Pathname.new(destination_root)+Pathname.new("config/environment.rb")
32
+ @pagination_plugin_found = File.open(env_rb).grep(/Praxis::Plugins::PaginationPlugin.*/).reject{|l| l.strip[0] == '#'}.present?
33
+ if options[:design]
34
+ say_status 'Design', "Generating scaffold for #{plural_class}", :blue
35
+ template 'design/media_types/item.rb', "design/#{version_dir}/media_types/#{collection_name.singularize}.rb"
36
+ template 'design/endpoints/collection.rb', "design/#{version_dir}/endpoints/#{collection_name}.rb"
37
+ end
38
+ if options[:implementation]
39
+ say_status 'Implement', "Generating scaffold for #{plural_class}", :blue
40
+ if options[:resource]
41
+ base_resource = Pathname.new(destination_root)+Pathname.new("app/#{version_dir}/resources/base.rb")
42
+ unless base_resource.exist?
43
+ # Copy an appropriate base resource for the version (resources within same version must share same base)
44
+ say_status "NOTE:",
45
+ "Creating a base resource file for resources to inherit from (at 'app/#{version_dir}/resources/base.rb')",
46
+ :yellow
47
+ say_status "",
48
+ "If you had already other resources in the app, change them to derive from this Base"
49
+ template 'implementation/resources/base.rb', "app/#{version_dir}/resources/base.rb"
50
+ end
51
+ template 'implementation/resources/item.rb', "app/#{version_dir}/resources/#{collection_name.singularize}.rb"
52
+ end
53
+ template 'implementation/controllers/collection.rb', "app/#{version_dir}/controllers/#{collection_name}.rb"
54
+ end
55
+ nil
56
+ end
57
+
58
+ # Helper functions (which are available in the ERB contexts)
59
+ no_commands do
60
+ def plural_class
61
+ collection_name.camelize
62
+ end
63
+
64
+ def singular_class
65
+ collection_name.singularize.camelize
66
+ end
67
+
68
+ def version
69
+ options[:version]
70
+ end
71
+
72
+ def version_module
73
+ "V#{version}"
74
+ end
75
+
76
+ def version_dir
77
+ version_module.camelize(:lower)
78
+ end
79
+
80
+ def action_enabled?(action)
81
+ @actions_hash[action.to_sym]
82
+ end
83
+
84
+ def pagination_enabled?
85
+ @pagination_plugin_found
86
+ end
87
+ end
88
+
89
+ def self.compose_actions_hash(actions_opt)
90
+ required = { index: true, show: true }
91
+ case actions_opt
92
+ when nil
93
+ required
94
+ when 'cr'
95
+ required.merge(create: true)
96
+ when 'cru'
97
+ required.merge(create: true, update: true)
98
+ when 'crud'
99
+ required.merge(create: true, update: true, delete: true)
100
+ when 'u'
101
+ required.merge(update: true)
102
+ when 'ud'
103
+ required.merge(update: true, delete: true)
104
+ when 'd'
105
+ required.merge(delete: true)
106
+ else
107
+ raise "actions option does not support the string #{actions_opt}"
108
+ end
109
+ end
110
+
111
+ def self.check_name(name)
112
+ sanitized = name.downcase.gsub(/[^a-z0-9_]/, '')
113
+ # TODO: bail or support CamelCase collections (for now only snake case)
114
+ raise "Please use only downcase letters, numbers and underscores for the collection" unless sanitized == name
115
+ end
116
+ end
117
+ end
@@ -18,6 +18,7 @@ Praxis::Application.configure do |application|
18
18
  # map :models, 'models/**/*'
19
19
  # map :responses, '**/responses/**/*'
20
20
  # map :exceptions, '**/exceptions/**/*'
21
+ # map :concerns, '**/concerns/**/*'
21
22
  # map :resources, '**/resources/**/*'
22
23
  # map :controllers, '**/controllers/**/*'
23
24
  # end
@@ -32,10 +32,17 @@ namespace :db do
32
32
  puts "Database migrated."
33
33
  end
34
34
 
35
+ desc 'Fully receate, migrate and seed the DB'
36
+ task :recreate do
37
+ Rake::Task['db:drop'].invoke rescue nil
38
+ Rake::Task['db:create'].invoke
39
+ Rake::Task['db:migrate'].invoke
40
+ Rake::Task['db:seed'].invoke
41
+ end
42
+
35
43
  desc 'seed with example data'
36
44
  task seed: 'praxis:environment' do
37
- require_relative 'spec/helpers/database_helper'
38
- DatabaseHelper.seed!
45
+ require_relative 'db/seeds.rb'
39
46
  end
40
47
 
41
48
  desc 'drops current database'
@@ -0,0 +1,24 @@
1
+ module V1
2
+ module Concerns
3
+ module ControllerBase
4
+ extend ActiveSupport::Concern
5
+ # Controller concen that wraps an API with a transaction, and automatically rolls it back
6
+ # for non-2xx (or 3xx) responses
7
+ included do
8
+ around :action do |controller, callee|
9
+ begin
10
+ # TODO: Support Sequel as well
11
+ ActiveRecord::Base.transaction do
12
+ callee.call
13
+ res = controller.response
14
+ # Force a rollback for non 2xx or 3xx responses
15
+ raise ActiveRecord::Rollback unless res.status >= 200 && res.status < 400
16
+ end
17
+ rescue ActiveRecord::Rollback
18
+ # No need to do anything, let the responses flow normally
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end