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`:
|
**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
|
```bash
|
||||||
python scripts/create_metric.py --sdm <SDM> --name <Metric_mtc> --verify-only
|
# --verify-only ignores --label/--calculated-field/--time-* but argparse still requires them; pass any values.
|
||||||
python scripts/create_calc_field.py --sdm <SDM> --type measurement --name <Field_clc> --verify-only
|
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).
|
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
|
python scripts/update_sdm.py {{SDM_NAME}} --agent-enabled --dry-run
|
||||||
|
|
||||||
# Confirm via discovery (stored state, not CLI success message)
|
# 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`.
|
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
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
"error": {
|
"errorCode": "INVALID_SESSION_ID",
|
||||||
"code": "INVALID_FIELD",
|
"message": "Session expired or invalid.",
|
||||||
"message": "Field 'Amount' not found in semantic model 'Sales_Cloud12_backward'",
|
"fields": []
|
||||||
"details": {
|
|
||||||
"fieldName": "Amount",
|
|
||||||
"availableFields": ["Total_Amount", "Close_Date", "Stage"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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:**
|
**Common Error Codes:**
|
||||||
- `INVALID_TOKEN`: Authentication token is invalid or expired
|
- `INVALID_TOKEN`: Authentication token is invalid or expired
|
||||||
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions
|
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions
|
||||||
|
|||||||
@ -26,7 +26,7 @@ python scripts/create_calc_field.py \
|
|||||||
--type measurement \
|
--type measurement \
|
||||||
--name Total_Revenue_clc \
|
--name Total_Revenue_clc \
|
||||||
--label "Total Revenue" \
|
--label "Total Revenue" \
|
||||||
--expression "SUM([Amount])" \
|
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Amount])" \
|
||||||
--aggregation Sum
|
--aggregation Sum
|
||||||
|
|
||||||
# Step 2: Create metric
|
# Step 2: Create metric
|
||||||
@ -141,7 +141,7 @@ python scripts/create_calc_field.py \
|
|||||||
--type measurement \
|
--type measurement \
|
||||||
--name Win_Rate_clc \
|
--name Win_Rate_clc \
|
||||||
--label "Win Rate" \
|
--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
|
--aggregation UserAgg
|
||||||
|
|
||||||
# Step 2: Create metric
|
# Step 2: Create metric
|
||||||
@ -169,7 +169,7 @@ python scripts/create_calc_field.py \
|
|||||||
--type measurement \
|
--type measurement \
|
||||||
--name Weighted_Pipeline_clc \
|
--name Weighted_Pipeline_clc \
|
||||||
--label "Weighted Pipeline Value" \
|
--label "Weighted Pipeline Value" \
|
||||||
--expression "SUM([Amount] * [Probability])" \
|
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Amount] * [Opportunity_TAB_Sales_Cloud].[Probability])" \
|
||||||
--aggregation Sum
|
--aggregation Sum
|
||||||
|
|
||||||
# Step 2: Create metric
|
# Step 2: Create metric
|
||||||
@ -197,7 +197,7 @@ python scripts/create_calc_field.py \
|
|||||||
--type measurement \
|
--type measurement \
|
||||||
--name Avg_Sales_Cycle_clc \
|
--name Avg_Sales_Cycle_clc \
|
||||||
--label "Average Sales Cycle (Days)" \
|
--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
|
--aggregation Avg
|
||||||
|
|
||||||
# Step 2: Create metric
|
# Step 2: Create metric
|
||||||
@ -375,12 +375,12 @@ After creating a metric, test it by:
|
|||||||
|
|
||||||
| Pattern | Calc Field Expression | Aggregation | Use Case |
|
| Pattern | Calc Field Expression | Aggregation | Use Case |
|
||||||
|---------|----------------------|-------------|----------|
|
|---------|----------------------|-------------|----------|
|
||||||
| Simple sum | `SUM([Field])` | Sum | Total revenue, total count |
|
| Simple sum | `SUM([Table].[Field])` | Sum | Total revenue, total count |
|
||||||
| Average | `AVG([Field])` | Avg | Average deal size, average duration |
|
| Average | `AVG([Table].[Field])` | Avg | Average deal size, average duration |
|
||||||
| Ratio | `SUM([A]) / SUM([B])` | UserAgg | Win rate, conversion rate |
|
| Ratio | `SUM([Table].[A]) / SUM([Table].[B])` | UserAgg | Win rate, conversion rate |
|
||||||
| Weighted | `SUM([A] * [B])` | Sum | Weighted pipeline, risk-adjusted |
|
| Weighted | `SUM([Table].[A] * [Table].[B])` | Sum | Weighted pipeline, risk-adjusted |
|
||||||
| Time calc | `AVG(DATEDIFF('day', [Start], [End]))` | Avg | Sales cycle, time to close |
|
| Time calc | `AVG(DATEDIFF('day', [Table].[Start], [Table].[End]))` | Avg | Sales cycle, time to close |
|
||||||
| Conditional sum | `SUM(IF condition THEN [Field] ELSE 0 END)` | Sum | Won revenue, qualified leads |
|
| Conditional sum | `SUM(IF condition THEN [Table].[Field] ELSE 0 END)` | Sum | Won revenue, qualified leads |
|
||||||
|
|
||||||
## Metric Lifecycle
|
## Metric Lifecycle
|
||||||
|
|
||||||
|
|||||||
@ -119,7 +119,7 @@ def build_calculated_measurement(
|
|||||||
api_name: API name (must end with _clc)
|
api_name: API name (must end with _clc)
|
||||||
label: Display label
|
label: Display label
|
||||||
expression: Tableau formula expression
|
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)
|
data_type: Data type (Number, Text, Boolean, DateTime)
|
||||||
decimal_place: Decimal places for Number fields
|
decimal_place: Decimal places for Number fields
|
||||||
description: Field description
|
description: Field description
|
||||||
@ -155,11 +155,11 @@ def build_calculated_measurement(
|
|||||||
final_agg_type = aggregation_type
|
final_agg_type = aggregation_type
|
||||||
level = "Row"
|
level = "Row"
|
||||||
# Validate explicit aggregation types are allowed
|
# 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
|
import warnings
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
f"Explicit aggregation type '{final_agg_type}' may not be supported. "
|
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 = {
|
payload = {
|
||||||
@ -262,7 +262,7 @@ def validate_calc_field(
|
|||||||
|
|
||||||
# Check aggregation type
|
# Check aggregation type
|
||||||
if field_type == "measurement" and 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:
|
if aggregation_type not in valid_agg:
|
||||||
errors.append(f"aggregationType must be one of: {', '.join(valid_agg)}")
|
errors.append(f"aggregationType must be one of: {', '.join(valid_agg)}")
|
||||||
|
|
||||||
|
|||||||
@ -139,7 +139,7 @@ def main() -> int:
|
|||||||
|
|
||||||
# Measurement-specific
|
# Measurement-specific
|
||||||
parser.add_argument("--aggregation", default="UserAgg",
|
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)")
|
help="Aggregation type (for measurements, default: UserAgg)")
|
||||||
parser.add_argument("--data-type", choices=["Number", "Text", "Boolean", "DateTime"],
|
parser.add_argument("--data-type", choices=["Number", "Text", "Boolean", "DateTime"],
|
||||||
help="Data type (default: Number for measurements, inferred for dimensions)")
|
help="Data type (default: Number for measurements, inferred for dimensions)")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user