-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #162 Adds a new `saved_queries` table, model, and endpoints. These can be used to expose specific queries over public or authed HTTP, for use in public dashboards and websites, where embedding a connection string for direct connection or using 'connection string auth' is unsuitable. See https://github.com/webhookdb/docs/blob/591f51b4c0ec5ebe3cf1172a71013a6dad21547c/docs/integrating/saved-queries.md for full documentation. See webhookdb/docs#5 for docs. See webhookdb/webhookdb-cli#64 for CLI changes. Also includes some webterm changes: - Inline input (the one used for prompts) submits automatically when multiple lines are pasted. This was required for pasting SQL to work. - The input is full width at the beginning (before, it would expand to the widest log line).
- Loading branch information
1 parent
cf920ce
commit 449ff62
Showing
16 changed files
with
893 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# frozen_string_literal: true | ||
|
||
Sequel.migration do | ||
change do | ||
create_table(:saved_queries) do | ||
primary_key :id | ||
timestamptz :created_at, null: false, default: Sequel.function(:now) | ||
timestamptz :updated_at | ||
foreign_key :organization_id, :organizations, null: false, unique: true, on_delete: :cascade | ||
foreign_key :created_by_id, :customers, on_delete: :set_null | ||
text :opaque_id, null: false, unique: true | ||
text :description, null: false | ||
text :sql, null: false | ||
boolean :public, null: false, default: false | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# frozen_string_literal: true | ||
|
||
require "webhookdb/api" | ||
require "webhookdb/saved_query" | ||
|
||
class Webhookdb::API::SavedQueries < Webhookdb::API::V1 | ||
resource :organizations do | ||
route_param :org_identifier do | ||
resource :saved_queries do | ||
helpers do | ||
def lookup! | ||
org = lookup_org! | ||
# We can add other identifiers in the future | ||
cq = org.saved_queries_dataset[opaque_id: params[:query_identifier]] | ||
merror!(403, "There is no saved query with that identifier.") if cq.nil? | ||
return cq | ||
end | ||
|
||
def guard_editable!(customer, cq) | ||
return if customer === cq.created_by | ||
return if has_admin?(cq.organization, customer:) | ||
permission_error!("You must be the query's creator or an org admin.") | ||
end | ||
|
||
def execute_readonly_query_with_suggestion(org, sql) | ||
r, message = execute_readonly_query(org, sql) | ||
return r, nil unless r.nil? | ||
msg = "Something went wrong running your query. Perhaps a table it depends on was deleted. " \ | ||
"Check out #{Webhookdb::SavedQuery::DOCS_URL} for troubleshooting tips. " \ | ||
"Here's what went wrong: #{message}" | ||
return r, msg | ||
end | ||
end | ||
|
||
desc "Returns a list of all custom queries associated with the org." | ||
get do | ||
queries = lookup_org!.saved_queries | ||
message = "" | ||
if queries.empty? | ||
message = "This organization doesn't have any saved queries yet.\n" \ | ||
"Use `webhookdb saved-query create` to set one up." | ||
end | ||
present_collection queries, with: SavedQueryEntity, message: | ||
end | ||
|
||
desc "Creates a custom query." | ||
params do | ||
optional :description, type: String, prompt: "What is the query used for? " | ||
optional :sql, type: String, prompt: "Enter the SQL you would like to run: " | ||
optional :public, type: Boolean | ||
end | ||
post :create do | ||
cust = current_customer | ||
org = lookup_org! | ||
_, errmsg = execute_readonly_query_with_suggestion(org, params[:sql]) | ||
if errmsg | ||
Webhookdb::API::Helpers.prompt_for_required_param!( | ||
request, | ||
:sql, | ||
"Enter a new query:", | ||
output: "That query was invalid. #{errmsg}\n" \ | ||
"You can iterate on your query by connecting to your database from any SQL editor.\n" \ | ||
"Use `webhookdb db connection` to get your query string.", | ||
) | ||
end | ||
cq = Webhookdb::SavedQuery.create( | ||
description: params[:description], | ||
sql: params[:sql], | ||
organization: org, | ||
created_by: cust, | ||
public: params[:public] || false, | ||
) | ||
message = "You have created a new saved query with an id of '#{cq.opaque_id}'. " \ | ||
"You can run it through the CLI, or through the API with or without authentication. " \ | ||
"See #{Webhookdb::SavedQuery::DOCS_URL} for more information." | ||
status 200 | ||
present cq, with: SavedQueryEntity, message: | ||
end | ||
|
||
route_param :query_identifier, type: String do | ||
desc "Returns the query with the given identifier." | ||
get do | ||
cq = lookup! | ||
status 200 | ||
message = "See #{Webhookdb::SavedQuery::DOCS_URL} to see how to run or modify your query." | ||
present cq, with: SavedQueryEntity, message: | ||
end | ||
|
||
desc "Runs the query with the given identifier." | ||
get :run do | ||
_customer = current_customer | ||
org = lookup_org! | ||
cq = lookup! | ||
r, msg = execute_readonly_query_with_suggestion(org, cq.sql) | ||
merror!(400, msg) if r.nil? | ||
status 200 | ||
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached}) | ||
end | ||
|
||
desc "Updates the field on a custom query." | ||
params do | ||
optional :field, type: String, prompt: "What field would you like to update (one of: " \ | ||
"#{Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.join(', ')}): " | ||
optional :value, type: String, prompt: "What is the new value? " | ||
end | ||
post :update do | ||
customer = current_customer | ||
cq = lookup! | ||
guard_editable!(customer, cq) | ||
# Instead of specifying which values are valid for the optional `field` param in the param declaration, | ||
# we do the validation here so that we can provide a more helpful error message | ||
unless Webhookdb::SavedQuery::CLI_EDITABLE_FIELDS.include?(params[:field]) | ||
merror!(400, "That field is not editable.") | ||
end | ||
value = params[:value] | ||
case params[:field] | ||
when "public" | ||
begin | ||
value = Webhookdb.parse_bool(value) | ||
rescue ArgumentError => e | ||
Webhookdb::API::Helpers.prompt_for_required_param!( | ||
request, | ||
:value, | ||
e.message + "\nAny boolean-like string (true, false, yes, no, etc) will work:", | ||
) | ||
end | ||
cq.public = value | ||
when "sql" | ||
r, msg = execute_readonly_query_with_suggestion(cq.organization, value) | ||
if r.nil? | ||
Webhookdb::API::Helpers.prompt_for_required_param!( | ||
request, | ||
:value, | ||
"Enter your query:", | ||
output: msg, | ||
) | ||
end | ||
cq.sql = value | ||
else | ||
cq.send(:"#{params[:field]}=", value) | ||
end | ||
cq.save_changes | ||
status 200 | ||
# Do not render the value here, it can be very long. | ||
message = "You have updated '#{params[:field]}' on saved query '#{cq.opaque_id}'." | ||
present cq, with: SavedQueryEntity, message: | ||
end | ||
|
||
params do | ||
optional :field, type: String, values: Webhookdb::SavedQuery::INFO_FIELDS.keys + [""] | ||
end | ||
post :info do | ||
cq = lookup! | ||
data = Webhookdb::SavedQuery::INFO_FIELDS. | ||
to_h { |k, v| [k.to_sym, cq.send(v)] } | ||
|
||
field_name = params[:field] | ||
blocks = Webhookdb::Formatting.blocks | ||
if field_name.present? | ||
blocks.line(data.fetch(field_name.to_sym)) | ||
else | ||
rows = data.map do |k, v| | ||
[k.to_s.humanize, v.to_s] | ||
end | ||
blocks.table(["Field", "Value"], rows) | ||
end | ||
r = {blocks: blocks.as_json} | ||
status 200 | ||
present r | ||
end | ||
|
||
post :delete do | ||
customer = current_customer | ||
cq = lookup! | ||
guard_editable!(customer, cq) | ||
cq.destroy | ||
status 200 | ||
present cq, with: SavedQueryEntity, | ||
message: "You have successfully deleted the saved query '#{cq.description}'." | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
resource :saved_queries do | ||
route_param :query_identifier, type: String do | ||
get :run do | ||
# This endpoint can be used publicly, so should expose as little information as possible. | ||
# Do not expose permissions or query details. | ||
cq = Webhookdb::SavedQuery[opaque_id: params[:query_identifier]] | ||
forbidden! if cq.nil? | ||
if cq.private? | ||
authed = Webhookdb::API::ConnstrAuth.find_authed([cq.organization], request) | ||
if !authed && (cust = current_customer?) | ||
authed = !cust.verified_memberships_dataset.where(organization: cq.organization).empty? | ||
end | ||
forbidden! unless authed | ||
end | ||
r, _ = execute_readonly_query(cq.organization, cq.sql) | ||
merror!(400, "Something went wrong running the query.") if r.nil? | ||
status 200 | ||
present({rows: r.rows, headers: r.columns, max_rows_reached: r.max_rows_reached}) | ||
end | ||
end | ||
end | ||
|
||
class SavedQueryEntity < Webhookdb::API::BaseEntity | ||
expose :opaque_id, as: :id | ||
expose :description | ||
expose :sql | ||
expose :public | ||
expose :run_url | ||
|
||
def self.display_headers | ||
return [[:id, "Id"], [:description, "Description"], [:public, "Public"], [:run_url, "Run URL"]] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# frozen_string_literal: true | ||
|
||
require "faker" | ||
|
||
require "webhookdb" | ||
require "webhookdb/fixtures" | ||
|
||
module Webhookdb::Fixtures::SavedQueries | ||
extend Webhookdb::Fixtures | ||
|
||
fixtured_class Webhookdb::SavedQuery | ||
|
||
base :saved_query do | ||
self.description ||= Faker::Lorem.sentence | ||
self.sql ||= "SELECT * FROM mytable" | ||
end | ||
|
||
before_saving do |instance| | ||
instance.organization ||= Webhookdb::Fixtures.organization.create | ||
instance | ||
end | ||
|
||
decorator :created_by do |c={}| | ||
c = Webhookdb::Fixtures.customer.create(c) unless c.is_a?(Webhookdb::Customer) | ||
self.created_by = c | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.