nquery 0.1.0 → 0.1.2

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +263 -24
  4. data/app/assets/builds/nquery/application.css +61 -4
  5. data/app/assets/builds/nquery/application.js +107 -12
  6. data/app/controllers/concerns/nquery/breadcrumbs.rb +5 -3
  7. data/app/controllers/concerns/nquery/chart_actions.rb +1 -0
  8. data/app/controllers/concerns/nquery/chart_results.rb +17 -3
  9. data/app/controllers/nquery/admin/data_sources_controller.rb +49 -2
  10. data/app/controllers/nquery/admin/groups_controller.rb +8 -2
  11. data/app/controllers/nquery/admin/users_controller.rb +6 -1
  12. data/app/controllers/nquery/application_controller.rb +45 -28
  13. data/app/controllers/nquery/dashboards_controller.rb +30 -0
  14. data/app/controllers/nquery/embed/charts_controller.rb +1 -0
  15. data/app/controllers/nquery/embed/dashboards_controller.rb +1 -0
  16. data/app/controllers/nquery/errors_controller.rb +11 -0
  17. data/app/controllers/nquery/home_controller.rb +1 -0
  18. data/app/controllers/nquery/onboarding/admins_controller.rb +42 -0
  19. data/app/controllers/nquery/onboarding/base_controller.rb +27 -0
  20. data/app/controllers/nquery/onboarding/companies_controller.rb +30 -0
  21. data/app/controllers/nquery/onboarding/confirmations_controller.rb +48 -0
  22. data/app/controllers/nquery/onboarding/congrats_controller.rb +15 -0
  23. data/app/controllers/nquery/queries_controller.rb +0 -1
  24. data/app/controllers/nquery/sessions_controller.rb +14 -17
  25. data/app/helpers/nquery/data_sources_helper.rb +13 -0
  26. data/app/helpers/nquery/groups_helper.rb +19 -0
  27. data/app/javascript/nquery/controllers/data_source_form_controller.js +25 -0
  28. data/app/mailers/nquery/application_mailer.rb +7 -0
  29. data/app/mailers/nquery/devise_mailer.rb +9 -0
  30. data/app/models/nquery/audit.rb +2 -2
  31. data/app/models/nquery/collection.rb +1 -1
  32. data/app/models/nquery/dashboard.rb +1 -1
  33. data/app/models/nquery/data_source.rb +139 -4
  34. data/app/models/nquery/organization.rb +38 -0
  35. data/app/models/nquery/query.rb +8 -0
  36. data/app/models/nquery/user.rb +45 -10
  37. data/app/views/nquery/admin/data_sources/_form.html.erb +121 -0
  38. data/app/views/nquery/admin/data_sources/edit.html.erb +6 -8
  39. data/app/views/nquery/admin/data_sources/new.html.erb +6 -7
  40. data/app/views/nquery/admin/groups/_form.html.erb +47 -0
  41. data/app/views/nquery/admin/groups/edit.html.erb +15 -6
  42. data/app/views/nquery/admin/groups/index.html.erb +25 -20
  43. data/app/views/nquery/admin/groups/new.html.erb +6 -6
  44. data/app/views/nquery/admin/groups/show.html.erb +98 -19
  45. data/app/views/nquery/admin/logs/index.html.erb +26 -30
  46. data/app/views/nquery/charts/_builder_form.html.erb +6 -4
  47. data/app/views/nquery/charts/edit.html.erb +2 -1
  48. data/app/views/nquery/dashboards/index.html.erb +6 -0
  49. data/app/views/nquery/dashboards/new.html.erb +34 -0
  50. data/app/views/nquery/devise_mailer/confirmation_instructions.html.erb +5 -0
  51. data/app/views/nquery/devise_mailer/confirmation_instructions.text.erb +5 -0
  52. data/app/views/nquery/errors/internal_server_error.html.erb +10 -0
  53. data/app/views/nquery/errors/not_found.html.erb +10 -0
  54. data/app/views/nquery/home/index.html.erb +7 -15
  55. data/app/views/nquery/onboarding/admins/new.html.erb +21 -0
  56. data/app/views/nquery/onboarding/companies/new.html.erb +25 -0
  57. data/app/views/nquery/onboarding/confirmations/show.html.erb +17 -0
  58. data/app/views/nquery/onboarding/congrats/show.html.erb +11 -0
  59. data/app/views/nquery/sessions/new.html.erb +8 -9
  60. data/app/views/nquery/shared/_auth_shell.html.erb +1 -1
  61. data/app/views/nquery/shared/_topbar.html.erb +0 -4
  62. data/config/routes.rb +26 -7
  63. data/db/migrate/20260721000001_create_nquery_tables.rb +1 -1
  64. data/db/migrate/20260722000001_add_key_to_nquery_data_sources.rb +8 -0
  65. data/db/migrate/20260722000002_create_nquery_organizations.rb +11 -0
  66. data/db/migrate/20260722000003_add_devise_to_nquery_users.rb +35 -0
  67. data/db/migrate/20260722000004_add_onboarding_completed_at_to_nquery_organizations.rb +7 -0
  68. data/lib/generators/nquery/install_generator.rb +9 -17
  69. data/lib/nquery/configuration.rb +7 -23
  70. data/lib/nquery/csv_importer.rb +3 -3
  71. data/lib/nquery/data_sources/adapter.rb +4 -0
  72. data/lib/nquery/data_sources/postgresql_adapter.rb +15 -1
  73. data/lib/nquery/data_sources/rails_adapter.rb +5 -0
  74. data/lib/nquery/data_sources/syncer.rb +50 -0
  75. data/lib/nquery/encryption.rb +50 -0
  76. data/lib/nquery/engine.rb +42 -3
  77. data/lib/nquery/onboarding/admin_provisioner.rb +40 -0
  78. data/lib/nquery/onboarding/password_confirmation.rb +46 -0
  79. data/lib/nquery/onboarding.rb +40 -0
  80. data/lib/nquery/permissions/resolver.rb +14 -4
  81. data/lib/nquery/query_runner.rb +5 -9
  82. data/lib/nquery/readonly_sql.rb +25 -0
  83. data/lib/nquery/seeder.rb +25 -34
  84. data/lib/nquery/setup.rb +75 -0
  85. data/lib/nquery/version.rb +1 -1
  86. data/lib/nquery.rb +9 -0
  87. data/lib/tasks/nquery.rake +6 -0
  88. data/nquery.gemspec +11 -5
  89. metadata +135 -26
  90. data/app/controllers/nquery/registrations_controller.rb +0 -31
  91. data/app/views/nquery/registrations/new.html.erb +0 -34
@@ -1,37 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators/base"
4
- require "rails/generators/migration"
5
4
 
6
5
  module Nquery
7
6
  module Generators
8
7
  class InstallGenerator < Rails::Generators::Base
9
- include Rails::Generators::Migration
10
8
  source_root File.expand_path("templates", __dir__)
11
9
 
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
10
+ desc "Install nquery: copies initializer and prints setup instructions"
17
11
 
18
12
  def copy_initializer
19
13
  template "nquery.rb.tt", "config/initializers/nquery.rb"
20
14
  end
21
15
 
22
16
  def show_route_snippet
23
- say "\nAdd to config/routes.rb:\n", :green
17
+ say "\nNext steps:\n", :green
18
+ say " 1. rails active_storage:install # required for organization logo/cover uploads"
19
+ say " 2. rails db:migrate # engine migrations load from the gem automatically"
20
+ say " 3. rails nquery:setup"
21
+ say " 4. Add to config/routes.rb:\n"
24
22
  say <<~ROUTE
25
- authenticate :user, ->(u) { u.admin? } do
26
- mount Nquery::Engine, at: "/nquery"
27
- end
23
+ mount Nquery::Engine, at: "/nquery"
28
24
  ROUTE
29
- end
30
-
31
- private
32
-
33
- def migration_version
34
- "[#{ActiveRecord::Migration.current_version}]"
25
+ say " 5. Set mailer_sender + smtp in config/initializers/nquery.rb"
26
+ say " 6. Visit /nquery and complete the onboarding wizard\n"
35
27
  end
36
28
  end
37
29
  end
@@ -2,39 +2,23 @@
2
2
 
3
3
  module Nquery
4
4
  class Configuration
5
- attr_accessor :authentication_mode
6
- attr_accessor :current_user_method
7
5
  attr_accessor :parent_controller
8
6
  attr_accessor :data_sources
7
+ attr_accessor :default_data_source
8
+ attr_accessor :mailer_sender
9
+ attr_accessor :smtp
9
10
  attr_accessor :query_timeout
10
11
  attr_accessor :query_row_limit
11
12
  attr_accessor :embed_secret
12
13
 
13
14
  def initialize
14
- @authentication_mode = :standalone
15
- @current_user_method = :current_nquery_user
16
- @data_sources = { main: :rails }
15
+ @data_sources = { main: { adapter: :rails, name: "Application database" } }
16
+ @default_data_source = :main
17
+ @mailer_sender = nil
18
+ @smtp = {}
17
19
  @query_timeout = 15
18
20
  @query_row_limit = 10_000
19
21
  @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
22
  end
39
23
 
40
24
  def embed_signing_key
@@ -31,10 +31,10 @@ module Nquery
31
31
  create_import_table(connection, table_name, columns)
32
32
  insert_rows(connection, table_name, rows, columns)
33
33
 
34
- data_source = DataSource.create!(
34
+ DataSource.create!(
35
35
  name: @name,
36
- adapter: connection.adapter_name.downcase.include?("postgres") ? "postgresql" : "rails",
37
- connection_config: { import_table: table_name }.to_json
36
+ adapter: "rails",
37
+ connection_config_hash: { "import_table" => table_name }
38
38
  )
39
39
 
40
40
  upload.update!(status: "completed", column_mapping: columns)
@@ -29,6 +29,10 @@ module Nquery
29
29
  def execute_readonly(statement, timeout: 15, row_limit: 10_000)
30
30
  raise NotImplementedError
31
31
  end
32
+
33
+ def test_connection
34
+ raise NotImplementedError
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -13,6 +13,11 @@ module Nquery
13
13
  end
14
14
  end
15
15
 
16
+ def test_connection
17
+ with_connection { |conn| conn.exec_query("SELECT 1") }
18
+ true
19
+ end
20
+
16
21
  def execute_readonly(statement, timeout: 15, row_limit: 10_000)
17
22
  with_connection do |conn|
18
23
  rows = []
@@ -35,12 +40,21 @@ module Nquery
35
40
 
36
41
  def with_connection
37
42
  config = @data_source.connection_config_hash
38
- klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
43
+ klass = ephemeral_connection_class
39
44
  klass.establish_connection(config)
40
45
  yield klass.connection
41
46
  ensure
42
47
  klass.remove_connection if defined?(klass) && klass.connected?
43
48
  end
49
+
50
+ def ephemeral_connection_class
51
+ class_name = "Nquery::DataSources::EphemeralConnection#{@data_source.id || "new"}#{object_id}"
52
+ Class.new(ActiveRecord::Base) do
53
+ self.abstract_class = true
54
+
55
+ define_singleton_method(:name) { class_name }
56
+ end
57
+ end
44
58
  end
45
59
  end
46
60
  end
@@ -13,6 +13,11 @@ module Nquery
13
13
  connection.columns(table_name).map { |c| { name: c.name, type: c.type.to_s } }
14
14
  end
15
15
 
16
+ def test_connection
17
+ connection.exec_query("SELECT 1")
18
+ true
19
+ end
20
+
16
21
  def execute_readonly(statement, timeout: 15, row_limit: 10_000)
17
22
  rows = []
18
23
  columns = []
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module DataSources
5
+ class Syncer
6
+ CREDENTIAL_KEYS = %i[host port database username password database_path sslmode].freeze
7
+
8
+ def self.call
9
+ new.call
10
+ end
11
+
12
+ def call
13
+ Nquery.configuration.data_sources.each do |key, definition|
14
+ sync_data_source!(key, definition)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def sync_data_source!(key, definition)
21
+ attrs = normalize_definition(definition, key)
22
+ data_source = DataSource.find_or_initialize_by(key: key.to_s)
23
+ data_source.name = attrs.fetch(:name)
24
+ data_source.adapter = attrs.fetch(:adapter).to_s
25
+ data_source.connection_config_hash = {} if data_source.adapter == "rails"
26
+ data_source.save!
27
+ data_source
28
+ end
29
+
30
+ def normalize_definition(definition, key)
31
+ if definition.is_a?(Hash)
32
+ symbolized = definition.deep_symbolize_keys
33
+ {
34
+ adapter: symbolized.fetch(:adapter),
35
+ name: symbolized.fetch(:name)
36
+ }
37
+ else
38
+ {
39
+ adapter: definition,
40
+ name: default_name_for(key)
41
+ }
42
+ end
43
+ end
44
+
45
+ def default_name_for(key)
46
+ key.to_s.humanize
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Nquery
6
+ module Encryption
7
+ module_function
8
+
9
+ def configure!(app)
10
+ encryption = app.config.active_record.encryption
11
+ return if encryption.primary_key.present?
12
+
13
+ unless derive_keys_allowed?(app)
14
+ warn_missing_keys(app)
15
+ return
16
+ end
17
+
18
+ secret = app.secret_key_base
19
+ encryption.primary_key = derived_key(secret, "primary")
20
+ encryption.deterministic_key = derived_key(secret, "deterministic")
21
+ encryption.key_derivation_salt = derived_key(secret, "salt")
22
+ encryption.support_unencrypted_data = support_unencrypted_data if encryption.support_unencrypted_data.nil?
23
+ end
24
+
25
+ def derive_keys_allowed?(app)
26
+ app.config.consider_all_requests_local
27
+ end
28
+
29
+ def support_unencrypted_data
30
+ if ENV.key?("NQUERY_SUPPORT_UNENCRYPTED_DATA")
31
+ return ActiveModel::Type::Boolean.new.cast(ENV.fetch("NQUERY_SUPPORT_UNENCRYPTED_DATA"))
32
+ end
33
+
34
+ true
35
+ end
36
+
37
+ def derived_key(secret, label)
38
+ Digest::SHA256.hexdigest("nquery-#{label}-#{secret}")
39
+ end
40
+
41
+ def warn_missing_keys(app)
42
+ return unless defined?(Rails) && Rails.logger
43
+
44
+ Rails.logger.warn(
45
+ "[nquery] Active Record encryption keys are not configured. " \
46
+ "Set active_record_encryption in Rails credentials before deploying to production."
47
+ )
48
+ end
49
+ end
50
+ end
data/lib/nquery/engine.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "devise"
4
+
3
5
  module Nquery
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace Nquery
@@ -36,12 +38,49 @@ module Nquery
36
38
  app.config.paths["db/migrate"] << migration_path unless app.config.paths["db/migrate"].include?(migration_path)
37
39
  end
38
40
 
39
- initializer "nquery.config" do
40
- Nquery.configure do |config|
41
- config.authentication_mode = ENV.fetch("NQUERY_AUTHENTICATION_MODE", "standalone").to_sym
41
+ initializer "nquery.schema_dumper" do
42
+ ActiveSupport.on_load(:active_record) do
43
+ ActiveRecord::SchemaDumper.ignore_tables |= Nquery::SampleData::Ecommerce::REQUIRED_TABLES
44
+ end
45
+ end
46
+
47
+ initializer "nquery.encryption", before: "active_record_encryption" do |app|
48
+ Nquery::Encryption.configure!(app)
49
+ end
50
+
51
+ initializer "nquery.filter_parameters" do |app|
52
+ app.config.filter_parameters += %i[
53
+ password
54
+ password_confirmation
55
+ connection_config
56
+ username
57
+ database
58
+ database_path
59
+ host
60
+ port
61
+ sslmode
62
+ ]
63
+ end
64
+
65
+ initializer "nquery.mailer" do
66
+ config.to_prepare do
67
+ mailer_sender = Nquery.configuration.mailer_sender
68
+ smtp_settings = Nquery.configuration.smtp&.deep_symbolize_keys
69
+
70
+ [Nquery::ApplicationMailer, Nquery::DeviseMailer].each do |mailer_class|
71
+ mailer_class.default from: mailer_sender if mailer_sender.present?
72
+ mailer_class.smtp_settings = smtp_settings if smtp_settings.present?
73
+ end
42
74
  end
43
75
  end
44
76
 
77
+ # Devise mapping options (router_name, sign_out_via) live on devise_for in
78
+ # config/routes.rb. Mail is sent via User#send_devise_notification.
79
+ # Do not mutate global Devise settings (parent_controller, Warden failure_app,
80
+ # navigational_formats, mailer) — host apps that also use Devise must keep
81
+ # their own settings. Stock Devise::FailureApp + router_name: :nquery preserves
82
+ # the engine mount prefix on unauthenticated redirects.
83
+
45
84
  initializer "nquery.components" do
46
85
  ActiveSupport.on_load(:action_controller) do
47
86
  append_view_path Nquery::Engine.root.join("app/components")
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Onboarding
5
+ class AdminProvisioner
6
+ Result = Data.define(:user, :mail_delivered?)
7
+
8
+ def self.call(user)
9
+ new(user).call
10
+ end
11
+
12
+ def initialize(user)
13
+ @user = user
14
+ end
15
+
16
+ def call
17
+ @user.skip_confirmation_notification!
18
+
19
+ ActiveRecord::Base.transaction do
20
+ @user.save!
21
+ administrators_group.group_memberships.find_or_create_by!(user: @user)
22
+ @user.ensure_all_users_membership!
23
+ @user.ensure_personal_collection!
24
+ end
25
+
26
+ Result.new(user: @user, mail_delivered?: deliver_confirmation_email)
27
+ end
28
+
29
+ private
30
+
31
+ def deliver_confirmation_email
32
+ @user.send_confirmation_instructions != false
33
+ end
34
+
35
+ def administrators_group
36
+ Group.find_by!(system_group: "administrators")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Onboarding
5
+ class PasswordConfirmation
6
+ Result = Data.define(:success?, :user)
7
+
8
+ def self.call(user:, password:, password_confirmation:)
9
+ new(user:, password:, password_confirmation:).call
10
+ end
11
+
12
+ def initialize(user:, password:, password_confirmation:)
13
+ @user = user
14
+ @password = password
15
+ @password_confirmation = password_confirmation
16
+ end
17
+
18
+ def call
19
+ return failure unless user
20
+
21
+ user.assign_attributes(password: password, password_confirmation: password_confirmation)
22
+ return failure unless user.save
23
+
24
+ user.confirm unless user.confirmed?
25
+ latch_onboarding_completion!
26
+ success
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :user, :password, :password_confirmation
32
+
33
+ def latch_onboarding_completion!
34
+ Organization.first&.update!(onboarding_completed_at: Time.current)
35
+ end
36
+
37
+ def success
38
+ Result.new(success?: true, user: user)
39
+ end
40
+
41
+ def failure
42
+ Result.new(success?: false, user: user)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Onboarding
5
+ module_function
6
+
7
+ def required?
8
+ !complete?
9
+ end
10
+
11
+ def complete?
12
+ Organization.exists? && (onboarding_latched? || confirmed_admin_exists?)
13
+ end
14
+
15
+ def pending_admin_confirmation?
16
+ return false if complete?
17
+ return false unless Organization.exists?
18
+
19
+ unconfirmed_admin_exists?
20
+ end
21
+
22
+ def onboarding_latched?
23
+ Organization.where.not(onboarding_completed_at: nil).exists?
24
+ end
25
+
26
+ def confirmed_admin_exists?
27
+ administrators = Group.find_by(system_group: "administrators")
28
+ return false unless administrators
29
+
30
+ administrators.users.active.where.not(confirmed_at: nil).exists?
31
+ end
32
+
33
+ def unconfirmed_admin_exists?
34
+ administrators = Group.find_by(system_group: "administrators")
35
+ return false unless administrators
36
+
37
+ administrators.users.active.where(confirmed_at: nil).exists?
38
+ end
39
+ end
40
+ end
@@ -20,10 +20,7 @@ module Nquery
20
20
  def collection_access(collection)
21
21
  return :curate if admin?
22
22
 
23
- levels = @groups.filter_map do |group|
24
- perm = Nquery::CollectionPermission.find_by(group: group, collection: collection)
25
- perm&.access_level
26
- end
23
+ levels = collection_access_levels_by_id[collection.id] || []
27
24
  max_level(levels, COLLECTION_LEVELS)&.to_sym || :no_access
28
25
  end
29
26
 
@@ -62,6 +59,19 @@ module Nquery
62
59
 
63
60
  private
64
61
 
62
+ def collection_access_levels_by_id
63
+ @collection_access_levels_by_id ||= load_collection_access_levels_by_id
64
+ end
65
+
66
+ def load_collection_access_levels_by_id
67
+ return {} if @groups.empty?
68
+
69
+ Nquery::CollectionPermission
70
+ .where(group_id: @groups.map(&:id))
71
+ .group_by(&:collection_id)
72
+ .transform_values { |permissions| permissions.map(&:access_level) }
73
+ end
74
+
65
75
  def max_level(levels, map)
66
76
  levels.max_by { |l| map[l] || -1 }
67
77
  end
@@ -5,8 +5,6 @@ module Nquery
5
5
  class Error < StandardError; end
6
6
  class PermissionError < Error; end
7
7
 
8
- FORBIDDEN_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|INTO)\b/i
9
-
10
8
  def initialize(data_source:, statement:, user: nil, query: nil)
11
9
  @data_source = data_source
12
10
  @statement = statement.to_s.strip
@@ -14,7 +12,7 @@ module Nquery
14
12
  @query = query
15
13
  end
16
14
 
17
- def run
15
+ def run(audit: true)
18
16
  validate_statement!
19
17
  check_permissions!
20
18
 
@@ -25,22 +23,20 @@ module Nquery
25
23
  row_limit: Nquery.configuration.query_row_limit
26
24
  )
27
25
 
28
- record_audit!(result, "success")
26
+ record_audit!(result, "success") if audit
29
27
  result
30
28
  rescue PermissionError
31
29
  raise
32
30
  rescue StandardError => e
33
- record_audit!({}, "error", e.message)
31
+ record_audit!({}, "error", e.message) if audit
34
32
  raise Error, e.message
35
33
  end
36
34
 
37
35
  private
38
36
 
39
37
  def validate_statement!
40
- raise Error, "Query cannot be blank" if @statement.blank?
41
- raise Error, "Multi-statement queries are not allowed" if @statement.include?(";")
42
- raise Error, "Query must start with SELECT or WITH" unless @statement.match?(/\A\s*(SELECT|WITH)\b/i)
43
- raise Error, "Only SELECT queries are allowed" if @statement.match?(FORBIDDEN_KEYWORDS)
38
+ message = ReadonlySql.error_message(@statement)
39
+ raise Error, message if message
44
40
  end
45
41
 
46
42
  def check_permissions!
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ # Validates that a SQL statement is read-only (SELECT / WITH … SELECT).
5
+ module ReadonlySql
6
+ FORBIDDEN_KEYWORDS = /\b(
7
+ INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|
8
+ GRANT|REVOKE|INTO|MERGE|REPLACE|CALL|EXEC|EXECUTE|
9
+ COPY|ATTACH|DETACH|VACUUM|PRAGMA|REINDEX|RENAME
10
+ )\b/ix
11
+
12
+ module_function
13
+
14
+ def error_message(statement, allow_blank: false)
15
+ sql = statement.to_s.strip
16
+
17
+ return "Query cannot be blank" if sql.blank? && !allow_blank
18
+ return if sql.blank?
19
+
20
+ return "Multi-statement queries are not allowed" if sql.include?(";")
21
+ return "Query must start with SELECT or WITH" unless sql.match?(/\A\s*(SELECT|WITH)\b/i)
22
+ "Only SELECT queries are allowed" if sql.match?(FORBIDDEN_KEYWORDS)
23
+ end
24
+ end
25
+ end
data/lib/nquery/seeder.rb CHANGED
@@ -7,10 +7,15 @@ module Nquery
7
7
  end
8
8
 
9
9
  def run!
10
+ Setup.run!
11
+
12
+ organization = Organization.find_or_create_by!(name: "Nquery Demo") do |org|
13
+ org.website = "https://nquery.dev"
14
+ end
15
+
10
16
  seed_sample_data!
11
17
 
12
- admin_group = ensure_group!("Administrators", "administrators")
13
- all_users_group = ensure_group!("All Users", "all_users")
18
+ admin_group = Group.find_by!(system_group: "administrators")
14
19
  engineering = ensure_group!("Engineering", "custom", "Engineering team")
15
20
 
16
21
  admin = ensure_user!("admin@nquery.dev", "Admin", "User", "password123")
@@ -20,15 +25,12 @@ module Nquery
20
25
  admin_group.group_memberships.find_or_create_by!(user: admin)
21
26
  engineering.group_memberships.find_or_create_by!(user: analyst)
22
27
 
23
- data_source = DataSource.find_or_create_by!(name: "Main Database") do |ds|
24
- ds.adapter = "rails"
25
- ds.connection_config = {}.to_json
26
- end
28
+ data_source = DataSource.find_by(key: "main") || DataSource.find_by!(name: "Application database")
27
29
 
28
- root_collection = Collection.find_or_create_by!(name: "Our analytics", kind: "root")
30
+ root_collection = Collection.find_or_create_by!(name: Setup::ROOT_COLLECTION_NAME, kind: "root")
29
31
  admin.ensure_personal_collection!
30
32
 
31
- seed_permissions!(admin_group, all_users_group, engineering, root_collection, data_source)
33
+ seed_demo_permissions!(engineering, root_collection, data_source)
32
34
 
33
35
  sample_tables = SampleData::Ecommerce::TABLES
34
36
  query = Query.find_or_initialize_by(name: "Monthly revenue")
@@ -74,11 +76,7 @@ module Nquery
74
76
  expires_at: 1.year.from_now
75
77
  ) unless EmbedToken.exists?(resource_type: "Nquery::Chart", resource_id: chart.id)
76
78
 
77
- ApplicationPermission::FEATURES.each do |feature|
78
- ApplicationPermission.find_or_create_by!(group: admin_group, feature: feature) do |p|
79
- p.access_level = "yes"
80
- end
81
- end
79
+ organization.update!(onboarding_completed_at: Time.current) if organization.onboarding_completed_at.blank?
82
80
  end
83
81
 
84
82
  private
@@ -97,34 +95,27 @@ module Nquery
97
95
  end
98
96
 
99
97
  def ensure_user!(email, first_name, last_name, password)
100
- User.find_or_create_by!(email: email) do |u|
101
- u.first_name = first_name
102
- u.last_name = last_name
103
- u.password = password
98
+ User.find_or_create_by!(email: email) do |user|
99
+ user.first_name = first_name
100
+ user.last_name = last_name
101
+ user.password = password
102
+ user.password_confirmation = password
103
+ user.confirmed_at = Time.current
104
+ end.tap do |user|
105
+ user.update!(confirmed_at: Time.current) if user.confirmed_at.blank?
104
106
  end
105
107
  end
106
108
 
107
- def seed_permissions!(admin_group, all_users_group, engineering, root_collection, data_source)
108
- CollectionPermission.find_or_create_by!(group: admin_group, collection: root_collection) do |p|
109
- p.access_level = "curate"
110
- end
111
- CollectionPermission.find_or_create_by!(group: all_users_group, collection: root_collection) do |p|
112
- p.access_level = "view"
113
- end
109
+ def seed_demo_permissions!(engineering, root_collection, data_source)
114
110
  CollectionPermission.find_or_create_by!(group: engineering, collection: root_collection) do |p|
115
111
  p.access_level = "view"
116
112
  end
117
113
 
118
- [admin_group, all_users_group, engineering].each do |group|
119
- level = group.system_group == "administrators" ? "can_view" : (group.system_group == "all_users" ? "can_view" : "blocked")
120
- create_level = group.system_group == "administrators" ? "query_builder_and_native" : "no"
121
-
122
- DataPermission.find_or_create_by!(group: group, data_source: data_source, permission_type: "view_data", resource_path: nil) do |p|
123
- p.access_level = level
124
- end
125
- DataPermission.find_or_create_by!(group: group, data_source: data_source, permission_type: "create_queries", resource_path: nil) do |p|
126
- p.access_level = create_level
127
- end
114
+ DataPermission.find_or_create_by!(group: engineering, data_source: data_source, permission_type: "view_data", resource_path: nil) do |p|
115
+ p.access_level = "blocked"
116
+ end
117
+ DataPermission.find_or_create_by!(group: engineering, data_source: data_source, permission_type: "create_queries", resource_path: nil) do |p|
118
+ p.access_level = "no"
128
119
  end
129
120
  end
130
121
  end