Module: Reports::Generator

Defined in:
app/commands/reports/generator.rb

Defined Under Namespace

Classes: GeneratorError

Class Method Summary collapse

Class Method Details

.all_flattener_classesObject

Lists all flatteners defined by reading the filesystem. This is used to be able to obtain flattener names with prefixes like HasHelpers::Organization.



242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'app/commands/reports/generator.rb', line 242

def self.all_flattener_classes
  base_path = Rails.root.join("app/models/flatteners")
  Dir.glob(base_path.join("**/*.rb")).filter_map do |path|
    const_name = Pathname.new(path).
      relative_path_from(base_path.parent).
      to_s.
      delete_suffix(".rb").
      split("/").
      map(&:camelize).
      join("::")
    const_name.safe_constantize
  end.select { |c| c.is_a?(Class) && c < Flattener::Base }
end

.build_option_name(*parts, column_type: nil, preserve_id: false) ⇒ Object

Examples

Removing redundant terms - in this case a second consecutive "Client" is removed.

build_option_name(:primary, :client, :client_id, preserve_id: false) # => "Primary Client Id"



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'app/commands/reports/generator.rb', line 272

def self.build_option_name(*parts, column_type: nil, preserve_id: false)
  # Create an enumerator which yields the words used to build the name.
  words = Enumerator.new do |yielder|
    parts.lazy.
      map(&:to_s).
      map do |part|
      if column_type == "boolean"
        part.gsub(/(^|_)is_/, '\1')
      else
        part
      end
    end.
      map do |part|
      if preserve_id
        part.tr("_", " ") # Prevents titleize from removing "_id"
      else
        part
      end
    end.
      map(&:titleize).
      each do |part|
      # Yield each word separately. E.g. "Client Id" => ["Client", "Id"]
      part.split(/\s+/).each do |sub_part|
        sub_part = sub_part.gsub("Id", "ID")
        yielder << sub_part
      end
    end
  end

  # Get all the parts of the name, excluding consecutive repeated elements.
  sanitized_parts = [*words.first] # Always include the first word
  words.each_cons(2) do |part1, part2|
    sanitized_parts << part2 unless part1 == part2
  end

  sanitized_parts.join(" ")
end

.check_resourcesObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'app/commands/reports/generator.rb', line 155

def self.check_resources
  # Fetch all resources with error handling
  all_resources = Reports::ExternalGraphqlClient.query_all_resources
  unless all_resources.key?("data")
    Rails.logger.error "Failed to fetch resources: #{all_resources.inspect}"
    return []
  end
  resource_array = all_resources.dig("data", "resources") || []
  # Get all flattener classes more efficiently
  flatteners = all_flattener_classes
  # Extract resource names once
  resource_names = (resource_array["edges"] || []).filter_map { |edge| edge["node"]&.dig("name")&.demodulize }.to_set
  # Find and process missing flatteners
  missing_flatteners = flatteners.each_with_object([]) do |flattener, arr|
    flattener_name = flattener.name.demodulize
    arr << flattener unless resource_names.include?(flattener_name)
  end
  # Create missing resources with logging
  missing_flatteners.each do |flattener|
    flattener_name = flattener.name.demodulize
    next if flattener_name.blank?

    create_resources(flattener_name)
  end

  missing_flatteners
end

.create_resources(klass_name = nil) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'app/commands/reports/generator.rb', line 110

def self.create_resources(klass_name = nil)
  # This method is used to ensure that the resources are created in the database.
  # It will create the resources if they do not exist.
  # It will return an array of errors if any resources could not be created.
  response_messages = []
  if klass_name
    response = Reports::ExternalGraphqlClient.query_resource_by_name(name: klass_name)
    resource_id = response.dig("data", "resourceName", "id")
    unless resource_id
      # If not found, create it using the mutation
      create_resource = Reports::ExternalGraphqlClient.create_resource(mutation_name: "createResource", input_name: "ReportResourceInput", attributes: { name: klass_name, type: "HasReports::Resource" })
      if create_resource["errors"]
        response_messages << "Failed to create resource for flattener #{klass_name}: #{create_resource["errors"]}"
      else
        response_messages << "Successfully created resource for flattener #{klass_name}"
      end
    end
  else
    flatteners = all_flattener_classes
    flatteners.each do |flattener|
      # Check if the resource exists via the external GraphQL client
      flattener_name = flattener.name.sub(/^Flatteners::/, "") # Remove the Flatteners:: prefix
      response = Reports::ExternalGraphqlClient.query_resource_by_name(name: flattener_name)
      resource_id = response.dig("data", "resourceName", "id")
      unless resource_id
        # If not found, create it using the mutation
        create_resource = Reports::ExternalGraphqlClient.create_resource(
          mutation_name: "createResource",
          input_name: "ReportResourceInput",
          attributes: {
            name: flattener_name,
            type: "HasReports::Resource"
          }
        )
        if create_resource["errors"]
          response_messages << "Failed to create resource for flattener #{flattener_name}: #{create_resource["errors"]}"
        else
          response_messages << "Successfully created resource for flattener #{flattener_name}"
        end
      end
    end
  end
  response_messages
end

.custom_select_options(klass, &block) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
# File 'app/commands/reports/generator.rb', line 256

def self.custom_select_options(klass, &block)
  if block
    previous_custom_select_options = custom_select_options_cache[klass]
    custom_select_options_cache[klass] = proc do |results = []|
      # Invoke the previous callback (if any)
      previous_custom_select_options.call(results) if previous_custom_select_options
      results.concat(block.call)
    end
  else
    custom_select_options_cache[klass].nil? ? [] : custom_select_options_cache[klass].call
  end
end

.generate_filter_options(klass, resource_id, **options) ⇒ Object



230
231
232
233
234
235
236
237
238
# File 'app/commands/reports/generator.rb', line 230

def self.generate_filter_options(klass, resource_id, **options)
  [].tap do |results|
    results.concat(::Reports::BaseOptions.generate_filter_options(klass, **options).to_a)
    results.concat(::Reports::ConstantOptions.generate_filter_options(klass, **options).to_a)
    results.concat(::Reports::BelongsToOptions.generate_filter_options(klass, resource_id, **options).to_a)
    results.concat(::Reports::HasManyOptions.generate_filter_options(klass, **options).to_a)
    results.concat(::Reports::NestedAttributeOptions.generate_filter_options(klass, **options).to_a)
  end
end

.generate_select_options(klass, resource_id, **options) ⇒ Object



220
221
222
223
224
225
226
227
228
# File 'app/commands/reports/generator.rb', line 220

def self.generate_select_options(klass, resource_id, **options)
  results = []
  results.concat(::Reports::BaseOptions.generate_select_options(klass, resource_id, **options).to_a)
  results.concat(::Reports::ConstantOptions.generate_select_options(klass, **options).to_a)
  results.concat(::Reports::BelongsToOptions.generate_select_options(klass, **options).to_a)
  results.concat(::Reports::HasManyOptions.generate_select_options(klass, **options).to_a)
  results.concat(::Reports::NestedAttributeOptions.generate_select_options(klass, **options).to_a)
  results
end

.run(klasses, verbose: false, **options) ⇒ Object

Arguments

klasses: A collection of the classes for which options will be generated. Eg. Generator.run([Advisor, Policy])

Options

:schema: Optional schema name. This will be the default schema for all table names.

Examples

::Reports::Generator.run(Advisor) ::Reports::Generator.run_all --> to create resources and run for all flatteners



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/commands/reports/generator.rb', line 50

def self.run(
  klasses,
  # audit: true,
  verbose: false,
  **options
)
  aggregate_errors = []

  # Generate the options inside a transaction. This ensures that if there
  # are any errors, we can rollback the entire generate, including any
  # modifications made in the `before_generate` hook.
  ::ActiveRecord::Base.transaction do
    # if defined? @@before_generate_block
    #   class_eval(&@@before_generate_block)
    # end

    begin
      Array(klasses).each do |klass|
        # Check klass for has_reports
        # raise GeneratorError.new(full_error_list: GeneratorError::MISSING_HAS_REPORTS_MSG % { class_name: klass.name }) unless klass.has_reports(meta_only: true)
        # just one request per class
        # each time we run the generator we need to check if all flatteners are created on db as a resource
        check_resources
        response = Reports::ExternalGraphqlClient.query_resource_by_name(name: klass.name)
        resource_id = response.dig("data", "resourceName", "id")
        # Select options
        begin
          # first generate the resource if it doesnt exist
          # call the mutation to generate the resource
          select_option_attributes = generate_select_options(klass, resource_id, **options)
          importer = ::Reports::Importer.new(resource_id, resource_name: klass.name, **options)
          # select_option_attributes.uniq! { |attrs| attrs[:name] }  # FIXME: temporary work-around until duplicate entries are fixed - Gotta test those reports!!! *
          errors = importer.import_select_options(select_option_attributes, resource_id, verbose: verbose)
          if errors.any?
            aggregate_errors.concat(errors)
          end
        rescue ::Reports::Importer::AuditError => e
          aggregate_errors << GeneratorError.create_error_message(msg: GeneratorError::ERROR_MSG % { error_type: "AUDIT", option_type: "SelectOption", option_name: klass.name }, base_error: e)
        end
        # Filter options
        begin
          importer = ::Reports::Importer.new(resource_id, resource_name: klass.name, **options)
          filter_option_attributes = generate_filter_options(klass, resource_id, **options)
          # filter_option_attributes.uniq! { |attrs| attrs[:name] }  # FIXME: temporary work-around until duplicate entries are fixed - Gotta test those reports!!! *
          errors = importer.import_filter_options(filter_option_attributes, resource_id, verbose: verbose)
          if errors.any?
            aggregate_errors.concat(errors)
          end
        rescue ::Reports::Importer::AuditError => e
          aggregate_errors << GeneratorError.create_error_message(msg: GeneratorError::ERROR_MSG % { error_type: "AUDIT", option_type: "FilterOption", option_name: klass.name }, base_error: e)
        end
      end
    rescue => e
      aggregate_errors << GeneratorError.create_error_message(msg: "FATAL EXCEPTION", base_error: e)
    end
    # Return errors
    aggregate_errors
  end
end

.run_allObject



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'app/commands/reports/generator.rb', line 183

def self.run_all
  response_messages = []
  flatteners = all_flattener_classes
  flatteners.each do |flattener|
    # Check if the resource exists via the external GraphQL client
    flattener_name = flattener.name.sub(/^Flatteners::/, "") # Remove the Flatteners:: prefix
    response = Reports::ExternalGraphqlClient.query_resource_by_name(name: flattener_name)
    resource_name = response.dig("data", "resourceName", "name")
    if resource_name.present?
      run(resource_name.constantize) unless resource_name.to_s.demodulize == "UserOrganization"
    else
      # If not found, create it using the mutation
      create_resource = Reports::ExternalGraphqlClient.create_resource(
        mutation_name: "createResource",
        input_name: "ReportResourceInput",
        attributes: {
          name: flattener_name,
          type: "HasReports::Resource"
        }
      )
      if create_resource["errors"]
        response_messages << "Failed to create resource for flattener #{flattener_name}: #{create_resource["errors"]}"
      else
        resource_name = Reports::ExternalGraphqlClient.query_resource_by_name(name: flattener_name)
        resource_name = resource_name.dig("data", "resourceName", "name")
        klass = resource_name&.safe_constantize
        if klass
          run(klass)
          response_messages << "Successfully created resource for flattener #{flattener_name}"
        else
          response_messages << "Resource created but class not found for #{flattener_name}"
        end
      end
    end
  end
end