strata-cli 0.1.4.beta → 0.1.5.beta

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/lib/strata/cli/api/client.rb +4 -4
  4. data/lib/strata/cli/api/connection_error_handler.rb +1 -1
  5. data/lib/strata/cli/configuration.rb +3 -1
  6. data/lib/strata/cli/credentials.rb +2 -0
  7. data/lib/strata/cli/descriptions/create/migration.txt +10 -0
  8. data/lib/strata/cli/descriptions/create/relation.txt +6 -0
  9. data/lib/strata/cli/descriptions/create/table.txt +13 -0
  10. data/lib/strata/cli/descriptions/datasource/add.txt +6 -0
  11. data/lib/strata/cli/descriptions/datasource/meta.txt +5 -0
  12. data/lib/strata/cli/descriptions/datasource/tables.txt +6 -0
  13. data/lib/strata/cli/descriptions/datasource/test.txt +5 -0
  14. data/lib/strata/cli/descriptions/deploy/deploy.txt +13 -0
  15. data/lib/strata/cli/descriptions/deploy/status.txt +3 -0
  16. data/lib/strata/cli/descriptions/init.txt +6 -0
  17. data/lib/strata/cli/error_reporter.rb +70 -0
  18. data/lib/strata/cli/generators/datasource.rb +4 -1
  19. data/lib/strata/cli/generators/group.rb +14 -8
  20. data/lib/strata/cli/generators/migration.rb +1 -1
  21. data/lib/strata/cli/generators/project.rb +146 -130
  22. data/lib/strata/cli/generators/table.rb +1 -3
  23. data/lib/strata/cli/guard.rb +2 -0
  24. data/lib/strata/cli/helpers/command_context.rb +23 -4
  25. data/lib/strata/cli/helpers/datasource_helper.rb +17 -1
  26. data/lib/strata/cli/helpers/description_helper.rb +2 -0
  27. data/lib/strata/cli/helpers/prompts.rb +2 -0
  28. data/lib/strata/cli/main.rb +5 -1
  29. data/lib/strata/cli/sub_commands/audit.rb +19 -5
  30. data/lib/strata/cli/sub_commands/create.rb +41 -19
  31. data/lib/strata/cli/sub_commands/datasource.rb +9 -3
  32. data/lib/strata/cli/sub_commands/deploy.rb +40 -26
  33. data/lib/strata/cli/sub_commands/project.rb +4 -6
  34. data/lib/strata/cli/sub_commands/table.rb +1 -1
  35. data/lib/strata/cli/terminal.rb +2 -0
  36. data/lib/strata/cli/ui/autocomplete.rb +2 -0
  37. data/lib/strata/cli/ui/field_editor.rb +2 -0
  38. data/lib/strata/cli/utils/archive.rb +1 -3
  39. data/lib/strata/cli/utils/deployment_monitor.rb +26 -24
  40. data/lib/strata/cli/utils/git.rb +10 -7
  41. data/lib/strata/cli/utils/import_manager.rb +21 -23
  42. data/lib/strata/cli/utils/test_reporter.rb +16 -25
  43. data/lib/strata/cli/utils/version_checker.rb +4 -6
  44. data/lib/strata/cli/utils/yaml_import_resolver.rb +5 -2
  45. data/lib/strata/cli/utils.rb +2 -0
  46. data/lib/strata/cli/version.rb +1 -1
  47. data/lib/strata/cli.rb +8 -0
  48. metadata +29 -27
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "group"
2
4
  require_relative "datasource"
3
5
  require_relative "../helpers/project_helper"
@@ -6,185 +8,199 @@ require "faraday"
6
8
  require "json"
7
9
  require "uri"
8
10
 
9
- module Strata::CLI
10
- module Generators
11
- class Project < Group
12
- include API::ConnectionErrorHandler
11
+ module Strata
12
+ module CLI
13
+ module Generators
14
+ class Project < Group
15
+ include API::ConnectionErrorHandler
16
+
17
+ BASE_SERVER_URL = "http://localhost:3000"
18
+
19
+ argument :name, type: :string, required: false, desc: "The name of the project. Optional when using --source."
20
+ class_option :datasource, type: :string, repeatable: true
21
+ class_option :source, type: :string
22
+ class_option :api_key, type: :string
23
+
24
+ desc "Generates a new Strata project."
25
+
26
+ def validate_options
27
+ if options.key?(:source)
28
+ unless options.key?(:api_key)
29
+ raise Strata::CommandError,
30
+ "API key is required when using --source option. Use --api-key option."
31
+ end
32
+ else
33
+ unless @name && !@name.to_s.strip.empty?
34
+ raise Strata::CommandError,
35
+ "PROJECT_NAME is required when not using --source option."
36
+ end
37
+ end
38
+ end
13
39
 
14
- BASE_SERVER_URL = "http://localhost:3000"
40
+ def fetch_and_clone_if_available
41
+ return unless options.key?(:source)
15
42
 
16
- argument :name, type: :string, required: false, desc: "The name of the project. Optional when using --source."
17
- class_option :datasource, type: :string, repeatable: true
18
- class_option :source, type: :string
19
- class_option :api_key, type: :string
43
+ project_info = fetch_project_info
44
+ @project_id = project_info["id"]
45
+ @project_info = project_info
20
46
 
21
- desc "Generates a new Strata project."
47
+ return unless project_info["git_url"] && !project_info["git_url"].empty?
22
48
 
23
- def validate_options
24
- if options.key?(:source)
25
- raise Strata::CommandError, "API key is required when using --source option. Use --api-key option." unless options.key?(:api_key)
26
- else
27
- raise Strata::CommandError, "PROJECT_NAME is required when not using --source option." unless @name && !@name.to_s.strip.empty?
49
+ @cloned_from_git = true
50
+ clone_project(project_info["git_url"])
28
51
  end
29
- end
30
-
31
- def fetch_and_clone_if_available
32
- return unless options.key?(:source)
33
-
34
- project_info = fetch_project_info
35
- @project_id = project_info["id"]
36
- @project_info = project_info
37
52
 
38
- return unless project_info["git_url"] && !project_info["git_url"].empty?
53
+ def create_project_structure
54
+ return if cloned_from_git?
39
55
 
40
- @cloned_from_git = true
41
- clone_project(project_info["git_url"])
42
- end
56
+ empty_directory uid
57
+ empty_directory File.join(uid, "models")
58
+ empty_directory File.join(uid, "tests")
59
+ end
43
60
 
44
- def create_project_structure
45
- return if cloned_from_git?
61
+ def create_strata_config_file
62
+ template "strata.yml", "#{uid}/.strata"
63
+ end
46
64
 
47
- empty_directory uid
48
- empty_directory File.join(uid, "models")
49
- empty_directory File.join(uid, "tests")
50
- end
65
+ def create_project_file
66
+ return if cloned_from_git?
51
67
 
52
- def create_strata_config_file
53
- template "strata.yml", "#{uid}/.strata"
54
- end
68
+ template "project.yml", "#{uid}/project.yml"
69
+ end
55
70
 
56
- def create_project_file
57
- return if cloned_from_git?
71
+ def persist_project_id_if_needed
72
+ # Persist project_id after project.yml exists (either from clone or creation)
73
+ return unless @project_id
58
74
 
59
- template "project.yml", "#{uid}/project.yml"
60
- end
75
+ project_yml_path = File.join(uid, "project.yml")
76
+ return unless File.exist?(project_yml_path)
61
77
 
62
- def persist_project_id_if_needed
63
- # Persist project_id after project.yml exists (either from clone or creation)
64
- return unless @project_id
78
+ persist_project_id_to_project_yml
79
+ end
65
80
 
66
- project_yml_path = File.join(uid, "project.yml")
67
- return unless File.exist?(project_yml_path)
81
+ def create_datasources_file
82
+ return if cloned_from_git?
68
83
 
69
- persist_project_id_to_project_yml
70
- end
84
+ template "datasources.yml", "#{uid}/datasources.yml"
71
85
 
72
- def create_datasources_file
73
- return if cloned_from_git?
86
+ return unless options.key?(:datasource)
74
87
 
75
- template "datasources.yml", "#{uid}/datasources.yml"
88
+ options[:datasource].each do |ds|
89
+ raise DWH::ConfigError, "Unsupported datasource #{ds}" unless DWH.adapter?(ds.to_sym)
76
90
 
77
- return unless options.key?(:datasource)
91
+ Datasource.new([ds.downcase.strip], options.merge({"path" => uid})).invoke_all
92
+ end
93
+ end
78
94
 
79
- options[:datasource].each do |ds|
80
- raise DWH::ConfigError, "Unsupported datasource #{ds}" unless DWH.adapter?(ds.to_sym)
95
+ def initialize_git
96
+ return if cloned_from_git?
81
97
 
82
- Datasource.new([ds.downcase.strip], options.merge({"path" => uid})).invoke_all
98
+ inside uid do
99
+ run "git init", verbose: false, capture: true
100
+ create_file ".gitignore", ".strata\n"
101
+ end
83
102
  end
84
- end
85
103
 
86
- def initialize_git
87
- return if cloned_from_git?
104
+ def setup_datasource
105
+ return if cloned_from_git?
106
+ return if options.key?(:datasource) # Already specified via CLI option
107
+
108
+ say "\n", :white
109
+ say_status :setup, "Let's configure your first datasource", :cyan
88
110
 
89
- inside uid do
90
- run "git init", verbose: false, capture: true
91
- create_file ".gitignore", ".strata\n"
111
+ # Change into the project directory and run the existing add command
112
+ inside(uid) do
113
+ require_relative "../sub_commands/datasource"
114
+ SubCommands::Datasource.new.add
115
+ end
92
116
  end
93
- end
94
117
 
95
- def setup_datasource
96
- return if cloned_from_git?
97
- return if options.key?(:datasource) # Already specified via CLI option
118
+ def completion_message
119
+ say "\n✔ Strata project '#{uid}' is ready!", :green
120
+ say "\nNext steps:", :yellow
121
+ say " 1. cd #{uid}", :cyan
122
+ say " 2. strata datasource add # To add more datasources", :cyan
123
+ say " 3. strata create table # Start adding tables", :cyan
124
+ say "\n"
125
+ end
98
126
 
99
- say "\n", :white
100
- say_status :setup, "Let's configure your first datasource", :cyan
127
+ private
101
128
 
102
- # Change into the project directory and run the existing add command
103
- inside(uid) do
104
- require_relative "../sub_commands/datasource"
105
- SubCommands::Datasource.new.add
129
+ def cloned_from_git?
130
+ @cloned_from_git ||= false
106
131
  end
107
- end
108
132
 
109
- def completion_message
110
- say "\n✔ Strata project '#{uid}' is ready!", :green
111
- say "\nNext steps:", :yellow
112
- say " 1. cd #{uid}", :cyan
113
- say " 2. strata datasource add # To add more datasources", :cyan
114
- say " 3. strata create table # Start adding tables", :cyan
115
- say "\n"
116
- end
133
+ def fetch_project_info
134
+ conn = Faraday.new(url: options[:source]) do |f|
135
+ f.request :authorization, "Bearer", options[:api_key]
136
+ f.response :json
137
+ end
117
138
 
118
- private
139
+ response = with_connection_error_handling(options[:source]) do
140
+ conn.get
141
+ end
119
142
 
120
- def cloned_from_git?
121
- @cloned_from_git ||= false
122
- end
143
+ unless response.success?
144
+ raise Strata::CommandError,
145
+ "Failed to fetch project info from #{options[:source]}. Status: #{response.status}"
146
+ end
123
147
 
124
- def fetch_project_info
125
- conn = Faraday.new(url: options[:source]) do |f|
126
- f.request :authorization, "Bearer", options[:api_key]
127
- f.response :json
148
+ response.body
128
149
  end
129
150
 
130
- response = with_connection_error_handling(options[:source]) do
131
- conn.get
132
- end
151
+ def clone_project(git_url)
152
+ say_status :clone, "Cloning project from #{git_url}", :green
153
+ run "git clone #{git_url} #{uid}", verbose: false, capture: true
154
+
155
+ raise Strata::CommandError, "Failed to clone repository from #{git_url}" unless File.directory?(uid)
133
156
 
134
- unless response.success?
135
- raise Strata::CommandError,
136
- "Failed to fetch project info from #{options[:source]}. Status: #{response.status}"
157
+ say_status :success, "Project cloned successfully", :green
137
158
  end
138
159
 
139
- response.body
140
- end
160
+ def persist_project_id_to_project_yml
161
+ return unless @project_id
141
162
 
142
- def clone_project(git_url)
143
- say_status :clone, "Cloning project from #{git_url}", :green
144
- run "git clone #{git_url} #{uid}", verbose: false, capture: true
163
+ project_yml_path = File.join(uid, "project.yml")
164
+ Helpers::ProjectHelper.persist_project_id_to_yml(@project_id, project_yml_path: project_yml_path)
165
+ end
145
166
 
146
- raise Strata::CommandError, "Failed to clone repository from #{git_url}" unless File.directory?(uid)
167
+ def uid
168
+ @uid ||= if options.key?(:source) && @project_info
169
+ @project_info["uid"] || options[:source].split("/").last
170
+ else
171
+ Utils.url_safe_str(name)
172
+ end
173
+ end
147
174
 
148
- say_status :success, "Project cloned successfully", :green
149
- end
175
+ def name
176
+ # Use name from server if available (when using --source), otherwise use argument
177
+ return @project_info["name"] if options.key?(:source) && @project_info
150
178
 
151
- def persist_project_id_to_project_yml
152
- return unless @project_id
179
+ @name
180
+ end
153
181
 
154
- project_yml_path = File.join(uid, "project.yml")
155
- Helpers::ProjectHelper.persist_project_id_to_yml(@project_id, project_yml_path: project_yml_path)
156
- end
182
+ def server_url
183
+ return options[:source] if options.key?(:source)
157
184
 
158
- def uid
159
- @uid ||= if options.key?(:source) && @project_info
160
- @project_info["uid"] || options[:source].split("/").last
161
- else
162
- Utils.url_safe_str(name)
185
+ BASE_SERVER_URL
163
186
  end
164
- end
165
187
 
166
- def name
167
- # Use name from server if available (when using --source), otherwise use argument
168
- return @project_info["name"] if options.key?(:source) && @project_info
169
- @name
170
- end
188
+ def description
189
+ return @project_info["description"] if options.key?(:source) && @project_info
171
190
 
172
- def server_url
173
- return options[:source] if options.key?(:source)
174
- BASE_SERVER_URL
175
- end
191
+ nil
192
+ end
176
193
 
177
- def description
178
- return @project_info["description"] if options.key?(:source) && @project_info
179
- nil
180
- end
194
+ def production_branch
195
+ if options.key?(:source) && @project_info && @project_info["production_branch"]
196
+ return @project_info["production_branch"]
197
+ end
181
198
 
182
- def production_branch
183
- return @project_info["production_branch"] if options.key?(:source) && @project_info && @project_info["production_branch"]
184
- "main"
185
- end
199
+ "main"
200
+ end
186
201
 
187
- attr_reader :project_id
202
+ attr_reader :project_id
203
+ end
188
204
  end
189
205
  end
190
206
  end
@@ -109,9 +109,7 @@ module Strata
109
109
  }
110
110
 
111
111
  synonyms = normalized[:synonyms]
112
- if synonyms.is_a?(Array) && !synonyms.empty?
113
- field_hash["synonyms"] = synonyms
114
- end
112
+ field_hash["synonyms"] = synonyms if synonyms.is_a?(Array) && !synonyms.empty?
115
113
 
116
114
  # Build expression with proper nested format
117
115
  expr = normalized[:expression]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "utils"
2
4
  module Strata
3
5
  module CLI
@@ -4,6 +4,7 @@ require "tty-prompt"
4
4
  require_relative "datasource_helper"
5
5
  require_relative "color_helper"
6
6
  require_relative "prompts"
7
+ require_relative "../error_reporter"
7
8
 
8
9
  module Strata
9
10
  module CLI
@@ -23,18 +24,36 @@ module Strata
23
24
  @datasource_key ||= resolve_datasource(prompt: prompt)
24
25
  end
25
26
 
26
- def all_tables
27
- @all_tables ||= begin
27
+ def table_fetch_result
28
+ @table_fetch_result ||= begin
28
29
  tables = with_spinner("Fetching tables from #{datasource_key}...") { adapter.tables }
29
30
  if tables.empty?
30
31
  # Assuming Prompts and ColorHelper are available in the class or via module
31
32
  say Prompts::MSG_NO_TABLES_FOUND % datasource_key, ColorHelper.warning
32
- []
33
+ {tables: [], failed: false}
34
+ else
35
+ {tables: tables, failed: false}
36
+ end
37
+ rescue => e
38
+ ErrorReporter.log_error(e, context: "create table: failed fetching tables for '#{datasource_key}'")
39
+
40
+ if ErrorReporter.connection_error?(e)
41
+ say "Could not connect to datasource '#{datasource_key}'.", ColorHelper.warning
33
42
  else
34
- tables
43
+ say "Could not fetch tables from datasource '#{datasource_key}'.", ColorHelper.warning
44
+ say "Reason: #{ErrorReporter.user_message_for(e)}", ColorHelper.warning
35
45
  end
46
+
47
+ say "Hint: verify credentials/settings and run 'strata datasource test #{datasource_key}'.",
48
+ ColorHelper.info
49
+ say "Details logged to '#{ErrorReporter.log_relative_path}'.", ColorHelper.info
50
+ {tables: [], failed: true}
36
51
  end
37
52
  end
53
+
54
+ def all_tables
55
+ table_fetch_result[:tables]
56
+ end
38
57
  end
39
58
  end
40
59
  end
@@ -105,6 +105,22 @@ module Strata
105
105
  datasources[ds_key]
106
106
  end
107
107
 
108
+ def resolve_datasource_value(value)
109
+ return nil if value.nil? || value.to_s.strip.empty?
110
+
111
+ normalized = value.to_s.downcase.strip
112
+
113
+ # Match by key first
114
+ return value if datasources.key?(normalized)
115
+
116
+ # Match by name (case-insensitive)
117
+ datasources.each do |key, config|
118
+ return key if config.is_a?(Hash) && config["name"].to_s.downcase.strip == normalized
119
+ end
120
+
121
+ nil
122
+ end
123
+
108
124
  private
109
125
 
110
126
  def validate_datasource(ds_key)
@@ -116,7 +132,7 @@ module Strata
116
132
  end
117
133
 
118
134
  def datasources
119
- @datasources_cache ||= begin
135
+ @datasources ||= begin
120
136
  YAML.safe_load_file("datasources.yml", permitted_classes: [Date, Time], aliases: true) || {}
121
137
  rescue Errno::ENOENT
122
138
  {}
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Strata
2
4
  module CLI
3
5
  module Helpers
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Strata
2
4
  module CLI
3
5
  module Prompts
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "generators/project"
2
4
  require_relative "sub_commands/datasource"
3
5
  require_relative "sub_commands/deploy"
@@ -33,7 +35,9 @@ module Strata
33
35
  unless project_name || options[:source]
34
36
  raise Strata::CommandError, "PROJECT_NAME is required when not using --source option."
35
37
  end
36
- say_status :started, "Creating #{project_name || "project from source"} - #{options[:datasource]}", ColorHelper.info
38
+
39
+ say_status :started, "Creating #{project_name || "project from source"} - #{options[:datasource]}",
40
+ ColorHelper.info
37
41
  invoke Generators::Project, [project_name], options
38
42
  end
39
43
 
@@ -19,10 +19,10 @@ module Strata
19
19
  include Terminal
20
20
  include DatasourceHelper
21
21
 
22
- REQUIRED_KEYS_FOR_TABLE_MODEL = %w[name physical_name fields datasource]
23
- REQUIRED_KEYS_FOR_RELATIONSHIP_MODEL = ["datasource"]
24
- REQUIRED_KEYS_FOR_RELATIONSHIP_DEFINITION = %w[left right sql cardinality]
25
- RELATIONSHIP_CARDINALITIES = %w[one_to_one one_to_many many_to_one many_to_many]
22
+ REQUIRED_KEYS_FOR_TABLE_MODEL = %w[name physical_name fields datasource].freeze
23
+ REQUIRED_KEYS_FOR_RELATIONSHIP_MODEL = ["datasource"].freeze
24
+ REQUIRED_KEYS_FOR_RELATIONSHIP_DEFINITION = %w[left right sql cardinality].freeze
25
+ RELATIONSHIP_CARDINALITIES = %w[one_to_one one_to_many many_to_one many_to_many].freeze
26
26
 
27
27
  # Set default command so `strata audit` still works as `strata audit all`
28
28
  default_command :all
@@ -131,21 +131,35 @@ module Strata
131
131
  end
132
132
 
133
133
  def validate_structure(content, file, failures)
134
- content.keys.each do |k|
134
+ content.each_key do |k|
135
135
  failures << {file: file, message: "Top level key #{k} is not a string"} unless k.is_a?(String)
136
136
  end
137
137
  end
138
138
 
139
139
  def validate_table_model(content, file, failures)
140
140
  validate_required_keys(content, file, REQUIRED_KEYS_FOR_TABLE_MODEL, failures)
141
+ validate_datasource_reference(content, file, failures)
141
142
  validate_table_fields(content["fields"], file, failures) if content.key?("fields")
142
143
  end
143
144
 
144
145
  def validate_relationship_model(content, file, failures)
145
146
  validate_required_keys(content, file, REQUIRED_KEYS_FOR_RELATIONSHIP_MODEL, failures)
147
+ validate_datasource_reference(content, file, failures)
146
148
  validate_relationship_definitions(content, file, failures)
147
149
  end
148
150
 
151
+ def validate_datasource_reference(content, file, failures)
152
+ ds_value = content["datasource"]
153
+ return if ds_value.nil? || ds_value.to_s.strip.empty?
154
+
155
+ return if resolve_datasource_value(ds_value)
156
+
157
+ failures << {
158
+ file: file,
159
+ message: "Datasource '#{ds_value}' not found. Must match a key or name in datasources.yml"
160
+ }
161
+ end
162
+
149
163
  def validate_required_keys(content, file, required_keys, failures)
150
164
  missing = required_keys - content.keys
151
165
  failures << {file: file, message: "Missing required keys: #{missing.join(", ")}"} if missing.any?
@@ -126,9 +126,15 @@ module Strata
126
126
  end
127
127
 
128
128
  def handle_table_creation_interactive
129
- return if all_tables.empty?
129
+ fetch_result = table_fetch_result
130
+ tables = fetch_result[:tables]
131
+ if tables.empty?
132
+ return handle_table_creation_without_connection if fetch_result[:failed]
130
133
 
131
- selected_table = resolve_table_selection(nil)
134
+ return
135
+ end
136
+
137
+ selected_table = resolve_table_selection(tables, nil)
132
138
  return unless selected_table
133
139
 
134
140
  # For interactive, derive path from table name
@@ -191,21 +197,26 @@ module Strata
191
197
  schema_info = parsed[:schema] ? " in schema '#{parsed[:schema]}'" : ""
192
198
  say "\nCould not find table '#{table_name}'#{schema_info} in datasource '#{datasource_key}'.", :red
193
199
 
194
- if prompt.yes?("Proceed anyway or cancel?")
195
- proceed_with_table_creation(nil, parsed)
200
+ if prompt.yes?("Proceed with template-only file (no generated fields)?", default: true)
201
+ proceed_with_table_creation(nil, parsed, skip_field_editor: true)
196
202
  else
197
203
  say "Cancelled.", :yellow
198
204
  end
199
205
  end
200
206
 
201
- def proceed_with_table_creation(selected_table, parsed)
207
+ def proceed_with_table_creation(selected_table, parsed, skip_field_editor: false)
202
208
  # If table not found, use parsed physical_name
203
209
  physical_table_name = selected_table || (parsed[:schema] ? "#{parsed[:schema]}.#{parsed[:physical_name]}" : parsed[:physical_name])
204
210
 
205
211
  model_config = prompt_model_config(parsed)
206
212
  return unless model_config[:name]
207
213
 
208
- # Only fetch columns if we have a valid table
214
+ if skip_field_editor || selected_table.nil?
215
+ say "Skipping field generation. Template file will be created without fields.", :yellow
216
+ save_model_file(parsed, physical_table_name, [], model_config)
217
+ return
218
+ end
219
+
209
220
  columns = []
210
221
  if selected_table
211
222
  columns = fetch_columns(selected_table)
@@ -218,7 +229,6 @@ module Strata
218
229
  []
219
230
  end
220
231
 
221
- # Interactive field editor
222
232
  result = run_field_editor(fields, physical_table_name, columns, model_config)
223
233
  return if result.nil?
224
234
 
@@ -235,16 +245,27 @@ module Strata
235
245
  save_model_file(parsed, physical_table_name, confirmed_fields, final_model_context)
236
246
  end
237
247
 
238
- def resolve_table_selection(initial_filter)
248
+ def resolve_table_selection(tables, initial_filter)
239
249
  autocomplete = Autocomplete.new(prompt: prompt)
240
250
  autocomplete.select(
241
251
  MSG_SEARCH_TABLE,
242
- all_tables,
252
+ tables,
243
253
  display_transform: ->(table) { TableFilter.display_name(table) },
244
254
  default_filter: initial_filter
245
255
  )
246
256
  end
247
257
 
258
+ def handle_table_creation_without_connection
259
+ say "\nConnection failed. You can still create a template-only table model.", :yellow
260
+ return unless prompt.yes?("Continue without fetching table metadata?", default: true)
261
+
262
+ table_path = prompt.ask(" Table path (e.g., games/event_details):")
263
+ return unless table_path
264
+
265
+ parsed = parse_table_path(table_path)
266
+ proceed_with_table_creation(nil, parsed, skip_field_editor: true)
267
+ end
268
+
248
269
  def prompt_model_config(parsed)
249
270
  # Default name should be the table name (without schema), not a display name
250
271
  # This matches the requirement: "The name and physical name will be event_details"
@@ -367,14 +388,15 @@ module Strata
367
388
  []
368
389
  end
369
390
 
370
- unless valid_entities.include?(entity&.downcase)
371
- valid_list = valid_entities.join(", ")
372
- if operation == "swap" && entity&.downcase == "datasource"
373
- raise Strata::CommandError, "Swap operation does not support 'datasource' entity type. Valid types: #{valid_list}"
374
- else
375
- raise Strata::CommandError, "Invalid entity type: #{entity}. Valid types for #{operation}: #{valid_list}"
376
- end
391
+ return if valid_entities.include?(entity&.downcase)
392
+
393
+ valid_list = valid_entities.join(", ")
394
+ unless operation == "swap" && entity&.downcase == "datasource"
395
+ raise Strata::CommandError, "Invalid entity type: #{entity}. Valid types for #{operation}: #{valid_list}"
377
396
  end
397
+
398
+ raise Strata::CommandError,
399
+ "Swap operation does not support 'datasource' entity type. Valid types: #{valid_list}"
378
400
  end
379
401
 
380
402
  def validate_required_params(entity, from, to)
@@ -382,9 +404,9 @@ module Strata
382
404
  raise Strata::CommandError, "Missing required option: --from" if from.nil? || from.strip.empty?
383
405
  raise Strata::CommandError, "Missing required option: --to" if to.nil? || to.strip.empty?
384
406
 
385
- if from.strip == to.strip
386
- say "Warning: --from and --to have the same value. Migration will have no effect.", ColorHelper.warning
387
- end
407
+ return unless from.strip == to.strip
408
+
409
+ say "Warning: --from and --to have the same value. Migration will have no effect.", ColorHelper.warning
388
410
  end
389
411
 
390
412
  def prompt_migration_hook(operation)