Module: Motor::BuildSchema::LoadFromRails

Defined in:
lib/motor/build_schema/load_from_rails.rb

Constant Summary collapse

MUTEX =
Mutex.new
UNIFIED_TYPES =
ActiveRecordUtils::Types::UNIFIED_TYPES
I18N_SCOPES_KEY =
'activerecord.scopes'
ACTION_TEXT_REFLECTION_PREFIX =
'rich_text_'
ACTION_TEXT_COLUMN_SUFFIX =
'_body'
ACTION_TEXT_SCOPE_PREFIX =
'with_rich_text_'
ACTIVE_STORAGE_SCOPE_PREFIX =
'with_attached_'
DEFAULT_CURRENCY_FORMAT_HASH =
{
  currency: 'USD',
  currency_base: 'units'
}.freeze

Class Method Summary collapse

Class Method Details

.build_action_text_column(name, model, ref) ⇒ Object

rubocop:enable Metrics/AbcSize


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/motor/build_schema/load_from_rails.rb', line 218

def build_action_text_column(name, model, ref)
  name = name.delete_prefix(ACTION_TEXT_REFLECTION_PREFIX)

  {
    name: name + ACTION_TEXT_COLUMN_SUFFIX,
    display_name: model.human_attribute_name(name),
    column_type: ColumnTypes::RICHTEXT,
    column_source: ColumnSources::REFLECTION,
    access_type: ColumnAccessTypes::READ_WRITE,
    default_value: '',
    validators: fetch_validators(model, name, ref),
    format: {},
    reference: nil,
    virtual: true
  }
end

.build_model_schema(model) ⇒ Object


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/motor/build_schema/load_from_rails.rb', line 59

def build_model_schema(model)
  model_name = model.name

  return Motor::BuildSchema::ActiveStorageAttachmentSchema.call if model_name == 'ActiveStorage::Attachment'

  {
    name: model_name.underscore,
    slug: Utils.slugify(model),
    table_name: model.table_name,
    class_name: model.name,
    primary_key: model.primary_key,
    display_name: model.model_name.human(count: :many, default: model_name.titleize.pluralize),
    display_column: FindDisplayColumn.call(model),
    columns: fetch_columns(model),
    associations: fetch_associations(model),
    icon: Motor::FindIcon.call(model_name),
    scopes: fetch_scopes(model),
    actions: BuildSchema::Defaults.actions,
    tabs: BuildSchema::Defaults.tabs,
    custom_sql: nil,
    visible: true,
    display_primary_key: true
  }.with_indifferent_access
end

.build_reference(model, name, reflection) ⇒ Object


235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/motor/build_schema/load_from_rails.rb', line 235

def build_reference(model, name, reflection)
  {
    name: name,
    display_name: model.human_attribute_name(name),
    model_name: reflection.polymorphic? ? nil : reflection.klass.name.underscore,
    reference_type: reflection.belongs_to? ? 'belongs_to' : 'has_one',
    foreign_key: reflection.join_foreign_key,
    primary_key: reflection.polymorphic? ? 'id' : reflection.join_primary_key,
    options: reflection.options.slice(:through, :source),
    polymorphic: reflection.polymorphic?,
    virtual: false
  }
end

.build_reflection_column(name, model, ref, default_attrs) ⇒ Object

rubocop:disable Metrics/AbcSize


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/motor/build_schema/load_from_rails.rb', line 192

def build_reflection_column(name, model, ref, default_attrs)
  if !ref.polymorphic? && ref.klass.name == 'ActionText::RichText'
    return build_action_text_column(name, model, ref)
  end

  column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
  is_attachment = !ref.polymorphic? && ref.klass.name == 'ActiveStorage::Attachment'
  access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
  column_type = is_attachment ? ColumnTypes::FILE : ColumnTypes::REFERENCE
  column_source = model.columns_hash[column_name] ? ColumnSources::TABLE : ColumnSources::REFLECTION

  {
    name: column_name,
    display_name: model.human_attribute_name(name),
    column_type: column_type,
    column_source: column_source,
    access_type: access_type,
    default_value: default_attrs[column_name],
    validators: fetch_validators(model, column_name, ref),
    format: {},
    reference: build_reference(model, name, ref),
    virtual: false
  }
end

.build_table_column(column, model, default_attrs) ⇒ Object


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/motor/build_schema/load_from_rails.rb', line 118

def build_table_column(column, model, default_attrs)
  access_type =
    if model.primary_key == column.name
      ColumnAccessTypes::READ_ONLY
    else
      COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE)
    end

  {
    name: column.name,
    display_name: Utils.humanize_column_name(model.human_attribute_name(column.name)),
    column_type: fetch_column_type(column, model),
    column_source: ColumnSources::TABLE,
    is_array: column.array?,
    access_type: access_type,
    default_value: default_attrs[column.name],
    validators: fetch_validators(model, column.name),
    reference: nil,
    format: fetch_format_hash(column, model),
    virtual: false
  }
end

.build_validator_hash(validator) ⇒ Object


295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/motor/build_schema/load_from_rails.rb', line 295

def build_validator_hash(validator)
  options = validator.options.reject { |_, v| v.is_a?(Proc) || v.is_a?(Symbol) }

  case validator
  when ActiveModel::Validations::InclusionValidator
    { includes: validator.send(:delimiter) }
  when ActiveRecord::Validations::PresenceValidator
    { required: true }
  when ActiveRecord::Validations::LengthValidator
    { length: normalize_length_validation_options(options) }
  when ActiveModel::Validations::NumericalityValidator
    { numeric: options }
  end
end

.callObject


23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/motor/build_schema/load_from_rails.rb', line 23

def call
  models.map do |model|
    model = Object.const_get(model.name)

    next unless model.table_exists?

    schema = build_model_schema(model)

    if model.respond_to?(:devise_modules)
      Motor::BuildSchema::AdjustDeviseModelSchema.call(schema, model.devise_modules)
    end

    schema
  rescue StandardError, NotImplementedError => e
    Rails.logger.error(e)

    next
  end.compact.uniq
end

.eager_load_models!Object


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/motor/build_schema/load_from_rails.rb', line 329

def eager_load_models!
  MUTEX.synchronize do
    if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
      Zeitwerk::Loader.eager_load_all
    else
      Rails.application.eager_load!
    end

    ActiveRecord::Base.descendants.each do |model|
      model.reflections.each do |_, ref|
        ref.klass
      rescue StandardError
        next
      end
    end
  end
end

.fetch_associations(model) ⇒ Object

rubocop:disable Metrics/AbcSize


250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/motor/build_schema/load_from_rails.rb', line 250

def fetch_associations(model)
  model.reflections.map do |name, ref|
    next if ref.has_one? || ref.belongs_to?
    next unless valid_reflection?(ref)

    model_class = ref.klass

    next if model_class.name == 'ActiveStorage::Blob'

    {
      name: name,
      display_name: model.human_attribute_name(name),
      slug: name.underscore,
      model_name: model_class.name.underscore,
      foreign_key: ref.join_primary_key,
      primary_key: ref.join_foreign_key,
      polymorphic: ref.options[:as].present?,
      icon: Motor::FindIcon.call(name),
      options: ref.options.slice(:through, :source),
      virtual: false,
      visible: true
    }
  end.compact
end

.fetch_column_type(column, model) ⇒ Object


160
161
162
163
164
165
166
167
168
169
# File 'lib/motor/build_schema/load_from_rails.rb', line 160

def fetch_column_type(column, model)
  return ColumnTypes::CURRENCY if column.name == 'price'
  return ColumnTypes::COLOR if %w[hex color].include?(column.name)
  return ColumnTypes::TAG if model.defined_enums[column.name]
  return ColumnTypes::TAG if model.validators_on(column.name).any?(ActiveModel::Validations::InclusionValidator)
  return ColumnTypes::RICHTEXT if column.name.ends_with?('_html')
  return ColumnTypes::COLOR if column.name.match?(/_(color|hex)\z/)

  UNIFIED_TYPES[column.type.to_s] || column.type.to_s
end

.fetch_columns(model) ⇒ Object


103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/motor/build_schema/load_from_rails.rb', line 103

def fetch_columns(model)
  default_attrs = model.new.attributes

  reference_columns = fetch_reference_columns(model)

  table_columns =
    model.columns.map do |column|
      next if reference_columns.find { |c| c[:name] == column.name }

      build_table_column(column, model, default_attrs)
    end.compact

  reference_columns + table_columns
end

.fetch_format_hash(column, model) ⇒ Object


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/motor/build_schema/load_from_rails.rb', line 141

def fetch_format_hash(column, model)
  return DEFAULT_CURRENCY_FORMAT_HASH if column.name == 'price'

  inclusion_validator, = model.validators_on(column.name).grep(ActiveModel::Validations::InclusionValidator)

  return { select_options: inclusion_validator.send(:delimiter) } if inclusion_validator

  enum = model.defined_enums[column.name]

  return { select_options: enum.keys } if enum

  return {} if column.name == 'year'

  return { number_format: true } if !column.name.match?(/_(?:id|year)\z/) &&
                                    %i[integer float].include?(column.type)

  {}
end

.fetch_reference_columns(model) ⇒ Object


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/motor/build_schema/load_from_rails.rb', line 171

def fetch_reference_columns(model)
  default_attrs = model.new.attributes

  model.reflections.map do |name, ref|
    next if !ref.has_one? && !ref.belongs_to?

    unless ref.polymorphic?
      begin
        next if ref.klass.name == 'ActiveStorage::Blob'
      rescue StandardError => e
        Rails.logger.error(e)

        next
      end
    end

    build_reflection_column(name, model, ref, default_attrs)
  end.compact
end

.fetch_scopes(model) ⇒ Object


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/motor/build_schema/load_from_rails.rb', line 84

def fetch_scopes(model)
  model.defined_scopes.map do |scope_name|
    scope_name = scope_name.to_s

    next if scope_name.starts_with?(ACTIVE_STORAGE_SCOPE_PREFIX)
    next if scope_name.starts_with?(ACTION_TEXT_SCOPE_PREFIX)

    {
      name: scope_name,
      display_name: I18n.t(scope_name,
                           scope: [I18N_SCOPES_KEY, model.name.underscore].join('.'),
                           default: scope_name.humanize),
      scope_type: DEFAULT_TYPE,
      visible: true,
      preferences: {}
    }
  end.compact
end

.fetch_validators(model, column_name, reflection = nil) ⇒ Object

rubocop:enable Metrics/AbcSize


276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/motor/build_schema/load_from_rails.rb', line 276

def fetch_validators(model, column_name, reflection = nil)
  validators =
    if reflection&.belongs_to? && !reflection.options[:optional]
      [{ required: true }]
    else
      []
    end

  enum = model.defined_enums[column_name]

  validators += [{ includes: enum.keys }] if enum

  validators += model.validators_on(column_name).map do |validator|
    build_validator_hash(validator)
  end.compact

  validators.uniq
end

.modelsObject


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/motor/build_schema/load_from_rails.rb', line 43

def models
  eager_load_models!

  models = ActiveRecord::Base.descendants.reject { |k| k.abstract_class || k.anonymous? }

  models -= Motor::ApplicationRecord.descendants
  models -= [Motor::Audit]
  models -= [ActiveRecord::SchemaMigration] if defined?(ActiveRecord::SchemaMigration)
  models -= [ActiveRecord::InternalMetadata] if defined?(ActiveRecord::InternalMetadata)
  models -= [ActiveStorage::Blob] if defined?(ActiveStorage::Blob)
  models -= [ActionText::RichText] if defined?(ActionText::RichText)
  models -= [ActiveStorage::VariantRecord] if defined?(ActiveStorage::VariantRecord)

  models
end

.normalize_length_validation_options(options) ⇒ Object


310
311
312
313
314
315
316
# File 'lib/motor/build_schema/load_from_rails.rb', line 310

def normalize_length_validation_options(options)
  return options if options[:in].blank?

  in_range = options[:in]

  options.merge(in: in_range.minmax)
end

.valid_reflection?(reflection) ⇒ Boolean

Returns:

  • (Boolean)

318
319
320
321
322
323
324
325
326
327
# File 'lib/motor/build_schema/load_from_rails.rb', line 318

def valid_reflection?(reflection)
  reflection.klass
  reflection.foreign_key

  true
rescue StandardError => e
  Rails.logger.error(e)

  false
end