brick 1.0.191 → 1.0.192

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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