Skip to content

Commit

Permalink
Generate unique database and unique connection in Superset for each u…
Browse files Browse the repository at this point in the history
…ser. Add session info in the left sidebar
  • Loading branch information
bartmichalak committed Jan 20, 2025
1 parent e5d2508 commit 5e8baa2
Show file tree
Hide file tree
Showing 19 changed files with 284 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ vendor/.bun
/config/credentials/production.key

# generated duckdb database file
lib/fhir-export/duckdb_persistent.duckdb
lib/fhir-export/*.duckdb
22 changes: 19 additions & 3 deletions app/concepts/sessions/services/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ def call
session = Session.create!
assign_sample_analyses(session)
create_superset_user(session)
create_duckdb_database(session)
create_superset_database_connection(session)
session
end
end
Expand All @@ -26,11 +28,11 @@ def assign_sample_analyses(session)
analyses_batch.each do |analysis|
new_analysis = build_duplicated_analysis(analysis, session)
new_analyses << new_analysis

analysis.view_definitions.each do |view_definition|
new_view_definitions << build_duplicated_view_definition(
view_definition,
new_analysis,
view_definition,
new_analysis,
session
)
end
Expand All @@ -45,6 +47,20 @@ def create_superset_user(session)
Superset::Services::ApiService.new(current_session: session).create_user
end

def create_duckdb_database(session)
Utils::Services::InitializeDuckdbDatabase.new(current_session: session).call
end

def create_superset_database_connection(session)
api_service = Superset::Services::ApiService.new(current_session: session)
response = api_service.create_database_connection

raise "Failed to create Superset database: #{response.body}" unless response.success?

database_id = response.body["id"]
session.update(superset_db_connection_id: database_id)
end

def sample_analyses
Analysis.includes(:view_definitions).where(sample: true)
end
Expand Down
20 changes: 10 additions & 10 deletions app/concepts/superset/services/api_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module Superset
module Services
class ApiService
SUPERSET_BASE_URL = "#{ENV['SUPERSET_INTERNAL_URL']}/api/v1/"
SUPERSET_BASE_URL = "#{ENV.fetch('SUPERSET_INTERNAL_URL', nil)}/api/v1/".freeze
SUPERSET_ADMIN_ROLE_ID = 1

def initialize(current_session: nil)
Expand Down Expand Up @@ -36,8 +36,8 @@ def login
def login_as_admin_user
response = @conn.post("security/login") do |req|
req.body = {
username: ENV['SUPERSET_ADMIN_USERNAME'],
password: ENV['SUPERSET_ADMIN_PASSWORD'],
username: ENV.fetch("SUPERSET_ADMIN_USERNAME", nil),
password: ENV.fetch("SUPERSET_ADMIN_PASSWORD", nil),
provider: "db",
refresh: true
}
Expand Down Expand Up @@ -65,14 +65,14 @@ def save_query(sql, label)
req.headers["Authorization"] = "Bearer #{@auth_token}"
req.headers["x-csrftoken"] = @csrf_token
req.body = {
label: label,
sql: sql,
label: label,
sql: sql,
db_id: Setting.get("superset_duckdb_database_id")
}
end
end

def create_database(name)
def create_database_connection
login_as_admin_user
get_csrf_token

Expand All @@ -82,17 +82,17 @@ def create_database(name)
req.body = {
allow_dml: true,
configuration_method: "sqlalchemy_form",
database_name: name,
sqlalchemy_uri: "duckdb:////app/fhir-export/#{name}.duckdb"
database_name: @current_session.token,
sqlalchemy_uri: "duckdb:////app/fhir-export/#{@current_session.token}.duckdb"
}
end
end

def create_user
login_as_admin_user
get_csrf_token
response = @conn.post("security/users/") do |req|

@conn.post("security/users/") do |req|
req.headers["Authorization"] = "Bearer #{@auth_token}"
req.headers["x-csrftoken"] = @csrf_token
req.body = {
Expand Down
29 changes: 29 additions & 0 deletions app/concepts/utils/services/initialize_duckdb_database.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Utils
module Services
class InitializeDuckdbDatabase
MACROS = [
"CREATE OR REPLACE MACRO as_list(a) AS if(a IS NULL, [], [a]);",
"CREATE OR REPLACE MACRO ifnull2(a, b) AS ifnull(a, b);",
"CREATE OR REPLACE MACRO slice(a,i) AS a[i];",
"CREATE OR REPLACE MACRO is_false(a) AS a = false;",
"CREATE OR REPLACE MACRO is_true(a) AS a = true;",
"CREATE OR REPLACE MACRO is_null(a) AS a IS NULL;",
"CREATE OR REPLACE MACRO is_not_null(a) AS a IS NOT NULL;",
"CREATE OR REPLACE MACRO as_value(a) AS if(len(a) > 1, error('unexpected collection returned'), a[1]);"
].freeze

def initialize(current_session:)
@current_session = current_session
end

def call
db = DuckDB::Database.open("lib/fhir-export/#{@current_session.token}.duckdb")
con = db.connect

con.query(MACROS.join("\n"))
end
end
end
end
20 changes: 11 additions & 9 deletions app/concepts/view_definitions/services/generate_query.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# frozen_string_literal: true

module ViewDefinitions
module Services
class GenerateQuery
class Error < StandardError; end

TEMPLATES = {
create_view: 'lib/view_definitions/create_view_template.sql',
query: 'lib/view_definitions/query_template.sql'
create_view: "lib/view_definitions/create_view_template.sql",
query: "lib/view_definitions/query_template.sql"
}.freeze

def initialize(view_definition, template_type: :query)
Expand All @@ -31,10 +33,10 @@ def template_path
end

def validate_template_type!
unless TEMPLATES.key?(template_type)
valid_types = TEMPLATES.keys.join(', ')
raise Error, "Invalid template type: #{template_type}. Valid types are: #{valid_types}"
end
return if TEMPLATES.key?(template_type)

valid_types = TEMPLATES.keys.join(", ")
raise Error, "Invalid template type: #{template_type}. Valid types are: #{valid_types}"
end

def write_temp_file
Expand All @@ -51,11 +53,11 @@ def run_flatquack
# Quick workaround to remove console.log() statement from the result (*** compiling ... ***)
# Probably there is a better way to resolve this
def clean_query_result(result)
result.sub(/^.*\*\*\* compiling.*\n/, '')
result.sub(/^.*\*\*\* compiling.*\n/, "")
end

def cleanup_temp_file
File.delete(temp_file_path) if File.exist?(temp_file_path)
FileUtils.rm_f(temp_file_path)
end

def temp_file_path
Expand All @@ -78,4 +80,4 @@ def command
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/analyses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def export_to_superset

def save_as_views
begin
db = DuckDB::Database.open("/app/fhir-export/duckdb_persistent.duckdb")
db = DuckDB::Database.open("/app/fhir-export/#{@current_session.token}.duckdb")
con = db.connect

@analysis.view_definitions.each do |vd|
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/view_definitions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def run_query
Rails.logger.info "Executing query: #{@query}"

begin
db = DuckDB::Database.open("lib/fhir-export/duckdb_persistent.duckdb")
db = DuckDB::Database.open("lib/fhir-export/#{@current_session.token}.duckdb")
con = db.connect

@result = con.query(@query)
Expand Down
15 changes: 0 additions & 15 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,4 @@ module ApplicationHelper
def session_analyses
@session_analyses ||= Analysis.where(session_id: current_session.id)
end

def session_link
session_token = current_session.token
url = root_url(session_token:)
text = "Click here to copy the session link to return to your dashboard or share it with others."


link_to text, url,
class: "text-white block text-center text-lg",
data: {
controller: "clipboard",
action: "click->clipboard#copy",
clipboard_target: "source"
}
end
end
19 changes: 11 additions & 8 deletions app/models/session.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: sessions
#
# id :integer not null, primary key
# token :string not null
# created_at :datetime not null
# updated_at :datetime not null
# superset_username :string
# superset_password :string
# superset_email :string
# id :integer not null, primary key
# token :string not null
# created_at :datetime not null
# updated_at :datetime not null
# superset_username :string
# superset_password :string
# superset_email :string
# superset_db_connection_id :integer
#
# Indexes
#
Expand All @@ -20,7 +23,7 @@ class Session < ApplicationRecord
validates :superset_username, presence: true, uniqueness: true
validates :superset_password, presence: true
validates :superset_email, presence: true, uniqueness: true

before_validation :generate_token, on: :create
before_validation :generate_superset_credentials, on: :create

Expand Down
2 changes: 1 addition & 1 deletion app/views/analyses/save_as_views.turbo_stream.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="bg-green-100 p-4 rounded-lg shadow">
<p class="text-md">
Query saved to Superset.
<%= link_to "Open SQL lab", "#{ENV['SUPERSET_URL']}/sqllab",
<%= link_to "Open SQL lab", "#{ENV['SUPERSET_PUBLIC_URL']}/sqllab",
target: :_blank,
class: "font-semibold text-blue-600 hover:text-blue-800 underline" %>
</p>
Expand Down
56 changes: 47 additions & 9 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,64 @@
</svg>
</button>
</div>

<!-- Sidebar component, swap this element with another sidebar if you like -->
</div>
</div>
</div>

<!-- Static sidebar for desktop -->
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<!-- Sidebar component, swap this element with another sidebar if you like -->
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900 px-6">
<div class="flex h-16 shrink-0 items-center">
<img class="h-8 w-auto" src="https://tailwindui.com/plus/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
<div class="flex mt-10 shrink-0 items-center">
<div class="text-white space-y-2">
<div class="flex items-center">
<span class="text-sm font-semibold">Current session:</span>
</div>
<div class="text-sm text-gray-300 flex items-center space-x-2" data-controller="clipboard">
<p>Token:</p><br>
<a
href="<%= root_url(session_token: current_session.token) %>"
data-clipboard-target="source"
data-action="click->clipboard#copy"
class="font-mono text-custom-10 cursor-pointer"
>
<%= truncate(current_session.token, length: 15, omission: "...") %>
</a>
<button
data-action="clipboard#copy"
class="px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded-md transition-colors duration-200"
title="Copy token"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
<div class="text-sm text-gray-300">
Superset e-mail: <span class="font-mono text-custom-10"><%= current_session.superset_email %></span>
</div>
<div class="text-sm text-gray-300">
Superset password: <span class="font-mono text-custom-10"><%= current_session.superset_password %></span>
</div>

<div class="mt-4">
<a
href="<%= ENV["SUPERSET_PUBLIC_URL"] %>/sqllab"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-4 py-2 text-sm text-white bg-gray-700 hover:bg-gray-600 rounded-md transition-colors duration-200"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Open Superset
</a>
</div>
</div>
</div>
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
<li>
<div class="flex items-center justify-between px-2 mb-2">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-semibold text-white">Analyses</h2>
<%= link_to new_analysis_path, class: "flex items-center rounded-md bg-indigo-600 px-2 py-1 text-sm font-semibold text-white hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" do %>
<svg class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
Expand All @@ -102,9 +143,6 @@
</div>

<main class="py-10 lg:pl-72">
<div class="bg-indigo-700 px-4 py-2 mb-4">
<%= session_link %>
</div>
<div class="px-4 sm:px-6 lg:px-8">
<%= yield %>
</div>
Expand Down
3 changes: 3 additions & 0 deletions config/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ module.exports = {
fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
},
fontSize: {
'custom-10': '10px'
}
},
},
plugins: [
Expand Down
Binary file modified database_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddSupersetDbConnectionIdToSessions < ActiveRecord::Migration[8.0]
def change
add_column :sessions, :superset_db_connection_id, :integer
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5e8baa2

Please sign in to comment.