brick 1.0.72 → 1.0.74

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: 06c3efed47ee7086f01a4f59c177f362343f376b1d6534f86e61ee38001b0d7b
4
- data.tar.gz: 8b521c56eb5afc09b06aec0adf0085136cb9e94d2c1a93355a8a2fc79da0316b
3
+ metadata.gz: a7f077a36e2fd62546cdebfde2b9651807ceeaad9571008460423d9e994df44d
4
+ data.tar.gz: 368ca31e726fa377325b7ff7838e38cd6b79aa222bcec73cdd6cebc645908d28
5
5
  SHA512:
6
- metadata.gz: 1566ddaa89e45496b752b64ec218456e05dca50cc81a722d5a88e4d4bbe19b28ca981eeb253688f44e55a8ef9949d195bdc5a61f5ba7980b2507c315277b4d4d
7
- data.tar.gz: 7643f78bf92721c89bdb0065f112592cba26023ea7745ba34551ce39d9378cd286f50b3ea1491c813e10f6d93521ef16c54fa70edc5dbcd4d70cb5ad6a859ad3
6
+ metadata.gz: a4154837ca3926cb1cd8d4803d62a789f1aa6449625560c63c06708508543e218765288d13a763aab238ea946dfdc33391454793b02805a8cdf46f729cf436da
7
+ data.tar.gz: 599cd989ef47a99cb0b6b4cb1a1c53da892d5758ccd208ecb4bf1383a2f29c1bc4600458c4de357e5496f90f664984c10634f469a40f80bcc89a9e5592735899
data/lib/brick/config.rb CHANGED
@@ -56,6 +56,31 @@ module Brick
56
56
  @mutex.synchronize { @enable_routes = enable }
57
57
  end
58
58
 
59
+ def enable_api
60
+ @mutex.synchronize { @enable_api }
61
+ end
62
+
63
+ def enable_api=(enable)
64
+ @mutex.synchronize { @enable_api = enable }
65
+ end
66
+
67
+ def api_root
68
+ ver = api_version
69
+ @mutex.synchronize { @api_root || "/api/#{ver}/" }
70
+ end
71
+
72
+ def api_root=(path)
73
+ @mutex.synchronize { @api_root = path }
74
+ end
75
+
76
+ def api_version
77
+ @mutex.synchronize { @api_version || 'v1' }
78
+ end
79
+
80
+ def api_version=(ver)
81
+ @mutex.synchronize { @api_version = ver }
82
+ end
83
+
59
84
  # Additional table associations to use (Think of these as virtual foreign keys perhaps)
60
85
  def additional_references
61
86
  @mutex.synchronize { @additional_references }
@@ -107,7 +107,8 @@ module ActiveRecord
107
107
  bracket_name = nil
108
108
  prefix = [prefix] unless prefix.is_a?(Array)
109
109
  if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
110
- dsl2 = +''
110
+ dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
111
+ dsl3 = +'' # To return expanded DSL that is nested from another model
111
112
  klass = nil
112
113
  dsl.each_char do |ch|
113
114
  if bracket_name
@@ -129,11 +130,16 @@ module ActiveRecord
129
130
  end
130
131
  if klass.column_names.exclude?(parts.last) &&
131
132
  (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
133
+ # Expand this entry which refers to an association name
132
134
  members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, true)
133
135
  members += members2
134
136
  dsl2 << dsl2a
137
+ dsl3 << dsl2a
135
138
  else
136
139
  dsl2 << "[#{bracket_name}]"
140
+ if emit_dsl
141
+ dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
142
+ end
137
143
  members << parts
138
144
  end
139
145
  bracket_name = nil
@@ -145,15 +151,22 @@ module ActiveRecord
145
151
  klass = self
146
152
  else
147
153
  dsl2 << ch
154
+ dsl3 << ch
148
155
  end
149
156
  end
150
157
  # Rewrite the DSL in case it's now different from having to expand it
158
+ # if ::Brick.config.model_descrips[name] != dsl2
159
+ # puts ::Brick.config.model_descrips[name]
160
+ # puts dsl2.inspect
161
+ # puts dsl3.inspect
162
+ # binding.pry
163
+ # end
151
164
  ::Brick.config.model_descrips[name] = dsl2 unless emit_dsl
152
165
  else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
153
166
  x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
154
167
  x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
155
168
  end
156
- emit_dsl ? [members, dsl2] : members
169
+ emit_dsl ? [members, dsl3] : members
157
170
  end
158
171
 
159
172
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
@@ -221,13 +234,21 @@ module ActiveRecord
221
234
  end
222
235
 
223
236
  def self.bt_link(assoc_name)
224
- model_underscore = name.underscore
225
237
  assoc_name = CGI.escapeHTML(assoc_name.to_s)
226
- model_path = Rails.application.routes.url_helpers.send("#{model_underscore.tr('/', '_').pluralize}_path".to_sym)
238
+ model_path = Rails.application.routes.url_helpers.send("#{_brick_index}_path".to_sym)
227
239
  av_class = Class.new.extend(ActionView::Helpers::UrlHelper)
228
240
  av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('7')
229
241
  link = av_class.link_to(name, model_path)
230
- model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
242
+ table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
243
+ end
244
+
245
+ def self._brick_index
246
+ tbl_parts = table_name.split('.')
247
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
248
+ if (index = tbl_parts.map(&:underscore).join('_')) == index.singularize
249
+ index << '_index' # Rails applies an _index suffix to that route when the resource name is singular
250
+ end
251
+ index
231
252
  end
232
253
 
233
254
  def self.brick_import_template
@@ -407,7 +428,7 @@ module ActiveRecord
407
428
  is_distinct = nil
408
429
  wheres = {}
409
430
  params.each do |k, v|
410
- next if ['_brick_schema', '_brick_order'].include?(k)
431
+ next if ['_brick_schema', '_brick_order', 'controller', 'action'].include?(k)
411
432
 
412
433
  case (ks = k.split('.')).length
413
434
  when 1
@@ -427,6 +448,9 @@ module ActiveRecord
427
448
  # %%% Skip the metadata columns
428
449
  if selects&.empty? # Default to all columns
429
450
  tbl_no_schema = table.name.split('.').last
451
+ # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
452
+ # ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
453
+ # Relevant info here: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/402
430
454
  columns.each do |col|
431
455
  col_alias = " AS #{col.name}_" if (col_name = col.name) == 'class'
432
456
  selects << if is_mysql
@@ -726,7 +750,8 @@ Module.class_exec do
726
750
  # Vabc instead of VABC)
727
751
  full_class_name = +''
728
752
  full_class_name << "::#{self.name}" unless self == Object
729
- full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
753
+ singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
754
+ full_class_name << "::#{singular_class_name}"
730
755
  if plural_class_name == 'BrickSwagger' ||
731
756
  (
732
757
  (::Brick.config.add_status || ::Brick.config.add_orphans) &&
@@ -1111,64 +1136,65 @@ class Object
1111
1136
  end
1112
1137
  return [new_controller_class, code + "end # BrickGem controller\n"]
1113
1138
  when 'BrickSwagger'
1114
- is_swagger = true # if request.format == :json)
1139
+ is_swagger = true
1115
1140
  end
1116
1141
 
1117
1142
  self.protect_from_forgery unless: -> { self.request.format.js? }
1118
1143
  self.define_method :index do
1119
- # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1120
- # wants to do an ORDER BY based on any of that
1121
- translations = {}
1122
- join_array = ::Brick::JoinArray.new
1123
- is_add_bts = is_add_hms = true
1124
- # This builds out bt_descrip and hm_counts on the model
1125
- model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1144
+ if (is_swagger || request.env['REQUEST_PATH'].start_with?(::Brick.api_root)) &&
1145
+ !params&.key?('_brick_schema') &&
1146
+ (referrer_params = request.env['HTTP_REFERER']&.split('?')&.last&.split('&')&.map { |x| x.split('=') }).present?
1147
+ if params
1148
+ referrer_params.each { |k, v| params.send(:parameters)[k] = v }
1149
+ else
1150
+ api_params = referrer_params&.to_h
1151
+ end
1152
+ end
1153
+ ::Brick.set_db_schema(params || api_params)
1126
1154
 
1127
1155
  if is_swagger
1128
- json = { 'openapi': '3.0.1', 'info': { 'title': 'API V1', 'version': 'v1' },
1156
+ json = { 'openapi': '3.0.1', 'info': { 'title': Rswag::Ui.config.config_object[:urls].last&.fetch(:name, 'API documentation'), 'version': ::Brick.config.api_version },
1129
1157
  'servers': [
1130
- { 'url': 'https://{defaultHost}', 'variables': { 'defaultHost': { 'default': 'www.example.com' } } }
1158
+ { 'url': '{scheme}://{defaultHost}', 'variables': {
1159
+ 'scheme': { 'default': request.env['rack.url_scheme'] },
1160
+ 'defaultHost': { 'default': request.env['HTTP_HOST'] }
1161
+ } }
1131
1162
  ]
1132
1163
  }
1133
- json['paths'] = relations.inject({}) do |s, v|
1134
- s["/api/v1/#{v.first}"] = {
1135
- 'get': {
1136
- 'summary': "list #{v.first}",
1137
- 'parameters': v.last[:cols].map { |k, v| { 'name' => k, 'schema': { 'type': v.first } } },
1138
- 'responses': { '200': { 'description': 'successful' } }
1139
- }
1140
- }
1141
- # next if v.last[:isView]
1142
-
1143
- s["/api/v1/#{v.first}/{id}"] = {
1144
- 'patch': {
1145
- 'summary': "update a #{v.first.singularize}",
1146
- 'parameters': v.last[:cols].reject { |k, v| Brick.config.metadata_columns.include?(k) }.map do |k, v|
1147
- { 'name' => k, 'schema': { 'type': v.first } }
1148
- end,
1149
- 'responses': { '200': { 'description': 'successful' } }
1164
+ json['paths'] = relations.inject({}) do |s, relation|
1165
+ unless ::Brick.config.enable_api == false
1166
+ table_description = relation.last[:description]
1167
+ s["#{::Brick.config.api_root}#{relation.first.tr('.', '/')}"] = {
1168
+ 'get': {
1169
+ 'summary': "list #{relation.first}",
1170
+ 'description': table_description,
1171
+ 'parameters': relation.last[:cols].map do |k, v|
1172
+ param = { 'name' => k, 'schema': { 'type': v.first } }
1173
+ if (col_descrip = relation.last.fetch(:col_descrips, nil)&.fetch(k, nil))
1174
+ param['description'] = col_descrip
1175
+ end
1176
+ param
1177
+ end,
1178
+ 'responses': { '200': { 'description': 'successful' } }
1179
+ }
1150
1180
  }
1151
- # "/api/v1/books/{id}": {
1152
- # "parameters": [
1153
- # {
1154
- # "name": "id",
1155
- # "in": "path",
1156
- # "description": "id",
1157
- # "required": true,
1158
- # "schema": {
1159
- # "type": "string"
1160
- # }
1161
- # },
1162
- # {
1163
- # "name": "Authorization",
1164
- # "in": "header",
1165
- # "schema": {
1166
- # "type": "string"
1167
- # }
1168
- # }
1169
- # ],
1170
- }
1171
- s
1181
+
1182
+ s["#{::Brick.config.api_root}#{relation.first.tr('.', '/')}/{id}"] = {
1183
+ 'patch': {
1184
+ 'summary': "update a #{relation.first.singularize}",
1185
+ 'description': table_description,
1186
+ 'parameters': relation.last[:cols].reject { |k, v| Brick.config.metadata_columns.include?(k) }.map do |k, v|
1187
+ param = { 'name' => k, 'schema': { 'type': v.first } }
1188
+ if (col_descrip = relation.last.fetch(:col_descrips, nil)&.fetch(k, nil))
1189
+ param['description'] = col_descrip
1190
+ end
1191
+ param
1192
+ end,
1193
+ 'responses': { '200': { 'description': 'successful' } }
1194
+ }
1195
+ } unless relation.last.fetch(:isView, nil)
1196
+ s
1197
+ end
1172
1198
  end
1173
1199
  render inline: json.to_json, content_type: request.format
1174
1200
  return
@@ -1176,6 +1202,14 @@ class Object
1176
1202
 
1177
1203
  # Normal (non-swagger) request
1178
1204
 
1205
+ # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1206
+ # wants to do an ORDER BY based on any of that
1207
+ translations = {}
1208
+ join_array = ::Brick::JoinArray.new
1209
+ is_add_bts = is_add_hms = true
1210
+ # This builds out bt_descrip and hm_counts on the model
1211
+ model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1212
+
1179
1213
  # %%% Allow params to define which columns to use for order_by
1180
1214
  # Overriding the default by providing a querystring param?
1181
1215
  ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
@@ -1189,8 +1223,9 @@ class Object
1189
1223
  end
1190
1224
  render inline: exported_csv, content_type: request.format
1191
1225
  return
1192
- elsif request.format == :js # Asking for JSON?
1193
- render inline: model.df_export(model.brick_import_template).to_json, content_type: request.format
1226
+ elsif request.format == :js || request.path.start_with?('/api/') # Asking for JSON?
1227
+ data = (model.is_view? || !Object.const_defined?('DutyFree')) ? model.limit(1000) : model.df_export(model.brick_import_template)
1228
+ render inline: data.to_json, content_type: request.format == '*/*' ? 'application/json' : request.format
1194
1229
  return
1195
1230
  end
1196
1231
 
@@ -1206,7 +1241,7 @@ class Object
1206
1241
  "b_r_#{v.first}.c_t_ AS \"b_r_#{v.first}_ct\""
1207
1242
  end
1208
1243
  end
1209
- instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
1244
+ instance_variable_set("@#{table_name.pluralize}".to_sym, ar_relation.dup._select!(*selects, *counts))
1210
1245
  if namespace && (idx = lookup_context.prefixes.index(table_name))
1211
1246
  lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
1212
1247
  end
@@ -1221,32 +1256,34 @@ class Object
1221
1256
  @_brick_erd = params['_brick_erd']&.to_i
1222
1257
  end
1223
1258
 
1224
- _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk))
1225
- code << " def index\n"
1226
- code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1227
- code << " @#{table_name}.brick_select(params)\n"
1228
- code << " end\n"
1229
-
1230
- is_pk_string = nil
1231
- if (pk_col = model&.primary_key)
1232
- code << " def show\n"
1233
- code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1259
+ unless is_swagger
1260
+ ::Brick.set_db_schema
1261
+ _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk)) if pk
1262
+ code << " def index\n"
1263
+ code << " @#{table_name.pluralize} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1264
+ code << " @#{table_name.pluralize}.brick_select(params)\n"
1234
1265
  code << " end\n"
1235
- self.define_method :show do
1236
- ::Brick.set_db_schema(params)
1237
- id = if model.columns_hash[pk_col]&.type == :string
1238
- is_pk_string = true
1239
- params[:id]
1240
- else
1241
- params[:id]&.split(/[\/,_]/)
1242
- end
1243
- id = id.first if id.is_a?(Array) && id.length == 1
1244
- instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1266
+
1267
+ is_pk_string = nil
1268
+ if pk.present?
1269
+ code << " def show\n"
1270
+ code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1271
+ code << " end\n"
1272
+ self.define_method :show do
1273
+ ::Brick.set_db_schema(params)
1274
+ id = if model.columns_hash[pk.first]&.type == :string
1275
+ is_pk_string = true
1276
+ params[:id]
1277
+ else
1278
+ params[:id]&.split(/[\/,_]/)
1279
+ end
1280
+ id = id.first if id.is_a?(Array) && id.length == 1
1281
+ instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1282
+ end
1245
1283
  end
1246
- end
1247
1284
 
1248
- # By default, views get marked as read-only
1249
- # unless model.readonly # (relation = relations[model.table_name]).key?(:isView)
1285
+ # By default, views get marked as read-only
1286
+ # unless model.readonly # (relation = relations[model.table_name]).key?(:isView)
1250
1287
  code << " def new\n"
1251
1288
  code << " @#{singular_table_name} = #{model.name}.new\n"
1252
1289
  code << " end\n"
@@ -1280,7 +1317,7 @@ class Object
1280
1317
  end
1281
1318
  end
1282
1319
 
1283
- if pk_col
1320
+ if pk.present?
1284
1321
  # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
1285
1322
  # ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
1286
1323
  # end
@@ -1327,9 +1364,9 @@ class Object
1327
1364
  end
1328
1365
  end
1329
1366
 
1330
- code << "private\n" if pk_col || is_need_params
1367
+ code << "private\n" if pk.present? || is_need_params
1331
1368
 
1332
- if pk_col
1369
+ if pk.present?
1333
1370
  code << " def find_#{singular_table_name}
1334
1371
  id = params[:id]&.split(/[\\/,_]/)
1335
1372
  @#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
@@ -1357,16 +1394,16 @@ class Object
1357
1394
  private params_name
1358
1395
  # Get column names for params from relations[model.table_name][:cols].keys
1359
1396
  end
1360
- # end
1397
+ end # unless is_swagger
1361
1398
  code << "end # #{namespace_name}#{class_name}\n"
1362
1399
  end # class definition
1363
1400
  [built_controller, code]
1364
1401
  end
1365
1402
 
1366
1403
  def _brick_get_hm_assoc_name(relation, hm_assoc, source = nil)
1367
- if (relation[:hm_counts][hm_assoc[:assoc_name]]&.> 1) &&
1404
+ if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1368
1405
  hm_assoc[:alternate_name] != (source || name.underscore)
1369
- plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
1406
+ plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1370
1407
  new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1371
1408
  # uniq = 1
1372
1409
  # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
@@ -1497,6 +1534,8 @@ module ActiveRecord::ConnectionHandling
1497
1534
  r['schema']
1498
1535
  end
1499
1536
  relation_name = schema_name ? "#{schema_name}.#{r['relation_name']}" : r['relation_name']
1537
+ # Both uppers and lowers as well as underscores?
1538
+ apply_double_underscore_patch if relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
1500
1539
  relation = relations[relation_name]
1501
1540
  relation[:isView] = true if r['table_type'] == 'VIEW'
1502
1541
  relation[:description] = r['table_description'] if r['table_description']
@@ -1513,6 +1552,7 @@ module ActiveRecord::ConnectionHandling
1513
1552
  cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
1514
1553
  cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
1515
1554
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
1555
+ relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
1516
1556
  end
1517
1557
  else # MySQL2 and OracleEnhanced act a little differently, bringing back an array for each row
1518
1558
  schema_and_tables = case ActiveRecord::Base.connection.adapter_name
@@ -1542,6 +1582,9 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1542
1582
 
1543
1583
  if (relation_name = r[1]) =~ /^[A-Z0-9_]+$/
1544
1584
  relation_name.downcase!
1585
+ # Both uppers and lowers as well as underscores?
1586
+ elsif relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
1587
+ apply_double_underscore_patch
1545
1588
  end
1546
1589
  # Expect the default schema for SQL Server to be 'dbo'.
1547
1590
  if (::Brick.is_oracle && r[0] != schema) || (is_mssql && r[0] != 'dbo')
@@ -1660,54 +1703,27 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1660
1703
  end
1661
1704
  end
1662
1705
 
1663
- tables = []
1664
- views = []
1665
- table_class_length = 0
1666
- view_class_length = 0
1667
1706
  relations.each do |k, v|
1668
- name_parts = k.split('.')
1669
- idx = 1
1670
- name_parts = name_parts.map do |x|
1671
- (idx += 1) < name_parts.length ? x : x.singularize
1672
- end
1673
- name_parts.shift if apartment && name_parts.length > 1 && name_parts.first == Apartment.default_schema
1674
- class_name = name_parts.map(&:camelize).join('::')
1675
- if v.key?(:isView)
1676
- view_class_length = class_name.length if class_name.length > view_class_length
1677
- views
1678
- else
1679
- table_class_length = class_name.length if class_name.length > table_class_length
1680
- tables
1681
- end << [class_name, name_parts]
1682
- end
1683
- puts "\n" if tables.present? || views.present?
1684
- if tables.present?
1685
- puts "Classes that can be built from tables:"
1686
- display_classes(tables, table_class_length)
1707
+ rel_name = k.split('.').map { |rel_part| ::Brick.namify(rel_part, :underscore) }
1708
+ schema_names = rel_name[0..-2]
1709
+ schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == Apartment.default_schema
1710
+ v[:schema] = schema_names.join('.') unless schema_names.empty?
1711
+ # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
1712
+ v[:resource] = rel_name.last
1713
+ v[:class_name] = (schema_names + [rel_name.last.singularize]).map(&:camelize).join('::')
1687
1714
  end
1688
- if views.present?
1689
- puts "Classes that can be built from views:"
1690
- display_classes(views, view_class_length)
1691
- end
1692
-
1693
1715
  ::Brick.load_additional_references if initializer_loaded
1694
1716
  end
1695
1717
 
1696
- def display_classes(rels, max_length)
1697
- rels.sort.each do |rel|
1698
- rel_link = rel.last.dup.map(&:underscore)
1699
- rel_link[-1] = rel_link[-1].pluralize
1700
- puts "#{rel.first}#{' ' * (max_length - rel.first.length)} /#{rel_link.join('/')}"
1701
- end
1702
- puts "\n"
1703
- end
1704
-
1705
1718
  def retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil)
1706
1719
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if is_mssql.nil?
1707
1720
  sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
1708
1721
  pg_catalog.obj_description(
1709
- ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass, 'pg_class'
1710
- ) AS table_description," if is_postgres}
1722
+ ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, 'pg_class'
1723
+ ) AS table_description,
1724
+ pg_catalog.col_description(
1725
+ ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, c.ordinal_position
1726
+ ) AS column_description," if is_postgres}
1711
1727
  c.column_name, c.data_type,
1712
1728
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
1713
1729
  kcu.constraint_type AS const, kcu.constraint_name AS \"key\",
@@ -1751,6 +1767,43 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1751
1767
  ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : ''
1752
1768
  [ar_smtn, ar_imtn]
1753
1769
  end
1770
+
1771
+ def apply_double_underscore_patch
1772
+ unless @double_underscore_applied
1773
+ # Same as normal #camelize and #underscore, just that double-underscores turn into a single underscore
1774
+ ActiveSupport::Inflector.class_eval do
1775
+ def camelize(term, uppercase_first_letter = true)
1776
+ strings = term.to_s.split('__').map do |string|
1777
+ # String#camelize takes a symbol (:upper or :lower), so here we also support :lower to keep the methods consistent.
1778
+ if !uppercase_first_letter || uppercase_first_letter == :lower
1779
+ string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase! || match }
1780
+ else
1781
+ string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize! || match }
1782
+ end
1783
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do
1784
+ word = $2
1785
+ substituted = inflections.acronyms[word] || word.capitalize! || word
1786
+ $1 ? "::#{substituted}" : substituted
1787
+ end
1788
+ string
1789
+ end
1790
+ strings.join('_')
1791
+ end
1792
+
1793
+ def underscore(camel_cased_word)
1794
+ return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word)
1795
+ camel_cased_word.to_s.gsub("::", "/").split('_').map do |word|
1796
+ word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
1797
+ word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }
1798
+ word.tr!("-", "_")
1799
+ word.downcase!
1800
+ word
1801
+ end.join('__')
1802
+ end
1803
+ end
1804
+ @double_underscore_applied = true
1805
+ end
1806
+ end
1754
1807
  end
1755
1808
 
1756
1809
  # ==========================================
@@ -1777,7 +1830,7 @@ module Brick
1777
1830
 
1778
1831
  class << self
1779
1832
  def _add_bt_and_hm(fk, relations, is_polymorphic = false, is_optional = false)
1780
- bt_assoc_name = ::Brick.namify(fk[2], true)
1833
+ bt_assoc_name = ::Brick.namify(fk[2], :downcase)
1781
1834
  unless is_polymorphic
1782
1835
  bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
1783
1836
  bt_assoc_name[-3] == '_' ? bt_assoc_name[0..-4] : bt_assoc_name[0..-3]
@@ -1878,6 +1931,11 @@ module Brick
1878
1931
 
1879
1932
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?
1880
1933
 
1934
+ # if fk[1].end_with?('Suppliers') && fk[4] == 'People'
1935
+ # puts fk.inspect
1936
+ # binding.pry
1937
+ # end
1938
+
1881
1939
  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
1882
1940
  if assoc_hm[:fk].is_a?(String)
1883
1941
  assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
@@ -1885,7 +1943,6 @@ module Brick
1885
1943
  assoc_hm[:fk] << fk[2]
1886
1944
  end
1887
1945
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1888
- assoc_hm[:inverse] = assoc_bt
1889
1946
  else
1890
1947
  inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == Apartment.default_schema
1891
1948
  for_tbl
@@ -1896,7 +1953,7 @@ module Brick
1896
1953
  inverse_table: inv_tbl, inverse: assoc_bt }
1897
1954
  assoc_hm[:polymorphic] = true if is_polymorphic
1898
1955
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1899
- hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
1956
+ this_hm_count = hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
1900
1957
  end
1901
1958
  assoc_bt[:inverse] = assoc_hm
1902
1959
  end
@@ -159,7 +159,7 @@ module Brick
159
159
  hms_columns << hm_entry
160
160
  when 'show', 'new', 'update'
161
161
  hm_stuff << if hm_fk_name
162
- "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
162
+ "<%= link_to '#{assoc_name}', #{hm_assoc.klass._brick_index}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
163
163
  else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
164
164
  assoc_name
165
165
  end
@@ -173,12 +173,13 @@ module Brick
173
173
  # environment or whatever, then get either the controllers or routes list instead
174
174
  apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
175
175
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
176
+ binding.pry if tbl.is_a?(Symbol)
176
177
  if (tbl_parts = tbl.split('.')).first == apartment_default_schema
177
178
  tbl = tbl_parts.last
178
179
  end
179
180
  s[tbl] = nil
180
181
  end.keys.sort.each_with_object(+'') do |v, s|
181
- s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>"
182
+ s << "<option value=\"#{v.underscore.gsub('.', '/')}\">#{v}</option>"
182
183
  end.html_safe
183
184
  table_options << '<option value="brick_status">(Status)</option>'.html_safe if ::Brick.config.add_status
184
185
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
@@ -351,15 +352,39 @@ def hide_bcrypt(val, max_len = 200)
351
352
  end
352
353
  def display_value(col_type, val)
353
354
  case col_type
354
- when 'geometry'
355
+ when 'geometry', 'geography'
355
356
  if Object.const_defined?('RGeo')
356
357
  @is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2' if @is_mysql.nil?
357
- if @is_mysql
358
- # MySQL's \"Internal Geometry Format\" is like WKB, but with an initial 4 bytes that indicates the SRID.
359
- srid = val[0..3].unpack('I')
360
- val = val[4..-1]
358
+ @is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if @is_mssql.nil?
359
+ val_err = nil
360
+ if @is_mysql || @is_mssql
361
+ # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
362
+ if (srid = val&.[](0..3)&.unpack('I'))
363
+ val = val.force_encoding('BINARY')[4..-1].bytes
364
+
365
+ # MSSQL spatial bitwise flags, often 0C for a point:
366
+ # xxxx xxx1 = HasZValues
367
+ # xxxx xx1x = HasMValues
368
+ # xxxx x1xx = IsValid
369
+ # xxxx 1xxx = IsSinglePoint
370
+ # xxx1 xxxx = IsSingleLineSegment
371
+ # xx1x xxxx = IsWholeGlobe
372
+ # Convert Microsoft's unique geography binary to standard WKB
373
+ # (MSSQL point usually has two doubles, lng / lat, and can also have Z)
374
+ if @is_mssql
375
+ if val[0] == 1 && (val[1] & 8 > 0) && # Single point?
376
+ (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
377
+ idx = 2
378
+ new_val = [0, 0, 0, 0, 1]
379
+ new_val.concat(val[idx - 8...idx].reverse) while (idx += 8) <= val.length
380
+ val = new_val
381
+ else
382
+ val_err = '(Microsoft internal SQL geography type)'
383
+ end
384
+ end
385
+ end
361
386
  end
362
- RGeo::WKRep::WKBParser.new.parse(val)
387
+ val_err || (val ? RGeo::WKRep::WKBParser.new.parse(val.pack('c*')) : nil)
363
388
  else
364
389
  '(Add RGeo gem to parse geometry detail)'
365
390
  end
@@ -448,7 +473,7 @@ function changeout(href, param, value, trimAfter) {
448
473
  var params = hrefParts.length > 1 ? hrefParts[1].split(\"&\") : [];
449
474
  if (param === undefined || param === null || param === -1) {
450
475
  hrefParts = hrefParts[0].split(\"://\");
451
- var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
476
+ var pathParts = hrefParts[hrefParts.length - 1].split(\"/\").filter(function (pp) {return pp !== \"\";});
452
477
  if (value === undefined)
453
478
  // A couple possibilities if it's namespaced, starting with two parts in the path -- and then try just one
454
479
  return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)[0]];
@@ -562,7 +587,7 @@ erDiagram
562
587
  <%= \"#\{model_short_name} }o..o{ #\{hm_name} : \\\"#\{hm.first}\\\"\".html_safe %><%
563
588
  else # has_many
564
589
  %> <%= \"#\{model_short_name} ||--o{ #\{hm_name} : \\\"#\{
565
- hm_name unless hm.first.to_s == hm_class.name.underscore.pluralize.tr('/', '_')
590
+ hm.first.to_s unless hm.first.to_s.downcase == hm_class.name.underscore.pluralize.tr('/', '_')
566
591
  }\\\"\".html_safe %><%
567
592
  end %>
568
593
  <% end
@@ -604,7 +629,7 @@ erDiagram
604
629
  end
605
630
  if Object.const_defined?('DutyFree')
606
631
  template_link = "
607
- <%= link_to 'CSV', #{table_name}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
632
+ <%= link_to 'CSV', #{@_brick_model._brick_index}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
608
633
  <div id=\"dropper\" contenteditable=\"true\"></div>
609
634
  <input type=\"button\" id=\"btnImport\" value=\"Import\">
610
635
 
@@ -667,7 +692,7 @@ erDiagram
667
692
  console.log(\"x1\", sheetUrl);
668
693
 
669
694
  // Get JSON data
670
- fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
695
+ fetch(changeout(<%= #{@_brick_model._brick_index}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
671
696
  response.json().then(function (data) {
672
697
  gapi.client.sheets.spreadsheets.values.append({
673
698
  spreadsheetId: spreadsheetId,
@@ -698,10 +723,10 @@ erDiagram
698
723
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
699
724
  <select id=\"tbl\">#{table_options}</select>
700
725
  <table id=\"resourceName\"><tr>
701
- <td><h1>#{model_plural = model_name.pluralize}</h1></td>
726
+ <td><h1>#{model_name}</h1></td>
702
727
  <td id=\"imgErd\" title=\"Show ERD\"></td>
703
728
  </tr></table>#{template_link}<%
704
- if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
729
+ if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)).present? %><%=
705
730
  description %><br><%
706
731
  end
707
732
  # FILTER PARAMETERS
@@ -712,10 +737,10 @@ erDiagram
712
737
  origin = (key_parts = k.split('.')).length == 1 ? #{model_name} : #{model_name}.reflect_on_association(key_parts.first).klass
713
738
  if (destination_fk = Brick.relations[origin.table_name][:fks].values.find { |fk| fk[:fk] == key_parts.last }) &&
714
739
  (obj = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id)) %>
715
- <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination.name.underscore.tr('/', '_')\}_path\".to_sym, id) %></h3><%
740
+ <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination._brick_index\}_path\".to_sym, id) %></h3><%
716
741
  end
717
742
  end %>
718
- (<%= link_to 'See all #{model_plural.split('::').last}', #{path_obj_name.pluralize}_path %>)
743
+ (<%= link_to 'See all #{model_name.split('::').last.pluralize}', #{@_brick_model._brick_index}_path %>)
719
744
  <% end
720
745
  # COLUMN EXCLUSIONS
721
746
  if @_brick_excl&.present? %>
@@ -768,7 +793,7 @@ erDiagram
768
793
  end
769
794
  elsif col # HM column
770
795
  s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
771
- s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1].name.underscore.tr('/', '_').pluralize}_path\"))}\")
796
+ s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
772
797
  else # Bad column name!
773
798
  s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
774
799
  end
@@ -776,7 +801,11 @@ erDiagram
776
801
  end.html_safe
777
802
  %></tr></thead>
778
803
  <tbody>
779
- <% @#{table_name}.each do |#{obj_name}|
804
+ <% # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
805
+ # ActiveRecord::StatementTimeout in Warehouse::ColdRoomTemperatures_Archive#index
806
+ # TinyTds::Error: Adaptive Server connection timed out
807
+ # (After restarting the server it worked fine again.)
808
+ @#{table_name}.each do |#{obj_name}|
780
809
  hms_cols = {#{hms_columns.join(', ')}} %>
781
810
  <tr>#{"
782
811
  <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
@@ -811,10 +840,11 @@ erDiagram
811
840
  else
812
841
  \"#\{hms_col[1] || 'View'\} #\{hms_col.first}\"
813
842
  end %>
814
- <%= link_to txt, send(\"#\{klass.name.underscore.tr('/', '_').pluralize}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
843
+ <%= link_to txt, send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
815
844
  <% end
816
845
  elsif (col = cols[col_name])
817
- %><%= display_value(col&.type || col&.sql_type, val) %><%
846
+ col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
847
+ %><%= display_value(col_type || col&.sql_type, val) %><%
818
848
  else # Bad column name!
819
849
  %>?<%
820
850
  end
@@ -908,7 +938,7 @@ erDiagram
908
938
  if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
909
939
  description %><br><%
910
940
  end
911
- %><%= link_to '(See all #{obj_name.pluralize})', #{path_obj_name.pluralize}_path %>
941
+ %><%= link_to '(See all #{obj_name.pluralize})', #{@_brick_model._brick_index}_path %>
912
942
  #{erd_markup}
913
943
  <% if obj %>
914
944
  <br><br>
@@ -974,7 +1004,8 @@ end
974
1004
  \"<span class=\\\"orphan\\\">Orphaned ID: #\{val}</span>\".html_safe
975
1005
  end %>
976
1006
  <% else
977
- case (col_type = col.type || col.sql_type)
1007
+ col_type = col.sql_type == 'geography' ? col.sql_type : col.type
1008
+ case (col_type ||= col.sql_type)
978
1009
  when :string, :text %>
979
1010
  <% if is_bcrypt?(val) # || .readonly?
980
1011
  is_revert = false %>
@@ -1008,8 +1039,8 @@ end
1008
1039
  <% when :binary, :primary_key
1009
1040
  is_revert = false %>
1010
1041
  <% else %>
1011
- <%= display_value(col_type, val)
1012
- is_revert = false %>
1042
+ <%= is_revert = false
1043
+ display_value(col_type, val) %>
1013
1044
  <% end
1014
1045
  end
1015
1046
  if is_revert
@@ -1077,7 +1108,7 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1077
1108
  var imgErd = document.getElementById(\"imgErd\");
1078
1109
  var mermaidErd = document.getElementById(\"mermaidErd\");
1079
1110
  var mermaidCode;
1080
- var cbs = {<%= callbacks.map { |k, v| \"#\{k}: \\\"#\{v.name.underscore.pluralize}\\\"\" }.join(', ').html_safe %>};
1111
+ var cbs = {<%= callbacks.map { |k, v| \"#\{k}: \\\"#\{send(\"#\{v._brick_index}_path\".to_sym)}\\\"\" }.join(', ').html_safe %>};
1081
1112
  if (imgErd) imgErd.addEventListener(\"click\", showErd);
1082
1113
  function showErd() {
1083
1114
  imgErd.style.display = \"none\";
@@ -1101,7 +1132,7 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1101
1132
  function (evt) {
1102
1133
  location.href = changeout(changeout(
1103
1134
  changeout(location.href, '_brick_order', null), // Remove any ordering
1104
- -1, cbs[this.id]), \"_brick_erd\", \"1\");
1135
+ -1, cbs[this.id].replace(/^[\/]+/, \"\")), \"_brick_erd\", \"1\");
1105
1136
  }
1106
1137
  );
1107
1138
  }
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 72
8
+ TINY = 74
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
@@ -125,8 +125,8 @@ module Brick
125
125
  class << self
126
126
  attr_accessor :default_schema, :db_schemas, :routes_done, :is_oracle
127
127
 
128
- def set_db_schema(params)
129
- schema = params['_brick_schema'] || 'public'
128
+ def set_db_schema(params = nil)
129
+ schema = (params ? params['_brick_schema'] : ::Brick.default_schema) || 'public'
130
130
  if schema && ::Brick.db_schemas&.include?(schema)
131
131
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
132
132
  schema
@@ -152,17 +152,19 @@ module Brick
152
152
  end
153
153
 
154
154
  # Convert spaces to underscores if the second character and onwards is mixed case
155
- def namify(name, attempt_downcase = false)
156
- name.downcase! if attempt_downcase && name =~ /^[A-Z0-9_]+$/
155
+ def namify(name, action = nil)
156
+ has_uppers = name =~ /[A-Z]+/
157
+ has_lowers = name =~ /[a-z]+/
158
+ name.downcase! if has_uppers && action == :downcase
157
159
  if name.include?(' ')
158
160
  # All uppers or all lowers?
159
- if name[1..-1] =~ /^[A-Z0-9_]+$/ || name[1..-1] =~ /^[a-z0-9_]+$/
161
+ if !has_uppers || !has_lowers
160
162
  name.titleize.tr(' ', '_')
161
163
  else # Mixed uppers and lowers -- just remove existing spaces
162
164
  name.tr(' ', '')
163
165
  end
164
166
  else
165
- name
167
+ action == :underscore ? name.underscore : name
166
168
  end
167
169
  end
168
170
 
@@ -266,6 +268,26 @@ module Brick
266
268
  !!Brick.config.enable_routes
267
269
  end
268
270
 
271
+ # @api public
272
+ def enable_api=(path)
273
+ Brick.config.enable_api = path
274
+ end
275
+
276
+ # @api public
277
+ def enable_api
278
+ Brick.config.enable_api
279
+ end
280
+
281
+ # @api public
282
+ def api_root=(path)
283
+ Brick.config.api_root = path
284
+ end
285
+
286
+ # @api public
287
+ def api_root
288
+ Brick.config.api_root
289
+ end
290
+
269
291
  # @api public
270
292
  def skip_database_views=(value)
271
293
  Brick.config.skip_database_views = value
@@ -472,6 +494,13 @@ In config/initializers/brick.rb appropriate entries would look something like:
472
494
  def version
473
495
  VERSION::STRING
474
496
  end
497
+
498
+ def display_classes(rels, max_length)
499
+ rels.sort.each do |rel|
500
+ puts "#{rel.first}#{' ' * (max_length - rel.first.length)} /#{rel.last}"
501
+ end
502
+ puts "\n"
503
+ end
475
504
  end
476
505
 
477
506
  module RouteSet
@@ -479,35 +508,65 @@ In config/initializers/brick.rb appropriate entries would look something like:
479
508
  return super if ::Brick.routes_done
480
509
 
481
510
  ::Brick.routes_done = true
511
+ tables = []
512
+ views = []
513
+ table_class_length = 38 # Length of "Classes that can be built from tables:"
514
+ view_class_length = 37 # Length of "Classes that can be built from views:"
482
515
  existing_controllers = routes.each_with_object({}) { |r, s| c = r.defaults[:controller]; s[c] = nil if c }
483
516
  ::Rails.application.routes.append do
484
517
  # %%% TODO: If no auto-controllers then enumerate the controllers folder in order to build matching routes
485
518
  # If auto-controllers and auto-models are both enabled then this makes sense:
486
- ::Brick.relations.each do |rel_name, v|
487
- rel_name = rel_name.split('.').map { |x| ::Brick.namify(x).underscore }
488
- schema_names = rel_name[0..-2]
489
- schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == Apartment.default_schema
490
- # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
491
- k = rel_name.last
492
- unless existing_controllers.key?(controller_name = k.pluralize)
519
+ ::Brick.relations.each do |k, v|
520
+ unless !(controller_name = v.fetch(:resource, nil)&.pluralize) || existing_controllers.key?(controller_name)
493
521
  options = {}
494
522
  options[:only] = [:index, :show] if v.key?(:isView)
495
- if schema_names.present? # && !Object.const_defined('Apartment')
496
- send(:namespace, schema_names.first) do
497
- send(:resources, controller_name.to_sym, **options)
523
+ full_resource = nil
524
+ if (schema_name = v.fetch(:schema, nil)) # && !Object.const_defined('Apartment')
525
+ send(:namespace, schema_name) do
526
+ send(:resources, v[:resource].to_sym, **options)
498
527
  end
528
+ full_resource = "#{schema_name}/#{v[:resource]}"
529
+ send(:get, "#{::Brick.api_root}#{full_resource}", { to: "#{schema_name}/#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
499
530
  else
500
- send(:resources, controller_name.to_sym, **options)
531
+ send(:resources, v[:resource].to_sym, **options)
532
+ # Normally goes to something like: /api/v1/employees
533
+ send(:get, "#{::Brick.api_root}#{v[:resource]}", { to: "#{controller_name}#index" }) if Object.const_defined?('Rswag::Ui')
534
+ end
535
+
536
+ if (class_name = v.fetch(:class_name, nil))
537
+ if v.key?(:isView)
538
+ view_class_length = class_name.length if class_name.length > view_class_length
539
+ views
540
+ else
541
+ table_class_length = class_name.length if class_name.length > table_class_length
542
+ tables
543
+ end << [class_name, full_resource || v[:resource]]
501
544
  end
502
545
  end
503
- if ::Brick.config.add_status && instance_variable_get(:@set).named_routes.names.exclude?(:brick_status)
504
- get('/brick_status', to: 'brick_gem#status', as: 'brick_status')
505
- end
506
- if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
507
- get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
508
- end
509
546
  end
510
- send(:get, '/api-docs/v1/swagger.json', { to: 'brick_swagger#index' }) if Object.const_defined?('Rswag::Ui')
547
+ puts "\n" if tables.present? || views.present?
548
+ if tables.present?
549
+ puts "Classes that can be built from tables:#{' ' * (table_class_length - 38)} Path:"
550
+ puts "======================================#{' ' * (table_class_length - 38)} ====="
551
+ ::Brick.display_classes(tables, table_class_length)
552
+ end
553
+ if views.present?
554
+ puts "Classes that can be built from views:#{' ' * (view_class_length - 37)} Path:"
555
+ puts "=====================================#{' ' * (view_class_length - 37)} ====="
556
+ ::Brick.display_classes(views, view_class_length)
557
+ end
558
+
559
+ if ::Brick.config.add_status && instance_variable_get(:@set).named_routes.names.exclude?(:brick_status)
560
+ get('/brick_status', to: 'brick_gem#status', as: 'brick_status')
561
+ end
562
+ if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
563
+ get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
564
+ end
565
+ if Object.const_defined?('Rswag::Ui') && doc_endpoint = Rswag::Ui.config.config_object[:urls].last
566
+ # Serves JSON swagger info from a path such as '/api-docs/v1/swagger.json'
567
+ puts "Mounting swagger info endpoint for \"#{doc_endpoint[:name]}\" on #{doc_endpoint[:url]}"
568
+ send(:get, doc_endpoint[:url], { to: 'brick_swagger#index' })
569
+ end
511
570
  end
512
571
  super
513
572
  end
@@ -147,6 +147,8 @@ module Brick
147
147
  # Brick.enable_controllers = true # Setting this to \"false\" will disable controllers in development
148
148
  # Brick.enable_views = true # Setting this to \"false\" will disable views in development
149
149
 
150
+ # ::Brick.api_root = '/api/v1/' # Path from which to serve out API resources when the RSwag gem is present
151
+
150
152
  # # By default models are auto-created for database views, and set to be read-only. This can be skipped.
151
153
  # Brick.skip_database_views = true
152
154
 
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.72
4
+ version: 1.0.74
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-20 00:00:00.000000000 Z
11
+ date: 2022-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord