brick 1.0.190 → 1.0.192

Sign up to get free protection for your applications and to get access to all the features.
data/lib/brick.rb CHANGED
@@ -107,7 +107,7 @@ module Brick
107
107
  end
108
108
 
109
109
  attr_accessor :default_schema, :db_schemas, :test_schema,
110
- :routes_done, :established_drf,
110
+ :established_drf,
111
111
  :is_oracle, :is_eager_loading, :auto_models, :initializer_loaded,
112
112
  :table_name_lookup
113
113
  ::Brick.auto_models = []
@@ -619,6 +619,10 @@ module Brick
619
619
  Brick.config.license = key
620
620
  end
621
621
 
622
+ def omit_empty_tables_in_dropdown=(setting)
623
+ Brick.config.omit_empty_tables_in_dropdown = setting
624
+ end
625
+
622
626
  def always_load_fields=(field_set)
623
627
  Brick.config.always_load_fields = field_set
624
628
  end
@@ -877,339 +881,6 @@ In config/initializers/brick.rb appropriate entries would look something like:
877
881
  end
878
882
  end
879
883
 
880
- module RouteMapper
881
- def add_brick_routes
882
- routeset_to_use = ::Rails.application.routes
883
- path_prefix = ::Brick.config.path_prefix
884
- existing_controllers = routeset_to_use.routes.each_with_object({}) do |r, s|
885
- if r.verb == 'GET' && (c = r.defaults[:controller])
886
- path = r.path.ast.to_s
887
- path = path[0..((path.index('(') || 0) - 1)]
888
- # Skip adding this if it's the default_route_fallback set from the initializers/brick.rb file
889
- next if "#{path}##{r.defaults[:action]}" == ::Brick.established_drf
890
-
891
- # next unless [:index, :show, :new, :edit].incude?(a = r.defaults[:action])
892
-
893
- # c_parts = c.split('/')
894
- # while c_parts.length > 0
895
- # c_dotted = c_parts.join('.')
896
- # if (relation = ::Brick.relations[c_dotted]) # Does it match up with an existing Brick table / resource name?
897
- # puts path
898
- # puts " #{c_dotted}##{r.defaults[:action]}"
899
- # s[c_dotted.tr('.', '/')] = nil
900
- # break
901
- # end
902
- # c_parts.shift
903
- # end
904
- s[c] = nil
905
- end
906
- end
907
-
908
- tables = []
909
- views = []
910
- table_class_length = 38 # Length of "Classes that can be built from tables:"
911
- view_class_length = 37 # Length of "Classes that can be built from views:"
912
-
913
- brick_namespace_create = lambda do |path_names, res_name, options|
914
- if path_names&.present?
915
- if (path_name = path_names.pop).is_a?(Array)
916
- module_name = path_name[1]
917
- path_name = path_name.first
918
- end
919
- send(:scope, { module: module_name || path_name, path: path_name, as: path_name }) do
920
- brick_namespace_create.call(path_names, res_name, options)
921
- end
922
- else
923
- send(:resources, res_name.to_sym, **options)
924
- end
925
- end
926
-
927
- # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
928
- # If auto-controllers and auto-models are both enabled then this makes sense:
929
- controller_prefix = (path_prefix ? "#{path_prefix}/" : '')
930
- sti_subclasses = ::Brick.config.sti_namespace_prefixes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
931
- # Turn something like {"::Spouse"=>"Person", "::Friend"=>"Person"} into {"Person"=>["Spouse", "Friend"]}
932
- s[v.last] << v.first[2..-1] unless v.first.end_with?('::')
933
- end
934
- versioned_views = {} # Track which views have already been done for each api_root
935
- ::Brick.relations.each do |k, v|
936
- next if k.is_a?(Symbol)
937
-
938
- if (schema_name = v.fetch(:schema, nil))
939
- schema_prefix = "#{schema_name}."
940
- end
941
-
942
- resource_name = v.fetch(:resource, nil)
943
- next if !resource_name ||
944
- existing_controllers.key?(
945
- controller_prefix + (resource_name = "#{schema_prefix&.tr('.', '/')}#{resource_name}".pluralize)
946
- )
947
-
948
- object_name = k.split('.').last # Take off any first schema part
949
-
950
- full_schema_prefix = if (full_aps = aps = v.fetch(:auto_prefixed_schema, nil))
951
- aps = aps[0..-2] if aps[-1] == '_'
952
- (schema_prefix&.dup || +'') << "#{aps}."
953
- else
954
- schema_prefix
955
- end
956
-
957
- # Track routes being built
958
- if (class_name = v.fetch(:class_name, nil))
959
- if v.key?(:isView)
960
- view_class_length = class_name.length if class_name.length > view_class_length
961
- views
962
- else
963
- table_class_length = class_name.length if class_name.length > table_class_length
964
- tables
965
- end << [class_name, aps, k.tr('.', '/')[full_aps&.length || 0 .. -1]]
966
- end
967
-
968
- options = {}
969
- options[:only] = [:index, :show] if v.key?(:isView)
970
-
971
- # First do the normal routes
972
- prefixes = []
973
- prefixes << [aps, v[:class_name]&.split('::')[-2]&.underscore] if aps
974
- prefixes << schema_name if schema_name
975
- prefixes << path_prefix if path_prefix
976
- brick_namespace_create.call(prefixes, v[:resource], options)
977
- sti_subclasses.fetch(class_name, nil)&.each do |sc| # Add any STI subclass routes for this relation
978
- brick_namespace_create.call(prefixes, sc.underscore.tr('/', '_').pluralize, options)
979
- end
980
-
981
- # Now the API routes if necessary
982
- full_resource = nil
983
- ::Brick.api_roots&.each do |api_root|
984
- api_done_views = (versioned_views[api_root] ||= {})
985
- found = nil
986
- test_ver_num = nil
987
- view_relation = nil
988
- # If it's a view then see if there's a versioned one available by searching for resource names
989
- # versioned with the closest number (equal to or less than) compared with our API version number.
990
- if v.key?(:isView)
991
- if (ver = object_name.match(/^v([\d_]*)/)&.captures&.first) && ver[-1] == '_'
992
- core_object_name = object_name[ver.length + 1..-1]
993
- next if api_done_views.key?(unversioned = "#{schema_prefix}v_#{core_object_name}")
994
-
995
- # Expect that the last item in the path generally holds versioning information
996
- api_ver = api_root.split('/')[-1]&.gsub('_', '.')
997
- vn_idx = api_ver.rindex(/[^\d._]/) # Position of the first numeric digit at the end of the version number
998
- # Was: .to_d
999
- test_ver_num = api_ver_num = api_ver[vn_idx + 1..-1].gsub('_', '.').to_i # Attempt to turn something like "v3" into the decimal value 3
1000
- # puts [api_ver, vn_idx, api_ver_num, unversioned].inspect
1001
-
1002
- next if ver.to_i > api_ver_num # Don't surface any newer views in an older API
1003
-
1004
- test_ver_num -= 1 until test_ver_num.zero? ||
1005
- (view_relation = ::Brick.relations.fetch(
1006
- found = "#{schema_prefix}v#{test_ver_num}_#{core_object_name}", nil
1007
- ))
1008
- api_done_views[unversioned] = nil # Mark that for this API version this view is done
1009
-
1010
- # puts "Found #{found}" if view_relation
1011
- # If we haven't found "v3_view_name" or "v2_view_name" or so forth, at the last
1012
- # fall back to simply looking for "v_view_name", and then finally "view_name".
1013
- no_v_prefix_name = "#{schema_prefix}#{core_object_name}"
1014
- standard_prefix = 'v_'
1015
- else
1016
- core_object_name = object_name
1017
- end
1018
- if (rvp = ::Brick.config.api_remove_view_prefix) && core_object_name.start_with?(rvp)
1019
- core_object_name.slice!(0, rvp.length)
1020
- end
1021
- no_prefix_name = "#{schema_prefix}#{core_object_name}"
1022
- unversioned = "#{schema_prefix}#{standard_prefix}#{::Brick.config.api_add_view_prefix}#{core_object_name}"
1023
- else
1024
- unversioned = k
1025
- end
1026
-
1027
- view_relation ||= ::Brick.relations.fetch(found = unversioned, nil) ||
1028
- (no_v_prefix_name && ::Brick.relations.fetch(found = no_v_prefix_name, nil)) ||
1029
- (no_prefix_name && ::Brick.relations.fetch(found = no_prefix_name, nil))
1030
- if view_relation
1031
- actions = view_relation.key?(:isView) ? [:index, :show] : ::Brick::ALL_API_ACTIONS # By default all actions are allowed
1032
- # Call proc that limits which endpoints get surfaced based on version, table or view name, method (get list / get one / post / patch / delete)
1033
- # Returning nil makes it do nothing, false makes it skip creating this endpoint, and an array of up to
1034
- # these 3 things controls and changes the nature of the endpoint that gets built:
1035
- # (updated api_name, name of different relation to route to, allowed actions such as :index, :show, :create, etc)
1036
- proc_result = if (filter = ::Brick.config.api_filter).is_a?(Proc)
1037
- begin
1038
- num_args = filter.arity.negative? ? 6 : filter.arity
1039
- filter.call(*[unversioned, k, view_relation, actions, api_ver_num, found, test_ver_num][0...num_args])
1040
- rescue StandardError => e
1041
- puts "::Brick.api_filter Proc error: #{e.message}"
1042
- end
1043
- end
1044
- # proc_result expects to receive back: [updated_api_name, to_other_relation, allowed_actions]
1045
-
1046
- case proc_result
1047
- when NilClass
1048
- # Do nothing differently than what normal behaviour would be
1049
- when FalseClass # Skip implementing this endpoint
1050
- view_relation[:api][api_ver_num] = nil
1051
- next
1052
- when Array # Did they give back an array of actions?
1053
- unless proc_result.any? { |pr| ::Brick::ALL_API_ACTIONS.exclude?(pr) }
1054
- proc_result = [unversioned, to_relation, proc_result]
1055
- end
1056
- # Otherwise don't change this array because it's probably legit
1057
- when String
1058
- proc_result = [proc_result] # Treat this as the surfaced api_name (path) they want to use for this endpoint
1059
- else
1060
- puts "::Brick.api_filter Proc warning: Unable to parse this result returned: \n #{proc_result.inspect}"
1061
- proc_result = nil # Couldn't understand what in the world was returned
1062
- end
1063
-
1064
- if proc_result&.present?
1065
- if proc_result[1] # to_other_relation
1066
- if (new_view_relation = ::Brick.relations.fetch(proc_result[1], nil))
1067
- k = proc_result[1] # Route this call over to this different relation
1068
- view_relation = new_view_relation
1069
- else
1070
- puts "::Brick.api_filter Proc warning: Unable to find new suggested relation with name #{proc_result[1]} -- sticking with #{k} instead."
1071
- end
1072
- end
1073
- if proc_result.first&.!=(k) # updated_api_name -- a different name than this relation would normally have
1074
- found = proc_result.first
1075
- end
1076
- actions &= proc_result[2] if proc_result[2] # allowed_actions
1077
- end
1078
- (view_relation[:api][api_ver_num] ||= {})[unversioned] = actions # Add to the list of API paths this resource responds to
1079
-
1080
- # view_ver_num = if (first_part = k.split('_').first) =~ /^v[\d_]+/
1081
- # first_part[1..-1].gsub('_', '.').to_i
1082
- # end
1083
- controller_name = if (last = view_relation.fetch(:resource, nil)&.pluralize)
1084
- "#{full_schema_prefix}#{last}"
1085
- else
1086
- found
1087
- end.tr('.', '/')
1088
-
1089
- { :index => 'get', :create => 'post' }.each do |action, method|
1090
- if actions.include?(action)
1091
- # Normally goes to something like: /api/v1/employees
1092
- send(method, "#{api_root}#{unversioned.tr('.', '/')}", { to: "#{controller_prefix}#{controller_name}##{action}" })
1093
- end
1094
- end
1095
- # %%% We do not yet surface the #show action
1096
- if (id_col = view_relation[:pk]&.first) # ID-dependent stuff
1097
- { :update => ['put', 'patch'], :destroy => ['delete'] }.each do |action, methods|
1098
- if actions.include?(action)
1099
- methods.each do |method|
1100
- send(method, "#{api_root}#{unversioned.tr('.', '/')}/:#{id_col}", { to: "#{controller_prefix}#{controller_name}##{action}" })
1101
- end
1102
- end
1103
- end
1104
- end
1105
- end
1106
- end
1107
-
1108
- # Trestle compatibility
1109
- if Object.const_defined?('Trestle') && ::Trestle.config.options&.key?(:site_title) &&
1110
- !Object.const_defined?("#{(res_name = resource_name.tr('/', '_')).camelize}Admin")
1111
- begin
1112
- ::Trestle.resource(res_sym = res_name.to_sym, model: class_name&.constantize) do
1113
- menu { item res_sym, icon: "fa fa-star" }
1114
- end
1115
- rescue
1116
- end
1117
- end
1118
- end
1119
-
1120
- if (named_routes = instance_variable_get(:@set).named_routes).respond_to?(:find)
1121
- if ::Brick.config.add_status && (status_as = "#{controller_prefix.tr('/', '_')}brick_status".to_sym)
1122
- (
1123
- !(status_route = instance_variable_get(:@set).named_routes.find { |route| route.first == status_as }&.last) ||
1124
- !status_route.ast.to_s.include?("/#{controller_prefix}brick_status/")
1125
- )
1126
- get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: status_as.to_s)
1127
- end
1128
-
1129
- # ::Brick.config.add_schema &&
1130
- if (schema_as = "#{controller_prefix.tr('/', '_')}brick_schema".to_sym)
1131
- (
1132
- !(schema_route = instance_variable_get(:@set).named_routes.find { |route| route.first == schema_as }&.last) ||
1133
- !schema_route.ast.to_s.include?("/#{controller_prefix}brick_schema/")
1134
- )
1135
- post("/#{controller_prefix}brick_schema", to: 'brick_gem#schema_create', as: schema_as.to_s)
1136
- end
1137
-
1138
- if ::Brick.config.add_orphans && (orphans_as = "#{controller_prefix.tr('/', '_')}brick_orphans".to_sym)
1139
- (
1140
- !(orphans_route = instance_variable_get(:@set).named_routes.find { |route| route.first == orphans_as }&.last) ||
1141
- !orphans_route.ast.to_s.include?("/#{controller_prefix}brick_orphans/")
1142
- )
1143
- get("/#{controller_prefix}brick_orphans", to: 'brick_gem#orphans', as: 'brick_orphans')
1144
- end
1145
- end
1146
-
1147
- if instance_variable_get(:@set).named_routes.names.exclude?(:brick_crosstab)
1148
- get("/#{controller_prefix}brick_crosstab", to: 'brick_gem#crosstab', as: 'brick_crosstab')
1149
- get("/#{controller_prefix}brick_crosstab/data", to: 'brick_gem#crosstab_data')
1150
- end
1151
-
1152
- if ((rswag_ui_present = Object.const_defined?('Rswag::Ui')) &&
1153
- (rswag_path = routeset_to_use.routes.find { |r| r.app.app == ::Rswag::Ui::Engine }
1154
- &.instance_variable_get(:@path_formatter)
1155
- &.instance_variable_get(:@parts)&.join) &&
1156
- (doc_endpoints = ::Rswag::Ui.config.config_object[:urls])) ||
1157
- (doc_endpoints = ::Brick.instance_variable_get(:@swagger_endpoints))
1158
- last_endpoint_parts = nil
1159
- doc_endpoints.each do |doc_endpoint|
1160
- puts "Mounting OpenApi 3.0 documentation endpoint for \"#{doc_endpoint[:name]}\" on #{doc_endpoint[:url]}" unless ::Brick.routes_done
1161
- send(:get, doc_endpoint[:url], { to: 'brick_openapi#index' })
1162
- endpoint_parts = doc_endpoint[:url]&.split('/')
1163
- last_endpoint_parts = endpoint_parts
1164
- end
1165
- end
1166
- return if ::Brick.routes_done
1167
-
1168
- if doc_endpoints.present?
1169
- if rswag_ui_present
1170
- if rswag_path
1171
- puts "API documentation now available when navigating to: /#{last_endpoint_parts&.find(&:present?)}/index.html"
1172
- else
1173
- puts "In order to make documentation available you can put this into your routes.rb:"
1174
- puts " mount Rswag::Ui::Engine => '/#{last_endpoint_parts&.find(&:present?) || 'api-docs'}'"
1175
- end
1176
- else
1177
- puts "Having this exposed, one easy way to leverage this to create HTML-based API documentation is to use Scalar.
1178
- It will jump to life when you put these two lines into a view template or other HTML resource:
1179
- <script id=\"api-reference\" data-url=\"#{last_endpoint_parts.join('/')}\"></script>
1180
- <script src=\"https://cdn.jsdelivr.net/@scalar/api-reference\"></script>
1181
- Alternatively you can add the rswag-ui gem."
1182
- end
1183
- elsif rswag_ui_present
1184
- sample_path = rswag_path || '/api-docs'
1185
- puts
1186
- puts "Brick: rswag-ui gem detected -- to make OpenAPI 3.0 documentation available from a path such as '#{sample_path}/v1/swagger.json',"
1187
- puts ' put code such as this in an initializer:'
1188
- puts ' Rswag::Ui.configure do |config|'
1189
- puts " config.swagger_endpoint '#{sample_path}/v1/swagger.json', 'API V1 Docs'"
1190
- puts ' end'
1191
- unless rswag_path
1192
- puts
1193
- puts ' and put this into your routes.rb:'
1194
- puts " mount Rswag::Ui::Engine => '/api-docs'"
1195
- end
1196
- end
1197
-
1198
- puts "\n" if tables.present? || views.present?
1199
- if tables.present?
1200
- puts "Classes that can be built from tables:#{' ' * (table_class_length - 38)} Path:"
1201
- puts "======================================#{' ' * (table_class_length - 38)} ====="
1202
- ::Brick.display_classes(controller_prefix, tables, table_class_length)
1203
- end
1204
- if views.present?
1205
- puts "Classes that can be built from views:#{' ' * (view_class_length - 37)} Path:"
1206
- puts "=====================================#{' ' * (view_class_length - 37)} ====="
1207
- ::Brick.display_classes(controller_prefix, views, view_class_length)
1208
- end
1209
- ::Brick.routes_done = true
1210
- end
1211
- end
1212
-
1213
884
  end
1214
885
 
1215
886
  require 'brick/version_number'
@@ -2004,7 +1675,11 @@ end
2004
1675
 
2005
1676
  # Now the Ransack Polyamorous version of #build
2006
1677
  if Gem::Dependency.new('ransack').matching_specs.present?
2007
- require "polyamorous/activerecord_#{::ActiveRecord::VERSION::STRING[0, 3]}_ruby_2/join_dependency"
1678
+ begin # First try the new way of requiring Polyamorous
1679
+ require 'polyamorous/activerecord/join_dependency'
1680
+ rescue LoadError => e
1681
+ require "polyamorous/activerecord_#{::ActiveRecord::VERSION::STRING[0, 3]}_ruby_2/join_dependency"
1682
+ end
2008
1683
  module Polyamorous::JoinDependencyExtensions
2009
1684
  def build(associations, base_klass, root = nil, path = '')
2010
1685
  root ||= associations
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'brick'
4
+ require 'rails/generators'
5
+ require 'rails/generators/active_record'
6
+ require 'fancy_gets'
7
+
8
+ module Brick
9
+ # Auto-generates controllers
10
+ class ControllersGenerator < ::Rails::Generators::Base
11
+ include FancyGets
12
+ # include ::Rails::Generators::Migration
13
+
14
+ desc 'Auto-generates controllers'
15
+
16
+ def brick_controllers
17
+ # %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
18
+
19
+ ::Brick.mode = :on
20
+ ActiveRecord::Base.establish_connection
21
+
22
+ # Load all models and controllers
23
+ ::Brick.eager_load_classes
24
+
25
+ # Generate a list of viable controllers that can be chosen
26
+ longest_length = 0
27
+ model_info = Hash.new { |h, k| h[k] = {} }
28
+ tableless = Hash.new { |h, k| h[k] = [] }
29
+ existing_controllers = ActionController::Base.descendants.reject do |c|
30
+ c.name.start_with?('Turbo::Native::')
31
+ end.map(&:name)
32
+ controllers = ::Brick.relations.each_with_object([]) do |rel, s|
33
+ next if rel.first.is_a?(Symbol)
34
+
35
+ tbl_parts = rel.first.split('.')
36
+ tbl_parts.shift if [::Brick.default_schema, 'public'].include?(tbl_parts.first)
37
+ tbl_parts[-1] = tbl_parts[-1].pluralize
38
+ begin
39
+ s << ControllerOption.new(tbl_parts.join('/').camelize, rel.last[:class_name].constantize)
40
+ rescue
41
+ end
42
+ end.reject { |c| existing_controllers.include?(c.to_s) }
43
+ controllers.sort! do |a, b| # Sort first to separate namespaced stuff from the rest, then alphabetically
44
+ is_a_namespaced = a.to_s.include?('::')
45
+ is_b_namespaced = b.to_s.include?('::')
46
+ if is_a_namespaced && !is_b_namespaced
47
+ 1
48
+ elsif !is_a_namespaced && is_b_namespaced
49
+ -1
50
+ else
51
+ a.to_s <=> b.to_s
52
+ end
53
+ end
54
+ controllers.each do |m| # Find longest name in the list for future use to show lists on the right side of the screen
55
+ if longest_length < (len = m.to_s.length)
56
+ longest_length = len
57
+ end
58
+ end
59
+ chosen = gets_list(list: controllers, chosen: controllers.dup)
60
+ relations = ::Brick.relations
61
+ chosen.each do |controller_option|
62
+ if (controller_parts = controller_option.to_s.split('::')).length > 1
63
+ namespace = controller_parts.first.constantize
64
+ end
65
+ _built_controller, code = Object.send(:build_controller, namespace, controller_parts.last, controller_parts.last.pluralize, controller_option.model, relations)
66
+ path = ['controllers']
67
+ path.concat(controller_parts.map(&:underscore))
68
+ dir = +"#{::Rails.root}/app"
69
+ path[0..-2].each do |path_part|
70
+ dir << "/#{path_part}"
71
+ Dir.mkdir(dir) unless Dir.exist?(dir)
72
+ end
73
+ File.open("#{dir}/#{path.last}.rb", 'w') { |f| f.write code } unless code.blank?
74
+ end
75
+ puts "\n*** Created #{chosen.length} controller files under app/controllers ***"
76
+ end
77
+ end
78
+ end
79
+
80
+ class ControllerOption
81
+ attr_accessor :name, :model
82
+
83
+ def initialize(name, model)
84
+ self.name = name
85
+ self.model = model
86
+ end
87
+
88
+ def to_s
89
+ name
90
+ end
91
+ end