databasium 0.1.0

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.
Files changed (125) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +109 -0
  5. data/Rakefile +6 -0
  6. data/app/assets/builds/application.js +9045 -0
  7. data/app/assets/builds/application.js.map +7 -0
  8. data/app/assets/builds/databasium.css +2 -0
  9. data/app/assets/config/databasium_manifest.js +1 -0
  10. data/app/assets/javascript/databasium/application.js +2 -0
  11. data/app/assets/javascript/databasium/controllers/attribute_controller.js +27 -0
  12. data/app/assets/javascript/databasium/controllers/collapse_controller.js +18 -0
  13. data/app/assets/javascript/databasium/controllers/error_controller.js +15 -0
  14. data/app/assets/javascript/databasium/controllers/filter_controller.js +224 -0
  15. data/app/assets/javascript/databasium/controllers/flash_controller.js +18 -0
  16. data/app/assets/javascript/databasium/controllers/graph_controller.js +193 -0
  17. data/app/assets/javascript/databasium/controllers/index.js +7 -0
  18. data/app/assets/javascript/databasium/controllers/layout_controller.js +13 -0
  19. data/app/assets/javascript/databasium/controllers/model_controller.js +32 -0
  20. data/app/assets/javascript/databasium/controllers/new_migration_controller.js +107 -0
  21. data/app/assets/javascript/databasium/controllers/relation_controller.js +10 -0
  22. data/app/assets/javascript/databasium/controllers/search_controller.js +23 -0
  23. data/app/assets/javascript/databasium/controllers/table_controller.js +283 -0
  24. data/app/assets/javascript/databasium/controllers/table_select_controller.js +19 -0
  25. data/app/assets/javascript/databasium/controllers/toggle_controller.js +28 -0
  26. data/app/assets/javascript/databasium/controllers/validation_controller.js +78 -0
  27. data/app/assets/javascript/databasium/shapes/erd_table_shape.js +54 -0
  28. data/app/assets/stylesheets/databasium/application.css +15 -0
  29. data/app/assets/stylesheets/databasium/colors.css +55 -0
  30. data/app/assets/stylesheets/databasium/custom.css +36 -0
  31. data/app/assets/stylesheets/databasium/databasium_engine.css +6 -0
  32. data/app/assets/stylesheets/databasium/pagy-tailwind.css +66 -0
  33. data/app/components/base.rb +50 -0
  34. data/app/components/databasium/collapsable.rb +62 -0
  35. data/app/components/databasium/forms/model.rb +147 -0
  36. data/app/components/databasium/forms/search.rb +31 -0
  37. data/app/components/databasium/global/error.rb +60 -0
  38. data/app/components/databasium/global/flash.rb +73 -0
  39. data/app/components/databasium/global/header_actions.rb +36 -0
  40. data/app/components/databasium/global/sidebar.rb +45 -0
  41. data/app/components/databasium/global/suggestion.rb +25 -0
  42. data/app/components/databasium/migrations/action.rb +39 -0
  43. data/app/components/databasium/migrations/file.rb +58 -0
  44. data/app/components/databasium/migrations/form.rb +222 -0
  45. data/app/components/databasium/migrations/header_actions.rb +87 -0
  46. data/app/components/databasium/migrations/migration_status.rb +22 -0
  47. data/app/components/databasium/migrations/preview.rb +29 -0
  48. data/app/components/databasium/migrations/show_turbo_stream.rb +19 -0
  49. data/app/components/databasium/migrations/sidebar.rb +28 -0
  50. data/app/components/databasium/models/attributes.rb +49 -0
  51. data/app/components/databasium/models/form.rb +100 -0
  52. data/app/components/databasium/models/header_actions.rb +51 -0
  53. data/app/components/databasium/models/model_preview.rb +31 -0
  54. data/app/components/databasium/models/sidebar.rb +25 -0
  55. data/app/components/databasium/models/templates/attribute.rb +99 -0
  56. data/app/components/databasium/models/templates/base.rb +6 -0
  57. data/app/components/databasium/models/templates/relation.rb +56 -0
  58. data/app/components/databasium/models/templates/validation.rb +285 -0
  59. data/app/components/databasium/navigation/base_icon.rb +32 -0
  60. data/app/components/databasium/navigation/frontend_icon.rb +17 -0
  61. data/app/components/databasium/navigation/get_icon.rb +26 -0
  62. data/app/components/databasium/navigation/icon.rb +28 -0
  63. data/app/components/databasium/navigation/icon_panel.rb +26 -0
  64. data/app/components/databasium/navigation/post_icon.rb +25 -0
  65. data/app/components/databasium/navigation/put_icon.rb +18 -0
  66. data/app/components/databasium/records/filter.rb +73 -0
  67. data/app/components/databasium/records/foreign_records.rb +84 -0
  68. data/app/components/databasium/records/header_actions.rb +110 -0
  69. data/app/components/databasium/records/show_turbo_stream.rb +75 -0
  70. data/app/components/databasium/records/sidebar.rb +23 -0
  71. data/app/components/databasium/records/table/record_panel.rb +60 -0
  72. data/app/components/databasium/records/table/row.rb +104 -0
  73. data/app/components/databasium/records/table.rb +125 -0
  74. data/app/components/databasium/records/table_turbo_frame.rb +37 -0
  75. data/app/components/databasium/records/utilities.rb +25 -0
  76. data/app/components/databasium/schemas/header_actions.rb +99 -0
  77. data/app/components/databasium/schemas/sidebar.rb +25 -0
  78. data/app/components/databasium/search_results/migrations.rb +37 -0
  79. data/app/components/databasium/search_results/models.rb +36 -0
  80. data/app/components/databasium/search_results/schema_models.rb +37 -0
  81. data/app/components/databasium/search_results/tables.rb +31 -0
  82. data/app/components/databasium/type_select.rb +35 -0
  83. data/app/controllers/databasium/application_controller.rb +68 -0
  84. data/app/controllers/databasium/homepage_controller.rb +5 -0
  85. data/app/controllers/databasium/migrations_controller.rb +186 -0
  86. data/app/controllers/databasium/models_controller.rb +105 -0
  87. data/app/controllers/databasium/records_controller.rb +156 -0
  88. data/app/controllers/databasium/schemas_controller.rb +52 -0
  89. data/app/helpers/databasium/application_helper.rb +4 -0
  90. data/app/helpers/databasium/heroicon_helper.rb +21 -0
  91. data/app/helpers/databasium/models_helper.rb +4 -0
  92. data/app/jobs/databasium/application_job.rb +4 -0
  93. data/app/mailers/databasium/application_mailer.rb +6 -0
  94. data/app/models/databasium/application_record.rb +5 -0
  95. data/app/models/model.json +0 -0
  96. data/app/services/databasium/migration.rb +176 -0
  97. data/app/services/databasium/model.rb +182 -0
  98. data/app/services/databasium/record.rb +65 -0
  99. data/app/services/databasium/schema.rb +146 -0
  100. data/app/views/base.rb +13 -0
  101. data/app/views/databasium/errors/non_development.rb +21 -0
  102. data/app/views/databasium/homepage/index.rb +29 -0
  103. data/app/views/databasium/migrations/index.rb +33 -0
  104. data/app/views/databasium/migrations/new.rb +29 -0
  105. data/app/views/databasium/models/index.rb +31 -0
  106. data/app/views/databasium/models/new.rb +37 -0
  107. data/app/views/databasium/records/index.rb +24 -0
  108. data/app/views/databasium/schemas/index.rb +39 -0
  109. data/app/views/layouts/databasium/application.rb +56 -0
  110. data/config/importmap.rb +12 -0
  111. data/config/initializers/heroicon.rb +12 -0
  112. data/config/initializers/pagy.rb +48 -0
  113. data/config/initializers/phlex.rb +19 -0
  114. data/config/routes.rb +31 -0
  115. data/config/tailwind.config.js +10 -0
  116. data/lib/databasium/engine.rb +57 -0
  117. data/lib/databasium/engine_mount.rb +37 -0
  118. data/lib/databasium/middleware/conditional_check_pending.rb +27 -0
  119. data/lib/databasium/templates/create_table_migration.rb.tt +29 -0
  120. data/lib/databasium/templates/migration.rb.tt +48 -0
  121. data/lib/databasium/templates/model.rb.tt +23 -0
  122. data/lib/databasium/version.rb +3 -0
  123. data/lib/databasium.rb +11 -0
  124. data/lib/tasks/databasium_tasks.rake +4 -0
  125. metadata +272 -0
@@ -0,0 +1,156 @@
1
+ class Databasium::RecordsController < Databasium::ApplicationController
2
+ before_action :create_schema_service
3
+ before_action :set_model_and_record_service
4
+ include Pagy::Method
5
+ include ActionView::RecordIdentifier
6
+
7
+ def index
8
+ render Views::Databasium::Records::Index.new
9
+ end
10
+
11
+ def create
12
+ new_record = @record_service.create_new(attributes: model_columns_params)
13
+ raise ActiveRecord::RecordInvalid, new_record.errors.full_messages.join(", ") unless new_record
14
+
15
+ render turbo_stream: [
16
+ turbo_stream.append(
17
+ "records_body",
18
+ Components::Databasium::Records::Table::Row.new(
19
+ record: new_record,
20
+ turbo_frame: "records_list"
21
+ )
22
+ ),
23
+ turbo_stream.remove("suggestion")
24
+ ]
25
+ end
26
+
27
+ def records
28
+ @context = records_context
29
+ pagy, records =
30
+ pagy(
31
+ @record_service.filter_records(filter_params),
32
+ limit: params[:limit].presence || 10,
33
+ root_key: "records"
34
+ )
35
+
36
+ if foreign_records_frame?(@context[:turbo_frame])
37
+ render_foreign_records_table
38
+ else
39
+ respond_to do |format|
40
+ format.html { render_records_table(records: records, pagy: pagy) }
41
+ format.turbo_stream { render_records_table_turbo_stream(records: records, pagy: pagy) }
42
+ end
43
+ end
44
+ end
45
+
46
+ def update
47
+ record = @record_service.update_by_id(params[:id], attributes: model_columns_params)
48
+ raise ActiveRecord::RecordInvalid, record.errors.full_messages.join(", ") unless record
49
+ render turbo_stream:
50
+ turbo_stream.replace(
51
+ dom_id(record),
52
+ Components::Databasium::Records::Table::Row.new(
53
+ record: record,
54
+ turbo_frame: "records_list"
55
+ )
56
+ )
57
+ end
58
+
59
+ def bulk_destroy
60
+ deleted_records = @record_service.bulk_destroy(params[:ids])
61
+ unless deleted_records
62
+ raise ActiveRecord::RecordInvalid, deleted_records.errors.full_messages.join(", ")
63
+ end
64
+
65
+ doms_ids = deleted_records.map { |record| dom_id(record) }
66
+
67
+ render turbo_stream:
68
+ doms_ids.flat_map { |dom_id|
69
+ [
70
+ turbo_stream.remove(dom_id),
71
+ turbo_stream.remove("record-tab-#{dom_id}"),
72
+ turbo_stream.remove("record-form-#{dom_id}")
73
+ ]
74
+ }
75
+ end
76
+
77
+ def sidebar
78
+ @pagy_tables, @tables =
79
+ pagy(@schema_service.get_tables(params[:search]), limit: 7, root_key: "tables")
80
+ render Components::Databasium::SearchResults::Tables.new(tables: @tables, pagy: @pagy_tables)
81
+ end
82
+
83
+ private
84
+
85
+ def render_foreign_records_table
86
+ render Components::Databasium::Records::ForeignRecords.new(
87
+ model: @model,
88
+ columns_names_types: @context[:columns_names_types],
89
+ frame_id: @context[:turbo_frame]
90
+ )
91
+ end
92
+
93
+ def render_records_table_turbo_stream(records:, pagy:)
94
+ render Components::Databasium::Records::ShowTurboStream.new(
95
+ **@context,
96
+ records: records,
97
+ pagy: pagy
98
+ ),
99
+ layout: false
100
+ end
101
+
102
+ def render_records_table(records:, pagy:)
103
+ render Components::Databasium::Records::Table.new(
104
+ records: records,
105
+ model: @context[:model],
106
+ turbo_frame: @context[:turbo_frame],
107
+ pagy: pagy,
108
+ feedback: @context[:feedback]
109
+ )
110
+ end
111
+
112
+ def records_context
113
+ {
114
+ refresh: params[:refresh].presence || false,
115
+ filter: filter_params,
116
+ table: params[:table],
117
+ model: @model,
118
+ turbo_frame: params[:frame_id].presence || "records_list",
119
+ feedback: @feedback,
120
+ columns_names_types: @schema_service.get_columns(@model),
121
+ limit: params[:limit].presence || 10
122
+ }
123
+ end
124
+
125
+ def set_model_and_record_service
126
+ @model, @feedback = @schema_service.get_model_from_table(params[:table])
127
+ @record_service = Databasium::Record.new(model: @model)
128
+ end
129
+
130
+ def create_schema_service
131
+ @schema_service = Databasium::Schema.new
132
+ end
133
+
134
+ def foreign_records_frame?(frame_id)
135
+ frame_id.start_with?("foreign_records_") && !foreign_records_table_frame?(frame_id)
136
+ end
137
+
138
+ def foreign_records_table_frame?(frame_id)
139
+ frame_id.start_with?("foreign_records_table_")
140
+ end
141
+
142
+ def filter_params
143
+ return nil if @model.nil?
144
+ allowed_columns = @model&.columns.map { |c| c.name.to_s }
145
+
146
+ params.fetch(:filter, {}).permit(
147
+ allowed_columns.index_with { |_col| %i[operator value] },
148
+ operator_types: []
149
+ )
150
+ end
151
+
152
+ def model_columns_params
153
+ return if params[:table].nil?
154
+ params.require(:record).permit(*@schema_service.get_columns_names(params[:table]))
155
+ end
156
+ end
@@ -0,0 +1,52 @@
1
+ class Databasium::SchemasController < Databasium::ApplicationController
2
+ include Pagy::Method
3
+ before_action :create_schema_service, except: [ :sidebar ]
4
+
5
+ def index
6
+ layers = params[:layers].nil? ? nil : params[:layers].presence.try(:to_i) || 1
7
+ model = params[:model]
8
+ if params[:model].present?
9
+ schema = @schema_service.get_model_and_layers_BFS(model, layers)
10
+ else
11
+ schema = @schema_service.schema
12
+ end
13
+
14
+ models, pagy = get_models
15
+
16
+ respond_to do |format|
17
+ format.html do
18
+ render Views::Databasium::Schemas::Index.new(
19
+ schema: schema,
20
+ models: models,
21
+ pagy: pagy,
22
+ model: model,
23
+ layers: layers
24
+ )
25
+ end
26
+ format.json { render json: @schema }
27
+ end
28
+ end
29
+
30
+ def sidebar
31
+ models, pagy = get_models
32
+
33
+ render Components::Databasium::SearchResults::SchemaModels.new(models: models, pagy: pagy)
34
+ end
35
+
36
+ def sync_schema
37
+ @schema_service.sync!
38
+ redirect_back fallback_location: schemas_path
39
+ end
40
+
41
+ private
42
+
43
+ def create_schema_service
44
+ @schema_service = Databasium::Schema.new
45
+ end
46
+
47
+ def get_models
48
+ raw_models = Databasium::Model.new.get_all_models_from_db(search: params[:search])
49
+ pagy, models = pagy(raw_models, limit: 7, root_key: "models")
50
+ [ models, pagy ]
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ module Databasium
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Databasium
4
+ module HeroiconHelper
5
+ def heroicon(name, variant: Heroicon.configuration.variant, options: {}, path_options: {})
6
+ svg =
7
+ Heroicon::Icon.render(
8
+ name: name,
9
+ variant: variant,
10
+ options: options,
11
+ path_options: path_options
12
+ ).to_s
13
+
14
+ if respond_to?(:safe)
15
+ raw safe(svg)
16
+ else
17
+ svg.html_safe
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ module Databasium
2
+ module ModelsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Databasium
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Databasium
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Databasium
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
File without changes
@@ -0,0 +1,176 @@
1
+ class Databasium::Migration
2
+ attr_reader :migration_context, :migrations, :pending_migrations
3
+ MIGRATIONS_PATHS = [ "db/migrate" ]
4
+ MIGRATIONS_TEMPLATE_PATH =
5
+ Databasium::Engine.root.join("lib/databasium/templates/migration.rb.tt")
6
+ CREATE_TABLE_MIGRATIONS_TEMPLATE_PATH =
7
+ Databasium::Engine.root.join("lib/databasium/templates/create_table_migration.rb.tt")
8
+
9
+ def initialize
10
+ @migration_context = ActiveRecord::MigrationContext.new(MIGRATIONS_PATHS)
11
+ @migrations = @migration_context.migrations
12
+ @pending_migrations = @migration_context.pending_migration_versions
13
+ end
14
+
15
+ def get_migrations(search)
16
+ return @migrations unless search.present?
17
+ @migrations.select { |m| m.name =~ /#{Regexp.escape(search)}/i } if search
18
+ end
19
+
20
+ def find_migration!(version)
21
+ migration = migration_context.migrations.find { |m| m.version.to_s == version.to_s }
22
+ unless migration && File.file?(migration.filename)
23
+ raise ActiveRecord::RecordNotFound, "Migration #{version} not found"
24
+ end
25
+ migration
26
+ end
27
+
28
+ def run_pending_migrations
29
+ versions = migration_context.pending_migration_versions
30
+ begin
31
+ versions.each { |version| migration_context.run(:up, version) }
32
+ rescue => e
33
+ raise "There was an error running the pending migrations: #{e.message}"
34
+ end
35
+ versions
36
+ end
37
+
38
+ def rollback_migration(version, rollback_steps, till_this_migration)
39
+ begin
40
+ if rollback_steps.present?
41
+ migration_context.rollback(rollback_steps.to_i)
42
+ elsif till_this_migration == "true"
43
+ migration_context.down(version.to_i)
44
+ else
45
+ migration_context.run(:down, version.to_i)
46
+ end
47
+ rescue => e
48
+ raise "There was an error rolling back the #{version} migration: #{e.message}"
49
+ end
50
+ end
51
+
52
+ def run_migration(version)
53
+ begin
54
+ migration_context.run(:up, version.to_i)
55
+ rescue => e
56
+ raise "There was an error running the #{version} migration: #{e.message}"
57
+ end
58
+ end
59
+
60
+ def save_migration(params)
61
+ require "rails/generators/active_record/migration/migration_generator"
62
+ require "rails/generators"
63
+ Rails.application.load_generators
64
+ args = build_generator_args(params)
65
+ if params[:add_migration] == "Save" && params[:add_model] == "1"
66
+ generator = "model"
67
+ else
68
+ generator = "migration"
69
+ end
70
+ Rails::Generators.invoke(generator, args, behavior: :invoke, destination_root: Rails.root.to_s)
71
+ true
72
+ end
73
+
74
+ def generate_migration(params)
75
+ unless params[:table_name_from].present? || params[:table_name_to].present? ||
76
+ params[:table_name].present?
77
+ raise "Please provide a table name to generate a migration"
78
+ end
79
+
80
+ require "rails/generators/active_record/migration/migration_generator"
81
+ require "rails/generators"
82
+ Rails.application.load_generators
83
+ args = build_generator_args(params)
84
+ gen =
85
+ ActiveRecord::Generators::MigrationGenerator.new(
86
+ args,
87
+ {},
88
+ behavior: :invoke,
89
+ destination_root: Rails.root.to_s
90
+ )
91
+
92
+ gen.send(:set_local_assigns!)
93
+ gen.set_migration_assigns!(gen.file_name)
94
+
95
+ if params[:migration_action] == "create"
96
+ source = CREATE_TABLE_MIGRATIONS_TEMPLATE_PATH
97
+ else
98
+ source = MIGRATIONS_TEMPLATE_PATH
99
+ end
100
+ ERB.new(File.read(source), trim_mode: "-", eoutvar: "@output_buffer").result(
101
+ gen.instance_eval("binding")
102
+ )
103
+ end
104
+
105
+ private
106
+
107
+ def build_generator_args(params)
108
+ table_name_with_action = set_generator_base(params)
109
+
110
+ table_name_with_action +=
111
+ if params[:migration_action] != "create"
112
+ set_all_affected_columns(params)
113
+ else
114
+ params[:table_name]&.capitalize&.pluralize
115
+ end
116
+
117
+ table_name_with_action +=
118
+ if params[:migration_action] == "add"
119
+ "To#{params[:table_name_to]&.capitalize&.pluralize}"
120
+ elsif params[:migration_action] == "remove"
121
+ "From#{params[:table_name_from]&.capitalize&.pluralize}"
122
+ else
123
+ ""
124
+ end
125
+
126
+ generator_args = [ table_name_with_action ]
127
+
128
+ generator_args += set_contrains_on_columns(params)
129
+ end
130
+
131
+ def set_generator_base(params)
132
+ if params[:add_migration] != "Save" || params[:add_model] != "1"
133
+ params[:migration_action]&.capitalize
134
+ else
135
+ ""
136
+ end
137
+ end
138
+
139
+ def set_all_affected_columns(params)
140
+ return "" unless params[:columns].present?
141
+ if params[:columns].size > 4
142
+ "Columns"
143
+ else
144
+ params[:columns]
145
+ .filter { |c| c[:column_name].present? && c[:column_type].present? }
146
+ .map { |c| c[:column_name].capitalize }
147
+ .join("And")
148
+ end
149
+ end
150
+
151
+ def set_contrains_on_columns(params)
152
+ return nil unless params[:columns].present?
153
+ not_null_validation = build_not_null_validation(params)
154
+ uniqueness_validation = build_uniqueness_validation(params)
155
+
156
+ params[:columns]
157
+ .filter { |c| c[:column_name].present? && c[:column_type].present? }
158
+ .map do |c|
159
+ "#{c[:column_name]}:#{c[:column_type]}" +
160
+ (not_null_validation.include?(c[:column_name]) ? "!" : "") +
161
+ (uniqueness_validation.include?(c[:column_name]) ? ":uniq" : "")
162
+ end
163
+ end
164
+
165
+ def build_not_null_validation(params)
166
+ params[:validation]
167
+ .filter { |c| c[:column_name].present? && c[:type] == "not_null" }
168
+ .map { |c| c[:column_name] }
169
+ end
170
+
171
+ def build_uniqueness_validation(params)
172
+ params[:validation]
173
+ .filter { |c| c[:column_name].present? && c[:type] == "uniqueness" }
174
+ .map { |c| c[:column_name] }
175
+ end
176
+ end
@@ -0,0 +1,182 @@
1
+ class Databasium::Model
2
+ attr_reader :model_name, :attributes, :relations
3
+ PATHS = [ "models" ].freeze
4
+ RELATIONS = %w[belongs_to has_many has_one has_and_belongs_to_many].freeze
5
+ RELATIONS_REGEX = /\A(#{Regexp.union(RELATIONS).source})/
6
+
7
+ def initialize
8
+ end
9
+
10
+ # might be worth switching to reading from the dir directly in future
11
+ # def get_all_models_from_dir(search: nil)
12
+ # model_files = []
13
+ # PATHS.each { |path| model_files += Dir.glob(Rails.root.join("app", path, "**/*.rb")) }
14
+ # model_names =
15
+ # model_files
16
+ # .map { |file| File.basename(file).sub(/\.rb$/, "").classify }
17
+ # .reject do |name|
18
+ # %w[ApplicationRecord Concerns].include?(name) || !name.safe_constantize&.table_exists?
19
+ # end
20
+ # model_names = model_names.select { |name| name =~ /#{search}/i } if search
21
+ # model_names
22
+ # end
23
+
24
+ def get_all_models_from_db(search: nil)
25
+ conn = ActiveRecord::Base.connection
26
+ tables = conn.tables - %w[ar_internal_metadata schema_migrations]
27
+ tables = tables.map { |t| t.classify }
28
+ tables = tables.select { |t| t =~ /#{search}/i } if search
29
+ tables
30
+ end
31
+
32
+ def read_model_file(model_name)
33
+ File.read(model_file_path(model_name))
34
+ end
35
+
36
+ def get_model_data_from_file(model_name)
37
+ raw_model = constantize_model(model_name)
38
+ model = { validations: [], columns: [], unknown: [], relations: [], columns_hash: {} }
39
+ raw_model.columns.each do |column|
40
+ model[:columns_hash][column.name] = { type: column.type, validations: [] }
41
+ end
42
+
43
+ index = 0
44
+ inside_class = false
45
+ File.foreach(model_file_path(model_name)) do |line|
46
+ raw_line = line
47
+ line = line.strip.lstrip
48
+
49
+ parsed_line = {}
50
+
51
+ if line.start_with?("#")
52
+ parsed_line = parse_column(line)
53
+ elsif line.start_with?("validates :")
54
+ parsed_line = parse_validation(line)
55
+ scan_name = parsed_line[:content][:name]
56
+ model_column = model[:columns_hash].fetch(scan_name, nil)
57
+ if model_column.present?
58
+ model_column[:validations] << {
59
+ type: parsed_line[:content][:type],
60
+ value: parsed_line[:content][:value]
61
+ }
62
+ end
63
+ elsif line.match?(RELATIONS_REGEX)
64
+ parsed_line = parse_relation(line)
65
+ else
66
+ if !raw_line.include?("class")
67
+ parsed_line = { type: :unknown, content: {} }
68
+ else
69
+ inside_class = true
70
+ end
71
+ end
72
+
73
+ if parsed_line.present? && !(raw_line.blank? && !inside_class)
74
+ parsed_line[:content].merge!(
75
+ { index: index, line: parsed_line[:type] == :unknown ? raw_line : line }
76
+ )
77
+ model[parsed_line[:type]] << parsed_line[:content]
78
+ end
79
+ index += 1
80
+ end
81
+ model
82
+ end
83
+
84
+ def create_model_data(model_name:, attributes:, relations:, unknown:)
85
+ ModelData.new(
86
+ model_name: model_name,
87
+ attributes: attributes,
88
+ relations: relations,
89
+ unknown: unknown
90
+ )
91
+ end
92
+
93
+ private
94
+
95
+ class ModelData
96
+ attr_reader :model_name, :attributes, :relations, :unknown
97
+
98
+ def initialize(model_name:, attributes:, relations:, unknown: [])
99
+ @model_name = model_name
100
+ @attributes = attributes
101
+ @relations = relations
102
+ @unknown = unknown
103
+ end
104
+
105
+ def get_binding
106
+ binding
107
+ end
108
+
109
+ def longest_name_length
110
+ attributes.map { |a| (a[:name] || a["name"]).to_s.length }.max || 0
111
+ end
112
+
113
+ def relation_name(relation)
114
+ table_name = relation[:table_name].to_s
115
+
116
+ case relation[:type]
117
+ when "has_many", "has_and_belongs_to_many"
118
+ table_name.tableize
119
+ when "belongs_to", "has_one"
120
+ table_name.singularize.underscore
121
+ end
122
+ end
123
+
124
+ class Validation
125
+ attr_reader :name, :value
126
+ def initialize(name:, value:)
127
+ @name = name
128
+ @value = value
129
+ end
130
+ end
131
+
132
+ class Attribute
133
+ attr_reader :name, :type, :validations, :relations
134
+
135
+ def initialize(name:, type:, validations:)
136
+ @name = name
137
+ @type = type
138
+ @validations = validations
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def model_file_path(model_name)
146
+ Rails.root.join("app/models/#{model_name.to_s.underscore.singularize}.rb")
147
+ end
148
+
149
+ def constantize_model(model_name)
150
+ model_name.to_s.safe_constantize || model_name.to_s.classify.safe_constantize
151
+ end
152
+
153
+ def parse_column(line)
154
+ scan =
155
+ line
156
+ .scan(/# (\w+)\s*:\s*(\w+)(.*)/)
157
+ .map { |match| { name: match[0], type: match[1], unknown: match[2] } }
158
+ parsed_line = { type: :columns, content: scan.first } if scan.any?
159
+ parsed_line = { type: :unknown, content: {} } if scan.empty?
160
+ parsed_line
161
+ end
162
+
163
+ def parse_validation(line)
164
+ scan =
165
+ line
166
+ .scan(/validates :(\w+), (\w+): (.*)/)
167
+ .map { |match| { name: match[0], type: match[1], value: match[2] } }
168
+ parsed_line = { type: :validations, content: scan.first } if scan.any?
169
+ parsed_line = { type: :unknown, content: {} } if scan.empty?
170
+ parsed_line
171
+ end
172
+
173
+ def parse_relation(line)
174
+ scan =
175
+ line
176
+ .scan(/(\w+) :(\w+)(.*)/)
177
+ .map { |match| { name: match[0], type: match[1], unknown: match[2] } }
178
+ parsed_line = { type: :unknown, content: {} } if scan.empty?
179
+ parsed_line = { type: :relations, content: scan.first } if scan.any?
180
+ parsed_line
181
+ end
182
+ end
@@ -0,0 +1,65 @@
1
+ class Databasium::Record
2
+ def initialize(model: nil)
3
+ @model = model
4
+ end
5
+
6
+ def update(record, params)
7
+ return false unless record
8
+ record.update(params)
9
+ end
10
+
11
+ def create_new(attributes:)
12
+ return false unless @model
13
+ @model.create(attributes)
14
+ end
15
+
16
+ def update_by_id(id, attributes:)
17
+ return nil unless @model || id.blank?
18
+ record = @model.find(id)
19
+ return nil unless record
20
+
21
+ record.update(attributes)
22
+ record
23
+ end
24
+
25
+ def bulk_destroy(ids)
26
+ return nil if @model.blank? || ids.blank?
27
+ @model.where(id: ids).destroy_all
28
+ end
29
+
30
+ def filter_records(filter)
31
+ records = @model&.all
32
+ return records if filter.nil? || @model.nil?
33
+ connectors = Array(filter[:operator_types]).map(&:to_s)
34
+ allowed_operators = %w[eq not_eq gt lt gteq lteq matches does_not_match]
35
+ combined_predicate = nil
36
+ predicate_index = 0
37
+
38
+ filter
39
+ .except(:operator_types)
40
+ .each do |name, value|
41
+ next if value[:operator].blank? || value[:value].blank?
42
+
43
+ operator = value[:operator].to_s
44
+ next unless allowed_operators.include?(operator)
45
+
46
+ column = @model.arel_table[name]
47
+ predicate_value = value[:value].to_s
48
+ predicate_value = "%#{predicate_value}%" if %w[matches does_not_match].include?(operator)
49
+ current_predicate = column.public_send(operator, predicate_value)
50
+
51
+ if combined_predicate.nil?
52
+ combined_predicate = current_predicate
53
+ else
54
+ connector = connectors[predicate_index - 1] == "or" ? :or : :and
55
+ combined_predicate = combined_predicate.public_send(connector, current_predicate)
56
+ end
57
+
58
+ predicate_index += 1
59
+ end
60
+
61
+ return records if combined_predicate.nil?
62
+
63
+ records.where(combined_predicate)
64
+ end
65
+ end