strata-cli 0.1.0.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.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +65 -0
- data/LICENSE +21 -0
- data/README.md +465 -0
- data/Rakefile +10 -0
- data/exe/strata +6 -0
- data/lib/strata/cli/ai/client.rb +63 -0
- data/lib/strata/cli/ai/configuration.rb +48 -0
- data/lib/strata/cli/ai/services/table_generator.rb +282 -0
- data/lib/strata/cli/api/client.rb +170 -0
- data/lib/strata/cli/api/connection_error_handler.rb +54 -0
- data/lib/strata/cli/configuration.rb +135 -0
- data/lib/strata/cli/credentials.rb +83 -0
- data/lib/strata/cli/descriptions/create/migration.txt +25 -0
- data/lib/strata/cli/descriptions/create/relation.txt +14 -0
- data/lib/strata/cli/descriptions/create/table.txt +23 -0
- data/lib/strata/cli/descriptions/datasource/add.txt +15 -0
- data/lib/strata/cli/descriptions/datasource/auth.txt +14 -0
- data/lib/strata/cli/descriptions/datasource/exec.txt +7 -0
- data/lib/strata/cli/descriptions/datasource/meta.txt +11 -0
- data/lib/strata/cli/descriptions/datasource/tables.txt +12 -0
- data/lib/strata/cli/descriptions/datasource/test.txt +8 -0
- data/lib/strata/cli/descriptions/deploy/deploy.txt +24 -0
- data/lib/strata/cli/descriptions/deploy/status.txt +9 -0
- data/lib/strata/cli/descriptions/init.txt +14 -0
- data/lib/strata/cli/generators/datasource.rb +83 -0
- data/lib/strata/cli/generators/group.rb +13 -0
- data/lib/strata/cli/generators/migration.rb +71 -0
- data/lib/strata/cli/generators/project.rb +190 -0
- data/lib/strata/cli/generators/relation.rb +64 -0
- data/lib/strata/cli/generators/table.rb +143 -0
- data/lib/strata/cli/generators/templates/adapters/athena.yml +53 -0
- data/lib/strata/cli/generators/templates/adapters/druid.yml +42 -0
- data/lib/strata/cli/generators/templates/adapters/duckdb.yml +36 -0
- data/lib/strata/cli/generators/templates/adapters/mysql.yml +45 -0
- data/lib/strata/cli/generators/templates/adapters/postgres.yml +48 -0
- data/lib/strata/cli/generators/templates/adapters/snowflake.yml +69 -0
- data/lib/strata/cli/generators/templates/adapters/sqlserver.yml +45 -0
- data/lib/strata/cli/generators/templates/adapters/trino.yml +56 -0
- data/lib/strata/cli/generators/templates/datasources.yml +4 -0
- data/lib/strata/cli/generators/templates/migration.rename.yml +15 -0
- data/lib/strata/cli/generators/templates/migration.swap.yml +13 -0
- data/lib/strata/cli/generators/templates/project.yml +36 -0
- data/lib/strata/cli/generators/templates/rel.domain.yml +43 -0
- data/lib/strata/cli/generators/templates/strata.yml +24 -0
- data/lib/strata/cli/generators/templates/table.table_name.yml +118 -0
- data/lib/strata/cli/generators/templates/test.yml +34 -0
- data/lib/strata/cli/generators/test.rb +48 -0
- data/lib/strata/cli/guard.rb +21 -0
- data/lib/strata/cli/helpers/color_helper.rb +103 -0
- data/lib/strata/cli/helpers/command_context.rb +41 -0
- data/lib/strata/cli/helpers/datasource_helper.rb +62 -0
- data/lib/strata/cli/helpers/description_helper.rb +18 -0
- data/lib/strata/cli/helpers/project_helper.rb +85 -0
- data/lib/strata/cli/helpers/prompts.rb +42 -0
- data/lib/strata/cli/helpers/table_filter.rb +48 -0
- data/lib/strata/cli/main.rb +71 -0
- data/lib/strata/cli/sub_commands/audit.rb +262 -0
- data/lib/strata/cli/sub_commands/create.rb +419 -0
- data/lib/strata/cli/sub_commands/datasource.rb +353 -0
- data/lib/strata/cli/sub_commands/deploy.rb +433 -0
- data/lib/strata/cli/sub_commands/project.rb +38 -0
- data/lib/strata/cli/sub_commands/table.rb +58 -0
- data/lib/strata/cli/terminal.rb +102 -0
- data/lib/strata/cli/ui/autocomplete.rb +93 -0
- data/lib/strata/cli/ui/field_editor.rb +215 -0
- data/lib/strata/cli/utils/archive.rb +137 -0
- data/lib/strata/cli/utils/deployment_monitor.rb +445 -0
- data/lib/strata/cli/utils/git.rb +253 -0
- data/lib/strata/cli/utils/import_manager.rb +190 -0
- data/lib/strata/cli/utils/test_reporter.rb +131 -0
- data/lib/strata/cli/utils/yaml_import_resolver.rb +91 -0
- data/lib/strata/cli/utils.rb +39 -0
- data/lib/strata/cli/version.rb +7 -0
- data/lib/strata/cli.rb +36 -0
- data/sig/strata/cli.rbs +6 -0
- metadata +306 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
|
|
4
|
+
module Strata
|
|
5
|
+
module CLI
|
|
6
|
+
class Credentials
|
|
7
|
+
include Thor::Shell
|
|
8
|
+
|
|
9
|
+
# Retrieve existing credentials for the
|
|
10
|
+
# given ds_key or empty hash.
|
|
11
|
+
def self.fetch(ds_key)
|
|
12
|
+
CLI.config[ds_key] || {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :adapter, :credentials
|
|
16
|
+
|
|
17
|
+
def initialize(adapter)
|
|
18
|
+
@adapter = adapter.downcase.strip
|
|
19
|
+
@prompt = TTY::Prompt.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def required?
|
|
23
|
+
adapter != "duckdb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def collect
|
|
27
|
+
credentials = {}
|
|
28
|
+
|
|
29
|
+
case adapter
|
|
30
|
+
when "snowflake"
|
|
31
|
+
auth_mode = @prompt.select("Authentication mode:", %w[pat kp oauth], default: "pat")
|
|
32
|
+
credentials["auth_mode"] = auth_mode
|
|
33
|
+
|
|
34
|
+
case auth_mode
|
|
35
|
+
when "pat"
|
|
36
|
+
credentials["personal_access_token"] = @prompt.ask("Enter Personal Access Token:")
|
|
37
|
+
when "kp"
|
|
38
|
+
credentials["username"] = @prompt.ask("Enter Username:")
|
|
39
|
+
credentials["private_key"] = @prompt.ask("Enter Private Key Absolute Path:")
|
|
40
|
+
when "oauth"
|
|
41
|
+
credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
|
|
42
|
+
credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
|
|
43
|
+
credentials["oauth_redirect_uri"] = @prompt.ask("OAuth Redirect URI:", default: "https://localhost:3420/callback")
|
|
44
|
+
oauth_scope = @prompt.ask("OAuth Scope (optional):")
|
|
45
|
+
credentials["oauth_scope"] = oauth_scope unless oauth_scope.empty?
|
|
46
|
+
end
|
|
47
|
+
when "athena"
|
|
48
|
+
credentials["access_key_id"] = @prompt.ask("AWS Access Key ID:")
|
|
49
|
+
credentials["secret_access_key"] = @prompt.ask("AWS Secret Access Key:")
|
|
50
|
+
else
|
|
51
|
+
if required?
|
|
52
|
+
unless %w[postgres mysql trino sqlserver].include?(adapter)
|
|
53
|
+
credentials["username"] = @prompt.ask("Enter Username:")
|
|
54
|
+
end
|
|
55
|
+
credentials["password"] = @prompt.mask("Enter Password:")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@credentials = credentials
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def collected?
|
|
63
|
+
!@credentials.keys.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def write_local(ds_key, context)
|
|
67
|
+
context.gsub_file Configuration::STRATA_CONFIG_FILE, Utils.yaml_block_matcher(ds_key), ""
|
|
68
|
+
creds = "#{ds_key}:"
|
|
69
|
+
credentials.each do |key, val|
|
|
70
|
+
creds << "\n #{key}: #{val}" unless val.nil? || val.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context.append_to_file Configuration::STRATA_CONFIG_FILE, creds
|
|
74
|
+
|
|
75
|
+
# Set restrictive permissions (read/write for owner only)
|
|
76
|
+
strata_file = Configuration::STRATA_CONFIG_FILE
|
|
77
|
+
File.chmod(0o600, strata_file) if File.exist?(strata_file)
|
|
78
|
+
rescue Errno::EACCES => e
|
|
79
|
+
raise Strata::CommandError, "Permission denied setting permissions on #{strata_file}: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Create migration files for renaming or swapping entities in your Strata project.
|
|
2
|
+
|
|
3
|
+
Migration subcommands:
|
|
4
|
+
rename Create a migration to rename an entity (dimension, measure, table, datasource)
|
|
5
|
+
swap Create a migration to swap entity references (dimension, measure, table)
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-e, --entity TYPE Entity type: dimension, measure, table, or datasource (required)
|
|
9
|
+
-f, --from NAME Current/source entity name (required)
|
|
10
|
+
-t, --to NAME New/target entity name (required)
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
# Rename a dimension
|
|
14
|
+
strata create migration rename -e dimension -f old_name -t new_name
|
|
15
|
+
|
|
16
|
+
# Rename a table
|
|
17
|
+
strata create migration rename -e table -f old_table -t new_table
|
|
18
|
+
|
|
19
|
+
# Swap two measures
|
|
20
|
+
strata create migration swap -e measure -f measure_a -t measure_b
|
|
21
|
+
|
|
22
|
+
# Rename a datasource
|
|
23
|
+
strata create migration rename -e datasource -f old_ds -t new_ds
|
|
24
|
+
|
|
25
|
+
Note: Swap operation does not support 'datasource' entity type.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Create a relation YAML file for defining joins between tables.
|
|
2
|
+
|
|
3
|
+
RELATION_PATH is the path where the relation file will be created.
|
|
4
|
+
If RELATION_PATH contains a /, the file will be created at models/<RELATION_PATH>/rel.<last_part>.yml
|
|
5
|
+
If RELATION_PATH has no /, the file will be created at models/rel.<RELATION_PATH>.yml
|
|
6
|
+
The relation name is derived from the last part of the path.
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
strata create relation customer/orders # Creates models/customer/rel.orders.yml
|
|
10
|
+
strata create relation customer # Creates models/rel.customer.yml
|
|
11
|
+
|
|
12
|
+
This will create a template relation file with examples and comments.
|
|
13
|
+
You can then edit it to define your joins.
|
|
14
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Create a semantic table model from a datasource table.
|
|
2
|
+
|
|
3
|
+
TABLE_PATH can be:
|
|
4
|
+
- Simple: call_center
|
|
5
|
+
- Nested: games/event_details
|
|
6
|
+
- With schema: contact/dse.call_center_d
|
|
7
|
+
- Deep nested: contact/help_center/visits
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
strata create table call_center
|
|
11
|
+
strata create table games/event_details
|
|
12
|
+
strata create table contact/dse.call_center_d
|
|
13
|
+
strata create table contact/help_center/visits
|
|
14
|
+
|
|
15
|
+
If no argument is provided, you'll be prompted to search and select a table.
|
|
16
|
+
|
|
17
|
+
The command will:
|
|
18
|
+
1. Check if the table exists in the datasource
|
|
19
|
+
2. Fetch column metadata from the database
|
|
20
|
+
3. Analyze columns with AI to suggest field definitions
|
|
21
|
+
4. Allow you to review and edit fields interactively
|
|
22
|
+
5. Generate the model YAML file at models/<TABLE_PATH>/tbl.<table_name>.yml
|
|
23
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Add a new datasource interactively. This command will guide you through:
|
|
2
|
+
|
|
3
|
+
1. Selecting a data warehouse adapter (PostgreSQL, MySQL, Snowflake, etc.)
|
|
4
|
+
2. Configuring connection settings (host, port, database, etc.)
|
|
5
|
+
3. Setting up authentication credentials
|
|
6
|
+
4. Optionally configuring AI features for table generation
|
|
7
|
+
|
|
8
|
+
ADAPTER is optional - if not provided, you'll be prompted to select one.
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
strata datasource add # Interactive mode - select adapter
|
|
12
|
+
strata datasource add postgres # Directly add PostgreSQL datasource
|
|
13
|
+
strata datasource add snowflake # Directly add Snowflake datasource
|
|
14
|
+
|
|
15
|
+
Supported adapters: postgres, mysql, sqlserver, snowflake, athena, trino, duckdb, druid
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Example to set local credentials for a datasource with key games_dwh:
|
|
2
|
+
|
|
3
|
+
strata ds auth games_dwh
|
|
4
|
+
|
|
5
|
+
Example to set remote credentials for the same datasource:
|
|
6
|
+
|
|
7
|
+
strata ds auth games_dwh -r
|
|
8
|
+
|
|
9
|
+
Local credentials are saved in the projects .strata file. This will should not
|
|
10
|
+
be checked into git.
|
|
11
|
+
|
|
12
|
+
Remote credentials will securely send the credentials to the Strata server. This is
|
|
13
|
+
not requires for some modes like OAuth. In that case each user will be prompted to
|
|
14
|
+
go through the OAuth flow. Not all adapters support OAuth.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Show the schema/structure of a specific table in the datasource.
|
|
2
|
+
Displays column names, data types, and other metadata.
|
|
3
|
+
|
|
4
|
+
Options:
|
|
5
|
+
-c, --catalog CATALOG Override catalog from datasource config
|
|
6
|
+
-s, --schema SCHEMA Override schema from datasource config
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
strata datasource meta my_db customers # Show customers table structure
|
|
10
|
+
strata datasource meta my_db dbo.orders # Show orders table in dbo schema
|
|
11
|
+
strata datasource meta my_db sales.customers -s sales # Explicitly specify schema
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
List all tables available in the specified datasource. If DS_KEY is not provided,
|
|
2
|
+
you'll be prompted to select from available datasources.
|
|
3
|
+
|
|
4
|
+
Options:
|
|
5
|
+
-p, --pattern PATTERN Filter tables by regex pattern
|
|
6
|
+
-c, --catalog CATALOG Override catalog from datasource config
|
|
7
|
+
-s, --schema SCHEMA Override schema from datasource config
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
strata datasource tables my_db # List all tables
|
|
11
|
+
strata datasource tables my_db -p user # List tables matching "user"
|
|
12
|
+
strata datasource tables my_db -s dbo # List tables in "dbo" schema
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Test the connection to a datasource. This verifies that:
|
|
2
|
+
- The datasource configuration is valid
|
|
3
|
+
- Credentials are correct
|
|
4
|
+
- The connection can be established
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
strata datasource test my_db # Test connection to my_db datasource
|
|
8
|
+
strata ds test postgres_db # Using alias
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Deploys your Strata project to the configured server. This command will:
|
|
2
|
+
|
|
3
|
+
1. Run pre-deployment audit checks (unless --skip-audit is used)
|
|
4
|
+
2. Check git repository status and ensure all changes are committed
|
|
5
|
+
3. Create an archive of changed files since the last deployment
|
|
6
|
+
4. Upload the archive to the Strata server
|
|
7
|
+
5. Monitor deployment progress and display results
|
|
8
|
+
|
|
9
|
+
The deployment includes all YAML files in your project (models, datasources, tests).
|
|
10
|
+
Only files that have changed since the last successful deployment are included,
|
|
11
|
+
unless --force is used to deploy all files.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-e, --environment ENV Deploy to specific environment (dev, staging, prod)
|
|
15
|
+
--skip-audit Skip pre-deployment audit checks
|
|
16
|
+
--yes, -y Skip confirmation prompts (useful for CI/CD)
|
|
17
|
+
-f, --force Force deploy even if no files have changed
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
strata deploy # Deploy to default environment
|
|
21
|
+
strata deploy -e production # Deploy to production environment
|
|
22
|
+
strata deploy --skip-audit # Skip pre-deployment checks
|
|
23
|
+
strata deploy --force # Force deploy even if no changes
|
|
24
|
+
strata deploy --yes # Skip confirmation prompts
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Displays the current deployment status for the active git branch.
|
|
2
|
+
Shows deployment stage, status (succeeded/failed/in_progress), and test results if available.
|
|
3
|
+
|
|
4
|
+
Options:
|
|
5
|
+
-e, --environment ENV Check status for specific environment
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
strata deploy status # Show status for current branch
|
|
9
|
+
strata deploy status -e production # Show status for production environment
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Initialize a new Strata project or clone from an existing repository.
|
|
2
|
+
|
|
3
|
+
PROJECT_NAME is optional when using --source option. If provided, creates a new project
|
|
4
|
+
with that name. If using --source, clones the existing project from the URL.
|
|
5
|
+
|
|
6
|
+
Options:
|
|
7
|
+
-d, --datasource ADAPTER Add one or more datasource adapters (repeatable)
|
|
8
|
+
-s, --source URL URL of existing project to clone
|
|
9
|
+
-a, --api-key KEY API key (required when using --source)
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
strata init my-project
|
|
13
|
+
strata init my-project -d postgres -d snowflake
|
|
14
|
+
strata init --source https://github.com/org/project.git --api-key YOUR_KEY
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require_relative "group"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
module Strata
|
|
5
|
+
module CLI
|
|
6
|
+
module Generators
|
|
7
|
+
class Datasource < Group
|
|
8
|
+
argument :adapter, type: :string, desc: "Data warehouse adapter to add as a datasource.", required: true
|
|
9
|
+
argument :name, type: :string, desc: "Optional name to be used as the key for the datasource.", required: false
|
|
10
|
+
class_option :path, type: :string, desc: "Need path when outside project directory"
|
|
11
|
+
|
|
12
|
+
def check_duckdb_requirements
|
|
13
|
+
return unless adapter.downcase == "duckdb"
|
|
14
|
+
|
|
15
|
+
unless duckdb_installed?
|
|
16
|
+
raise DWH::ConfigError,
|
|
17
|
+
"DuckDB is not installed. Please install DuckDB. We will need the header files to compile libraries."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
install_duckdb_gem
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_datasource_config
|
|
24
|
+
@ds_key = get_unique_ds_key
|
|
25
|
+
say_status :adapter, "adding #{adapter} config to datasources", :yellow
|
|
26
|
+
|
|
27
|
+
# Interactive mode: write config directly from prompts
|
|
28
|
+
config_yaml = {@ds_key => options[:config]}.to_yaml.sub(/^---\n/, "\n")
|
|
29
|
+
append_to_file pathify("datasources.yml"), config_yaml
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def duckdb_installed?
|
|
35
|
+
# Check if DuckDB is installed globally by trying to run it
|
|
36
|
+
# Use Open3 for cross-platform compatibility (Windows doesn't support shell redirection)
|
|
37
|
+
_, _, status = Open3.capture3("duckdb", "--version")
|
|
38
|
+
status.success?
|
|
39
|
+
rescue Errno::ENOENT
|
|
40
|
+
# duckdb command not found
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def install_duckdb_gem
|
|
45
|
+
say_status :gem, "Installing duckdb gem...", :yellow
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
# Install the duckdb gem
|
|
49
|
+
run "gem install duckdb", verbose: false, capture: true
|
|
50
|
+
say_status :success, "duckdb gem installed successfully", :green
|
|
51
|
+
rescue => e
|
|
52
|
+
raise DWH::ConfigError, "Failed to install duckdb gem: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def pathify(file)
|
|
57
|
+
options["path"].nil? ? file : "#{options["path"].gsub(%r{$/}, "")}/#{file}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get_unique_ds_key
|
|
61
|
+
base_key = (name && !name.strip.empty?) ? Utils.url_safe_str(name) : adapter
|
|
62
|
+
key_id = 1
|
|
63
|
+
ds_key = base_key
|
|
64
|
+
while current_ds.key?(ds_key)
|
|
65
|
+
ds_key = "#{base_key}_#{key_id}"
|
|
66
|
+
key_id += 1
|
|
67
|
+
end
|
|
68
|
+
ds_key
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def current_ds
|
|
72
|
+
@ds ||= YAML.safe_load_file(pathify("datasources.yml"), permitted_classes: [Date, Time], aliases: true) || {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def render_template(source, context: instance_eval("binding", __FILE__, __LINE__))
|
|
76
|
+
source = File.expand_path(find_in_source_paths(source.to_s))
|
|
77
|
+
capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer")
|
|
78
|
+
capturable_erb.tap { |erb| erb.filename = source }.result(context)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "group"
|
|
4
|
+
|
|
5
|
+
module Strata
|
|
6
|
+
module CLI
|
|
7
|
+
module Generators
|
|
8
|
+
# Generates migration YAML files from templates.
|
|
9
|
+
class Migration < Group
|
|
10
|
+
argument :operation, type: :string, desc: "Migration operation: rename or swap"
|
|
11
|
+
argument :entity, type: :string, desc: "Entity type"
|
|
12
|
+
argument :from, type: :string, desc: "Source/current entity name"
|
|
13
|
+
argument :to, type: :string, desc: "Target/new entity name"
|
|
14
|
+
|
|
15
|
+
def create_migration_file
|
|
16
|
+
timestamp = generate_timestamp
|
|
17
|
+
filename = generate_filename(operation, entity, from, to, timestamp)
|
|
18
|
+
output_path = File.join("migrations", filename)
|
|
19
|
+
|
|
20
|
+
# Ensure directory exists
|
|
21
|
+
empty_directory "migrations"
|
|
22
|
+
|
|
23
|
+
# Load template and update with user inputs
|
|
24
|
+
template_content = load_template(operation)
|
|
25
|
+
hook = options[:hook] || ((operation == "swap") ? "post" : "pre")
|
|
26
|
+
updated_content = update_template(template_content, hook)
|
|
27
|
+
|
|
28
|
+
# Write the updated template
|
|
29
|
+
create_file output_path, updated_content
|
|
30
|
+
|
|
31
|
+
say_status :created, output_path, :green
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_template(operation)
|
|
37
|
+
template_name = "migration.#{operation}.yml"
|
|
38
|
+
template_path = File.join(File.dirname(__FILE__), "templates", template_name)
|
|
39
|
+
File.read(template_path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update_template(template_content, hook)
|
|
43
|
+
template_content.gsub("<entity_type>", entity)
|
|
44
|
+
.gsub("<from_name>", from)
|
|
45
|
+
.gsub("<to_name>", to)
|
|
46
|
+
.gsub("<hook>", hook)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def generate_timestamp
|
|
50
|
+
Time.now.strftime("%Y%m%d%H%M%S")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_filename(operation, entity, from, to, timestamp)
|
|
54
|
+
# Generate descriptive name following server pattern: timestamp_descriptive_name.yml
|
|
55
|
+
# Example: 20250101120000_rename_user_table.yml
|
|
56
|
+
from_slug = slugify(from)
|
|
57
|
+
to_slug = slugify(to)
|
|
58
|
+
descriptive_name = "#{operation}_#{from_slug}_to_#{to_slug}"
|
|
59
|
+
"#{timestamp}_#{descriptive_name}.yml"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def slugify(text)
|
|
63
|
+
text.to_s.downcase
|
|
64
|
+
.gsub(/[^a-z0-9]+/, "_")
|
|
65
|
+
.gsub(/^_|_$/, "")
|
|
66
|
+
.slice(0, 50) # Limit length
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require_relative "group"
|
|
2
|
+
require_relative "datasource"
|
|
3
|
+
require_relative "../helpers/project_helper"
|
|
4
|
+
require_relative "../api/connection_error_handler"
|
|
5
|
+
require "faraday"
|
|
6
|
+
require "json"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
module Strata::CLI
|
|
10
|
+
module Generators
|
|
11
|
+
class Project < Group
|
|
12
|
+
include API::ConnectionErrorHandler
|
|
13
|
+
|
|
14
|
+
BASE_SERVER_URL = "http://localhost:3000"
|
|
15
|
+
|
|
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
|
|
20
|
+
|
|
21
|
+
desc "Generates a new Strata project."
|
|
22
|
+
|
|
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?
|
|
28
|
+
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
|
+
|
|
38
|
+
return unless project_info["git_url"] && !project_info["git_url"].empty?
|
|
39
|
+
|
|
40
|
+
@cloned_from_git = true
|
|
41
|
+
clone_project(project_info["git_url"])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_project_structure
|
|
45
|
+
return if cloned_from_git?
|
|
46
|
+
|
|
47
|
+
empty_directory uid
|
|
48
|
+
empty_directory File.join(uid, "models")
|
|
49
|
+
empty_directory File.join(uid, "tests")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_strata_config_file
|
|
53
|
+
template "strata.yml", "#{uid}/.strata"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def create_project_file
|
|
57
|
+
return if cloned_from_git?
|
|
58
|
+
|
|
59
|
+
template "project.yml", "#{uid}/project.yml"
|
|
60
|
+
end
|
|
61
|
+
|
|
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
|
|
65
|
+
|
|
66
|
+
project_yml_path = File.join(uid, "project.yml")
|
|
67
|
+
return unless File.exist?(project_yml_path)
|
|
68
|
+
|
|
69
|
+
persist_project_id_to_project_yml
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_datasources_file
|
|
73
|
+
return if cloned_from_git?
|
|
74
|
+
|
|
75
|
+
template "datasources.yml", "#{uid}/datasources.yml"
|
|
76
|
+
|
|
77
|
+
return unless options.key?(:datasource)
|
|
78
|
+
|
|
79
|
+
options[:datasource].each do |ds|
|
|
80
|
+
raise DWH::ConfigError, "Unsupported datasource #{ds}" unless DWH.adapter?(ds.to_sym)
|
|
81
|
+
|
|
82
|
+
Datasource.new([ds.downcase.strip], options.merge({"path" => uid})).invoke_all
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def initialize_git
|
|
87
|
+
return if cloned_from_git?
|
|
88
|
+
|
|
89
|
+
inside uid do
|
|
90
|
+
run "git init", verbose: false, capture: true
|
|
91
|
+
create_file ".gitignore", ".strata\n"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def setup_datasource
|
|
96
|
+
return if cloned_from_git?
|
|
97
|
+
return if options.key?(:datasource) # Already specified via CLI option
|
|
98
|
+
|
|
99
|
+
say "\n", :white
|
|
100
|
+
say_status :setup, "Let's configure your first datasource", :cyan
|
|
101
|
+
|
|
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
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
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
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def cloned_from_git?
|
|
121
|
+
@cloned_from_git ||= false
|
|
122
|
+
end
|
|
123
|
+
|
|
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
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
response = with_connection_error_handling(options[:source]) do
|
|
131
|
+
conn.get
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
unless response.success?
|
|
135
|
+
raise Strata::CommandError,
|
|
136
|
+
"Failed to fetch project info from #{options[:source]}. Status: #{response.status}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
response.body
|
|
140
|
+
end
|
|
141
|
+
|
|
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
|
|
145
|
+
|
|
146
|
+
raise Strata::CommandError, "Failed to clone repository from #{git_url}" unless File.directory?(uid)
|
|
147
|
+
|
|
148
|
+
say_status :success, "Project cloned successfully", :green
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def persist_project_id_to_project_yml
|
|
152
|
+
return unless @project_id
|
|
153
|
+
|
|
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
|
|
157
|
+
|
|
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)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
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
|
|
171
|
+
|
|
172
|
+
def server_url
|
|
173
|
+
return options[:source] if options.key?(:source)
|
|
174
|
+
BASE_SERVER_URL
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def description
|
|
178
|
+
return @project_info["description"] if options.key?(:source) && @project_info
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def production_branch
|
|
183
|
+
return @project_info["production_branch"] if options.key?(:source) && @project_info && @project_info["production_branch"]
|
|
184
|
+
"main"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
attr_reader :project_id
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|