mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-08 16:25:58 +08:00
fix(tableau-next-semantic-model-generate): correct broken CLI examples, unqualified syntax, aggregation choices, error docs
This commit is contained in:
parent
2fe32896e4
commit
8018d30d9d
@ -330,8 +330,12 @@ Format: `fieldApiName:tableApiName` (repeat `--additional-dimension` for multipl
|
||||
**Creation success is not data success.** Empty results (empty source, filter excludes everything, expression resolves to nothing) → dashboard shows "No results to show." Check with `--verify-only`:
|
||||
|
||||
```bash
|
||||
python scripts/create_metric.py --sdm <SDM> --name <Metric_mtc> --verify-only
|
||||
python scripts/create_calc_field.py --sdm <SDM> --type measurement --name <Field_clc> --verify-only
|
||||
# --verify-only ignores --label/--calculated-field/--time-* but argparse still requires them; pass any values.
|
||||
python scripts/create_metric.py --sdm <SDM> --name <Metric_mtc> \
|
||||
--label "verify" --calculated-field <ANY_clc> \
|
||||
--time-field <ANY_TimeField> --time-table <ANY_TimeTable> \
|
||||
--verify-only
|
||||
python scripts/create_calc_field.py --sdm <SDM> --type measurement --name <Field_clc> --label "verify" --verify-only
|
||||
```
|
||||
|
||||
Non-zero exit + **NOT shippable** = don't proceed to dashboards. Full protocol (why not to re-POST, `query_data.py --count` for raw objects, empty-source guidance) in **[references/concepts.md](references/concepts.md)** and [references/empty-source-handling.md](references/empty-source-handling.md).
|
||||
@ -353,7 +357,7 @@ python scripts/update_sdm.py {{SDM_NAME}} \
|
||||
python scripts/update_sdm.py {{SDM_NAME}} --agent-enabled --dry-run
|
||||
|
||||
# Confirm via discovery (stored state, not CLI success message)
|
||||
python scripts/discover_sdm.py {{SDM_NAME}} --json
|
||||
python scripts/discover_sdm.py --sdm {{SDM_NAME}} --json
|
||||
```
|
||||
|
||||
The update is a PATCH (partial body — server merges), idempotent. `--categories` takes controlled "Semantic Category" values only — the server rejects unknown values with `Invalid Semantic Category`.
|
||||
|
||||
@ -418,21 +418,34 @@ Downstream builders that consume this contract should enforce this pre-POST and
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return structured error responses:
|
||||
Salesforce API error responses come in two shapes depending on the endpoint.
|
||||
|
||||
**Array form (most common):** a top-level JSON array of one or more error objects.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"errorCode": "INVALID_API_INPUT",
|
||||
"message": "Field 'Amount' is not found in the semantic model.",
|
||||
"fields": ["dataObjectFields"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Single-object form:** a bare JSON object (used by some endpoints and by the platform for auth/session errors).
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "INVALID_FIELD",
|
||||
"message": "Field 'Amount' not found in semantic model 'Sales_Cloud12_backward'",
|
||||
"details": {
|
||||
"fieldName": "Amount",
|
||||
"availableFields": ["Total_Amount", "Close_Date", "Stage"]
|
||||
}
|
||||
}
|
||||
"errorCode": "INVALID_SESSION_ID",
|
||||
"message": "Session expired or invalid.",
|
||||
"fields": []
|
||||
}
|
||||
```
|
||||
|
||||
A `localizedMessage` key may appear in place of (or alongside) `message` in platform-level errors.
|
||||
|
||||
The `fields` array is present when the error is scoped to specific request fields; it is omitted or empty otherwise.
|
||||
|
||||
**Common Error Codes:**
|
||||
- `INVALID_TOKEN`: Authentication token is invalid or expired
|
||||
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions
|
||||
|
||||
@ -26,7 +26,7 @@ python scripts/create_calc_field.py \
|
||||
--type measurement \
|
||||
--name Total_Revenue_clc \
|
||||
--label "Total Revenue" \
|
||||
--expression "SUM([Amount])" \
|
||||
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Amount])" \
|
||||
--aggregation Sum
|
||||
|
||||
# Step 2: Create metric
|
||||
@ -141,7 +141,7 @@ python scripts/create_calc_field.py \
|
||||
--type measurement \
|
||||
--name Win_Rate_clc \
|
||||
--label "Win Rate" \
|
||||
--expression "SUM([Won_Count]) / SUM([Total_Count])" \
|
||||
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Won_Count]) / SUM([Opportunity_TAB_Sales_Cloud].[Total_Count])" \
|
||||
--aggregation UserAgg
|
||||
|
||||
# Step 2: Create metric
|
||||
@ -169,7 +169,7 @@ python scripts/create_calc_field.py \
|
||||
--type measurement \
|
||||
--name Weighted_Pipeline_clc \
|
||||
--label "Weighted Pipeline Value" \
|
||||
--expression "SUM([Amount] * [Probability])" \
|
||||
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Amount] * [Opportunity_TAB_Sales_Cloud].[Probability])" \
|
||||
--aggregation Sum
|
||||
|
||||
# Step 2: Create metric
|
||||
@ -197,7 +197,7 @@ python scripts/create_calc_field.py \
|
||||
--type measurement \
|
||||
--name Avg_Sales_Cycle_clc \
|
||||
--label "Average Sales Cycle (Days)" \
|
||||
--expression "AVG(DATEDIFF('day', [Created_Date], [Close_Date]))" \
|
||||
--expression "AVG(DATEDIFF('day', [Opportunity_TAB_Sales_Cloud].[Created_Date], [Opportunity_TAB_Sales_Cloud].[Close_Date]))" \
|
||||
--aggregation Avg
|
||||
|
||||
# Step 2: Create metric
|
||||
@ -375,12 +375,12 @@ After creating a metric, test it by:
|
||||
|
||||
| Pattern | Calc Field Expression | Aggregation | Use Case |
|
||||
|---------|----------------------|-------------|----------|
|
||||
| Simple sum | `SUM([Field])` | Sum | Total revenue, total count |
|
||||
| Average | `AVG([Field])` | Avg | Average deal size, average duration |
|
||||
| Ratio | `SUM([A]) / SUM([B])` | UserAgg | Win rate, conversion rate |
|
||||
| Weighted | `SUM([A] * [B])` | Sum | Weighted pipeline, risk-adjusted |
|
||||
| Time calc | `AVG(DATEDIFF('day', [Start], [End]))` | Avg | Sales cycle, time to close |
|
||||
| Conditional sum | `SUM(IF condition THEN [Field] ELSE 0 END)` | Sum | Won revenue, qualified leads |
|
||||
| Simple sum | `SUM([Table].[Field])` | Sum | Total revenue, total count |
|
||||
| Average | `AVG([Table].[Field])` | Avg | Average deal size, average duration |
|
||||
| Ratio | `SUM([Table].[A]) / SUM([Table].[B])` | UserAgg | Win rate, conversion rate |
|
||||
| Weighted | `SUM([Table].[A] * [Table].[B])` | Sum | Weighted pipeline, risk-adjusted |
|
||||
| Time calc | `AVG(DATEDIFF('day', [Table].[Start], [Table].[End]))` | Avg | Sales cycle, time to close |
|
||||
| Conditional sum | `SUM(IF condition THEN [Table].[Field] ELSE 0 END)` | Sum | Won revenue, qualified leads |
|
||||
|
||||
## Metric Lifecycle
|
||||
|
||||
|
||||
@ -119,7 +119,7 @@ def build_calculated_measurement(
|
||||
api_name: API name (must end with _clc)
|
||||
label: Display label
|
||||
expression: Tableau formula expression
|
||||
aggregation_type: Aggregation type (Sum, Avg, Count, Min, Max, UserAgg, Median)
|
||||
aggregation_type: Aggregation type (Sum, Avg, Count, CountDistinct, Min, Max, UserAgg)
|
||||
data_type: Data type (Number, Text, Boolean, DateTime)
|
||||
decimal_place: Decimal places for Number fields
|
||||
description: Field description
|
||||
@ -155,11 +155,11 @@ def build_calculated_measurement(
|
||||
final_agg_type = aggregation_type
|
||||
level = "Row"
|
||||
# Validate explicit aggregation types are allowed
|
||||
if final_agg_type not in ("Sum", "Avg", "Count", "Min", "Max", "Median", "UserAgg"):
|
||||
if final_agg_type not in ("Sum", "Avg", "Count", "CountDistinct", "Min", "Max", "UserAgg"):
|
||||
import warnings
|
||||
warnings.warn(
|
||||
f"Explicit aggregation type '{final_agg_type}' may not be supported. "
|
||||
f"Common types: Sum, Avg, Count, Min, Max, Median"
|
||||
f"Common types: Sum, Avg, Count, CountDistinct, Min, Max"
|
||||
)
|
||||
|
||||
payload = {
|
||||
@ -262,7 +262,7 @@ def validate_calc_field(
|
||||
|
||||
# Check aggregation type
|
||||
if field_type == "measurement" and aggregation_type:
|
||||
valid_agg = ["Sum", "Avg", "Count", "Min", "Max", "UserAgg", "Median"]
|
||||
valid_agg = ["Sum", "Avg", "Count", "CountDistinct", "Min", "Max", "UserAgg"]
|
||||
if aggregation_type not in valid_agg:
|
||||
errors.append(f"aggregationType must be one of: {', '.join(valid_agg)}")
|
||||
|
||||
|
||||
@ -139,7 +139,7 @@ def main() -> int:
|
||||
|
||||
# Measurement-specific
|
||||
parser.add_argument("--aggregation", default="UserAgg",
|
||||
choices=["Sum", "Avg", "Count", "Min", "Max", "UserAgg", "Median"],
|
||||
choices=["Sum", "Avg", "Count", "CountDistinct", "Min", "Max", "UserAgg"],
|
||||
help="Aggregation type (for measurements, default: UserAgg)")
|
||||
parser.add_argument("--data-type", choices=["Number", "Text", "Boolean", "DateTime"],
|
||||
help="Data type (default: Number for measurements, inferred for dimensions)")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user