model_explorer 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/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.simplecov +17 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +139 -0
- data/Rakefile +12 -0
- data/app/assets/config/model_explorer/manifest.js +2 -0
- data/app/assets/javascripts/model_explorer/application.js +5 -0
- data/app/assets/javascripts/model_explorer/association_manager.js +192 -0
- data/app/assets/javascripts/model_explorer/association_select.js +23 -0
- data/app/assets/javascripts/model_explorer/copy_button.js +25 -0
- data/app/assets/javascripts/model_explorer/model_form.js +61 -0
- data/app/assets/javascripts/model_explorer/models_controller.js +43 -0
- data/app/assets/stylesheets/model_explorer/application.css +7 -0
- data/app/assets/stylesheets/model_explorer/models.css +45 -0
- data/app/controllers/model_explorer/application_controller.rb +41 -0
- data/app/controllers/model_explorer/exports_controller.rb +53 -0
- data/app/controllers/model_explorer/models_controller.rb +23 -0
- data/app/serializers/application_serializer.rb +11 -0
- data/app/serializers/association_serializer.rb +21 -0
- data/app/serializers/model_serializer.rb +39 -0
- data/app/views/layouts/model_explorer/application.html.erb +28 -0
- data/app/views/model_explorer/models/index.html.erb +14 -0
- data/app/views/model_explorer/models/partials/_form.html.erb +49 -0
- data/app/views/model_explorer/models/partials/_record_details.html.erb +43 -0
- data/app/views/model_explorer/models/partials/_select_template.html.erb +53 -0
- data/config/locales/en.yml +3 -0
- data/config/locales/fr.yml +3 -0
- data/config/locales/views/layouts/model_explorer/application.en.yml +5 -0
- data/config/locales/views/layouts/model_explorer/application.fr.yml +5 -0
- data/config/locales/views/model_explorer/models/partials/form.en.yml +10 -0
- data/config/locales/views/model_explorer/models/partials/form.fr.yml +10 -0
- data/config/locales/views/model_explorer/models/partials/record_details.en.yml +9 -0
- data/config/locales/views/model_explorer/models/partials/record_details.fr.yml +9 -0
- data/config/locales/views/model_explorer/models/partials/select_template.en.yml +8 -0
- data/config/locales/views/model_explorer/models/partials/select_template.fr.yml +8 -0
- data/config/routes.rb +8 -0
- data/docs/example.png +0 -0
- data/lib/generators/model_explorer/USAGE +7 -0
- data/lib/generators/model_explorer/install_generator.rb +24 -0
- data/lib/generators/model_explorer/templates/model_explorer.rb.tt +24 -0
- data/lib/model_explorer/associations/base.rb +74 -0
- data/lib/model_explorer/associations/many.rb +43 -0
- data/lib/model_explorer/associations/singular.rb +19 -0
- data/lib/model_explorer/associations.rb +33 -0
- data/lib/model_explorer/engine.rb +39 -0
- data/lib/model_explorer/export.rb +53 -0
- data/lib/model_explorer/import.rb +64 -0
- data/lib/model_explorer/record.rb +15 -0
- data/lib/model_explorer/scopes.rb +23 -0
- data/lib/model_explorer/select.rb +28 -0
- data/lib/model_explorer/version.rb +5 -0
- data/lib/model_explorer.rb +77 -0
- data/sig/model_explorer.rbs +4 -0
- data/vendor/assets/javascripts/bootstrap.min.js +7 -0
- data/vendor/assets/javascripts/prism.min.js +4 -0
- data/vendor/assets/javascripts/tom_select.min.js +440 -0
- data/vendor/assets/stylesheets/bootstrap.min.css +6 -0
- data/vendor/assets/stylesheets/prism.min.css +3 -0
- data/vendor/assets/stylesheets/tom_select.min.css +15 -0
- metadata +128 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
pre#json-pre {
|
2
|
+
border: 1px solid #d0d0d0;
|
3
|
+
border-radius: 3px;
|
4
|
+
margin-bottom: 0px;
|
5
|
+
margin-top: 0px;
|
6
|
+
padding: 10px;
|
7
|
+
height: 80vh;
|
8
|
+
}
|
9
|
+
|
10
|
+
.overflow-unset {
|
11
|
+
overflow: unset !important;
|
12
|
+
}
|
13
|
+
|
14
|
+
.was-validated .ts-wrapper.invalid .ts-control {
|
15
|
+
border: 1px solid #dc3545;
|
16
|
+
padding-right: calc(1.5em + .75rem)!important;
|
17
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
|
18
|
+
background-repeat: no-repeat;
|
19
|
+
background-position: right calc(.375em + .1875rem) center;
|
20
|
+
background-size: calc(.75em + .375rem) calc(.75em + .375rem)
|
21
|
+
}
|
22
|
+
|
23
|
+
.was-validated .form-control:valid {
|
24
|
+
border: 1px solid #dee2e6;
|
25
|
+
padding-right: calc(1.5em + .75rem)!important;
|
26
|
+
background-image: none;
|
27
|
+
background-repeat: no-repeat;
|
28
|
+
background-position: auto auto;
|
29
|
+
background-size: auto auto
|
30
|
+
}
|
31
|
+
|
32
|
+
.was-validated .form-control:valid:focus {
|
33
|
+
border-color: #86b7fe;
|
34
|
+
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25)
|
35
|
+
}
|
36
|
+
|
37
|
+
.ts-control input {
|
38
|
+
line-height: 1.5 !important;
|
39
|
+
padding: 0.01rem .75rem !important;
|
40
|
+
}
|
41
|
+
|
42
|
+
.ts-control {
|
43
|
+
border: var(--bs-border-width) solid var(--bs-border-color);
|
44
|
+
border-radius: var(--bs-border-radius);
|
45
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModelExplorer
|
4
|
+
class ApplicationController < ActionController::Base
|
5
|
+
layout "model_explorer/application"
|
6
|
+
|
7
|
+
before_action :verify_access
|
8
|
+
|
9
|
+
http_basic_authenticate_with(
|
10
|
+
name: ModelExplorer.basic_auth_username.to_s,
|
11
|
+
password: ModelExplorer.basic_auth_password.to_s,
|
12
|
+
if: -> { ModelExplorer.basic_auth_enabled }
|
13
|
+
)
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def render_not_found(error)
|
18
|
+
render json: {error: error.message}, status: :not_found
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_bad_request(error)
|
22
|
+
render json: {error: error.message}, status: :bad_request
|
23
|
+
end
|
24
|
+
|
25
|
+
def ensure_valid_model_name(model_name = nil)
|
26
|
+
return if ModelExplorer.models.map(&:name).include?(model_name)
|
27
|
+
|
28
|
+
raise "Model '#{model_name}' not found"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def verify_access
|
34
|
+
return if ModelExplorer.verify_access_proc.call(self)
|
35
|
+
|
36
|
+
flash[:error] = t("unauthorized", scope: "model_explorer")
|
37
|
+
|
38
|
+
redirect_to "/"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModelExplorer
|
4
|
+
class ExportsController < ApplicationController
|
5
|
+
# Warning: all parameters are permitted.
|
6
|
+
# Associations must not be called directly on the record.
|
7
|
+
def show
|
8
|
+
ensure_valid_model_name(params[:model])
|
9
|
+
|
10
|
+
render_export
|
11
|
+
rescue => error
|
12
|
+
render_bad_request(error)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def render_export
|
18
|
+
render json: {
|
19
|
+
export: build_export,
|
20
|
+
path: exports_path(params.except(:controller, :action))
|
21
|
+
}.to_json
|
22
|
+
rescue ActiveRecord::RecordNotFound => error
|
23
|
+
render_not_found(error)
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_export
|
27
|
+
export = ModelExplorer::Export.new(
|
28
|
+
record: build_record(params[:model].constantize),
|
29
|
+
associations: ModelExplorer::Associations.build_from_params(params.permit!.to_h)
|
30
|
+
)
|
31
|
+
|
32
|
+
export.data
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_record(model)
|
36
|
+
select = ModelExplorer::Select.new(model, params[:columns]).to_a
|
37
|
+
record = model.select(select).find(params[:record_id])
|
38
|
+
|
39
|
+
attributes =
|
40
|
+
if params[:columns].present?
|
41
|
+
record.attributes.slice(*params[:columns])
|
42
|
+
else
|
43
|
+
record.attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
ModelExplorer::Record.new(
|
47
|
+
record.attributes[model.primary_key],
|
48
|
+
attributes,
|
49
|
+
model
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModelExplorer
|
4
|
+
class ModelsController < ApplicationController
|
5
|
+
def index
|
6
|
+
@models = ModelExplorer.models.map(&:name).sort
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
model_name = params[:id]
|
11
|
+
|
12
|
+
ensure_valid_model_name(model_name)
|
13
|
+
|
14
|
+
render json: ModelSerializer.new(
|
15
|
+
model: model_name.constantize,
|
16
|
+
macro: params[:macro],
|
17
|
+
parent: params[:parent]
|
18
|
+
).to_json
|
19
|
+
rescue => error
|
20
|
+
render_bad_request(error)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AssociationSerializer < ApplicationSerializer
|
4
|
+
attr_reader :association
|
5
|
+
|
6
|
+
def initialize(association)
|
7
|
+
@association = association
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
{
|
12
|
+
name: association_name,
|
13
|
+
macro: association.macro,
|
14
|
+
model: association.class_name || association_name.classify
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
delegate :name, to: :association, prefix: true
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ModelSerializer < ApplicationSerializer
|
4
|
+
attr_reader :model, :macro, :parent
|
5
|
+
|
6
|
+
def initialize(model:, macro:, parent: nil)
|
7
|
+
@model = model
|
8
|
+
@macro = macro
|
9
|
+
@parent = parent
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
{
|
14
|
+
model: model.name,
|
15
|
+
columns: model.column_names,
|
16
|
+
scopes: build_scopes,
|
17
|
+
associations: build_associations
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def build_scopes
|
24
|
+
case macro
|
25
|
+
when "has_many"
|
26
|
+
model.model_explorer_scopes.map(&:to_s)
|
27
|
+
else
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_associations
|
33
|
+
model.reflect_on_all_associations.filter_map do |association|
|
34
|
+
next if association.options[:through] || association.name.to_s == parent
|
35
|
+
|
36
|
+
AssociationSerializer.new(association).to_h
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="<%= I18n.locale %>">
|
3
|
+
<head>
|
4
|
+
<title>
|
5
|
+
<%= t(".title") %>
|
6
|
+
</title>
|
7
|
+
|
8
|
+
<%= csrf_meta_tags %>
|
9
|
+
<%= csp_meta_tag %>
|
10
|
+
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
12
|
+
|
13
|
+
<%= stylesheet_link_tag("model_explorer/application", media: "all") %>
|
14
|
+
<%= javascript_include_tag("model_explorer/application") %>
|
15
|
+
</head>
|
16
|
+
|
17
|
+
<body>
|
18
|
+
<nav class="navbar navbar-expand-lg bg-primary navbar-dark">
|
19
|
+
<div class="container">
|
20
|
+
<a class="navbar-brand" href="<%= model_explorer.models_path %>">
|
21
|
+
<%= t(".title") %>
|
22
|
+
</a>
|
23
|
+
</div>
|
24
|
+
</nav>
|
25
|
+
|
26
|
+
<%= yield %>
|
27
|
+
</body>
|
28
|
+
</html>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="container">
|
2
|
+
<div class="row g-4">
|
3
|
+
<div class="col-12 col-md-6">
|
4
|
+
<%= render(
|
5
|
+
partial: "model_explorer/models/partials/form",
|
6
|
+
locals: {models: @models}
|
7
|
+
) %>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<div class="col-12 col-md-6">
|
11
|
+
<%= render(partial: "model_explorer/models/partials/record_details") %>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
<span class="h4 my-3 d-block">
|
2
|
+
<%= t(".select_model") %>
|
3
|
+
</span>
|
4
|
+
|
5
|
+
<%= form_with(
|
6
|
+
url: exports_path,
|
7
|
+
method: :get,
|
8
|
+
local: false,
|
9
|
+
html: {
|
10
|
+
id: "export-form",
|
11
|
+
class: "needs-validation",
|
12
|
+
novalidate: true
|
13
|
+
}
|
14
|
+
) do |f| %>
|
15
|
+
<div class="row">
|
16
|
+
<div class="col-9">
|
17
|
+
<%= f.select(
|
18
|
+
:model,
|
19
|
+
models,
|
20
|
+
{prompt: t(".select_prompt"), include_blank: true},
|
21
|
+
{id: "association-select", class: "p-0", required: true}
|
22
|
+
) %>
|
23
|
+
<div class="invalid-feedback">
|
24
|
+
<%= t(".required_field") %>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div class="col-3 pl-0">
|
29
|
+
<%= f.text_field(
|
30
|
+
:record_id,
|
31
|
+
placeholder: t(".record_id_placeholder"),
|
32
|
+
class: "form-control",
|
33
|
+
required: true
|
34
|
+
) %>
|
35
|
+
|
36
|
+
<div class="invalid-feedback">
|
37
|
+
<%= t(".required_field") %>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<div id="associations-container" class="my-3"></div>
|
43
|
+
|
44
|
+
<%= render(partial: "model_explorer/models/partials/select_template") %>
|
45
|
+
|
46
|
+
<div class="w-100 my-3">
|
47
|
+
<%= f.submit(t(".submit"), class: "btn btn-primary w-100") %>
|
48
|
+
</div>
|
49
|
+
<% end %>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<div id="record-details">
|
2
|
+
<span class="h4 my-3 d-block">
|
3
|
+
<%= t(".title") %>
|
4
|
+
</span>
|
5
|
+
|
6
|
+
<div class="vh-50 overflow-auto shadow-sm">
|
7
|
+
<div class="card" id="no-record">
|
8
|
+
<span class="d-block text-center w-100 m-4">
|
9
|
+
<%= t(".no_record") %>
|
10
|
+
</span>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<div id="json-data" class="position-relative d-none">
|
14
|
+
<div class="d-flex position-absolute top-0 end-0 m-2 gap-2">
|
15
|
+
<button
|
16
|
+
id="copy-record-details"
|
17
|
+
class="btn btn-sm btn-primary"
|
18
|
+
data-text="<%= t(".copy_text") %>"
|
19
|
+
>
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
21
|
+
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
|
22
|
+
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
|
23
|
+
</svg>
|
24
|
+
</button>
|
25
|
+
|
26
|
+
<a id="view-record-details" class="btn btn-sm btn-primary" target="_blank" rel="noopener noreferrer nofollow">
|
27
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
28
|
+
<path d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 14.25 15h-9a.75.75 0 0 1 0-1.5h9a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 10 4.25V1.5H5.75a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0Zm1.72 4.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.47-1.47-1.47-1.47a.75.75 0 0 1 0-1.06ZM3.28 7.78 1.81 9.25l1.47 1.47a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Zm8.22-6.218V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"></path>
|
29
|
+
</svg>
|
30
|
+
</a>
|
31
|
+
|
32
|
+
<a id="download-record-details" class="btn btn-sm btn-primary" download>
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
34
|
+
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path>
|
35
|
+
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"></path>
|
36
|
+
</svg>
|
37
|
+
</a>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<pre id="json-pre" class="language-json border-none"></pre>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</div>
|
@@ -0,0 +1,53 @@
|
|
1
|
+
<template
|
2
|
+
id="associations-select-template"
|
3
|
+
data-max-items="<%= ModelExplorer.max_items_per_association %>"
|
4
|
+
data-max-scopes="<%= ModelExplorer.max_scopes_per_association %>"
|
5
|
+
>
|
6
|
+
<div class="accordion mt-3" id="associations-accordion-TEMP_ID">
|
7
|
+
<div class="card overflow-unset">
|
8
|
+
<button
|
9
|
+
class="card-header btn btn-link btn-block text-decoration-none text-left text-dark"
|
10
|
+
type="button"
|
11
|
+
data-bs-toggle="collapse"
|
12
|
+
data-bs-target="#associations-collapse-TEMP_ID"
|
13
|
+
aria-expanded="false"
|
14
|
+
></button>
|
15
|
+
|
16
|
+
<div class="collapse show" id="associations-collapse-TEMP_ID">
|
17
|
+
<div class="card-body" id="associations-container-TEMP_ID">
|
18
|
+
<select
|
19
|
+
id="columns-select-TEMP_ID"
|
20
|
+
class="p-0"
|
21
|
+
data-placeholder="<%= t(".select_columns") %>"
|
22
|
+
multiple
|
23
|
+
></select>
|
24
|
+
|
25
|
+
<% if ModelExplorer.association_scopes_enabled? %>
|
26
|
+
<select
|
27
|
+
id="scopes-select-TEMP_ID"
|
28
|
+
class="mt-3 p-0"
|
29
|
+
data-placeholder="<%= t(".select_scopes") %>"
|
30
|
+
multiple
|
31
|
+
></select>
|
32
|
+
<% end %>
|
33
|
+
|
34
|
+
<% if ModelExplorer.association_select_enabled? %>
|
35
|
+
<select
|
36
|
+
id="associations-select-TEMP_ID"
|
37
|
+
name="association_attributes[scopes][]"
|
38
|
+
class="mt-3 p-0"
|
39
|
+
data-placeholder="<%= t(".select_associations") %>"
|
40
|
+
multiple
|
41
|
+
></select>
|
42
|
+
<% end %>
|
43
|
+
|
44
|
+
<input
|
45
|
+
id="associations-input-TEMP_ID"
|
46
|
+
name="association_attributes[name]"
|
47
|
+
type="hidden"
|
48
|
+
>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
</template>
|
data/config/routes.rb
ADDED
data/docs/example.png
ADDED
Binary file
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Description:
|
2
|
+
Generates an initializer to configure ModelExplorer and mount the engine in the routes file.
|
3
|
+
|
4
|
+
Example:
|
5
|
+
`bin/rails generate model_explorer:install --routes
|
6
|
+
create config/initializers/model_explorer.rb
|
7
|
+
route mount ModelExplorer::Engine, at: "/model_explorer"`
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/active_record"
|
4
|
+
|
5
|
+
module ModelExplorer
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
|
9
|
+
class_option :routes, desc: "Generate routes", type: :boolean, default: false
|
10
|
+
|
11
|
+
def copy_initializer
|
12
|
+
template "model_explorer.rb", "config/initializers/model_explorer.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_model_explorer_routes
|
16
|
+
return unless options.routes?
|
17
|
+
|
18
|
+
route <<~ROUTE
|
19
|
+
mount ModelExplorer::Engine, at: "/model_explorer"
|
20
|
+
|
21
|
+
ROUTE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ModelExplorer.configure do |config|
|
4
|
+
# Enable the basic auth. Disabled by default.
|
5
|
+
# config.basic_auth_enabled = true
|
6
|
+
# config.basic_auth_username = "admin"
|
7
|
+
# config.basic_auth_password = "password"
|
8
|
+
|
9
|
+
# Add a custom proc to verify the access.
|
10
|
+
# config.verify_access_proc = ->(controller) do
|
11
|
+
# controller.current_admin_user&.super_admin?
|
12
|
+
# end
|
13
|
+
|
14
|
+
# Customize the regular expression for filtering attributes.
|
15
|
+
# config.filter_attributes_regexp = /password|token|secret/i
|
16
|
+
|
17
|
+
# Set the maximum number of items to select per association.
|
18
|
+
# Disable by setting to 0.
|
19
|
+
# config.max_items_per_association = 5
|
20
|
+
|
21
|
+
# Set the maximum number of scopes to select per association.
|
22
|
+
# Disable by setting to 0.
|
23
|
+
# config.max_scopes_per_association = 5
|
24
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ModelExplorer
|
2
|
+
module Associations
|
3
|
+
class Base
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :reflection, :name, :macro, :klass
|
7
|
+
|
8
|
+
attr_reader :record, :reflection, :association
|
9
|
+
|
10
|
+
def initialize(record, reflection, association)
|
11
|
+
@record = record
|
12
|
+
@reflection = reflection
|
13
|
+
@association = association
|
14
|
+
end
|
15
|
+
|
16
|
+
def export
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def records
|
21
|
+
@_records ||=
|
22
|
+
klass
|
23
|
+
.connection
|
24
|
+
.exec_query(query.to_sql)
|
25
|
+
.map { |record_attributes| build_record(record_attributes) }
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def query
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_query
|
35
|
+
klass
|
36
|
+
.select(ModelExplorer::Select.new(klass, association[:columns]).to_a)
|
37
|
+
.where(reflection_query)
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_record(record_attributes)
|
41
|
+
attributes =
|
42
|
+
if association[:columns].present?
|
43
|
+
record_attributes.slice(*association[:columns])
|
44
|
+
else
|
45
|
+
record_attributes
|
46
|
+
end
|
47
|
+
|
48
|
+
ModelExplorer::Record.new(
|
49
|
+
record_attributes[klass.primary_key],
|
50
|
+
attributes,
|
51
|
+
klass
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reflection_query
|
56
|
+
foreign_key = reflection.foreign_key
|
57
|
+
|
58
|
+
case reflection.macro
|
59
|
+
when :has_many, :has_one then {foreign_key => record[klass.primary_key]}
|
60
|
+
when :belongs_to then {"#{reflection.table_name}.id" => record[klass.primary_key]}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def export_records
|
65
|
+
records.map do |relation_record|
|
66
|
+
ModelExplorer::Export.new(
|
67
|
+
record: relation_record,
|
68
|
+
associations: association[:associations]
|
69
|
+
).data
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|