greeenboii 0.1.7 → 0.1.9
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 +4 -4
- data/.idea/greeenboii.iml +30 -23
- data/.idea/jsLibraryMappings.xml +6 -0
- data/.idea/vcs.xml +0 -1
- data/.idea/workspace.xml +63 -6
- data/CHANGELOG.md +16 -0
- data/lib/greeenboii/version.rb +1 -1
- data/lib/greeenboii.rb +433 -0
- data/lib/libsql.rb +159 -0
- data/logs/turso_requests.log +54 -0
- data/logs/turso_responses.log +27 -0
- metadata +36 -9
- data/.idea/dataSources/28fe2501-d682-44de-9f2e-9ff4bf02ce84/storage_v2/_src_/schema/main.uQUzAA.meta +0 -2
- data/.idea/dataSources/28fe2501-d682-44de-9f2e-9ff4bf02ce84.xml +0 -1617
- data/.idea/dataSources.local.xml +0 -18
- data/.idea/dataSources.xml +0 -12
- data/.idea/sqldialects.xml +0 -7
data/lib/greeenboii.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "greeenboii/version"
|
4
|
+
require_relative "libsql"
|
4
5
|
require "cli/ui"
|
5
6
|
require "date"
|
6
7
|
require "console_table"
|
@@ -10,6 +11,13 @@ require "httparty"
|
|
10
11
|
|
11
12
|
require "sqlite3"
|
12
13
|
|
14
|
+
require 'json'
|
15
|
+
require 'net/http'
|
16
|
+
require 'uri'
|
17
|
+
|
18
|
+
require 'dotenv'
|
19
|
+
require 'fileutils'
|
20
|
+
|
13
21
|
module Greeenboii
|
14
22
|
class Error < StandardError; end
|
15
23
|
|
@@ -412,6 +420,430 @@ module Greeenboii
|
|
412
420
|
end
|
413
421
|
end
|
414
422
|
|
423
|
+
class GistManager
|
424
|
+
def initialize
|
425
|
+
Dotenv.load
|
426
|
+
ensure_connection
|
427
|
+
end
|
428
|
+
|
429
|
+
private def ensure_connection
|
430
|
+
return if ENV['TURSO_DATABASE_URL'] && ENV['TURSO_AUTH_TOKEN']
|
431
|
+
|
432
|
+
CLI::UI.puts(CLI::UI.fmt("{{yellow:⚠}} Turso credentials not found"))
|
433
|
+
manage_credentials
|
434
|
+
end
|
435
|
+
|
436
|
+
def add_gist
|
437
|
+
unless ENV['TURSO_DATABASE_URL'] && ENV['TURSO_AUTH_TOKEN']
|
438
|
+
CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Cloud credentials required"))
|
439
|
+
return
|
440
|
+
end
|
441
|
+
|
442
|
+
title = CLI::UI.ask("Enter a title for this gist:")
|
443
|
+
return if title.empty?
|
444
|
+
|
445
|
+
url = CLI::UI.ask("Enter GitHub Gist URL:")
|
446
|
+
return if url.empty? || !url.match?(/https:\/\/gist\.github\.com\//)
|
447
|
+
|
448
|
+
description = CLI::UI.ask("Enter a description (optional):", default: "")
|
449
|
+
tags = CLI::UI.ask("Enter tags (comma separated):", default: "")
|
450
|
+
|
451
|
+
# Extract gist ID from URL
|
452
|
+
gist_id = url.split('/').last
|
453
|
+
created_at = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
454
|
+
|
455
|
+
CLI::UI::Spinner.spin("Saving gist...") do |spinner|
|
456
|
+
begin
|
457
|
+
# First create table if needed
|
458
|
+
create_table_result = turso_execute(
|
459
|
+
"CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)"
|
460
|
+
)
|
461
|
+
|
462
|
+
# Then insert the data
|
463
|
+
insert_result = turso_execute(
|
464
|
+
"INSERT OR REPLACE INTO gists (gist_id, title, url, description, tags, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
465
|
+
[gist_id, title, url, description, tags, created_at]
|
466
|
+
)
|
467
|
+
|
468
|
+
spinner.update_title("Gist saved successfully")
|
469
|
+
rescue => e
|
470
|
+
spinner.update_title("Error saving gist: #{e.message}")
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def list_gists
|
476
|
+
unless ENV['TURSO_DATABASE_URL'] && ENV['TURSO_AUTH_TOKEN']
|
477
|
+
CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Cloud credentials required"))
|
478
|
+
return
|
479
|
+
end
|
480
|
+
|
481
|
+
CLI::UI::Spinner.spin("Fetching gists...") do |spinner|
|
482
|
+
begin
|
483
|
+
# Create table if needed
|
484
|
+
create_table_result = turso_execute(
|
485
|
+
"CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)"
|
486
|
+
)
|
487
|
+
|
488
|
+
# Get all gists
|
489
|
+
result = turso_execute(
|
490
|
+
"SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC"
|
491
|
+
)
|
492
|
+
|
493
|
+
@gists = extract_rows(result)
|
494
|
+
|
495
|
+
if @gists.empty?
|
496
|
+
spinner.update_title("No gists found")
|
497
|
+
else
|
498
|
+
spinner.update_title("Found #{@gists.length} gists")
|
499
|
+
end
|
500
|
+
rescue => e
|
501
|
+
spinner.update_title("Error fetching gists: #{e.message}")
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
return if !@gists || @gists.empty?
|
506
|
+
|
507
|
+
CLI::UI.puts("\nYour Gists:")
|
508
|
+
@gists.each do |gist|
|
509
|
+
gist_id, title, url, description, tags, created_at = gist
|
510
|
+
CLI::UI.puts(CLI::UI.fmt("{{cyan:#{gist_id}}}: {{bold:#{title}}} - #{url}"))
|
511
|
+
CLI::UI.puts(CLI::UI.fmt(" Description: #{description}")) if description && !description.empty?
|
512
|
+
CLI::UI.puts(CLI::UI.fmt(" Tags: #{tags}")) if tags && !tags.empty?
|
513
|
+
CLI::UI.puts(CLI::UI.fmt(" Created: #{created_at}"))
|
514
|
+
CLI::UI.puts("")
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
def search_gists
|
519
|
+
unless ENV['TURSO_DATABASE_URL'] && ENV['TURSO_AUTH_TOKEN']
|
520
|
+
CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Cloud credentials required"))
|
521
|
+
return
|
522
|
+
end
|
523
|
+
|
524
|
+
term = CLI::UI.ask("Enter search term:")
|
525
|
+
return if term.empty?
|
526
|
+
|
527
|
+
CLI::UI::Spinner.spin("Searching gists...") do |spinner|
|
528
|
+
begin
|
529
|
+
result = turso_execute(
|
530
|
+
"SELECT gist_id, title, url, description, tags, created_at FROM gists WHERE title LIKE ? OR description LIKE ? OR tags LIKE ? ORDER BY created_at DESC",
|
531
|
+
["%#{term}%", "%#{term}%", "%#{term}%"]
|
532
|
+
)
|
533
|
+
|
534
|
+
@search_results = extract_rows(result)
|
535
|
+
|
536
|
+
if @search_results.empty?
|
537
|
+
spinner.update_title("No matching gists found")
|
538
|
+
else
|
539
|
+
spinner.update_title("Found #{@search_results.length} matching gists")
|
540
|
+
end
|
541
|
+
rescue => e
|
542
|
+
spinner.update_title("Search error: #{e.message}")
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
return if !@search_results || @search_results.empty?
|
547
|
+
|
548
|
+
CLI::UI.puts(CLI::UI.fmt("\n{{bold:Search Results:}}"))
|
549
|
+
@search_results.each do |gist|
|
550
|
+
gist_id, title, url, description, tags, created_at = gist
|
551
|
+
CLI::UI.puts(CLI::UI.fmt("{{cyan:#{gist_id}}}: {{bold:#{title}}} - #{url}"))
|
552
|
+
CLI::UI.puts(CLI::UI.fmt(" Description: #{description}")) if description && !description.empty?
|
553
|
+
CLI::UI.puts(CLI::UI.fmt(" Tags: #{tags}")) if tags && !tags.empty?
|
554
|
+
CLI::UI.puts(CLI::UI.fmt(" Created: #{created_at}"))
|
555
|
+
CLI::UI.puts("")
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def open_gist
|
560
|
+
list_gists
|
561
|
+
|
562
|
+
return if !@gists || @gists.empty?
|
563
|
+
|
564
|
+
gist_id = CLI::UI.ask("Enter gist ID to open:")
|
565
|
+
return if gist_id.empty?
|
566
|
+
|
567
|
+
# Find the matching gist
|
568
|
+
gist = @gists.find { |g| g[0] == gist_id }
|
569
|
+
|
570
|
+
unless gist
|
571
|
+
CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Gist not found"))
|
572
|
+
return
|
573
|
+
end
|
574
|
+
|
575
|
+
url = gist[2] # URL is at index 2
|
576
|
+
|
577
|
+
if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
|
578
|
+
system("start #{url}")
|
579
|
+
elsif RUBY_PLATFORM.match?(/darwin/)
|
580
|
+
system("open #{url}")
|
581
|
+
elsif RUBY_PLATFORM.match?(/linux/)
|
582
|
+
system("xdg-open #{url}")
|
583
|
+
else
|
584
|
+
CLI::UI.puts(CLI::UI.fmt("{{yellow:⚠}} Couldn't determine how to open URL on your platform. URL: #{url}"))
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
def delete_gist
|
589
|
+
list_gists
|
590
|
+
|
591
|
+
return if !@gists || @gists.empty?
|
592
|
+
|
593
|
+
gist_id = CLI::UI.ask("Enter gist ID to delete:")
|
594
|
+
return if gist_id.empty?
|
595
|
+
|
596
|
+
CLI::UI::Spinner.spin("Deleting gist...") do |spinner|
|
597
|
+
begin
|
598
|
+
result = turso_execute("DELETE FROM gists WHERE gist_id = ?", [gist_id])
|
599
|
+
|
600
|
+
# Try to determine if rows were affected
|
601
|
+
affected_rows = get_affected_rows(result)
|
602
|
+
|
603
|
+
if affected_rows > 0
|
604
|
+
spinner.update_title("Gist deleted successfully")
|
605
|
+
else
|
606
|
+
spinner.update_title("Gist not found")
|
607
|
+
end
|
608
|
+
rescue => e
|
609
|
+
spinner.update_title("Error deleting gist: #{e.message}")
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def manage_credentials
|
615
|
+
CLI::UI::Frame.divider("{{v}} Cloud Credentials")
|
616
|
+
|
617
|
+
current_url = ENV['TURSO_DATABASE_URL'] || "Not set"
|
618
|
+
current_token = ENV['TURSO_AUTH_TOKEN'] ? "[Hidden]" : "Not set"
|
619
|
+
|
620
|
+
CLI::UI.puts(CLI::UI.fmt("Current settings:"))
|
621
|
+
CLI::UI.puts(CLI::UI.fmt("Database URL: {{cyan:#{current_url}}}"))
|
622
|
+
CLI::UI.puts(CLI::UI.fmt("Auth Token: {{cyan:#{current_token}}}"))
|
623
|
+
CLI::UI.puts("")
|
624
|
+
|
625
|
+
CLI::UI::Prompt.ask("Credential Options:") do |handler|
|
626
|
+
handler.option("Update Database URL") do
|
627
|
+
url = CLI::UI.ask("Enter Turso Database URL:")
|
628
|
+
update_env_file('TURSO_DATABASE_URL', url) unless url.empty?
|
629
|
+
end
|
630
|
+
|
631
|
+
handler.option("Update Auth Token") do
|
632
|
+
token = CLI::UI.ask("Enter Turso Auth Token:")
|
633
|
+
update_env_file('TURSO_AUTH_TOKEN', token) unless token.empty?
|
634
|
+
end
|
635
|
+
|
636
|
+
handler.option("Test Connection") do
|
637
|
+
test_connection
|
638
|
+
end
|
639
|
+
|
640
|
+
handler.option("Back") { return }
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
private def update_env_file(key, value)
|
645
|
+
env_file = '.env'
|
646
|
+
|
647
|
+
# Read existing .env content
|
648
|
+
content = File.exist?(env_file) ? File.read(env_file) : ""
|
649
|
+
lines = content.split("\n")
|
650
|
+
|
651
|
+
# Find and replace the line with the key, or add it
|
652
|
+
key_found = false
|
653
|
+
lines.map! do |line|
|
654
|
+
if line.start_with?("#{key}=")
|
655
|
+
key_found = true
|
656
|
+
"#{key}=#{value}"
|
657
|
+
else
|
658
|
+
line
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
lines << "#{key}=#{value}" unless key_found
|
663
|
+
|
664
|
+
# Write back to file
|
665
|
+
File.write(env_file, lines.join("\n"))
|
666
|
+
|
667
|
+
# Reload environment
|
668
|
+
ENV[key] = value
|
669
|
+
|
670
|
+
CLI::UI.puts(CLI::UI.fmt("{{v}} Updated #{key} in .env file"))
|
671
|
+
end
|
672
|
+
|
673
|
+
private def test_connection
|
674
|
+
CLI::UI::Spinner.spin("Testing Turso connection...") do |spinner|
|
675
|
+
begin
|
676
|
+
unless ENV['TURSO_DATABASE_URL'] && ENV['TURSO_AUTH_TOKEN']
|
677
|
+
spinner.update_title("Cloud credentials not found")
|
678
|
+
next
|
679
|
+
end
|
680
|
+
|
681
|
+
result = turso_execute("SELECT 1")
|
682
|
+
spinner.update_title("Connection successful!")
|
683
|
+
rescue => e
|
684
|
+
spinner.update_title("Connection failed: #{e.message}")
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
def show_menu
|
690
|
+
CLI::UI::Frame.divider("{{v}} Gist Manager")
|
691
|
+
loop do
|
692
|
+
CLI::UI::Prompt.ask("Gist Manager Options:") do |handler|
|
693
|
+
handler.option("Add Gist") { add_gist }
|
694
|
+
handler.option("List Gists") { list_gists }
|
695
|
+
handler.option("Search Gists") { search_gists }
|
696
|
+
handler.option("Open Gist") { open_gist }
|
697
|
+
handler.option("Delete Gist") { delete_gist }
|
698
|
+
handler.option("Cloud Settings") { manage_credentials }
|
699
|
+
handler.option("Exit") { return }
|
700
|
+
end
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
private def turso_execute(sql, params = [])
|
705
|
+
url = ENV['TURSO_DATABASE_URL']
|
706
|
+
auth_token = ENV['TURSO_AUTH_TOKEN']
|
707
|
+
|
708
|
+
# Ensure URL has the correct endpoint path for v2 pipeline
|
709
|
+
url = url.end_with?('/v2/pipeline') ? url : "#{url}/v2/pipeline"
|
710
|
+
|
711
|
+
uri = URI(url)
|
712
|
+
request = Net::HTTP::Post.new(uri)
|
713
|
+
request["Authorization"] = "Bearer #{auth_token}"
|
714
|
+
request["Content-Type"] = "application/json"
|
715
|
+
|
716
|
+
# Format parameters into the required format
|
717
|
+
formatted_params = params.map do |param|
|
718
|
+
type = case param
|
719
|
+
when Integer, /^\d+$/
|
720
|
+
"integer"
|
721
|
+
when Float, /^\d+\.\d+$/
|
722
|
+
"float"
|
723
|
+
when TrueClass, FalseClass, /^(true|false)$/i
|
724
|
+
"boolean"
|
725
|
+
when NilClass
|
726
|
+
"null"
|
727
|
+
else
|
728
|
+
"text"
|
729
|
+
end
|
730
|
+
|
731
|
+
{
|
732
|
+
"type": type,
|
733
|
+
"value": param.to_s
|
734
|
+
}
|
735
|
+
end
|
736
|
+
|
737
|
+
# Build the request body
|
738
|
+
stmt = {
|
739
|
+
"sql": sql,
|
740
|
+
"args": formatted_params
|
741
|
+
}
|
742
|
+
|
743
|
+
payload = {
|
744
|
+
"requests": [
|
745
|
+
{ "type": "execute", "stmt": stmt },
|
746
|
+
{ "type": "close" }
|
747
|
+
]
|
748
|
+
}
|
749
|
+
|
750
|
+
request.body = JSON.generate(payload)
|
751
|
+
|
752
|
+
begin
|
753
|
+
# Log the request for debugging
|
754
|
+
ensure_log_directory
|
755
|
+
File.open('logs/turso_requests.log', 'a') do |f|
|
756
|
+
f.puts "#{Time.now} - REQUEST to #{uri}"
|
757
|
+
f.puts "SQL: #{sql}"
|
758
|
+
f.puts "Params: #{params.inspect}"
|
759
|
+
f.puts "Formatted Params: #{formatted_params.inspect}"
|
760
|
+
f.puts "Payload: #{payload.to_json}"
|
761
|
+
f.puts "-" * 80
|
762
|
+
end
|
763
|
+
|
764
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
765
|
+
http.request(request)
|
766
|
+
end
|
767
|
+
|
768
|
+
# Log the response
|
769
|
+
ensure_log_directory
|
770
|
+
File.open('logs/turso_responses.log', 'a') do |f|
|
771
|
+
f.puts "#{Time.now} - RESPONSE (#{response.code})"
|
772
|
+
f.puts "Body: #{response.body}"
|
773
|
+
f.puts "-" * 80
|
774
|
+
end
|
775
|
+
|
776
|
+
if response.code != "200"
|
777
|
+
error_msg = "HTTP error: #{response.code} - #{response.body}"
|
778
|
+
log_error(error_msg)
|
779
|
+
raise error_msg
|
780
|
+
end
|
781
|
+
|
782
|
+
JSON.parse(response.body)
|
783
|
+
rescue JSON::ParserError => e
|
784
|
+
error_msg = "JSON parse error: #{e.message}. Response body: #{response&.body}"
|
785
|
+
log_error(error_msg)
|
786
|
+
raise error_msg
|
787
|
+
rescue => e
|
788
|
+
error_msg = "Query failed: #{e.message}"
|
789
|
+
log_error(error_msg)
|
790
|
+
raise error_msg
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
private def log_error(message)
|
795
|
+
ensure_log_directory
|
796
|
+
File.open('logs/turso_errors.log', 'a') do |f|
|
797
|
+
f.puts "#{Time.now} - ERROR"
|
798
|
+
f.puts message
|
799
|
+
f.puts "-" * 80
|
800
|
+
end
|
801
|
+
# Also output to console
|
802
|
+
CLI::UI.puts(CLI::UI.fmt("{{red:ERROR}}: #{message}"))
|
803
|
+
end
|
804
|
+
|
805
|
+
private def ensure_log_directory
|
806
|
+
FileUtils.mkdir_p('logs') unless Dir.exist?('logs')
|
807
|
+
end
|
808
|
+
|
809
|
+
private def extract_rows(result)
|
810
|
+
rows = []
|
811
|
+
|
812
|
+
if result &&
|
813
|
+
result["results"] &&
|
814
|
+
result["results"][0] &&
|
815
|
+
result["results"][0]["type"] == "ok" &&
|
816
|
+
result["results"][0]["response"] &&
|
817
|
+
result["results"][0]["response"]["type"] == "execute" &&
|
818
|
+
result["results"][0]["response"]["result"]
|
819
|
+
|
820
|
+
result_data = result["results"][0]["response"]["result"]
|
821
|
+
|
822
|
+
if result_data["rows"]
|
823
|
+
rows = result_data["rows"]
|
824
|
+
end
|
825
|
+
end
|
826
|
+
|
827
|
+
rows
|
828
|
+
end
|
829
|
+
|
830
|
+
private def get_affected_rows(result)
|
831
|
+
if result &&
|
832
|
+
result["results"] &&
|
833
|
+
result["results"][0] &&
|
834
|
+
result["results"][0]["type"] == "ok" &&
|
835
|
+
result["results"][0]["response"] &&
|
836
|
+
result["results"][0]["response"]["type"] == "execute" &&
|
837
|
+
result["results"][0]["response"]["result"] &&
|
838
|
+
result["results"][0]["response"]["result"]["affected_row_count"]
|
839
|
+
|
840
|
+
return result["results"][0]["response"]["result"]["affected_row_count"]
|
841
|
+
end
|
842
|
+
|
843
|
+
0
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
415
847
|
class Options
|
416
848
|
def self.show_options
|
417
849
|
CLI::UI::Prompt.instructions_color = CLI::UI::Color::GRAY
|
@@ -422,6 +854,7 @@ module Greeenboii
|
|
422
854
|
handler.option("{{green:Website Builder}}") { |_selection| WebsiteBuilder.build_website }
|
423
855
|
handler.option("{{yellow:Todo List}}") { |_selection| TodoList.new.show_menu }
|
424
856
|
handler.option("{{cyan:Search Engine}}") { |_selection| Search.perform_search }
|
857
|
+
handler.option("{{blue:Gist Manager}}") { |_selection| GistManager.new.show_menu }
|
425
858
|
handler.option("{{red:Exit}}") { |_selection| exit }
|
426
859
|
end
|
427
860
|
end
|
data/lib/libsql.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# lib/libsql.rb
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'dotenv'
|
6
|
+
|
7
|
+
module Libsql
|
8
|
+
class Database
|
9
|
+
def initialize(url:, auth_token:)
|
10
|
+
# Ensure URL has the correct endpoint path
|
11
|
+
@url = url.end_with?('/v2/pipeline') ? url : "#{url}/v2/pipeline"
|
12
|
+
@auth_token = auth_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def connect
|
16
|
+
connection = Connection.new(@url, @auth_token)
|
17
|
+
yield connection if block_given?
|
18
|
+
connection
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Connection
|
23
|
+
def initialize(url, auth_token)
|
24
|
+
@url = url
|
25
|
+
@auth_token = auth_token
|
26
|
+
@last_changes = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute(query, *params)
|
30
|
+
params = params.flatten if params.is_a?(Array)
|
31
|
+
execute_stmt(query, params)
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute_batch(batch_query)
|
35
|
+
# Split the batch query into individual statements
|
36
|
+
statements = batch_query.split(';').map(&:strip).reject(&:empty?)
|
37
|
+
|
38
|
+
requests = statements.map do |stmt|
|
39
|
+
{ "type" => "execute", "stmt" => { "sql" => stmt } }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add close request at the end
|
43
|
+
requests << { "type" => "close" }
|
44
|
+
|
45
|
+
execute_pipeline(requests)
|
46
|
+
end
|
47
|
+
|
48
|
+
def query(query, *params)
|
49
|
+
params = params.flatten if params.is_a?(Array)
|
50
|
+
result = execute_stmt(query, params)
|
51
|
+
QueryResult.new(result)
|
52
|
+
end
|
53
|
+
|
54
|
+
def changes
|
55
|
+
@last_changes
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def execute_stmt(sql, params)
|
61
|
+
requests = [
|
62
|
+
{ "type" => "execute", "stmt" => { "sql" => sql, "args" => params } },
|
63
|
+
{ "type" => "close" }
|
64
|
+
]
|
65
|
+
|
66
|
+
execute_pipeline(requests)
|
67
|
+
end
|
68
|
+
|
69
|
+
def execute_pipeline(requests)
|
70
|
+
uri = URI(@url)
|
71
|
+
request = Net::HTTP::Post.new(uri)
|
72
|
+
request["Authorization"] = "Bearer #{@auth_token}"
|
73
|
+
request["Content-Type"] = "application/json"
|
74
|
+
|
75
|
+
request.body = JSON.generate({
|
76
|
+
"requests" => requests
|
77
|
+
})
|
78
|
+
|
79
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
80
|
+
http.request(request)
|
81
|
+
end
|
82
|
+
|
83
|
+
if response.code != "200"
|
84
|
+
raise "Query failed: #{response.body}"
|
85
|
+
end
|
86
|
+
|
87
|
+
response_data = JSON.parse(response.body)
|
88
|
+
|
89
|
+
# Extract the affected row count for UPDATE/DELETE operations
|
90
|
+
if response_data["results"] &&
|
91
|
+
response_data["results"][0] &&
|
92
|
+
response_data["results"][0]["type"] == "ok" &&
|
93
|
+
response_data["results"][0]["response"] &&
|
94
|
+
response_data["results"][0]["response"]["type"] == "execute" &&
|
95
|
+
response_data["results"][0]["response"]["result"] &&
|
96
|
+
response_data["results"][0]["response"]["result"]["affected_row_count"]
|
97
|
+
@last_changes = response_data["results"][0]["response"]["result"]["affected_row_count"]
|
98
|
+
end
|
99
|
+
|
100
|
+
response_data
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class QueryResult
|
105
|
+
def initialize(result)
|
106
|
+
@result = result
|
107
|
+
|
108
|
+
# Extract columns and rows from Turso's format
|
109
|
+
if @result["results"] &&
|
110
|
+
@result["results"][0] &&
|
111
|
+
@result["results"][0]["type"] == "ok" &&
|
112
|
+
@result["results"][0]["response"] &&
|
113
|
+
@result["results"][0]["response"]["type"] == "execute" &&
|
114
|
+
@result["results"][0]["response"]["result"]
|
115
|
+
|
116
|
+
result_data = @result["results"][0]["response"]["result"]
|
117
|
+
@columns = result_data["cols"] || []
|
118
|
+
@rows = result_data["rows"] || []
|
119
|
+
else
|
120
|
+
@columns = []
|
121
|
+
@rows = []
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def empty?
|
126
|
+
@rows.empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
def to_a
|
130
|
+
@rows
|
131
|
+
end
|
132
|
+
|
133
|
+
def close
|
134
|
+
# No-op for HTTP API
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Helper method to create a database connection using .env credentials
|
139
|
+
def self.connect_with_env
|
140
|
+
Dotenv.load
|
141
|
+
|
142
|
+
# Check for required environment variables
|
143
|
+
unless ENV['TURSO_DATABASE_URL'] && ENV['TURSO_AUTH_TOKEN']
|
144
|
+
raise "Missing TURSO_DATABASE_URL or TURSO_AUTH_TOKEN in .env file"
|
145
|
+
end
|
146
|
+
|
147
|
+
db = Database.new(
|
148
|
+
url: ENV['TURSO_DATABASE_URL'],
|
149
|
+
auth_token: ENV['TURSO_AUTH_TOKEN']
|
150
|
+
)
|
151
|
+
|
152
|
+
# If a block is given, yield connection
|
153
|
+
if block_given?
|
154
|
+
db.connect { |conn| yield conn }
|
155
|
+
else
|
156
|
+
db
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
2025-05-06 12:20:41 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
2
|
+
SQL: CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)
|
3
|
+
Params: []
|
4
|
+
Formatted Params: []
|
5
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)","args":[]}},{"type":"close"}]}
|
6
|
+
--------------------------------------------------------------------------------
|
7
|
+
2025-05-06 12:20:42 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
8
|
+
SQL: INSERT OR REPLACE INTO gists (gist_id, title, url, description, tags, created_at) VALUES (?, ?, ?, ?, ?, ?)
|
9
|
+
Params: ["2fc9fa3eac1f0ddeb2f0b23aa21bbe4b", "compilerdesign", "https://gist.github.com/greeenboi/2fc9fa3eac1f0ddeb2f0b23aa21bbe4b", "nil", "nil", "2025-05-06 12:20:41"]
|
10
|
+
Formatted Params: [{:type=>"text", :value=>"2fc9fa3eac1f0ddeb2f0b23aa21bbe4b"}, {:type=>"text", :value=>"compilerdesign"}, {:type=>"text", :value=>"https://gist.github.com/greeenboi/2fc9fa3eac1f0ddeb2f0b23aa21bbe4b"}, {:type=>"text", :value=>"nil"}, {:type=>"text", :value=>"nil"}, {:type=>"text", :value=>"2025-05-06 12:20:41"}]
|
11
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"INSERT OR REPLACE INTO gists (gist_id, title, url, description, tags, created_at) VALUES (?, ?, ?, ?, ?, ?)","args":[{"type":"text","value":"2fc9fa3eac1f0ddeb2f0b23aa21bbe4b"},{"type":"text","value":"compilerdesign"},{"type":"text","value":"https://gist.github.com/greeenboi/2fc9fa3eac1f0ddeb2f0b23aa21bbe4b"},{"type":"text","value":"nil"},{"type":"text","value":"nil"},{"type":"text","value":"2025-05-06 12:20:41"}]}},{"type":"close"}]}
|
12
|
+
--------------------------------------------------------------------------------
|
13
|
+
2025-05-06 12:20:46 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
14
|
+
SQL: CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)
|
15
|
+
Params: []
|
16
|
+
Formatted Params: []
|
17
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)","args":[]}},{"type":"close"}]}
|
18
|
+
--------------------------------------------------------------------------------
|
19
|
+
2025-05-06 12:20:46 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
20
|
+
SQL: SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC
|
21
|
+
Params: []
|
22
|
+
Formatted Params: []
|
23
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC","args":[]}},{"type":"close"}]}
|
24
|
+
--------------------------------------------------------------------------------
|
25
|
+
2025-05-06 12:35:11 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
26
|
+
SQL: SELECT 1
|
27
|
+
Params: []
|
28
|
+
Formatted Params: []
|
29
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"SELECT 1","args":[]}},{"type":"close"}]}
|
30
|
+
--------------------------------------------------------------------------------
|
31
|
+
2025-05-06 12:35:14 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
32
|
+
SQL: CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)
|
33
|
+
Params: []
|
34
|
+
Formatted Params: []
|
35
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)","args":[]}},{"type":"close"}]}
|
36
|
+
--------------------------------------------------------------------------------
|
37
|
+
2025-05-06 12:35:14 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
38
|
+
SQL: SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC
|
39
|
+
Params: []
|
40
|
+
Formatted Params: []
|
41
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC","args":[]}},{"type":"close"}]}
|
42
|
+
--------------------------------------------------------------------------------
|
43
|
+
2025-05-06 12:38:06 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
44
|
+
SQL: CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)
|
45
|
+
Params: []
|
46
|
+
Formatted Params: []
|
47
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)","args":[]}},{"type":"close"}]}
|
48
|
+
--------------------------------------------------------------------------------
|
49
|
+
2025-05-06 12:38:06 +0530 - REQUEST to https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline
|
50
|
+
SQL: SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC
|
51
|
+
Params: []
|
52
|
+
Formatted Params: []
|
53
|
+
Payload: {"requests":[{"type":"execute","stmt":{"sql":"SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC","args":[]}},{"type":"close"}]}
|
54
|
+
--------------------------------------------------------------------------------
|