brick 1.0.191 → 1.0.192

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 193982ca353a787d619e46d02b5d7a671c2b05936a2bf96c7f4c92fbba2b467c
4
- data.tar.gz: 065f9abdc32f6d6413caf955ab7f01cd2a300b24532f215471aadf82ec9bdc93
3
+ metadata.gz: 148c8f46702d06322322c04487a1045e6d7a3927611394a84ffe138be78adb5e
4
+ data.tar.gz: 716bfaae50dc99809bc3aee4eecf5ff1c44325bca54271db225a765cdd97fb56
5
5
  SHA512:
6
- metadata.gz: 363c321fac7e13b4ccbe4bd9ed8b10f1a525641c0da3db0c5ee2bc49f6b1a7b0b0254c11fdb9d42c4ab2ab2c9c7e673a4f6c25e063315bcafcede48cc3096692
7
- data.tar.gz: 9f27685f8621482b2130a66d5c7d149d41b108dc73f800d060f26ab757f09f06bb3c675b22db0dc37e666a55cdcd0b5ead274232913893d48881809cbf58e72e
6
+ metadata.gz: d97d94ed8d951718b34d1fc0d3ee8d023b3765bafc759fd41161155c927014ddec54a67e371363bbb17b18a614f4be16862d9cb51be313438050a3ce6e3356a2
7
+ data.tar.gz: efbd8b062eccd5d5441aa8e9fe6162bce076e188761f5c7877f9ca454899fc13873fa6cdf4db21cd2154e24c824487f56a085f41402432bd99486e17213e7f00
@@ -82,8 +82,10 @@ module ActiveRecord
82
82
 
83
83
  def json_column?(col)
84
84
  col.type == :json || ::Brick.config.json_columns[table_name]&.include?(col.name) ||
85
- (respond_to?(:attribute_types) && (attr_types = attribute_types[col.name]).respond_to?(:coder) &&
86
- (attr_types.coder.is_a?(Class) ? attr_types.coder : attr_types.coder&.class)&.name&.end_with?('JSON'))
85
+ (
86
+ respond_to?(:attribute_types) && (attr_types = attribute_types[col.name]).respond_to?(:coder) &&
87
+ (attr_types.coder.is_a?(Class) ? attr_types.coder : attr_types.coder&.class)&.name&.end_with?('JSON')
88
+ )
87
89
  end
88
90
 
89
91
  def brick_foreign_type(assoc)
@@ -284,7 +286,10 @@ module ActiveRecord
284
286
  end
285
287
  this_obj&.to_s || ''
286
288
  end
287
- is_brackets_have_content = true unless datum.blank?
289
+ begin
290
+ is_brackets_have_content = true unless datum.blank?
291
+ rescue
292
+ end
288
293
  output << (datum || '')
289
294
  bracket_name = nil
290
295
  else
@@ -1357,7 +1362,7 @@ end
1357
1362
  end
1358
1363
  rescue NameError # If the const_get for the model has failed...
1359
1364
  skip_controller = true
1360
- # ... then just fall through and allow it to fail when trying to loading the ____Controller class normally.
1365
+ # ... then just fall through and allow it to fail when trying to load the ____Controller class normally.
1361
1366
  end
1362
1367
  end
1363
1368
  unless skip_controller
@@ -1742,7 +1747,7 @@ class Object
1742
1747
  options[:optional] = true if assoc.key?(:optional)
1743
1748
  if assoc.key?(:polymorphic) ||
1744
1749
  # If a polymorphic association is missing but could be established then go ahead and put it into place.
1745
- relations[assoc[:inverse_table]][:class_name].constantize.reflect_on_all_associations.find { |inv_assoc| !inv_assoc.belongs_to? && inv_assoc.options[:as].to_s == assoc[:assoc_name] }
1750
+ relations.fetch(assoc[:inverse_table], nil)&.fetch(:class_name, nil)&.constantize&.reflect_on_all_associations&.find { |inv_assoc| !inv_assoc.belongs_to? && inv_assoc.options[:as].to_s == assoc[:assoc_name] }
1746
1751
  assoc[:polymorphic] ||= true
1747
1752
  options[:polymorphic] = true
1748
1753
  else
@@ -2645,6 +2650,10 @@ end.class_exec do
2645
2650
  orig_schema = nil
2646
2651
  if (relations = ::Brick.relations).keys == [:db_name]
2647
2652
  ::Brick.remove_instance_variable(:@_additional_references_loaded) if ::Brick.instance_variable_defined?(:@_additional_references_loaded)
2653
+
2654
+ # --------------------------------------------
2655
+ # 1. Load three initializers early
2656
+ # (inflectsions.rb, brick.rb, apartment.rb)
2648
2657
  # Very first thing, load inflections since we'll be using .pluralize and .singularize on table and model names
2649
2658
  if File.exist?(inflections = ::Rails.root&.join('config/initializers/inflections.rb') || '')
2650
2659
  load inflections
@@ -2695,6 +2704,8 @@ end.class_exec do
2695
2704
  # Only for Postgres (Doesn't work in sqlite3 or MySQL)
2696
2705
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
2697
2706
 
2707
+ # ---------------------------
2708
+ # 2. Figure out schema things
2698
2709
  is_postgres = nil
2699
2710
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
2700
2711
  case ActiveRecord::Base.connection.adapter_name
@@ -2767,6 +2778,8 @@ end.class_exec do
2767
2778
 
2768
2779
  ::Brick.db_schemas ||= {}
2769
2780
 
2781
+ # ---------------------
2782
+ # 3. Tables and columns
2770
2783
  # %%% Retrieve internal ActiveRecord table names like this:
2771
2784
  # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
2772
2785
  # For if it's not SQLite -- so this is the Postgres and MySQL version
@@ -2896,20 +2909,34 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2896
2909
  # end
2897
2910
  # end
2898
2911
  # schema = ::Brick.default_schema # Reset back for this next round of fun
2912
+
2913
+ # ---------------------------------------------
2914
+ # 4. Foreign key info
2915
+ # (done in two parts which get JOINed together in Ruby code)
2899
2916
  kcus = nil
2917
+ entry_type = nil
2900
2918
  case ActiveRecord::Base.connection.adapter_name
2901
2919
  when 'PostgreSQL', 'Mysql2', 'Trilogy', 'SQLServer'
2902
- # All KCUs -- use this to virtually JOIN against fk_references in Ruby code
2920
+ # Part 1 -- all KCUs
2903
2921
  sql = "SELECT CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, ORDINAL_POSITION,
2904
2922
  TABLE_NAME, COLUMN_NAME
2905
2923
  FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE#{"
2906
- WHERE CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }"
2924
+ WHERE CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }#{"
2925
+ WHERE CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if is_mysql
2926
+ }"
2907
2927
  kcus = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |v, s|
2908
- key = "#{v.fetch('constraint_name', v[2])}.#{v.fetch('constraint_schema', v[1])}.#{v.fetch('constraint_catalog', v[0])}.#{v.fetch('ordinal_position', v[3])}"
2909
- key << ".#{v.fetch('table_name', v[4])}.#{v.fetch('column_name', v[5])}" unless is_postgres || is_mssql
2910
- s[key] = [v.fetch('constraint_schema', v[1]), v.fetch('table_name', v[4])]
2928
+ if (entry_type ||= v.is_a?(Array) ? :array : :hash) == :hash
2929
+ key = "#{v['constraint_name']}.#{v['constraint_schema']}.#{v['constraint_catalog']}.#{v['ordinal_position']}"
2930
+ key << ".#{v['table_name']}.#{v['column_name']}" unless is_postgres || is_mssql
2931
+ s[key] = [v['constraint_schema'], v['table_name']]
2932
+ else # Array
2933
+ key = "#{v[2]}.#{v[1]}.#{v[0]}.#{v[3]}"
2934
+ key << ".#{v[4]}.#{v[5]}" unless is_postgres || is_mssql
2935
+ s[key] = [v[1], v[4]]
2936
+ end
2911
2937
  end
2912
2938
 
2939
+ # Part 2 -- fk_references
2913
2940
  sql = "SELECT kcu.CONSTRAINT_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME,
2914
2941
  #{# These will get filled in with real values (effectively doing the JOIN in Ruby)
2915
2942
  is_postgres || is_mssql ? 'NULL as primary_schema, NULL as primary_table' :
@@ -2921,7 +2948,8 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2921
2948
  ON kcu.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
2922
2949
  AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
2923
2950
  AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME#{"
2924
- WHERE kcu.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }"
2951
+ WHERE kcu.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema}#{"
2952
+ WHERE kcu.CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if is_mysql}"
2925
2953
  fk_references = ActiveRecord::Base.execute_sql(sql)
2926
2954
  when 'SQLite'
2927
2955
  sql = "SELECT NULL AS constraint_schema, m.name, fkl.\"from\", NULL AS primary_schema, fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
@@ -2952,8 +2980,10 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2952
2980
  ::Brick.default_schema ||= 'public' if is_postgres
2953
2981
  fk_references&.each do |fk|
2954
2982
  fk = fk.values unless fk.is_a?(Array)
2955
- # Virtually JOIN against fk_references in order to change out the primary schema and primary table
2956
- if (kcu = kcus&.fetch("#{fk[6]}.#{fk[7]}.#{fk[8]}.#{fk[9]}", nil))
2983
+ # Virtually JOIN KCUs to fk_references in order to fill in the primary schema and primary table
2984
+ kcu_key = "#{fk[6]}.#{fk[7]}.#{fk[8]}.#{fk[9]}"
2985
+ kcu_key << ".#{fk[3]}.#{fk[4]}" unless is_postgres || is_mssql
2986
+ if (kcu = kcus&.fetch(kcu_key, nil))
2957
2987
  fk[3] = kcu[0]
2958
2988
  fk[4] = kcu[1]
2959
2989
  end
@@ -3044,24 +3074,39 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
3044
3074
  ::Brick.load_additional_references if ::Brick.initializer_loaded
3045
3075
 
3046
3076
  if is_postgres
3077
+ params = []
3047
3078
  ActiveRecord::Base.execute_sql("-- inherited and partitioned tables counts
3048
- SELECT parent.relname,
3079
+ SELECT n.nspname, parent.relname,
3049
3080
  ((SUM(child.reltuples::float) / greatest(SUM(child.relpages), 1))) *
3050
3081
  (SUM(pg_relation_size(child.oid))::float / (current_setting('block_size')::float))::integer AS rowcount
3051
3082
  FROM pg_inherits
3052
3083
  INNER JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
3053
3084
  INNER JOIN pg_class child ON pg_inherits.inhrelid = child.oid
3054
- GROUP BY parent.relname, child.reltuples, child.relpages, child.oid
3085
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = parent.relnamespace#{
3086
+ if schema
3087
+ params = params << schema
3088
+ "
3089
+ WHERE n.nspname = COALESCE(?, 'public')"
3090
+ end}
3091
+ GROUP BY n.nspname, parent.relname, child.reltuples, child.relpages, child.oid
3055
3092
 
3056
3093
  UNION ALL
3057
3094
 
3058
3095
  -- table count
3059
- SELECT relname,
3060
- (reltuples::float / greatest(relpages, 1)) *
3096
+ SELECT n.nspname, pg_class.relname,
3097
+ (pg_class.reltuples::float / greatest(pg_class.relpages, 1)) *
3061
3098
  (pg_relation_size(pg_class.oid)::float / (current_setting('block_size')::float))::integer AS rowcount
3062
3099
  FROM pg_class
3063
- GROUP BY relname, reltuples, relpages, oid").each do |tblcount|
3064
- relations.fetch(tblcount['relname'], nil)&.[]=(:rowcount, tblcount['rowcount'].round)
3100
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = pg_class.relnamespace#{
3101
+ if schema
3102
+ params = params << schema
3103
+ "
3104
+ WHERE n.nspname = COALESCE(?, 'public')"
3105
+ end}
3106
+ GROUP BY n.nspname, pg_class.relname, pg_class.reltuples, pg_class.relpages, pg_class.oid", params).each do |tblcount|
3107
+ # %%% What is the default schema here?
3108
+ prefix = "#{tblcount['nspname']}." unless tblcount['nspname'] == (schema || 'public')
3109
+ relations.fetch("#{prefix}#{tblcount['relname']}", nil)&.[]=(:rowcount, tblcount['rowcount'].to_i.round)
3065
3110
  end
3066
3111
  end
3067
3112
 
@@ -3430,7 +3475,7 @@ module Brick
3430
3475
  separator ||= '_'
3431
3476
  res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
3432
3477
  res_name << '.' if res_name
3433
- (res_name ||= +'') << (relation || ::Brick.relations.fetch(tbl_name, nil)&.fetch(:resource, nil) || tbl_name_parts.last)
3478
+ (res_name ||= +'') << (relation || ::Brick.relations.fetch(tbl_name, nil))&.fetch(:resource, nil) || tbl_name_parts.last
3434
3479
 
3435
3480
  res_parts = ((mode == :singular) ? res_name.singularize : res_name).split('.')
3436
3481
  res_parts.shift if ::Brick.apartment_multitenant && res_parts.length > 1 && res_parts.first == ::Brick.apartment_default_tenant
@@ -345,7 +345,8 @@ function linkSchemas() {
345
345
  end
346
346
  ::Brick.relations.each do |k, v|
347
347
  unless k.is_a?(Symbol) || existing.key?(class_name = v[:class_name]) || Brick.config.exclude_tables.include?(k) ||
348
- class_name.blank? || class_name.include?('::')
348
+ class_name.blank? || class_name.include?('::') ||
349
+ ['ActiveAdminComment', 'MotorAlert', 'MotorAlertLock', 'MotorApiConfig', 'MotorAudit', 'MotorConfig', 'MotorDashboard', 'MotorForm', 'MotorNote', 'MotorNoteTag', 'MotorNoteTagTag', 'MotorNotification', 'MotorQuery', 'MotorReminder', 'MotorResource', 'MotorTag', 'MotorTaggableTag'].include?(class_name)
349
350
  Object.const_get("#{class_name}Resource")
350
351
  end
351
352
  end
@@ -1583,7 +1584,13 @@ end %>#{"
1583
1584
  #{schema_options}" if schema_options}
1584
1585
  <select id=\"tbl\">#{table_options}</select>
1585
1586
  <table id=\"resourceName\"><td><h1><%= page_title %></h1></td>
1586
- <% if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) %>
1587
+ <% rel = Brick.relations[#{model_name}.table_name]
1588
+ if (in_app = rel.fetch(:existing, nil)&.fetch(:show, nil))
1589
+ in_app = send(\"#\{in_app}_path\", #{pk.is_a?(String) ? "obj.#{pk}" : '[' + pk.map { |pk_part| "obj.#{pk_part}" }.join(', ') + ']' }) if in_app.is_a?(Symbol) %>
1590
+ <td><%= link_to(::Brick::Rails::IN_APP.html_safe, in_app) %></td>
1591
+ <% end
1592
+
1593
+ if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) %>
1587
1594
  <td><%= link_to_brick(
1588
1595
  ::Brick::Rails::AVO_SVG.html_safe,
1589
1596
  { show_proc: Proc.new do |obj, relation|
@@ -1608,7 +1615,7 @@ end %>#{"
1608
1615
  end %>
1609
1616
  </table>
1610
1617
  <%
1611
- if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %>
1618
+ if (description = rel&.fetch(:description, nil)) %>
1612
1619
  <span class=\"__brick\"><%= description %></span><br><%
1613
1620
  end
1614
1621
  %><%= link_to \"(See all #\{model_name.pluralize})\", see_all_path, { class: '__brick' } %>
@@ -1939,6 +1946,7 @@ document.querySelectorAll(\"input, select\").forEach(function (inp) {
1939
1946
  end
1940
1947
 
1941
1948
  if ::Brick.enable_routes?
1949
+ require 'brick/route_mapper'
1942
1950
  ActionDispatch::Routing::RouteSet.class_exec do
1943
1951
  # In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
1944
1952
  prepend ::Brick::RouteSet
@@ -148,7 +148,8 @@ module Brick::Rails::FormBuilder
148
148
  '(hidden)'
149
149
  else
150
150
  if val.is_a?(String)
151
- val = val.dup.force_encoding('UTF-8').strip
151
+ return ::Brick::Rails.display_binary(val) unless (val_utf8 = val.dup.force_encoding('UTF-8')).valid_encoding?
152
+ val = val_utf8.strip
152
153
  return CGI.escapeHTML(val) if is_xml
153
154
 
154
155
  if val.length > max_len
@@ -2,7 +2,7 @@ module Brick::Rails::FormTags
2
2
  # Our super speedy grid
3
3
  def brick_grid(relation = nil, sequence = nil, inclusions = nil, exclusions = nil,
4
4
  cols = {}, bt_descrip: nil, poly_cols: nil, bts: {}, hms_keys: [], hms_cols: {},
5
- show_header: nil, show_row_count: nil, show_erd_button: nil, show_new_button: nil, show_avo_button: nil, show_aa_button: nil)
5
+ show_header: nil, show_row_count: nil, show_erd_button: nil, show_in_app_button: nil, show_new_button: nil, show_avo_button: nil, show_aa_button: nil)
6
6
  # When a relation is not provided, first see if one exists which matches the controller name
7
7
  unless (relation ||= instance_variable_get("@#{controller_name}".to_sym))
8
8
  # Failing that, dig through the instance variables with hopes to find something that is an ActiveRecord::Relation
@@ -33,6 +33,7 @@ module Brick::Rails::FormTags
33
33
  out = +"<div id=\"headerTopContainer\"><table id=\"headerTop\"></table>
34
34
  "
35
35
  klass = relation.klass
36
+ rel = ::Brick.relations&.fetch(relation.table_name, nil)
36
37
  unless show_header == false
37
38
  out << " <div id=\"headerTopAddNew\">
38
39
  <div id=\"headerButtonBox\">
@@ -43,6 +44,11 @@ module Brick::Rails::FormTags
43
44
  end
44
45
  unless show_erd_button == false
45
46
  out << " <div id=\"imgErd\" title=\"Show ERD\"></div>
47
+ "
48
+ end
49
+ if rel && show_in_app_button != false && (in_app = rel.fetch(:existing, nil)&.fetch(:index, nil))
50
+ in_app = send("#{in_app}_path") if in_app.is_a?(Symbol)
51
+ out << " <td title=\"Show in app\">#{link_to(::Brick::Rails::IN_APP.html_safe, in_app)}</td>
46
52
  "
47
53
  end
48
54
  if show_avo_button != false && Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) && klass.name.exclude?('::')
@@ -254,7 +260,7 @@ module Brick::Rails::FormTags
254
260
  out << '</tr>'
255
261
  row_count += 1
256
262
  end
257
- if (total_row_count = ::Brick.relations[table_name].fetch(:rowcount, nil))
263
+ if rel && (total_row_count = rel.fetch(:rowcount, nil))
258
264
  total_row_count = total_row_count > row_count ? " (out of #{total_row_count})" : nil
259
265
  end
260
266
  out << " </tbody>
@@ -15,4 +15,10 @@ module ::Brick::Rails
15
15
 
16
16
  AA_PNG = "<img src=\"data:image/png;base64,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\">
17
17
  "
18
+
19
+ IN_APP = "<svg height=\"36px\" viewBox=\"0 0 214 274\" xmlns=\"http://www.w3.org/2000/svg\">
20
+ <g transform=\"matrix(4.16667,0,0,4.16667,-1049.47,-789.371)\">
21
+ <path d=\"M281.386,193.134L287.086,193.134C287.433,193.134 287.716,193.417 287.716,193.764C287.716,194.11 287.433,194.394 287.086,194.394L281.386,194.394C281.04,194.394 280.756,194.11 280.756,193.764C280.756,193.417 281.04,193.134 281.386,193.134ZM284.252,245.638C282.456,245.638 281.008,247.086 281.008,248.851C281.008,250.645 282.456,252.094 284.252,252.094C286.047,252.094 287.496,250.645 287.496,248.851C287.496,247.086 286.047,245.638 284.252,245.638ZM284.252,246.331C282.835,246.331 281.701,247.465 281.701,248.851C281.701,250.268 282.835,251.401 284.252,251.401C285.638,251.401 286.803,250.268 286.803,248.851C286.803,247.465 285.638,246.331 284.252,246.331ZM275.843,208.63L287.559,218.11L275.843,227.622L275.843,221.858L251.874,221.858L251.874,214.394L275.843,214.394L275.843,208.63ZM278.866,239.906L278.866,233.102C278.866,232.851 278.677,232.693 278.456,232.693L271.622,232.693C271.401,232.693 271.212,232.851 271.212,233.102L271.212,239.906C271.212,240.157 271.401,240.314 271.622,240.314L278.456,240.314C278.677,240.314 278.866,240.157 278.866,239.906ZM280.41,233.102L280.41,239.906C280.41,240.157 280.599,240.314 280.819,240.314L287.653,240.314C287.874,240.314 288.063,240.157 288.063,239.906L288.063,233.102C288.063,232.851 287.874,232.693 287.653,232.693L280.819,232.693C280.599,232.693 280.41,232.851 280.41,233.102ZM289.606,233.102L289.606,239.906C289.606,240.157 289.795,240.314 290.047,240.314L296.851,240.314C297.071,240.314 297.26,240.157 297.26,239.906L297.26,233.102C297.26,232.851 297.071,232.693 296.851,232.693L290.047,232.693C289.795,232.693 289.606,232.851 289.606,233.102ZM269.197,189.449L299.276,189.449C301.354,189.449 303.055,191.149 303.055,193.197L303.055,251.275C303.055,253.355 301.354,255.055 299.276,255.055L269.197,255.055C267.118,255.055 265.449,253.355 265.449,251.275L265.449,223.496L267.842,223.496L267.842,242.488L300.63,242.488L300.63,198.394L267.842,198.394L267.842,212.725L265.449,212.725L265.449,193.197C265.449,191.149 267.118,189.449 269.197,189.449Z\" style=\"fill:rgb(31,147,209);\"/>
22
+ </g>
23
+ </svg>"
18
24
  end
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brick
4
+ class << self
5
+ attr_accessor :routes_done
6
+ end
7
+
8
+ module RouteMapper
9
+ def add_brick_routes
10
+ routeset_to_use = ::Rails.application.routes
11
+ path_prefix = ::Brick.config.path_prefix
12
+ existing_controllers = routeset_to_use.routes.each_with_object({}) do |r, s|
13
+ if (r.verb == 'GET' || (r.verb.is_a?(Regexp) && r.verb.source == '^GET$')) &&
14
+ (controller_name = r.defaults[:controller])
15
+ path = r.path.ast.to_s
16
+ path = path[0..((path.index('(') || 0) - 1)]
17
+ # Skip adding this if it's the default_route_fallback set from the initializers/brick.rb file
18
+ next if "#{path}##{r.defaults[:action]}" == ::Brick.established_drf ||
19
+ # or not a GET request
20
+ [:index, :show, :new, :edit].exclude?(action = r.defaults[:action].to_sym)
21
+
22
+ # Attempt to backtrack to original
23
+ c_parts = controller_name.split('/')
24
+ while c_parts.length > 0
25
+ c_dotted = c_parts.join('.')
26
+ if (relation = ::Brick.relations.fetch(c_dotted, nil)) # Does it match up with an existing Brick table / resource name?
27
+ # puts path
28
+ # puts " #{c_dotted}##{r.defaults[:action]}"
29
+ if (route_name = r.name&.to_sym) != :root
30
+ relation[:existing][action] = route_name
31
+ else
32
+ relation[:existing][action] ||= path
33
+ end
34
+ s[c_dotted.tr('.', '/')] = nil
35
+ break
36
+ end
37
+ c_parts.shift
38
+ end
39
+ s[controller_name] = nil if c_parts.length.zero?
40
+ end
41
+ end
42
+
43
+ tables = []
44
+ views = []
45
+ table_class_length = 38 # Length of "Classes that can be built from tables:"
46
+ view_class_length = 37 # Length of "Classes that can be built from views:"
47
+
48
+ brick_namespace_create = lambda do |path_names, res_name, options|
49
+ if path_names&.present?
50
+ if (path_name = path_names.pop).is_a?(Array)
51
+ module_name = path_name[1]
52
+ path_name = path_name.first
53
+ end
54
+ send(:scope, { module: module_name || path_name, path: path_name, as: path_name }) do
55
+ brick_namespace_create.call(path_names, res_name, options)
56
+ end
57
+ else
58
+ send(:resources, res_name.to_sym, **options)
59
+ end
60
+ end
61
+
62
+ # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
63
+ # If auto-controllers and auto-models are both enabled then this makes sense:
64
+ controller_prefix = (path_prefix ? "#{path_prefix}/" : '')
65
+ sti_subclasses = ::Brick.config.sti_namespace_prefixes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
66
+ # Turn something like {"::Spouse"=>"Person", "::Friend"=>"Person"} into {"Person"=>["Spouse", "Friend"]}
67
+ s[v.last] << v.first[2..-1] unless v.first.end_with?('::')
68
+ end
69
+ versioned_views = {} # Track which views have already been done for each api_root
70
+ ::Brick.relations.each do |k, v|
71
+ next if k.is_a?(Symbol)
72
+
73
+ if (schema_name = v.fetch(:schema, nil))
74
+ schema_prefix = "#{schema_name}."
75
+ end
76
+
77
+ resource_name = v.fetch(:resource, nil)
78
+ next if !resource_name ||
79
+ existing_controllers.key?(
80
+ controller_prefix + (resource_name = "#{schema_prefix&.tr('.', '/')}#{resource_name}".pluralize)
81
+ )
82
+
83
+ object_name = k.split('.').last # Take off any first schema part
84
+
85
+ full_schema_prefix = if (full_aps = aps = v.fetch(:auto_prefixed_schema, nil))
86
+ aps = aps[0..-2] if aps[-1] == '_'
87
+ (schema_prefix&.dup || +'') << "#{aps}."
88
+ else
89
+ schema_prefix
90
+ end
91
+
92
+ # Track routes being built
93
+ resource_name = v.fetch(:resource, nil) || k
94
+ if (class_name = v.fetch(:class_name, nil))
95
+ if v.key?(:isView)
96
+ view_class_length = class_name.length if class_name.length > view_class_length
97
+ views
98
+ else
99
+ table_class_length = class_name.length if class_name.length > table_class_length
100
+ tables
101
+ end << [class_name, aps, "#{"#{schema_name}/" if schema_name}#{resource_name[full_aps&.length || 0 .. -1]}"]
102
+ end
103
+
104
+ options = {}
105
+ options[:only] = [:index, :show] if v.key?(:isView)
106
+
107
+ # First do the normal routes
108
+ prefixes = []
109
+ prefixes << [aps, v[:class_name]&.split('::')[-2]&.underscore] if aps
110
+ prefixes << schema_name if schema_name
111
+ prefixes << path_prefix if path_prefix
112
+ brick_namespace_create.call(prefixes, resource_name, options)
113
+ sti_subclasses.fetch(class_name, nil)&.each do |sc| # Add any STI subclass routes for this relation
114
+ brick_namespace_create.call(prefixes, sc.underscore.tr('/', '_').pluralize, options)
115
+ end
116
+
117
+ # Now the API routes if necessary
118
+ full_resource = nil
119
+ ::Brick.api_roots&.each do |api_root|
120
+ api_done_views = (versioned_views[api_root] ||= {})
121
+ found = nil
122
+ test_ver_num = nil
123
+ view_relation = nil
124
+ # If it's a view then see if there's a versioned one available by searching for resource names
125
+ # versioned with the closest number (equal to or less than) compared with our API version number.
126
+ if v.key?(:isView)
127
+ if (ver = object_name.match(/^v([\d_]*)/)&.captures&.first) && ver[-1] == '_'
128
+ core_object_name = object_name[ver.length + 1..-1]
129
+ next if api_done_views.key?(unversioned = "#{schema_prefix}v_#{core_object_name}")
130
+
131
+ # Expect that the last item in the path generally holds versioning information
132
+ api_ver = api_root.split('/')[-1]&.gsub('_', '.')
133
+ vn_idx = api_ver.rindex(/[^\d._]/) # Position of the first numeric digit at the end of the version number
134
+ # Was: .to_d
135
+ 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
136
+ # puts [api_ver, vn_idx, api_ver_num, unversioned].inspect
137
+
138
+ next if ver.to_i > api_ver_num # Don't surface any newer views in an older API
139
+
140
+ test_ver_num -= 1 until test_ver_num.zero? ||
141
+ (view_relation = ::Brick.relations.fetch(
142
+ found = "#{schema_prefix}v#{test_ver_num}_#{core_object_name}", nil
143
+ ))
144
+ api_done_views[unversioned] = nil # Mark that for this API version this view is done
145
+
146
+ # puts "Found #{found}" if view_relation
147
+ # If we haven't found "v3_view_name" or "v2_view_name" or so forth, at the last
148
+ # fall back to simply looking for "v_view_name", and then finally "view_name".
149
+ no_v_prefix_name = "#{schema_prefix}#{core_object_name}"
150
+ standard_prefix = 'v_'
151
+ else
152
+ core_object_name = object_name
153
+ end
154
+ if (rvp = ::Brick.config.api_remove_view_prefix) && core_object_name.start_with?(rvp)
155
+ core_object_name.slice!(0, rvp.length)
156
+ end
157
+ no_prefix_name = "#{schema_prefix}#{core_object_name}"
158
+ unversioned = "#{schema_prefix}#{standard_prefix}#{::Brick.config.api_add_view_prefix}#{core_object_name}"
159
+ else
160
+ unversioned = k
161
+ end
162
+
163
+ view_relation ||= ::Brick.relations.fetch(found = unversioned, nil) ||
164
+ (no_v_prefix_name && ::Brick.relations.fetch(found = no_v_prefix_name, nil)) ||
165
+ (no_prefix_name && ::Brick.relations.fetch(found = no_prefix_name, nil))
166
+ if view_relation
167
+ actions = view_relation.key?(:isView) ? [:index, :show] : ::Brick::ALL_API_ACTIONS # By default all actions are allowed
168
+ # Call proc that limits which endpoints get surfaced based on version, table or view name, method (get list / get one / post / patch / delete)
169
+ # Returning nil makes it do nothing, false makes it skip creating this endpoint, and an array of up to
170
+ # these 3 things controls and changes the nature of the endpoint that gets built:
171
+ # (updated api_name, name of different relation to route to, allowed actions such as :index, :show, :create, etc)
172
+ proc_result = if (filter = ::Brick.config.api_filter).is_a?(Proc)
173
+ begin
174
+ num_args = filter.arity.negative? ? 6 : filter.arity
175
+ filter.call(*[unversioned, k, view_relation, actions, api_ver_num, found, test_ver_num][0...num_args])
176
+ rescue StandardError => e
177
+ puts "::Brick.api_filter Proc error: #{e.message}"
178
+ end
179
+ end
180
+ # proc_result expects to receive back: [updated_api_name, to_other_relation, allowed_actions]
181
+
182
+ case proc_result
183
+ when NilClass
184
+ # Do nothing differently than what normal behaviour would be
185
+ when FalseClass # Skip implementing this endpoint
186
+ view_relation[:api][api_ver_num] = nil
187
+ next
188
+ when Array # Did they give back an array of actions?
189
+ unless proc_result.any? { |pr| ::Brick::ALL_API_ACTIONS.exclude?(pr) }
190
+ proc_result = [unversioned, to_relation, proc_result]
191
+ end
192
+ # Otherwise don't change this array because it's probably legit
193
+ when String
194
+ proc_result = [proc_result] # Treat this as the surfaced api_name (path) they want to use for this endpoint
195
+ else
196
+ puts "::Brick.api_filter Proc warning: Unable to parse this result returned: \n #{proc_result.inspect}"
197
+ proc_result = nil # Couldn't understand what in the world was returned
198
+ end
199
+
200
+ if proc_result&.present?
201
+ if proc_result[1] # to_other_relation
202
+ if (new_view_relation = ::Brick.relations.fetch(proc_result[1], nil))
203
+ k = proc_result[1] # Route this call over to this different relation
204
+ view_relation = new_view_relation
205
+ else
206
+ puts "::Brick.api_filter Proc warning: Unable to find new suggested relation with name #{proc_result[1]} -- sticking with #{k} instead."
207
+ end
208
+ end
209
+ if proc_result.first&.!=(k) # updated_api_name -- a different name than this relation would normally have
210
+ found = proc_result.first
211
+ end
212
+ actions &= proc_result[2] if proc_result[2] # allowed_actions
213
+ end
214
+ (view_relation[:api][api_ver_num] ||= {})[unversioned] = actions # Add to the list of API paths this resource responds to
215
+
216
+ # view_ver_num = if (first_part = k.split('_').first) =~ /^v[\d_]+/
217
+ # first_part[1..-1].gsub('_', '.').to_i
218
+ # end
219
+ controller_name = if (last = view_relation.fetch(:resource, nil)&.pluralize)
220
+ "#{full_schema_prefix}#{last}"
221
+ else
222
+ found
223
+ end.tr('.', '/')
224
+
225
+ { :index => 'get', :create => 'post' }.each do |action, method|
226
+ if actions.include?(action)
227
+ # Normally goes to something like: /api/v1/employees
228
+ send(method, "#{api_root}#{unversioned.tr('.', '/')}", { to: "#{controller_prefix}#{controller_name}##{action}" })
229
+ end
230
+ end
231
+ # %%% We do not yet surface the #show action
232
+ if (id_col = view_relation[:pk]&.first) # ID-dependent stuff
233
+ { :update => ['put', 'patch'], :destroy => ['delete'] }.each do |action, methods|
234
+ if actions.include?(action)
235
+ methods.each do |method|
236
+ send(method, "#{api_root}#{unversioned.tr('.', '/')}/:#{id_col}", { to: "#{controller_prefix}#{controller_name}##{action}" })
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ # Trestle compatibility
245
+ if Object.const_defined?('Trestle') && ::Trestle.config.options&.key?(:site_title) &&
246
+ !Object.const_defined?("#{(res_name = resource_name.tr('/', '_')).camelize}Admin")
247
+ begin
248
+ ::Trestle.resource(res_sym = res_name.to_sym, model: class_name&.constantize) do
249
+ menu { item res_sym, icon: "fa fa-star" }
250
+ end
251
+ rescue
252
+ end
253
+ end
254
+ end
255
+
256
+ if (named_routes = instance_variable_get(:@set).named_routes).respond_to?(:find)
257
+ if ::Brick.config.add_status && (status_as = "#{controller_prefix.tr('/', '_')}brick_status".to_sym)
258
+ (
259
+ !(status_route = instance_variable_get(:@set).named_routes.find { |route| route.first == status_as }&.last) ||
260
+ !status_route.ast.to_s.include?("/#{controller_prefix}brick_status/")
261
+ )
262
+ get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: status_as.to_s)
263
+ end
264
+
265
+ # ::Brick.config.add_schema &&
266
+ if (schema_as = "#{controller_prefix.tr('/', '_')}brick_schema".to_sym)
267
+ (
268
+ !(schema_route = instance_variable_get(:@set).named_routes.find { |route| route.first == schema_as }&.last) ||
269
+ !schema_route.ast.to_s.include?("/#{controller_prefix}brick_schema/")
270
+ )
271
+ post("/#{controller_prefix}brick_schema", to: 'brick_gem#schema_create', as: schema_as.to_s)
272
+ end
273
+
274
+ if ::Brick.config.add_orphans && (orphans_as = "#{controller_prefix.tr('/', '_')}brick_orphans".to_sym)
275
+ (
276
+ !(orphans_route = instance_variable_get(:@set).named_routes.find { |route| route.first == orphans_as }&.last) ||
277
+ !orphans_route.ast.to_s.include?("/#{controller_prefix}brick_orphans/")
278
+ )
279
+ get("/#{controller_prefix}brick_orphans", to: 'brick_gem#orphans', as: 'brick_orphans')
280
+ end
281
+ end
282
+
283
+ if instance_variable_get(:@set).named_routes.names.exclude?(:brick_crosstab)
284
+ get("/#{controller_prefix}brick_crosstab", to: 'brick_gem#crosstab', as: 'brick_crosstab')
285
+ get("/#{controller_prefix}brick_crosstab/data", to: 'brick_gem#crosstab_data')
286
+ end
287
+
288
+ if ((rswag_ui_present = Object.const_defined?('Rswag::Ui')) &&
289
+ (rswag_path = routeset_to_use.routes.find { |r| r.app.app == ::Rswag::Ui::Engine }
290
+ &.instance_variable_get(:@path_formatter)
291
+ &.instance_variable_get(:@parts)&.join) &&
292
+ (doc_endpoints = ::Rswag::Ui.config.config_object[:urls])) ||
293
+ (doc_endpoints = ::Brick.instance_variable_get(:@swagger_endpoints))
294
+ last_endpoint_parts = nil
295
+ doc_endpoints.each do |doc_endpoint|
296
+ puts "Mounting OpenApi 3.0 documentation endpoint for \"#{doc_endpoint[:name]}\" on #{doc_endpoint[:url]}" unless ::Brick.routes_done
297
+ send(:get, doc_endpoint[:url], { to: 'brick_openapi#index' })
298
+ endpoint_parts = doc_endpoint[:url]&.split('/')
299
+ last_endpoint_parts = endpoint_parts
300
+ end
301
+ end
302
+ return if ::Brick.routes_done
303
+
304
+ if doc_endpoints.present?
305
+ if rswag_ui_present
306
+ if rswag_path
307
+ puts "API documentation now available when navigating to: /#{last_endpoint_parts&.find(&:present?)}/index.html"
308
+ else
309
+ puts "In order to make documentation available you can put this into your routes.rb:"
310
+ puts " mount Rswag::Ui::Engine => '/#{last_endpoint_parts&.find(&:present?) || 'api-docs'}'"
311
+ end
312
+ else
313
+ puts "Having this exposed, one easy way to leverage this to create HTML-based API documentation is to use Scalar.
314
+ It will jump to life when you put these two lines into a view template or other HTML resource:
315
+ <script id=\"api-reference\" data-url=\"#{last_endpoint_parts.join('/')}\"></script>
316
+ <script src=\"https://cdn.jsdelivr.net/@scalar/api-reference\"></script>
317
+ Alternatively you can add the rswag-ui gem."
318
+ end
319
+ elsif rswag_ui_present
320
+ sample_path = rswag_path || '/api-docs'
321
+ puts
322
+ puts "Brick: rswag-ui gem detected -- to make OpenAPI 3.0 documentation available from a path such as '#{sample_path}/v1/swagger.json',"
323
+ puts ' put code such as this in an initializer:'
324
+ puts ' Rswag::Ui.configure do |config|'
325
+ puts " config.swagger_endpoint '#{sample_path}/v1/swagger.json', 'API V1 Docs'"
326
+ puts ' end'
327
+ unless rswag_path
328
+ puts
329
+ puts ' and put this into your routes.rb:'
330
+ puts " mount Rswag::Ui::Engine => '/api-docs'"
331
+ end
332
+ end
333
+
334
+ puts "\n" if tables.present? || views.present?
335
+ if tables.present?
336
+ puts "Classes that can be built from tables:#{' ' * (table_class_length - 38)} Path:"
337
+ puts "======================================#{' ' * (table_class_length - 38)} ====="
338
+ ::Brick.display_classes(controller_prefix, tables, table_class_length)
339
+ end
340
+ if views.present?
341
+ puts "Classes that can be built from views:#{' ' * (view_class_length - 37)} Path:"
342
+ puts "=====================================#{' ' * (view_class_length - 37)} ====="
343
+ ::Brick.display_classes(controller_prefix, views, view_class_length)
344
+ end
345
+ ::Brick.routes_done = true
346
+ end
347
+ end
348
+ end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 191
8
+ TINY = 192
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
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 = []
@@ -881,340 +881,6 @@ In config/initializers/brick.rb appropriate entries would look something like:
881
881
  end
882
882
  end
883
883
 
884
- module RouteMapper
885
- def add_brick_routes
886
- routeset_to_use = ::Rails.application.routes
887
- path_prefix = ::Brick.config.path_prefix
888
- existing_controllers = routeset_to_use.routes.each_with_object({}) do |r, s|
889
- if r.verb == 'GET' && (c = r.defaults[:controller])
890
- path = r.path.ast.to_s
891
- path = path[0..((path.index('(') || 0) - 1)]
892
- # Skip adding this if it's the default_route_fallback set from the initializers/brick.rb file
893
- next if "#{path}##{r.defaults[:action]}" == ::Brick.established_drf
894
-
895
- # next unless [:index, :show, :new, :edit].incude?(a = r.defaults[:action])
896
-
897
- # c_parts = c.split('/')
898
- # while c_parts.length > 0
899
- # c_dotted = c_parts.join('.')
900
- # if (relation = ::Brick.relations[c_dotted]) # Does it match up with an existing Brick table / resource name?
901
- # puts path
902
- # puts " #{c_dotted}##{r.defaults[:action]}"
903
- # s[c_dotted.tr('.', '/')] = nil
904
- # break
905
- # end
906
- # c_parts.shift
907
- # end
908
- s[c] = nil
909
- end
910
- end
911
-
912
- tables = []
913
- views = []
914
- table_class_length = 38 # Length of "Classes that can be built from tables:"
915
- view_class_length = 37 # Length of "Classes that can be built from views:"
916
-
917
- brick_namespace_create = lambda do |path_names, res_name, options|
918
- if path_names&.present?
919
- if (path_name = path_names.pop).is_a?(Array)
920
- module_name = path_name[1]
921
- path_name = path_name.first
922
- end
923
- send(:scope, { module: module_name || path_name, path: path_name, as: path_name }) do
924
- brick_namespace_create.call(path_names, res_name, options)
925
- end
926
- else
927
- send(:resources, res_name.to_sym, **options)
928
- end
929
- end
930
-
931
- # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
932
- # If auto-controllers and auto-models are both enabled then this makes sense:
933
- controller_prefix = (path_prefix ? "#{path_prefix}/" : '')
934
- sti_subclasses = ::Brick.config.sti_namespace_prefixes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
935
- # Turn something like {"::Spouse"=>"Person", "::Friend"=>"Person"} into {"Person"=>["Spouse", "Friend"]}
936
- s[v.last] << v.first[2..-1] unless v.first.end_with?('::')
937
- end
938
- versioned_views = {} # Track which views have already been done for each api_root
939
- ::Brick.relations.each do |k, v|
940
- next if k.is_a?(Symbol)
941
-
942
- if (schema_name = v.fetch(:schema, nil))
943
- schema_prefix = "#{schema_name}."
944
- end
945
-
946
- resource_name = v.fetch(:resource, nil)
947
- next if !resource_name ||
948
- existing_controllers.key?(
949
- controller_prefix + (resource_name = "#{schema_prefix&.tr('.', '/')}#{resource_name}".pluralize)
950
- )
951
-
952
- object_name = k.split('.').last # Take off any first schema part
953
-
954
- full_schema_prefix = if (full_aps = aps = v.fetch(:auto_prefixed_schema, nil))
955
- aps = aps[0..-2] if aps[-1] == '_'
956
- (schema_prefix&.dup || +'') << "#{aps}."
957
- else
958
- schema_prefix
959
- end
960
-
961
- # Track routes being built
962
- resource_name = v.fetch(:resource, nil) || k
963
- if (class_name = v.fetch(:class_name, nil))
964
- if v.key?(:isView)
965
- view_class_length = class_name.length if class_name.length > view_class_length
966
- views
967
- else
968
- table_class_length = class_name.length if class_name.length > table_class_length
969
- tables
970
- end << [class_name, aps, resource_name.tr('.', '/')[full_aps&.length || 0 .. -1]]
971
- end
972
-
973
- options = {}
974
- options[:only] = [:index, :show] if v.key?(:isView)
975
-
976
- # First do the normal routes
977
- prefixes = []
978
- prefixes << [aps, v[:class_name]&.split('::')[-2]&.underscore] if aps
979
- prefixes << schema_name if schema_name
980
- prefixes << path_prefix if path_prefix
981
- brick_namespace_create.call(prefixes, resource_name, options)
982
- sti_subclasses.fetch(class_name, nil)&.each do |sc| # Add any STI subclass routes for this relation
983
- brick_namespace_create.call(prefixes, sc.underscore.tr('/', '_').pluralize, options)
984
- end
985
-
986
- # Now the API routes if necessary
987
- full_resource = nil
988
- ::Brick.api_roots&.each do |api_root|
989
- api_done_views = (versioned_views[api_root] ||= {})
990
- found = nil
991
- test_ver_num = nil
992
- view_relation = nil
993
- # If it's a view then see if there's a versioned one available by searching for resource names
994
- # versioned with the closest number (equal to or less than) compared with our API version number.
995
- if v.key?(:isView)
996
- if (ver = object_name.match(/^v([\d_]*)/)&.captures&.first) && ver[-1] == '_'
997
- core_object_name = object_name[ver.length + 1..-1]
998
- next if api_done_views.key?(unversioned = "#{schema_prefix}v_#{core_object_name}")
999
-
1000
- # Expect that the last item in the path generally holds versioning information
1001
- api_ver = api_root.split('/')[-1]&.gsub('_', '.')
1002
- vn_idx = api_ver.rindex(/[^\d._]/) # Position of the first numeric digit at the end of the version number
1003
- # Was: .to_d
1004
- 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
1005
- # puts [api_ver, vn_idx, api_ver_num, unversioned].inspect
1006
-
1007
- next if ver.to_i > api_ver_num # Don't surface any newer views in an older API
1008
-
1009
- test_ver_num -= 1 until test_ver_num.zero? ||
1010
- (view_relation = ::Brick.relations.fetch(
1011
- found = "#{schema_prefix}v#{test_ver_num}_#{core_object_name}", nil
1012
- ))
1013
- api_done_views[unversioned] = nil # Mark that for this API version this view is done
1014
-
1015
- # puts "Found #{found}" if view_relation
1016
- # If we haven't found "v3_view_name" or "v2_view_name" or so forth, at the last
1017
- # fall back to simply looking for "v_view_name", and then finally "view_name".
1018
- no_v_prefix_name = "#{schema_prefix}#{core_object_name}"
1019
- standard_prefix = 'v_'
1020
- else
1021
- core_object_name = object_name
1022
- end
1023
- if (rvp = ::Brick.config.api_remove_view_prefix) && core_object_name.start_with?(rvp)
1024
- core_object_name.slice!(0, rvp.length)
1025
- end
1026
- no_prefix_name = "#{schema_prefix}#{core_object_name}"
1027
- unversioned = "#{schema_prefix}#{standard_prefix}#{::Brick.config.api_add_view_prefix}#{core_object_name}"
1028
- else
1029
- unversioned = k
1030
- end
1031
-
1032
- view_relation ||= ::Brick.relations.fetch(found = unversioned, nil) ||
1033
- (no_v_prefix_name && ::Brick.relations.fetch(found = no_v_prefix_name, nil)) ||
1034
- (no_prefix_name && ::Brick.relations.fetch(found = no_prefix_name, nil))
1035
- if view_relation
1036
- actions = view_relation.key?(:isView) ? [:index, :show] : ::Brick::ALL_API_ACTIONS # By default all actions are allowed
1037
- # Call proc that limits which endpoints get surfaced based on version, table or view name, method (get list / get one / post / patch / delete)
1038
- # Returning nil makes it do nothing, false makes it skip creating this endpoint, and an array of up to
1039
- # these 3 things controls and changes the nature of the endpoint that gets built:
1040
- # (updated api_name, name of different relation to route to, allowed actions such as :index, :show, :create, etc)
1041
- proc_result = if (filter = ::Brick.config.api_filter).is_a?(Proc)
1042
- begin
1043
- num_args = filter.arity.negative? ? 6 : filter.arity
1044
- filter.call(*[unversioned, k, view_relation, actions, api_ver_num, found, test_ver_num][0...num_args])
1045
- rescue StandardError => e
1046
- puts "::Brick.api_filter Proc error: #{e.message}"
1047
- end
1048
- end
1049
- # proc_result expects to receive back: [updated_api_name, to_other_relation, allowed_actions]
1050
-
1051
- case proc_result
1052
- when NilClass
1053
- # Do nothing differently than what normal behaviour would be
1054
- when FalseClass # Skip implementing this endpoint
1055
- view_relation[:api][api_ver_num] = nil
1056
- next
1057
- when Array # Did they give back an array of actions?
1058
- unless proc_result.any? { |pr| ::Brick::ALL_API_ACTIONS.exclude?(pr) }
1059
- proc_result = [unversioned, to_relation, proc_result]
1060
- end
1061
- # Otherwise don't change this array because it's probably legit
1062
- when String
1063
- proc_result = [proc_result] # Treat this as the surfaced api_name (path) they want to use for this endpoint
1064
- else
1065
- puts "::Brick.api_filter Proc warning: Unable to parse this result returned: \n #{proc_result.inspect}"
1066
- proc_result = nil # Couldn't understand what in the world was returned
1067
- end
1068
-
1069
- if proc_result&.present?
1070
- if proc_result[1] # to_other_relation
1071
- if (new_view_relation = ::Brick.relations.fetch(proc_result[1], nil))
1072
- k = proc_result[1] # Route this call over to this different relation
1073
- view_relation = new_view_relation
1074
- else
1075
- puts "::Brick.api_filter Proc warning: Unable to find new suggested relation with name #{proc_result[1]} -- sticking with #{k} instead."
1076
- end
1077
- end
1078
- if proc_result.first&.!=(k) # updated_api_name -- a different name than this relation would normally have
1079
- found = proc_result.first
1080
- end
1081
- actions &= proc_result[2] if proc_result[2] # allowed_actions
1082
- end
1083
- (view_relation[:api][api_ver_num] ||= {})[unversioned] = actions # Add to the list of API paths this resource responds to
1084
-
1085
- # view_ver_num = if (first_part = k.split('_').first) =~ /^v[\d_]+/
1086
- # first_part[1..-1].gsub('_', '.').to_i
1087
- # end
1088
- controller_name = if (last = view_relation.fetch(:resource, nil)&.pluralize)
1089
- "#{full_schema_prefix}#{last}"
1090
- else
1091
- found
1092
- end.tr('.', '/')
1093
-
1094
- { :index => 'get', :create => 'post' }.each do |action, method|
1095
- if actions.include?(action)
1096
- # Normally goes to something like: /api/v1/employees
1097
- send(method, "#{api_root}#{unversioned.tr('.', '/')}", { to: "#{controller_prefix}#{controller_name}##{action}" })
1098
- end
1099
- end
1100
- # %%% We do not yet surface the #show action
1101
- if (id_col = view_relation[:pk]&.first) # ID-dependent stuff
1102
- { :update => ['put', 'patch'], :destroy => ['delete'] }.each do |action, methods|
1103
- if actions.include?(action)
1104
- methods.each do |method|
1105
- send(method, "#{api_root}#{unversioned.tr('.', '/')}/:#{id_col}", { to: "#{controller_prefix}#{controller_name}##{action}" })
1106
- end
1107
- end
1108
- end
1109
- end
1110
- end
1111
- end
1112
-
1113
- # Trestle compatibility
1114
- if Object.const_defined?('Trestle') && ::Trestle.config.options&.key?(:site_title) &&
1115
- !Object.const_defined?("#{(res_name = resource_name.tr('/', '_')).camelize}Admin")
1116
- begin
1117
- ::Trestle.resource(res_sym = res_name.to_sym, model: class_name&.constantize) do
1118
- menu { item res_sym, icon: "fa fa-star" }
1119
- end
1120
- rescue
1121
- end
1122
- end
1123
- end
1124
-
1125
- if (named_routes = instance_variable_get(:@set).named_routes).respond_to?(:find)
1126
- if ::Brick.config.add_status && (status_as = "#{controller_prefix.tr('/', '_')}brick_status".to_sym)
1127
- (
1128
- !(status_route = instance_variable_get(:@set).named_routes.find { |route| route.first == status_as }&.last) ||
1129
- !status_route.ast.to_s.include?("/#{controller_prefix}brick_status/")
1130
- )
1131
- get("/#{controller_prefix}brick_status", to: 'brick_gem#status', as: status_as.to_s)
1132
- end
1133
-
1134
- # ::Brick.config.add_schema &&
1135
- if (schema_as = "#{controller_prefix.tr('/', '_')}brick_schema".to_sym)
1136
- (
1137
- !(schema_route = instance_variable_get(:@set).named_routes.find { |route| route.first == schema_as }&.last) ||
1138
- !schema_route.ast.to_s.include?("/#{controller_prefix}brick_schema/")
1139
- )
1140
- post("/#{controller_prefix}brick_schema", to: 'brick_gem#schema_create', as: schema_as.to_s)
1141
- end
1142
-
1143
- if ::Brick.config.add_orphans && (orphans_as = "#{controller_prefix.tr('/', '_')}brick_orphans".to_sym)
1144
- (
1145
- !(orphans_route = instance_variable_get(:@set).named_routes.find { |route| route.first == orphans_as }&.last) ||
1146
- !orphans_route.ast.to_s.include?("/#{controller_prefix}brick_orphans/")
1147
- )
1148
- get("/#{controller_prefix}brick_orphans", to: 'brick_gem#orphans', as: 'brick_orphans')
1149
- end
1150
- end
1151
-
1152
- if instance_variable_get(:@set).named_routes.names.exclude?(:brick_crosstab)
1153
- get("/#{controller_prefix}brick_crosstab", to: 'brick_gem#crosstab', as: 'brick_crosstab')
1154
- get("/#{controller_prefix}brick_crosstab/data", to: 'brick_gem#crosstab_data')
1155
- end
1156
-
1157
- if ((rswag_ui_present = Object.const_defined?('Rswag::Ui')) &&
1158
- (rswag_path = routeset_to_use.routes.find { |r| r.app.app == ::Rswag::Ui::Engine }
1159
- &.instance_variable_get(:@path_formatter)
1160
- &.instance_variable_get(:@parts)&.join) &&
1161
- (doc_endpoints = ::Rswag::Ui.config.config_object[:urls])) ||
1162
- (doc_endpoints = ::Brick.instance_variable_get(:@swagger_endpoints))
1163
- last_endpoint_parts = nil
1164
- doc_endpoints.each do |doc_endpoint|
1165
- puts "Mounting OpenApi 3.0 documentation endpoint for \"#{doc_endpoint[:name]}\" on #{doc_endpoint[:url]}" unless ::Brick.routes_done
1166
- send(:get, doc_endpoint[:url], { to: 'brick_openapi#index' })
1167
- endpoint_parts = doc_endpoint[:url]&.split('/')
1168
- last_endpoint_parts = endpoint_parts
1169
- end
1170
- end
1171
- return if ::Brick.routes_done
1172
-
1173
- if doc_endpoints.present?
1174
- if rswag_ui_present
1175
- if rswag_path
1176
- puts "API documentation now available when navigating to: /#{last_endpoint_parts&.find(&:present?)}/index.html"
1177
- else
1178
- puts "In order to make documentation available you can put this into your routes.rb:"
1179
- puts " mount Rswag::Ui::Engine => '/#{last_endpoint_parts&.find(&:present?) || 'api-docs'}'"
1180
- end
1181
- else
1182
- puts "Having this exposed, one easy way to leverage this to create HTML-based API documentation is to use Scalar.
1183
- It will jump to life when you put these two lines into a view template or other HTML resource:
1184
- <script id=\"api-reference\" data-url=\"#{last_endpoint_parts.join('/')}\"></script>
1185
- <script src=\"https://cdn.jsdelivr.net/@scalar/api-reference\"></script>
1186
- Alternatively you can add the rswag-ui gem."
1187
- end
1188
- elsif rswag_ui_present
1189
- sample_path = rswag_path || '/api-docs'
1190
- puts
1191
- puts "Brick: rswag-ui gem detected -- to make OpenAPI 3.0 documentation available from a path such as '#{sample_path}/v1/swagger.json',"
1192
- puts ' put code such as this in an initializer:'
1193
- puts ' Rswag::Ui.configure do |config|'
1194
- puts " config.swagger_endpoint '#{sample_path}/v1/swagger.json', 'API V1 Docs'"
1195
- puts ' end'
1196
- unless rswag_path
1197
- puts
1198
- puts ' and put this into your routes.rb:'
1199
- puts " mount Rswag::Ui::Engine => '/api-docs'"
1200
- end
1201
- end
1202
-
1203
- puts "\n" if tables.present? || views.present?
1204
- if tables.present?
1205
- puts "Classes that can be built from tables:#{' ' * (table_class_length - 38)} Path:"
1206
- puts "======================================#{' ' * (table_class_length - 38)} ====="
1207
- ::Brick.display_classes(controller_prefix, tables, table_class_length)
1208
- end
1209
- if views.present?
1210
- puts "Classes that can be built from views:#{' ' * (view_class_length - 37)} Path:"
1211
- puts "=====================================#{' ' * (view_class_length - 37)} ====="
1212
- ::Brick.display_classes(controller_prefix, views, view_class_length)
1213
- end
1214
- ::Brick.routes_done = true
1215
- end
1216
- end
1217
-
1218
884
  end
1219
885
 
1220
886
  require 'brick/version_number'
@@ -2009,7 +1675,11 @@ end
2009
1675
 
2010
1676
  # Now the Ransack Polyamorous version of #build
2011
1677
  if Gem::Dependency.new('ransack').matching_specs.present?
2012
- 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
2013
1683
  module Polyamorous::JoinDependencyExtensions
2014
1684
  def build(associations, base_klass, root = nil, path = '')
2015
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
@@ -32,7 +32,7 @@ module Brick
32
32
  'bit' => 'boolean',
33
33
  'varbinary' => 'binary',
34
34
  'tinyint' => 'integer', # %%% Need to put in "limit: 2"
35
- 'year' => 'date',
35
+ 'year' => 'integer',
36
36
  'set' => 'string',
37
37
  # Sqlite data types
38
38
  'TEXT' => 'text',
@@ -6,12 +6,12 @@ require 'rails/generators/active_record'
6
6
  require 'fancy_gets'
7
7
 
8
8
  module Brick
9
- # Auto-generates models, controllers, or views
9
+ # Auto-generates models
10
10
  class ModelsGenerator < ::Rails::Generators::Base
11
11
  include FancyGets
12
12
  # include ::Rails::Generators::Migration
13
13
 
14
- desc 'Auto-generates models, controllers, or views.'
14
+ desc 'Auto-generates models.'
15
15
 
16
16
  def brick_models
17
17
  # %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.191
4
+ version: 1.0.192
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-09 00:00:00.000000000 Z
11
+ date: 2023-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -248,12 +248,14 @@ files:
248
248
  - lib/brick/frameworks/rails/form_tags.rb
249
249
  - lib/brick/frameworks/rspec.rb
250
250
  - lib/brick/join_array.rb
251
+ - lib/brick/route_mapper.rb
251
252
  - lib/brick/serializers/json.rb
252
253
  - lib/brick/serializers/yaml.rb
253
254
  - lib/brick/tasks/orphans.rake
254
255
  - lib/brick/util.rb
255
256
  - lib/brick/version_number.rb
256
257
  - lib/generators/brick/USAGE
258
+ - lib/generators/brick/controllers_generator.rb
257
259
  - lib/generators/brick/install_generator.rb
258
260
  - lib/generators/brick/migration_builder.rb
259
261
  - lib/generators/brick/migrations_generator.rb