nquery 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/LICENSE +21 -0
- data/README.md +52 -0
- data/Rakefile +9 -0
- data/app/assets/builds/nquery/application.css +339 -0
- data/app/assets/builds/nquery/application.js +989 -0
- data/app/assets/config/nquery_manifest.js +2 -0
- data/app/components/nquery/_card_component.html.erb +6 -0
- data/app/components/nquery/_card_table_component.html.erb +14 -0
- data/app/components/nquery/_flash_card_component.html.erb +32 -0
- data/app/components/nquery/_flash_cards_component.html.erb +3 -0
- data/app/components/nquery/_infinite_scroll_component.html.erb +11 -0
- data/app/components/nquery/card_component.rb +18 -0
- data/app/components/nquery/card_table_component.rb +19 -0
- data/app/components/nquery/component.rb +26 -0
- data/app/components/nquery/flash_card_component.rb +51 -0
- data/app/components/nquery/flash_cards_component.rb +20 -0
- data/app/components/nquery/infinite_scroll_component.rb +19 -0
- data/app/controllers/concerns/nquery/breadcrumbs.rb +238 -0
- data/app/controllers/concerns/nquery/browsable.rb +56 -0
- data/app/controllers/concerns/nquery/chart_actions.rb +104 -0
- data/app/controllers/concerns/nquery/chart_results.rb +30 -0
- data/app/controllers/nquery/admin/base_controller.rb +9 -0
- data/app/controllers/nquery/admin/data_sources_controller.rb +47 -0
- data/app/controllers/nquery/admin/groups_controller.rb +63 -0
- data/app/controllers/nquery/admin/logs_controller.rb +28 -0
- data/app/controllers/nquery/admin/permissions_controller.rb +53 -0
- data/app/controllers/nquery/admin/users_controller.rb +80 -0
- data/app/controllers/nquery/application_controller.rb +70 -0
- data/app/controllers/nquery/charts_controller.rb +51 -0
- data/app/controllers/nquery/collection/dashboards_controller.rb +38 -0
- data/app/controllers/nquery/collections_controller.rb +117 -0
- data/app/controllers/nquery/dashboard/charts_controller.rb +62 -0
- data/app/controllers/nquery/dashboards_controller.rb +90 -0
- data/app/controllers/nquery/embed/charts_controller.rb +24 -0
- data/app/controllers/nquery/embed/dashboards_controller.rb +17 -0
- data/app/controllers/nquery/home_controller.rb +16 -0
- data/app/controllers/nquery/imports_controller.rb +41 -0
- data/app/controllers/nquery/queries_controller.rb +105 -0
- data/app/controllers/nquery/registrations_controller.rb +31 -0
- data/app/controllers/nquery/sessions_controller.rb +28 -0
- data/app/helpers/nquery/charts_helper.rb +33 -0
- data/app/helpers/nquery/collections_helper.rb +13 -0
- data/app/helpers/nquery/components_helper.rb +29 -0
- data/app/helpers/nquery/home_helper.rb +23 -0
- data/app/helpers/nquery/icon_helper.rb +9 -0
- data/app/helpers/nquery/navigation_helper.rb +10 -0
- data/app/javascript/nquery/controllers/chart_preview_controller.js +125 -0
- data/app/javascript/nquery/controllers/csv_mapper_controller.js +23 -0
- data/app/javascript/nquery/controllers/dashboard_grid_controller.js +25 -0
- data/app/javascript/nquery/controllers/query_editor_controller.js +41 -0
- data/app/models/nquery/application_permission.rb +12 -0
- data/app/models/nquery/application_record.rb +7 -0
- data/app/models/nquery/audit.rb +38 -0
- data/app/models/nquery/chart.rb +37 -0
- data/app/models/nquery/collection.rb +35 -0
- data/app/models/nquery/collection_permission.rb +12 -0
- data/app/models/nquery/csv_upload.rb +11 -0
- data/app/models/nquery/dashboard.rb +27 -0
- data/app/models/nquery/dashboard_card.rb +8 -0
- data/app/models/nquery/data_permission.rb +13 -0
- data/app/models/nquery/data_source.rb +27 -0
- data/app/models/nquery/embed_token.rb +21 -0
- data/app/models/nquery/group.rb +27 -0
- data/app/models/nquery/group_membership.rb +10 -0
- data/app/models/nquery/query.rb +19 -0
- data/app/models/nquery/user.rb +58 -0
- data/app/views/layouts/nquery/application.html.erb +32 -0
- data/app/views/layouts/nquery/auth.html.erb +16 -0
- data/app/views/layouts/nquery/embed.html.erb +12 -0
- data/app/views/nquery/admin/data_sources/edit.html.erb +8 -0
- data/app/views/nquery/admin/data_sources/index.html.erb +21 -0
- data/app/views/nquery/admin/data_sources/new.html.erb +7 -0
- data/app/views/nquery/admin/groups/edit.html.erb +6 -0
- data/app/views/nquery/admin/groups/index.html.erb +21 -0
- data/app/views/nquery/admin/groups/new.html.erb +6 -0
- data/app/views/nquery/admin/groups/show.html.erb +24 -0
- data/app/views/nquery/admin/logs/index.html.erb +74 -0
- data/app/views/nquery/admin/permissions/_collection.html.erb +25 -0
- data/app/views/nquery/admin/permissions/_data_source.html.erb +27 -0
- data/app/views/nquery/admin/permissions/_group.html.erb +42 -0
- data/app/views/nquery/admin/permissions/by_collection.html.erb +10 -0
- data/app/views/nquery/admin/permissions/by_data_source.html.erb +7 -0
- data/app/views/nquery/admin/permissions/by_group.html.erb +12 -0
- data/app/views/nquery/admin/users/edit.html.erb +17 -0
- data/app/views/nquery/admin/users/index.html.erb +34 -0
- data/app/views/nquery/admin/users/new.html.erb +14 -0
- data/app/views/nquery/admin/users/show.html.erb +6 -0
- data/app/views/nquery/charts/_builder_form.html.erb +185 -0
- data/app/views/nquery/charts/_new_header.html.erb +31 -0
- data/app/views/nquery/charts/_schema_tree.html.erb +25 -0
- data/app/views/nquery/charts/edit.html.erb +6 -0
- data/app/views/nquery/charts/embed.html.erb +15 -0
- data/app/views/nquery/charts/new.html.erb +5 -0
- data/app/views/nquery/charts/show.html.erb +15 -0
- data/app/views/nquery/charts/update.turbo_stream.erb +1 -0
- data/app/views/nquery/collection/dashboards/new.html.erb +13 -0
- data/app/views/nquery/collections/_dashboard_card.html.erb +57 -0
- data/app/views/nquery/collections/edit.html.erb +15 -0
- data/app/views/nquery/collections/index.html.erb +41 -0
- data/app/views/nquery/collections/new.html.erb +15 -0
- data/app/views/nquery/collections/show.html.erb +100 -0
- data/app/views/nquery/dashboard/charts/new.html.erb +6 -0
- data/app/views/nquery/dashboards/_chart_card.html.erb +16 -0
- data/app/views/nquery/dashboards/edit.html.erb +34 -0
- data/app/views/nquery/dashboards/index.html.erb +35 -0
- data/app/views/nquery/dashboards/show.html.erb +46 -0
- data/app/views/nquery/embed/charts/show.html.erb +9 -0
- data/app/views/nquery/embed/dashboards/show.html.erb +7 -0
- data/app/views/nquery/home/index.html.erb +143 -0
- data/app/views/nquery/imports/new.html.erb +20 -0
- data/app/views/nquery/queries/edit.html.erb +40 -0
- data/app/views/nquery/queries/new.html.erb +37 -0
- data/app/views/nquery/registrations/new.html.erb +34 -0
- data/app/views/nquery/sessions/new.html.erb +29 -0
- data/app/views/nquery/shared/_auth_shell.html.erb +22 -0
- data/app/views/nquery/shared/_breadcrumbs.html.erb +15 -0
- data/app/views/nquery/shared/_chart_actions_menu.html.erb +27 -0
- data/app/views/nquery/shared/_chart_row_actions.html.erb +16 -0
- data/app/views/nquery/shared/_flash.html.erb +1 -0
- data/app/views/nquery/shared/_icon.html.erb +50 -0
- data/app/views/nquery/shared/_sidebar.html.erb +16 -0
- data/app/views/nquery/shared/_topbar.html.erb +12 -0
- data/config/importmap.rb +9 -0
- data/config/routes.rb +78 -0
- data/db/migrate/20260721000001_create_nquery_tables.rb +146 -0
- data/db/migrate/20260721180000_add_archived_at_to_nquery_dashboards.rb +8 -0
- data/db/migrate/20260721190000_add_archived_at_to_nquery_charts.rb +8 -0
- data/db/migrate/20260721200000_add_archived_at_to_nquery_collections.rb +8 -0
- data/lib/generators/nquery/install_generator.rb +38 -0
- data/lib/nquery/authorizes_collection.rb +50 -0
- data/lib/nquery/configuration.rb +44 -0
- data/lib/nquery/csv_importer.rb +68 -0
- data/lib/nquery/data_sources/adapter.rb +34 -0
- data/lib/nquery/data_sources/mysql_adapter.rb +24 -0
- data/lib/nquery/data_sources/postgresql_adapter.rb +46 -0
- data/lib/nquery/data_sources/rails_adapter.rb +54 -0
- data/lib/nquery/data_sources/sqlite_adapter.rb +8 -0
- data/lib/nquery/embed_token_service.rb +83 -0
- data/lib/nquery/engine.rb +55 -0
- data/lib/nquery/permissions/resolver.rb +83 -0
- data/lib/nquery/query_runner.rb +69 -0
- data/lib/nquery/sample_data/ecommerce.rb +157 -0
- data/lib/nquery/schema_explorer.rb +28 -0
- data/lib/nquery/seeder.rb +131 -0
- data/lib/nquery/version.rb +5 -0
- data/lib/nquery.rb +36 -0
- data/lib/tasks/nquery.rake +9 -0
- data/nquery.gemspec +40 -0
- metadata +395 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= nq_flash_cards %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<% classes = ["nq-icon", html_options[:class]].compact.join(" ") %>
|
|
2
|
+
<svg class="<%= classes %>" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
3
|
+
<% case name %>
|
|
4
|
+
<% when "edit" %>
|
|
5
|
+
<path d="M12 20h9" />
|
|
6
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
|
7
|
+
<% when "archive" %>
|
|
8
|
+
<rect x="2" y="3" width="20" height="5" rx="1" />
|
|
9
|
+
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
|
|
10
|
+
<path d="M10 12h4" />
|
|
11
|
+
<% when "unarchive" %>
|
|
12
|
+
<path d="M21 8v13H3V8" />
|
|
13
|
+
<path d="M1 3h22v5H1z" />
|
|
14
|
+
<path d="M12 12v6" />
|
|
15
|
+
<path d="m9 15 3-3 3 3" />
|
|
16
|
+
<% when "trash" %>
|
|
17
|
+
<path d="M3 6h18" />
|
|
18
|
+
<path d="M8 6V4h8v2" />
|
|
19
|
+
<path d="M10 11v6" />
|
|
20
|
+
<path d="M14 11v6" />
|
|
21
|
+
<path d="M5 6v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6" />
|
|
22
|
+
<% when "folder" %>
|
|
23
|
+
<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
|
24
|
+
<% when "chart" %>
|
|
25
|
+
<path d="M3 3v18h18" />
|
|
26
|
+
<path d="M7 16V9" />
|
|
27
|
+
<path d="M12 16V5" />
|
|
28
|
+
<path d="M17 16v-3" />
|
|
29
|
+
<% when "dashboard" %>
|
|
30
|
+
<rect x="3" y="3" width="7" height="9" rx="1" />
|
|
31
|
+
<rect x="14" y="3" width="7" height="5" rx="1" />
|
|
32
|
+
<rect x="14" y="12" width="7" height="9" rx="1" />
|
|
33
|
+
<rect x="3" y="16" width="7" height="5" rx="1" />
|
|
34
|
+
<% when "more" %>
|
|
35
|
+
<circle cx="12" cy="5" r="1" />
|
|
36
|
+
<circle cx="12" cy="12" r="1" />
|
|
37
|
+
<circle cx="12" cy="19" r="1" />
|
|
38
|
+
<% when "plus" %>
|
|
39
|
+
<path d="M12 5v14" />
|
|
40
|
+
<path d="M5 12h14" />
|
|
41
|
+
<% when "arrow-right" %>
|
|
42
|
+
<path d="M5 12h14" />
|
|
43
|
+
<path d="m12 5 7 7-7 7" />
|
|
44
|
+
<% when "format" %>
|
|
45
|
+
<path d="M21 10H3" />
|
|
46
|
+
<path d="M21 6H3" />
|
|
47
|
+
<path d="M11 14H3" />
|
|
48
|
+
<path d="M15 18H3" />
|
|
49
|
+
<% end %>
|
|
50
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<aside class="nq-sidebar">
|
|
2
|
+
<div class="nq-logo">nquery</div>
|
|
3
|
+
<nav class="nq-nav">
|
|
4
|
+
<%= link_to "Home", root_path, class: nav_link_class("nquery/home") %>
|
|
5
|
+
<%= link_to "Collections", collections_path, class: nav_link_class("nquery/collections") %>
|
|
6
|
+
<%= link_to "Dashboards", dashboards_path, class: nav_link_class("nquery/dashboards", "nquery/dashboard/charts") %>
|
|
7
|
+
<% if current_nquery_user&.admin? %>
|
|
8
|
+
<div class="nq-nav-section">Admin</div>
|
|
9
|
+
<%= link_to "Users", admin_users_path, class: nav_link_class("nquery/admin/users") %>
|
|
10
|
+
<%= link_to "Groups", admin_groups_path, class: nav_link_class("nquery/admin/groups") %>
|
|
11
|
+
<%= link_to "Data sources", admin_data_sources_path, class: nav_link_class("nquery/admin/data_sources") %>
|
|
12
|
+
<%= link_to "Permissions", by_group_admin_permissions_path, class: nav_link_class("nquery/admin/permissions") %>
|
|
13
|
+
<%= link_to "Logs", admin_logs_path, class: nav_link_class("nquery/admin/logs") %>
|
|
14
|
+
<% end %>
|
|
15
|
+
</nav>
|
|
16
|
+
</aside>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<header class="nq-topbar">
|
|
2
|
+
<div class="nq-search">
|
|
3
|
+
<input type="search" placeholder="Search charts, dashboards..." class="nq-input" />
|
|
4
|
+
</div>
|
|
5
|
+
<div class="nq-topbar-actions">
|
|
6
|
+
<span class="nq-badge"><%= Nquery::DataSource.active.first&.name || "Main DB" %></span>
|
|
7
|
+
<% if current_nquery_user %>
|
|
8
|
+
<span class="nq-user"><%= current_nquery_user.name %></span>
|
|
9
|
+
<%= button_to "Sign out", logout_path, method: :delete, class: "nq-btn nq-btn-ghost" %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
</header>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
pin "application", preload: true
|
|
4
|
+
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
|
5
|
+
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
|
6
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
|
7
|
+
pin_all_from Nquery::Engine.root.join("app/javascript/nquery/controllers"), under: "nquery/controllers"
|
|
8
|
+
pin "nquery/chart", to: "nquery/chart.bundle.js"
|
|
9
|
+
pin "nquery/application", to: "nquery/application.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Nquery::Engine.routes.draw do
|
|
4
|
+
root to: "home#index"
|
|
5
|
+
|
|
6
|
+
get "browse", to: redirect("/collections")
|
|
7
|
+
|
|
8
|
+
resources :collections do
|
|
9
|
+
member do
|
|
10
|
+
patch :archive
|
|
11
|
+
patch :unarchive
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
resources :collections, only: %i[new create]
|
|
15
|
+
resources :dashboards, only: %i[new create], module: :collection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
resources :queries, only: %i[new create edit update show] do
|
|
19
|
+
post :run, on: :collection
|
|
20
|
+
get :schema, on: :collection
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
resources :charts, only: %i[new create show edit update destroy] do
|
|
24
|
+
member do
|
|
25
|
+
get :embed
|
|
26
|
+
patch :archive
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
resources :dashboards, except: %i[new create] do
|
|
31
|
+
resources :charts, module: :dashboard do
|
|
32
|
+
member do
|
|
33
|
+
get :embed
|
|
34
|
+
patch :archive
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
member do
|
|
39
|
+
patch :update_layout
|
|
40
|
+
patch :archive
|
|
41
|
+
patch :unarchive
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
resources :imports, only: %i[new create]
|
|
46
|
+
|
|
47
|
+
namespace :admin do
|
|
48
|
+
resources :users, only: %i[index show new create edit update destroy] do
|
|
49
|
+
member do
|
|
50
|
+
patch :deactivate
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
resources :groups, only: %i[index show new create edit update] do
|
|
54
|
+
post :add_member, on: :member
|
|
55
|
+
delete :remove_member, on: :member
|
|
56
|
+
end
|
|
57
|
+
resources :data_sources, only: %i[index new create edit update]
|
|
58
|
+
resources :logs, only: %i[index]
|
|
59
|
+
resources :permissions, only: %i[index] do
|
|
60
|
+
collection do
|
|
61
|
+
get :by_group
|
|
62
|
+
get :by_data_source
|
|
63
|
+
get :by_collection
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
get "login", to: "sessions#new"
|
|
69
|
+
post "login", to: "sessions#create"
|
|
70
|
+
delete "logout", to: "sessions#destroy"
|
|
71
|
+
get "signup", to: "registrations#new"
|
|
72
|
+
post "signup", to: "registrations#create"
|
|
73
|
+
|
|
74
|
+
namespace :embed do
|
|
75
|
+
get "charts/show", to: "charts#show", as: :public_chart
|
|
76
|
+
get "dashboards/show", to: "dashboards#show", as: :public_dashboard
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateNqueryTables < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :nquery_users do |t|
|
|
6
|
+
t.string :email, null: false
|
|
7
|
+
t.string :password_digest
|
|
8
|
+
t.string :first_name
|
|
9
|
+
t.string :last_name
|
|
10
|
+
t.string :external_id
|
|
11
|
+
t.datetime :deactivated_at
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
add_index :nquery_users, :email, unique: true
|
|
15
|
+
add_index :nquery_users, :external_id, unique: true, where: "external_id IS NOT NULL"
|
|
16
|
+
|
|
17
|
+
create_table :nquery_groups do |t|
|
|
18
|
+
t.string :name, null: false
|
|
19
|
+
t.string :system_group, null: false, default: "custom"
|
|
20
|
+
t.text :description
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
add_index :nquery_groups, :system_group
|
|
24
|
+
|
|
25
|
+
create_table :nquery_group_memberships do |t|
|
|
26
|
+
t.references :user, null: false, foreign_key: { to_table: :nquery_users }
|
|
27
|
+
t.references :group, null: false, foreign_key: { to_table: :nquery_groups }
|
|
28
|
+
t.timestamps
|
|
29
|
+
end
|
|
30
|
+
add_index :nquery_group_memberships, %i[user_id group_id], unique: true
|
|
31
|
+
|
|
32
|
+
create_table :nquery_data_sources do |t|
|
|
33
|
+
t.string :name, null: false
|
|
34
|
+
t.string :adapter, null: false, default: "postgresql"
|
|
35
|
+
t.text :connection_config
|
|
36
|
+
t.boolean :active, null: false, default: true
|
|
37
|
+
t.timestamps
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
create_table :nquery_data_permissions do |t|
|
|
41
|
+
t.references :group, null: false, foreign_key: { to_table: :nquery_groups }
|
|
42
|
+
t.references :data_source, null: false, foreign_key: { to_table: :nquery_data_sources }
|
|
43
|
+
t.string :resource_path
|
|
44
|
+
t.string :permission_type, null: false
|
|
45
|
+
t.string :access_level, null: false
|
|
46
|
+
t.timestamps
|
|
47
|
+
end
|
|
48
|
+
add_index :nquery_data_permissions,
|
|
49
|
+
%i[group_id data_source_id resource_path permission_type],
|
|
50
|
+
unique: true,
|
|
51
|
+
name: "index_nquery_data_permissions_unique"
|
|
52
|
+
|
|
53
|
+
create_table :nquery_collections do |t|
|
|
54
|
+
t.string :name, null: false
|
|
55
|
+
t.string :kind, null: false, default: "standard"
|
|
56
|
+
t.references :parent, foreign_key: { to_table: :nquery_collections }
|
|
57
|
+
t.references :owner, foreign_key: { to_table: :nquery_users }
|
|
58
|
+
t.timestamps
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
create_table :nquery_collection_permissions do |t|
|
|
62
|
+
t.references :group, null: false, foreign_key: { to_table: :nquery_groups }
|
|
63
|
+
t.references :collection, null: false, foreign_key: { to_table: :nquery_collections }
|
|
64
|
+
t.string :access_level, null: false, default: "no_access"
|
|
65
|
+
t.timestamps
|
|
66
|
+
end
|
|
67
|
+
add_index :nquery_collection_permissions, %i[group_id collection_id], unique: true
|
|
68
|
+
|
|
69
|
+
create_table :nquery_application_permissions do |t|
|
|
70
|
+
t.references :group, null: false, foreign_key: { to_table: :nquery_groups }
|
|
71
|
+
t.string :feature, null: false
|
|
72
|
+
t.string :access_level, null: false, default: "no"
|
|
73
|
+
t.timestamps
|
|
74
|
+
end
|
|
75
|
+
add_index :nquery_application_permissions, %i[group_id feature], unique: true
|
|
76
|
+
|
|
77
|
+
create_table :nquery_queries do |t|
|
|
78
|
+
t.string :name
|
|
79
|
+
t.text :statement
|
|
80
|
+
t.references :data_source, foreign_key: { to_table: :nquery_data_sources }
|
|
81
|
+
t.references :creator, foreign_key: { to_table: :nquery_users }
|
|
82
|
+
t.references :collection, foreign_key: { to_table: :nquery_collections }
|
|
83
|
+
t.timestamps
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
create_table :nquery_charts do |t|
|
|
87
|
+
t.string :name, null: false
|
|
88
|
+
t.references :query, foreign_key: { to_table: :nquery_queries }
|
|
89
|
+
t.references :collection, foreign_key: { to_table: :nquery_collections }
|
|
90
|
+
t.references :creator, foreign_key: { to_table: :nquery_users }
|
|
91
|
+
t.json :visualization, null: false, default: {}
|
|
92
|
+
t.timestamps
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
create_table :nquery_dashboards do |t|
|
|
96
|
+
t.string :name, null: false
|
|
97
|
+
t.text :description
|
|
98
|
+
t.references :collection, foreign_key: { to_table: :nquery_collections }
|
|
99
|
+
t.references :creator, foreign_key: { to_table: :nquery_users }
|
|
100
|
+
t.json :settings, null: false, default: {}
|
|
101
|
+
t.timestamps
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
create_table :nquery_dashboard_cards do |t|
|
|
105
|
+
t.references :dashboard, null: false, foreign_key: { to_table: :nquery_dashboards }
|
|
106
|
+
t.references :chart, null: false, foreign_key: { to_table: :nquery_charts }
|
|
107
|
+
t.integer :pos_x, null: false, default: 0
|
|
108
|
+
t.integer :pos_y, null: false, default: 0
|
|
109
|
+
t.integer :width, null: false, default: 4
|
|
110
|
+
t.integer :height, null: false, default: 3
|
|
111
|
+
t.timestamps
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
create_table :nquery_csv_uploads do |t|
|
|
115
|
+
t.string :name
|
|
116
|
+
t.references :creator, foreign_key: { to_table: :nquery_users }
|
|
117
|
+
t.string :status, null: false, default: "pending"
|
|
118
|
+
t.json :column_mapping, null: false, default: {}
|
|
119
|
+
t.timestamps
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
create_table :nquery_embed_tokens do |t|
|
|
123
|
+
t.string :token, null: false
|
|
124
|
+
t.string :resource_type, null: false
|
|
125
|
+
t.bigint :resource_id, null: false
|
|
126
|
+
t.references :creator, foreign_key: { to_table: :nquery_users }
|
|
127
|
+
t.json :params, null: false, default: {}
|
|
128
|
+
t.datetime :expires_at
|
|
129
|
+
t.boolean :active, null: false, default: true
|
|
130
|
+
t.timestamps
|
|
131
|
+
end
|
|
132
|
+
add_index :nquery_embed_tokens, :token, unique: true
|
|
133
|
+
add_index :nquery_embed_tokens, %i[resource_type resource_id]
|
|
134
|
+
|
|
135
|
+
create_table :nquery_audits do |t|
|
|
136
|
+
t.references :user, foreign_key: { to_table: :nquery_users }
|
|
137
|
+
t.references :query, foreign_key: { to_table: :nquery_queries }
|
|
138
|
+
t.text :statement
|
|
139
|
+
t.string :status, null: false, default: "success"
|
|
140
|
+
t.integer :row_count
|
|
141
|
+
t.integer :duration_ms
|
|
142
|
+
t.text :error_message
|
|
143
|
+
t.timestamps
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module Nquery
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
|
|
12
|
+
desc "Install nquery: copies migration, initializer, and route mount snippet"
|
|
13
|
+
|
|
14
|
+
def copy_migration
|
|
15
|
+
migration_template "install.rb.tt", "db/migrate/install_nquery.rb", migration_version: migration_version
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_initializer
|
|
19
|
+
template "nquery.rb.tt", "config/initializers/nquery.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show_route_snippet
|
|
23
|
+
say "\nAdd to config/routes.rb:\n", :green
|
|
24
|
+
say <<~ROUTE
|
|
25
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
26
|
+
mount Nquery::Engine, at: "/nquery"
|
|
27
|
+
end
|
|
28
|
+
ROUTE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def migration_version
|
|
34
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module AuthorizesCollection
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
COLLECTION_REQUIREMENTS = Permissions::Resolver::COLLECTION_LEVELS
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def authorize_collection_access!(collection, required: :view)
|
|
12
|
+
return if permission_resolver.admin?
|
|
13
|
+
return if collection.nil?
|
|
14
|
+
|
|
15
|
+
access = permission_resolver.collection_access(collection)
|
|
16
|
+
required_level = COLLECTION_REQUIREMENTS.fetch(required.to_s)
|
|
17
|
+
actual_level = COLLECTION_REQUIREMENTS.fetch(access.to_s, 0)
|
|
18
|
+
return if actual_level >= required_level
|
|
19
|
+
|
|
20
|
+
deny_collection_access!("You do not have permission to access this collection.")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def authorize_data_source_access!(data_source, permission_type: "view_data")
|
|
24
|
+
return if permission_resolver.admin?
|
|
25
|
+
return if data_source.nil?
|
|
26
|
+
|
|
27
|
+
access = permission_resolver.data_access(data_source, permission_type: permission_type)
|
|
28
|
+
blocked = permission_type == "create_queries" ? access == :no : access == :blocked
|
|
29
|
+
return unless blocked
|
|
30
|
+
|
|
31
|
+
message = permission_type == "create_queries" ? "You do not have permission to run SQL queries" : "You do not have permission to view this data"
|
|
32
|
+
deny_collection_access!(message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def viewable_collection?(collection)
|
|
36
|
+
return true if permission_resolver.admin?
|
|
37
|
+
return false if collection.nil?
|
|
38
|
+
|
|
39
|
+
permission_resolver.collection_access(collection) != :no_access
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def deny_collection_access!(message)
|
|
43
|
+
if request.format.json?
|
|
44
|
+
render json: { error: message }, status: :forbidden
|
|
45
|
+
else
|
|
46
|
+
redirect_to root_path, alert: message
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :authentication_mode
|
|
6
|
+
attr_accessor :current_user_method
|
|
7
|
+
attr_accessor :parent_controller
|
|
8
|
+
attr_accessor :data_sources
|
|
9
|
+
attr_accessor :query_timeout
|
|
10
|
+
attr_accessor :query_row_limit
|
|
11
|
+
attr_accessor :embed_secret
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@authentication_mode = :standalone
|
|
15
|
+
@current_user_method = :current_nquery_user
|
|
16
|
+
@data_sources = { main: :rails }
|
|
17
|
+
@query_timeout = 15
|
|
18
|
+
@query_row_limit = 10_000
|
|
19
|
+
@embed_secret = nil
|
|
20
|
+
@authenticate_with = nil
|
|
21
|
+
@resolve_nquery_user = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def authenticate_with(&block)
|
|
25
|
+
@authenticate_with = block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def authenticate
|
|
29
|
+
@authenticate_with
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve_nquery_user(&block)
|
|
33
|
+
@resolve_nquery_user = block
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_user
|
|
37
|
+
@resolve_nquery_user
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def embed_signing_key
|
|
41
|
+
embed_secret || Rails.application.secret_key_base
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Nquery
|
|
6
|
+
class CsvImporter
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
def initialize(file:, name:, creator:, column_mapping: {})
|
|
10
|
+
@file = file
|
|
11
|
+
@name = name.presence || "Import #{Time.current.to_fs(:short)}"
|
|
12
|
+
@creator = creator
|
|
13
|
+
@column_mapping = column_mapping
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def import
|
|
17
|
+
raise Error, "No file provided" unless @file
|
|
18
|
+
|
|
19
|
+
upload = CsvUpload.create!(
|
|
20
|
+
name: @name,
|
|
21
|
+
creator: @creator,
|
|
22
|
+
status: "processing",
|
|
23
|
+
column_mapping: @column_mapping
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
rows = CSV.read(@file.path, headers: true)
|
|
27
|
+
table_name = "nquery_import_#{upload.id}"
|
|
28
|
+
|
|
29
|
+
connection = ActiveRecord::Base.connection
|
|
30
|
+
columns = infer_columns(rows.headers)
|
|
31
|
+
create_import_table(connection, table_name, columns)
|
|
32
|
+
insert_rows(connection, table_name, rows, columns)
|
|
33
|
+
|
|
34
|
+
data_source = DataSource.create!(
|
|
35
|
+
name: @name,
|
|
36
|
+
adapter: connection.adapter_name.downcase.include?("postgres") ? "postgresql" : "rails",
|
|
37
|
+
connection_config: { import_table: table_name }.to_json
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
upload.update!(status: "completed", column_mapping: columns)
|
|
41
|
+
upload
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
upload&.update!(status: "failed")
|
|
44
|
+
raise Error, e.message
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def infer_columns(headers)
|
|
50
|
+
headers.map { |h| { source: h, target: h.parameterize(separator: "_"), type: "text" } }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_import_table(connection, table_name, columns)
|
|
54
|
+
cols = columns.map { |c| "#{connection.quote_column_name(c[:target])} TEXT" }.join(", ")
|
|
55
|
+
connection.execute("CREATE TABLE IF NOT EXISTS #{connection.quote_table_name(table_name)} (#{cols})")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def insert_rows(connection, table_name, rows, columns)
|
|
59
|
+
rows.each do |row|
|
|
60
|
+
values = columns.map { |c| connection.quote(row[c[:source]]) }
|
|
61
|
+
col_names = columns.map { |c| connection.quote_column_name(c[:target]) }.join(", ")
|
|
62
|
+
connection.execute(
|
|
63
|
+
"INSERT INTO #{connection.quote_table_name(table_name)} (#{col_names}) VALUES (#{values.join(', ')})"
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module DataSources
|
|
5
|
+
class Adapter
|
|
6
|
+
def self.for(data_source)
|
|
7
|
+
case data_source.adapter
|
|
8
|
+
when "rails" then RailsAdapter.new(data_source)
|
|
9
|
+
when "postgresql" then PostgresqlAdapter.new(data_source)
|
|
10
|
+
when "mysql" then MysqlAdapter.new(data_source)
|
|
11
|
+
when "sqlite" then SqliteAdapter.new(data_source)
|
|
12
|
+
else
|
|
13
|
+
raise ArgumentError, "Unknown adapter: #{data_source.adapter}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(data_source)
|
|
18
|
+
@data_source = data_source
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tables
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def columns(table_name)
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def execute_readonly(statement, timeout: 15, row_limit: 10_000)
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module DataSources
|
|
5
|
+
class MysqlAdapter < PostgresqlAdapter
|
|
6
|
+
def execute_readonly(statement, timeout: 15, row_limit: 10_000)
|
|
7
|
+
with_connection do |conn|
|
|
8
|
+
rows = []
|
|
9
|
+
columns = []
|
|
10
|
+
duration = Benchmark.realtime do
|
|
11
|
+
conn.transaction do
|
|
12
|
+
conn.execute("SET SESSION TRANSACTION READ ONLY")
|
|
13
|
+
result = conn.exec_query("#{statement.strip.sub(/;\s*\z/, '')} LIMIT #{row_limit.to_i}")
|
|
14
|
+
columns = result.columns
|
|
15
|
+
rows = result.rows
|
|
16
|
+
raise ActiveRecord::Rollback
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
{ columns: columns, rows: rows, row_count: rows.size, duration_ms: (duration * 1000).round }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module DataSources
|
|
5
|
+
class PostgresqlAdapter < Adapter
|
|
6
|
+
def tables
|
|
7
|
+
with_connection { |conn| conn.tables }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def columns(table_name)
|
|
11
|
+
with_connection do |conn|
|
|
12
|
+
conn.columns(table_name).map { |c| { name: c.name, type: c.type.to_s } }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute_readonly(statement, timeout: 15, row_limit: 10_000)
|
|
17
|
+
with_connection do |conn|
|
|
18
|
+
rows = []
|
|
19
|
+
columns = []
|
|
20
|
+
duration = Benchmark.realtime do
|
|
21
|
+
conn.transaction do
|
|
22
|
+
conn.execute("SET TRANSACTION READ ONLY")
|
|
23
|
+
conn.execute("SET statement_timeout = '#{timeout.to_i}s'")
|
|
24
|
+
result = conn.exec_query("#{statement.strip.sub(/;\s*\z/, '')} LIMIT #{row_limit.to_i}")
|
|
25
|
+
columns = result.columns
|
|
26
|
+
rows = result.rows
|
|
27
|
+
raise ActiveRecord::Rollback
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
{ columns: columns, rows: rows, row_count: rows.size, duration_ms: (duration * 1000).round }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def with_connection
|
|
37
|
+
config = @data_source.connection_config_hash
|
|
38
|
+
klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
|
|
39
|
+
klass.establish_connection(config)
|
|
40
|
+
yield klass.connection
|
|
41
|
+
ensure
|
|
42
|
+
klass.remove_connection if defined?(klass) && klass.connected?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|