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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/MIT-LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +6 -0
- data/app/assets/builds/application.js +9045 -0
- data/app/assets/builds/application.js.map +7 -0
- data/app/assets/builds/databasium.css +2 -0
- data/app/assets/config/databasium_manifest.js +1 -0
- data/app/assets/javascript/databasium/application.js +2 -0
- data/app/assets/javascript/databasium/controllers/attribute_controller.js +27 -0
- data/app/assets/javascript/databasium/controllers/collapse_controller.js +18 -0
- data/app/assets/javascript/databasium/controllers/error_controller.js +15 -0
- data/app/assets/javascript/databasium/controllers/filter_controller.js +224 -0
- data/app/assets/javascript/databasium/controllers/flash_controller.js +18 -0
- data/app/assets/javascript/databasium/controllers/graph_controller.js +193 -0
- data/app/assets/javascript/databasium/controllers/index.js +7 -0
- data/app/assets/javascript/databasium/controllers/layout_controller.js +13 -0
- data/app/assets/javascript/databasium/controllers/model_controller.js +32 -0
- data/app/assets/javascript/databasium/controllers/new_migration_controller.js +107 -0
- data/app/assets/javascript/databasium/controllers/relation_controller.js +10 -0
- data/app/assets/javascript/databasium/controllers/search_controller.js +23 -0
- data/app/assets/javascript/databasium/controllers/table_controller.js +283 -0
- data/app/assets/javascript/databasium/controllers/table_select_controller.js +19 -0
- data/app/assets/javascript/databasium/controllers/toggle_controller.js +28 -0
- data/app/assets/javascript/databasium/controllers/validation_controller.js +78 -0
- data/app/assets/javascript/databasium/shapes/erd_table_shape.js +54 -0
- data/app/assets/stylesheets/databasium/application.css +15 -0
- data/app/assets/stylesheets/databasium/colors.css +55 -0
- data/app/assets/stylesheets/databasium/custom.css +36 -0
- data/app/assets/stylesheets/databasium/databasium_engine.css +6 -0
- data/app/assets/stylesheets/databasium/pagy-tailwind.css +66 -0
- data/app/components/base.rb +50 -0
- data/app/components/databasium/collapsable.rb +62 -0
- data/app/components/databasium/forms/model.rb +147 -0
- data/app/components/databasium/forms/search.rb +31 -0
- data/app/components/databasium/global/error.rb +60 -0
- data/app/components/databasium/global/flash.rb +73 -0
- data/app/components/databasium/global/header_actions.rb +36 -0
- data/app/components/databasium/global/sidebar.rb +45 -0
- data/app/components/databasium/global/suggestion.rb +25 -0
- data/app/components/databasium/migrations/action.rb +39 -0
- data/app/components/databasium/migrations/file.rb +58 -0
- data/app/components/databasium/migrations/form.rb +222 -0
- data/app/components/databasium/migrations/header_actions.rb +87 -0
- data/app/components/databasium/migrations/migration_status.rb +22 -0
- data/app/components/databasium/migrations/preview.rb +29 -0
- data/app/components/databasium/migrations/show_turbo_stream.rb +19 -0
- data/app/components/databasium/migrations/sidebar.rb +28 -0
- data/app/components/databasium/models/attributes.rb +49 -0
- data/app/components/databasium/models/form.rb +100 -0
- data/app/components/databasium/models/header_actions.rb +51 -0
- data/app/components/databasium/models/model_preview.rb +31 -0
- data/app/components/databasium/models/sidebar.rb +25 -0
- data/app/components/databasium/models/templates/attribute.rb +99 -0
- data/app/components/databasium/models/templates/base.rb +6 -0
- data/app/components/databasium/models/templates/relation.rb +56 -0
- data/app/components/databasium/models/templates/validation.rb +285 -0
- data/app/components/databasium/navigation/base_icon.rb +32 -0
- data/app/components/databasium/navigation/frontend_icon.rb +17 -0
- data/app/components/databasium/navigation/get_icon.rb +26 -0
- data/app/components/databasium/navigation/icon.rb +28 -0
- data/app/components/databasium/navigation/icon_panel.rb +26 -0
- data/app/components/databasium/navigation/post_icon.rb +25 -0
- data/app/components/databasium/navigation/put_icon.rb +18 -0
- data/app/components/databasium/records/filter.rb +73 -0
- data/app/components/databasium/records/foreign_records.rb +84 -0
- data/app/components/databasium/records/header_actions.rb +110 -0
- data/app/components/databasium/records/show_turbo_stream.rb +75 -0
- data/app/components/databasium/records/sidebar.rb +23 -0
- data/app/components/databasium/records/table/record_panel.rb +60 -0
- data/app/components/databasium/records/table/row.rb +104 -0
- data/app/components/databasium/records/table.rb +125 -0
- data/app/components/databasium/records/table_turbo_frame.rb +37 -0
- data/app/components/databasium/records/utilities.rb +25 -0
- data/app/components/databasium/schemas/header_actions.rb +99 -0
- data/app/components/databasium/schemas/sidebar.rb +25 -0
- data/app/components/databasium/search_results/migrations.rb +37 -0
- data/app/components/databasium/search_results/models.rb +36 -0
- data/app/components/databasium/search_results/schema_models.rb +37 -0
- data/app/components/databasium/search_results/tables.rb +31 -0
- data/app/components/databasium/type_select.rb +35 -0
- data/app/controllers/databasium/application_controller.rb +68 -0
- data/app/controllers/databasium/homepage_controller.rb +5 -0
- data/app/controllers/databasium/migrations_controller.rb +186 -0
- data/app/controllers/databasium/models_controller.rb +105 -0
- data/app/controllers/databasium/records_controller.rb +156 -0
- data/app/controllers/databasium/schemas_controller.rb +52 -0
- data/app/helpers/databasium/application_helper.rb +4 -0
- data/app/helpers/databasium/heroicon_helper.rb +21 -0
- data/app/helpers/databasium/models_helper.rb +4 -0
- data/app/jobs/databasium/application_job.rb +4 -0
- data/app/mailers/databasium/application_mailer.rb +6 -0
- data/app/models/databasium/application_record.rb +5 -0
- data/app/models/model.json +0 -0
- data/app/services/databasium/migration.rb +176 -0
- data/app/services/databasium/model.rb +182 -0
- data/app/services/databasium/record.rb +65 -0
- data/app/services/databasium/schema.rb +146 -0
- data/app/views/base.rb +13 -0
- data/app/views/databasium/errors/non_development.rb +21 -0
- data/app/views/databasium/homepage/index.rb +29 -0
- data/app/views/databasium/migrations/index.rb +33 -0
- data/app/views/databasium/migrations/new.rb +29 -0
- data/app/views/databasium/models/index.rb +31 -0
- data/app/views/databasium/models/new.rb +37 -0
- data/app/views/databasium/records/index.rb +24 -0
- data/app/views/databasium/schemas/index.rb +39 -0
- data/app/views/layouts/databasium/application.rb +56 -0
- data/config/importmap.rb +12 -0
- data/config/initializers/heroicon.rb +12 -0
- data/config/initializers/pagy.rb +48 -0
- data/config/initializers/phlex.rb +19 -0
- data/config/routes.rb +31 -0
- data/config/tailwind.config.js +10 -0
- data/lib/databasium/engine.rb +57 -0
- data/lib/databasium/engine_mount.rb +37 -0
- data/lib/databasium/middleware/conditional_check_pending.rb +27 -0
- data/lib/databasium/templates/create_table_migration.rb.tt +29 -0
- data/lib/databasium/templates/migration.rb.tt +48 -0
- data/lib/databasium/templates/model.rb.tt +23 -0
- data/lib/databasium/version.rb +3 -0
- data/lib/databasium.rb +11 -0
- data/lib/tasks/databasium_tasks.rake +4 -0
- 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,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
|
|
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
|