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,43 @@
|
|
|
1
|
+
# Define relationshps between tables for a given universe
|
|
2
|
+
|
|
3
|
+
# Required: The datasource these relationships are valid for.
|
|
4
|
+
# The chosen tables will be scoped to those within this datasource.
|
|
5
|
+
# Strata does not support cross datasource joins
|
|
6
|
+
datasource: "<datasource_name>"
|
|
7
|
+
|
|
8
|
+
# Define relationships between tables. The table names used here should
|
|
9
|
+
# correspond to the name given in its respective model file and not the
|
|
10
|
+
# physical name (often they are same).
|
|
11
|
+
# NOTE: Strata does not support many_to_many joins.
|
|
12
|
+
|
|
13
|
+
# Example: One-to-many relationship (customer has many orders)
|
|
14
|
+
# customer_orders:
|
|
15
|
+
# left: "customers"
|
|
16
|
+
# right: "orders"
|
|
17
|
+
# join: "left.id = right.customer_id"
|
|
18
|
+
# cardinality: "one_to_many"
|
|
19
|
+
|
|
20
|
+
# Example: Many-to-one relationship (orders belong to customer)
|
|
21
|
+
# order_customer:
|
|
22
|
+
# left: "orders"
|
|
23
|
+
# right: "customers"
|
|
24
|
+
# join: "left.customer_id = right.id"
|
|
25
|
+
# cardinality: "many_to_one"
|
|
26
|
+
# # Whether measurs should be aggregated from the low cardinality
|
|
27
|
+
# # table. In most cases this will overcount.
|
|
28
|
+
# allow_measure_expansion: true|false (default false)
|
|
29
|
+
|
|
30
|
+
# Example: One-to-one relationship (user has one profile)
|
|
31
|
+
# user_profile:
|
|
32
|
+
# left: "users"
|
|
33
|
+
# right: "user_profiles"
|
|
34
|
+
# join: "left.id = right.user_id"
|
|
35
|
+
# cardinality: "one_to_one"
|
|
36
|
+
|
|
37
|
+
# Example: Compound join
|
|
38
|
+
# user_roles:
|
|
39
|
+
# left: "users"
|
|
40
|
+
# right: "roles"
|
|
41
|
+
# join: "left.id = right.user_id AND left.id = right.role_id"
|
|
42
|
+
# cardinality: "one_to_many"
|
|
43
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Strata Configuration file. Do not check this into git.
|
|
2
|
+
#
|
|
3
|
+
# This file contains credentials and local-only configuration.
|
|
4
|
+
# Project-level configuration (project_id, server) should be in project.yml.
|
|
5
|
+
#
|
|
6
|
+
# Strata uses API keys to configure access. Please get your API key
|
|
7
|
+
# from your Strata admins. You can regenerate/revoke API keys as
|
|
8
|
+
# needed on the server. You must be Developer on a Project to modify
|
|
9
|
+
# it or you must be System Manager to be able to create new projects.
|
|
10
|
+
api_key: <%= options[:api_key] || "YOUR_STRATA_API_KEY" %>
|
|
11
|
+
|
|
12
|
+
# Alternatively, you can configure API keys per environment (different Strata server instances).
|
|
13
|
+
# Each environment represents a separate Strata cluster (dev, staging, production).
|
|
14
|
+
# Use -e [ENVIRONMENT] flag when deploying to select the environment.
|
|
15
|
+
# Example:
|
|
16
|
+
# production:
|
|
17
|
+
# api_key: YOUR_PRODUCTION_API_KEY
|
|
18
|
+
# staging:
|
|
19
|
+
# api_key: YOUR_STAGING_API_KEY
|
|
20
|
+
#
|
|
21
|
+
|
|
22
|
+
# Credentials here are stored per datasource. These credentials
|
|
23
|
+
# should be set via `strata ds auth DS_KEY`. DS_KEY should match the datasouces
|
|
24
|
+
# yaml node key for the datasource you are trying to update.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Semantic Table Model
|
|
2
|
+
#
|
|
3
|
+
# Define the Measures and Dimensions that should be attached to this table.
|
|
4
|
+
# Note: Relationships are managed separately and should not be defined in this file.
|
|
5
|
+
|
|
6
|
+
# Required: The datasource this table belongs to. This should be
|
|
7
|
+
# either the datasource key or datasource name.
|
|
8
|
+
datasource: "<datasource_name>"
|
|
9
|
+
|
|
10
|
+
# Required: The logical name for this table in the semantic model. Must be unique
|
|
11
|
+
# within this datasource. You can have mulitple tables point to the same physical table.
|
|
12
|
+
name: "<table_name>"
|
|
13
|
+
|
|
14
|
+
# Required: The physical table name in the actual database. This can be prefixed
|
|
15
|
+
# with catalog and schema as needed. Be sure to check whether your data wareshouse
|
|
16
|
+
# supports cross schema/catalog queries.
|
|
17
|
+
physical_name: "<physical_table_name>"
|
|
18
|
+
|
|
19
|
+
# Required: Cost number influences how the table is preferentially
|
|
20
|
+
# selected. If multiple tables can answer the same question,
|
|
21
|
+
# the lowest cost table is selected..
|
|
22
|
+
# Dimension tables should generally be lower cost and your
|
|
23
|
+
# hot tier tables should be lower than cold tier.
|
|
24
|
+
cost: 10
|
|
25
|
+
|
|
26
|
+
# Optional: If this is a snapshot table like an inventory table, you can
|
|
27
|
+
# set the snapshot date dimension here. This will allow the creation of
|
|
28
|
+
# snapshot measures on this table.
|
|
29
|
+
# snapshot_date: <snapshot dimension name>
|
|
30
|
+
|
|
31
|
+
# Optional: Tag table for additional metadata.
|
|
32
|
+
# tags:
|
|
33
|
+
# - marketing
|
|
34
|
+
# - ops
|
|
35
|
+
|
|
36
|
+
# Optional: Define table partitioning constraints
|
|
37
|
+
# This tells the semantic engine about data availability/constraints
|
|
38
|
+
# Only **between** and **in_list** predicates are supported.
|
|
39
|
+
# partition:
|
|
40
|
+
# # Example: Table only contains 2 years of data
|
|
41
|
+
# # *Must reference a dimension mapped to this table.
|
|
42
|
+
# - dimension: Region Date
|
|
43
|
+
# predicate: between
|
|
44
|
+
# filter_value: 2y
|
|
45
|
+
# filter_value_end: 1d
|
|
46
|
+
# description: "Table only has rolling 2 year data"
|
|
47
|
+
#
|
|
48
|
+
# # Example: Table is partitioned by region
|
|
49
|
+
# - dimension: Region
|
|
50
|
+
# predicate: in_list
|
|
51
|
+
# filter_value: us-east, us-west, europe
|
|
52
|
+
# description: "Table only has 3 regions of 5"
|
|
53
|
+
|
|
54
|
+
# Imports are processed first, then local field definitions are merged in
|
|
55
|
+
# This allows you to inherit common field definitions and override or extend them
|
|
56
|
+
# Paths should be relative to this file.
|
|
57
|
+
# imports:
|
|
58
|
+
# - "path/to/other/table.yml"
|
|
59
|
+
# - "common/shared_fields.yml"
|
|
60
|
+
|
|
61
|
+
# Define Measures and dimensions that should be mapped to this table
|
|
62
|
+
fields:
|
|
63
|
+
# Example field definition structure:
|
|
64
|
+
# - type: dimension|measure (Required)
|
|
65
|
+
# name: My Field Name (Required)
|
|
66
|
+
# description: Describing my awesome field
|
|
67
|
+
# data_type: string|integer|bigint|decimal|date|date_time|boolean|binary (Required)
|
|
68
|
+
# hidden: true|false
|
|
69
|
+
#
|
|
70
|
+
# # Optional: UI rendering of the field
|
|
71
|
+
# display_type: default|html|url|email|phone_number|image
|
|
72
|
+
#
|
|
73
|
+
# # Optional: Specific formatting to be applied (will supercede display_type)
|
|
74
|
+
# formatter: currency_usd|percent|thousands|millions|billions|(custom js function using numeraljs/momentjs)
|
|
75
|
+
#
|
|
76
|
+
# # Optional: disable listing individual elements (dimension only). Good to do that for
|
|
77
|
+
# # high cardinality columns like account_id
|
|
78
|
+
# disable_value_listing: true|false
|
|
79
|
+
# # Optional: limit on items from this field that should be shown. Will affect list cache size.
|
|
80
|
+
# # Dimensions only. Default is 1000
|
|
81
|
+
# value_list_size: 1000
|
|
82
|
+
#
|
|
83
|
+
# # Optional: For date/time types only. Set of grainularities this date should support.
|
|
84
|
+
# # Default all. raw, second, minute, hour, day, week, month, quarter, year
|
|
85
|
+
# grains:
|
|
86
|
+
# - day
|
|
87
|
+
# - week
|
|
88
|
+
# - month
|
|
89
|
+
# - quarter
|
|
90
|
+
# - year
|
|
91
|
+
#
|
|
92
|
+
# # Required: Defines how this field will query this table
|
|
93
|
+
# expresssion:
|
|
94
|
+
# primary_key: true|false (optional)
|
|
95
|
+
# lookup: true|false (optional)
|
|
96
|
+
# array: true|false (optional)
|
|
97
|
+
# sql: my_field_column (Required)
|
|
98
|
+
#
|
|
99
|
+
# # Optional: Exclude certain dimnesions from the group by/filter
|
|
100
|
+
# exclusion_type: exclude|exclude_all_except|exclude_all
|
|
101
|
+
# exclusions: # (Required when exclusion type is set and isnt exclude_all)
|
|
102
|
+
# - type: table|dimension|universe (Required)
|
|
103
|
+
# filter: ignore|only|apply (Required)
|
|
104
|
+
# entities:
|
|
105
|
+
# - <entity name>
|
|
106
|
+
# - <entity name>
|
|
107
|
+
#
|
|
108
|
+
# # Optional: Inclusions force calculation at lower level before rolling up
|
|
109
|
+
# inclusions:
|
|
110
|
+
# filter: ignore|only|apply
|
|
111
|
+
# aggregation: percentile_cont(0.5) WITHIN GROUP (ORDER BY @exp) # Final rollup calculation. Base calc is expression -> sql
|
|
112
|
+
# dimensions:
|
|
113
|
+
# - <dimension name>
|
|
114
|
+
# - <dimension name>
|
|
115
|
+
#
|
|
116
|
+
# # Optional: Snapshot measures type. Will force calc to starting or ending period.
|
|
117
|
+
# # Table must also set Snapshot date value.
|
|
118
|
+
# snapshot: ending|beginning
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Test Definition
|
|
2
|
+
#
|
|
3
|
+
# This file defines a test case for validating semantic model queries.
|
|
4
|
+
# Tests verify that the Planner generates correct SQL from your semantic model.
|
|
5
|
+
# Tests run on the server during deployment.
|
|
6
|
+
|
|
7
|
+
# Required: Display name for this test
|
|
8
|
+
name: <test_name>
|
|
9
|
+
|
|
10
|
+
# Required: List of semantic field names to query
|
|
11
|
+
# These must match field names defined in your model files
|
|
12
|
+
projections:
|
|
13
|
+
- Field Name 1
|
|
14
|
+
- Field Name 2
|
|
15
|
+
|
|
16
|
+
# Optional: Filter conditions to apply to the query
|
|
17
|
+
# filters:
|
|
18
|
+
# - field: Date Field
|
|
19
|
+
# predicate: between
|
|
20
|
+
# filter_value: "2024-01-01"
|
|
21
|
+
# filter_value_end: "2024-12-31"
|
|
22
|
+
# - field: Status
|
|
23
|
+
# predicate: equals
|
|
24
|
+
# filter_value: "active"
|
|
25
|
+
|
|
26
|
+
# Required: At least ONE assertion must be present (assert_sql or assert_regex)
|
|
27
|
+
|
|
28
|
+
# Full SQL assertion - the complete SQL you expect the Planner to generate
|
|
29
|
+
# Use this for exact matching (whitespace normalized)
|
|
30
|
+
# assert_sql: "SELECT column1, column2 FROM table_name WHERE condition"
|
|
31
|
+
|
|
32
|
+
# OR Regex assertion - pattern to match against generated SQL
|
|
33
|
+
# Use this for partial/flexible matching
|
|
34
|
+
# assert_regex: "column_name.*table_name"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "group"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
module Generators
|
|
9
|
+
# Generates test YAML files from templates.
|
|
10
|
+
class Test < Group
|
|
11
|
+
argument :name, type: :string, desc: "Test name"
|
|
12
|
+
|
|
13
|
+
def create_test_file
|
|
14
|
+
output_path = File.join("tests", "#{slugify(name)}.yml")
|
|
15
|
+
|
|
16
|
+
# Ensure directory exists
|
|
17
|
+
empty_directory "tests"
|
|
18
|
+
|
|
19
|
+
# Load template and update with user inputs
|
|
20
|
+
template_content = load_template
|
|
21
|
+
updated_content = update_template(template_content)
|
|
22
|
+
|
|
23
|
+
# Write the updated template
|
|
24
|
+
create_file output_path, updated_content
|
|
25
|
+
|
|
26
|
+
say_status :created, output_path, :green
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def load_template
|
|
32
|
+
template_path = File.join(File.dirname(__FILE__), "templates", "test.yml")
|
|
33
|
+
File.read(template_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update_template(template_content)
|
|
37
|
+
template_content.gsub("<test_name>", name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def slugify(text)
|
|
41
|
+
text.to_s.downcase
|
|
42
|
+
.gsub(/[^a-z0-9]+/, "_")
|
|
43
|
+
.gsub(/^_|_$/, "")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "utils"
|
|
2
|
+
module Strata
|
|
3
|
+
module CLI
|
|
4
|
+
module Guard
|
|
5
|
+
ALLOWED_COMMANDS = %w[
|
|
6
|
+
init
|
|
7
|
+
help
|
|
8
|
+
adapters
|
|
9
|
+
version
|
|
10
|
+
deploy
|
|
11
|
+
]
|
|
12
|
+
def invoke_command(command, *args)
|
|
13
|
+
Utils.exit_error_if_not_strata! unless ALLOWED_COMMANDS.include?(command.name)
|
|
14
|
+
super
|
|
15
|
+
rescue Strata::CommandError => e
|
|
16
|
+
shell.say_error "ERROR: #{e.message}", :red
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Strata
|
|
6
|
+
module CLI
|
|
7
|
+
module ColorHelper
|
|
8
|
+
THEME = {
|
|
9
|
+
success: :green,
|
|
10
|
+
error: %i[red bold],
|
|
11
|
+
warning: :yellow,
|
|
12
|
+
info: :cyan,
|
|
13
|
+
title: %i[cyan bold],
|
|
14
|
+
highlight: %i[cyan bold],
|
|
15
|
+
dim: :bright_black,
|
|
16
|
+
primary: :blue,
|
|
17
|
+
secondary: :magenta,
|
|
18
|
+
border: %i[cyan dim],
|
|
19
|
+
selected: %i[green bold],
|
|
20
|
+
disabled: :dim
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def pastel
|
|
25
|
+
@pastel ||= Pastel.new(enabled: $stdout.tty?)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def success(text = nil)
|
|
29
|
+
apply_theme(:success, text)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def error(text = nil)
|
|
33
|
+
apply_theme(:error, text)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def warning(text = nil)
|
|
37
|
+
apply_theme(:warning, text)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def info(text = nil)
|
|
41
|
+
apply_theme(:info, text)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def title(text = nil)
|
|
45
|
+
apply_theme(:title, text)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def highlight(text = nil)
|
|
49
|
+
apply_theme(:highlight, text)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dim(text = nil)
|
|
53
|
+
apply_theme(:dim, text)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def primary(text = nil)
|
|
57
|
+
apply_theme(:primary, text)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def secondary(text = nil)
|
|
61
|
+
apply_theme(:secondary, text)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def border(text = nil)
|
|
65
|
+
apply_theme(:border, text)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def selected(text = nil)
|
|
69
|
+
apply_theme(:selected, text)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def disabled(text = nil)
|
|
73
|
+
apply_theme(:disabled, text)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def bright_cyan(text = nil)
|
|
77
|
+
text ? pastel.bright_cyan(text) : :bright_cyan
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def bright_green(text = nil)
|
|
81
|
+
text ? pastel.bright_green(text) : :bright_green
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def bright_yellow(text = nil)
|
|
85
|
+
text ? pastel.bright_yellow(text) : :bright_yellow
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def apply_theme(key, text)
|
|
91
|
+
styles = THEME[key]
|
|
92
|
+
return styles unless text
|
|
93
|
+
|
|
94
|
+
if styles.is_a?(Array)
|
|
95
|
+
pastel.decorate(text, *styles)
|
|
96
|
+
else
|
|
97
|
+
pastel.send(styles, text)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
require_relative "datasource_helper"
|
|
5
|
+
require_relative "color_helper"
|
|
6
|
+
require_relative "prompts"
|
|
7
|
+
|
|
8
|
+
module Strata
|
|
9
|
+
module CLI
|
|
10
|
+
module Helpers
|
|
11
|
+
module CommandContext
|
|
12
|
+
include DatasourceHelper
|
|
13
|
+
|
|
14
|
+
def adapter
|
|
15
|
+
@adapter ||= create_adapter(datasource_key)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def prompt
|
|
19
|
+
@prompt ||= TTY::Prompt.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def datasource_key
|
|
23
|
+
@datasource_key ||= resolve_datasource(prompt: prompt)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def all_tables
|
|
27
|
+
@all_tables ||= begin
|
|
28
|
+
tables = with_spinner("Fetching tables from #{datasource_key}...") { adapter.tables }
|
|
29
|
+
if tables.empty?
|
|
30
|
+
# Assuming Prompts and ColorHelper are available in the class or via module
|
|
31
|
+
say Prompts::MSG_NO_TABLES_FOUND % datasource_key, ColorHelper.warning
|
|
32
|
+
[]
|
|
33
|
+
else
|
|
34
|
+
tables
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Strata
|
|
4
|
+
module CLI
|
|
5
|
+
module DatasourceHelper
|
|
6
|
+
def resolve_datasource(ds_key_arg = nil, prompt: TTY::Prompt.new)
|
|
7
|
+
# 1. Use argument if provided
|
|
8
|
+
return validate_datasource(ds_key_arg) if ds_key_arg
|
|
9
|
+
|
|
10
|
+
# 2. Use option if provided
|
|
11
|
+
return validate_datasource(options[:datasource]) if options[:datasource]
|
|
12
|
+
|
|
13
|
+
# 3. Check available datasources
|
|
14
|
+
ds_keys = datasources.keys
|
|
15
|
+
|
|
16
|
+
if ds_keys.empty?
|
|
17
|
+
say "No datasources configured. Run 'strata datasource add' first.", :red
|
|
18
|
+
nil
|
|
19
|
+
elsif ds_keys.length == 1
|
|
20
|
+
# Auto-select if only one
|
|
21
|
+
ds_key = ds_keys.first
|
|
22
|
+
say "Using datasource: #{ds_key}", :cyan
|
|
23
|
+
ds_key
|
|
24
|
+
else
|
|
25
|
+
# Prompt selection if multiple
|
|
26
|
+
prompt.select("Select datasource:", ds_keys)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_adapter(ds_key)
|
|
31
|
+
config = ds_config(ds_key).merge(Credentials.fetch(ds_key))
|
|
32
|
+
DWH.create(config["adapter"].to_sym, config)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ds_config(ds_key)
|
|
36
|
+
unless datasources.key?(ds_key)
|
|
37
|
+
raise "Datasource definition with key #{ds_key} was not found in datasources.yml file."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
datasources[ds_key]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def validate_datasource(ds_key)
|
|
46
|
+
unless datasources[ds_key]
|
|
47
|
+
say "Error: Datasource '#{ds_key}' not found in datasources.yml", :red
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
ds_key
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def datasources
|
|
54
|
+
@datasources_cache ||= begin
|
|
55
|
+
YAML.safe_load_file("datasources.yml", permitted_classes: [Date, Time], aliases: true) || {}
|
|
56
|
+
rescue Errno::ENOENT
|
|
57
|
+
{}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Strata
|
|
2
|
+
module CLI
|
|
3
|
+
module Helpers
|
|
4
|
+
module DescriptionHelper
|
|
5
|
+
def long_desc_from_file(path)
|
|
6
|
+
# Descriptions are in lib/strata/cli/descriptions/, not in helpers/
|
|
7
|
+
descriptions_dir = File.join(File.dirname(__dir__), "descriptions")
|
|
8
|
+
file_path = File.join(descriptions_dir, "#{path}.txt")
|
|
9
|
+
if File.exist?(file_path)
|
|
10
|
+
long_desc File.read(file_path)
|
|
11
|
+
else
|
|
12
|
+
warn "Warning: Description file not found at #{file_path}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "../utils/git"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
module Helpers
|
|
9
|
+
module ProjectHelper
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Persists project_id to project.yml file
|
|
13
|
+
def persist_project_id_to_yml(project_id, project_yml_path: "project.yml")
|
|
14
|
+
return false unless project_id && !project_id.to_s.strip.empty?
|
|
15
|
+
return false unless File.exist?(project_yml_path)
|
|
16
|
+
|
|
17
|
+
project_config = YAML.safe_load_file(project_yml_path, permitted_classes: [Date, Time], aliases: true) || {}
|
|
18
|
+
return false if project_config["project_id"] && !project_config["project_id"].to_s.strip.empty?
|
|
19
|
+
|
|
20
|
+
project_yml_content = File.read(project_yml_path)
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
if /^#\s*project_id:\s*$/.match?(project_yml_content)
|
|
24
|
+
updated_content = project_yml_content.gsub(/^#\s*project_id:\s*$/, "project_id: #{project_id}")
|
|
25
|
+
File.write(project_yml_path, updated_content)
|
|
26
|
+
else
|
|
27
|
+
warning_comment = <<~YAML
|
|
28
|
+
# WARNING: Do not change this project_id. It links this project to the Strata server.
|
|
29
|
+
# Changing it may result in creating a new project or deployment failures.
|
|
30
|
+
project_id: #{project_id}
|
|
31
|
+
YAML
|
|
32
|
+
|
|
33
|
+
File.open(project_yml_path, "a") do |f|
|
|
34
|
+
f.puts "\n" unless project_yml_content.end_with?("\n")
|
|
35
|
+
f.puts warning_comment
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
true
|
|
40
|
+
rescue Errno::EACCES => e
|
|
41
|
+
raise Strata::CommandError, "Permission denied writing to #{project_yml_path}: #{e.message}"
|
|
42
|
+
rescue Errno::ENOSPC => e
|
|
43
|
+
raise Strata::CommandError, "Disk full: #{e.message}"
|
|
44
|
+
rescue => e
|
|
45
|
+
raise Strata::CommandError, "Failed to write to #{project_yml_path}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Persists git URL to project.yml file if missing
|
|
50
|
+
def persist_git_url_if_missing(project_yml_path: "project.yml")
|
|
51
|
+
return false unless File.exist?(project_yml_path)
|
|
52
|
+
|
|
53
|
+
project_config = YAML.safe_load_file(project_yml_path, permitted_classes: [Date, Time], aliases: true) || {}
|
|
54
|
+
return false if project_config["git"] && !project_config["git"].to_s.strip.empty?
|
|
55
|
+
|
|
56
|
+
git_remote = Utils::Git.git_remote_url
|
|
57
|
+
return false unless git_remote
|
|
58
|
+
|
|
59
|
+
normalized_url = git_remote.chomp("/").chomp(".git")
|
|
60
|
+
project_yml_content = File.read(project_yml_path)
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
if /^(\s*)git:\s*$/.match?(project_yml_content)
|
|
64
|
+
updated_content = project_yml_content.gsub(/^(\s*)git:\s*$/, "\\1git: #{normalized_url}")
|
|
65
|
+
File.write(project_yml_path, updated_content)
|
|
66
|
+
else
|
|
67
|
+
File.open(project_yml_path, "a") do |f|
|
|
68
|
+
f.puts "\n" unless project_yml_content.end_with?("\n")
|
|
69
|
+
f.puts "git: #{normalized_url}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
true
|
|
74
|
+
rescue Errno::EACCES => e
|
|
75
|
+
raise Strata::CommandError, "Permission denied writing to #{project_yml_path}: #{e.message}"
|
|
76
|
+
rescue Errno::ENOSPC => e
|
|
77
|
+
raise Strata::CommandError, "Disk full: #{e.message}"
|
|
78
|
+
rescue => e
|
|
79
|
+
raise Strata::CommandError, "Failed to write to #{project_yml_path}: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Strata
|
|
2
|
+
module CLI
|
|
3
|
+
module Prompts
|
|
4
|
+
# Relation Command Prompts
|
|
5
|
+
MSG_SELECT_LEFT_TABLE = "Select LEFT table (the 'many' side usually):"
|
|
6
|
+
MSG_SELECT_RIGHT_TABLE_ALL = "Select RIGHT table (All):"
|
|
7
|
+
MSG_SELECT_RIGHT_TABLE_SUGGESTED = "Select RIGHT table (Suggested):"
|
|
8
|
+
MSG_NO_TABLES_FOUND = "No tables found in %s"
|
|
9
|
+
MSG_SHOW_OTHER_TABLES = "Show other tables..."
|
|
10
|
+
MSG_SELECT_FROM_LIST = "Select from list instead?"
|
|
11
|
+
MSG_JOIN_CONDITION = " Join Condition (SQL):"
|
|
12
|
+
MSG_RELATION_PATH = " Relationship Path:"
|
|
13
|
+
|
|
14
|
+
# Table Command Prompts
|
|
15
|
+
MSG_SEARCH_TABLE = "Search table:"
|
|
16
|
+
MSG_MODEL_DISPLAY_NAME = " Model display name:"
|
|
17
|
+
MSG_MODEL_DESCRIPTION = " Model description (optional):"
|
|
18
|
+
MSG_NO_FIELDS_CONFIRMED = "\n⚠️ Warning: No fields confirmed. All fields may have been skipped."
|
|
19
|
+
MSG_CONTINUE_NO_FIELDS = "Continue creating model with no fields?"
|
|
20
|
+
MSG_FIELDS_CONFIRMED = "\n %d field(s) confirmed"
|
|
21
|
+
MSG_CREATED_MODEL = "\n✔ Created %s"
|
|
22
|
+
MSG_EDIT_MODEL_HINT = "\n💡 Edit the file to customize field names & expressions"
|
|
23
|
+
|
|
24
|
+
MSG_NO_MODELS_DIR = "No models directory found. Run 'strata create table' to create your first model."
|
|
25
|
+
MSG_NO_MODELS_FOUND = "No models found. Run 'strata create table' to create one."
|
|
26
|
+
MSG_MODELS_LIST_HEADER = "\n Semantic Models:\n"
|
|
27
|
+
MSG_MODELS_COUNT = "\n Total: %d model(s)\n"
|
|
28
|
+
|
|
29
|
+
# Migration hook options
|
|
30
|
+
MIGRATION_HOOK_OPTIONS = {
|
|
31
|
+
"pre (before deployment)" => "pre",
|
|
32
|
+
"post (after deployment)" => "post"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def default_migration_hook(operation)
|
|
38
|
+
(operation == "swap") ? "post (after deployment)" : "pre (before deployment)"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|