Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/extract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Extract

on:
push:
branches: [main]
pull_request:

jobs:
extract-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install

- run: bun run bin/cli.js extract-spec --file /tmp/extracted-spec.md
- run: bun run bin/cli.js extract-schema --folder /tmp/extracted-schemas

- name: Compare extracted spec with core-spec/v1/spec.md
run: diff core-spec/v1/spec.md /tmp/extracted-spec.md

- name: Compare extracted schemas with core-spec/v1/schemas
run: diff -r core-spec/v1/schemas /tmp/extracted-schemas
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: Schema
name: Validate

on:
push:
branches: [main]
pull_request:

jobs:
schema-test:
validate-schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ npx @metabase/representations validate-schema --folder ./my-export

Omit `--folder` to validate the current directory.

### Extracting the spec

Copy the bundled `spec.md` to a target file:

```sh
npx @metabase/representations extract-spec --file ./spec.md
```

Omit `--file` to write `spec.md` into the current directory.

### Extracting schemas

Copy the bundled schemas (preserving folder structure) into a target directory:

```sh
npx @metabase/representations extract-schema --folder ./schemas
```

Omit `--folder` to extract into the current directory.

### Programmatic

```js
Expand Down
74 changes: 49 additions & 25 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,78 @@

import { parseArgs } from "node:util";
import { relative } from "path";

import { extractSchema } from "../src/extract-schema.js";
import { extractSpec } from "../src/extract-spec.js";
import { validateSchema } from "../src/validate-schema.js";

const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
folder: { type: "string" },
file: { type: "string" },
help: { type: "boolean", short: "h", default: false },
},
});

const command = positionals[0];

if (values.help || !command) {
console.log(`Usage: representations <command> [options]
const HELP = `Usage: representations <command> [options]

Commands:
validate-schema Validate YAML files against Metabase representation schemas
--folder <path> Folder to validate (default: cwd)

extract-spec Copy the bundled spec.md into a target file
--file <path> Destination file (default: ./spec.md)

extract-schema Copy bundled schemas into a target folder
--folder <path> Destination folder (default: cwd)

Options:
--folder <path> Folder to validate (default: current directory)
-h, --help Show this help message`);
process.exit(command ? 0 : 1);
}
-h, --help Show this help message`;

if (command !== "validate-schema") {
console.error(`Unknown command: ${command}`);
process.exit(1);
if (values.help || !command) {
console.log(HELP);
process.exit(command ? 0 : 1);
}

const folder = values.folder ?? process.cwd();
const { results, passed, failed } = validateSchema({ folder });
if (command === "validate-schema") {
const folder = values.folder ?? process.cwd();
const { results, passed, failed } = validateSchema({ folder });

if (results.length === 0) {
console.error(`No YAML files found in ${folder}`);
process.exit(1);
}
if (results.length === 0) {
console.error(`No YAML files found in ${folder}`);
process.exit(1);
}

for (const result of results) {
const path = relative(process.cwd(), `${folder}/${result.file}`);
if (result.status === "ok") {
console.log(`OK ${path} (${result.model})`);
} else {
console.error(`FAIL ${path}${result.model ? ` (${result.model})` : ""}`);
for (const error of result.errors) {
console.error(` ${error.path} ${error.message}`);
for (const result of results) {
const path = relative(process.cwd(), `${folder}/${result.file}`);
if (result.status === "ok") {
console.log(`OK ${path} (${result.model})`);
} else {
console.error(`FAIL ${path}${result.model ? ` (${result.model})` : ""}`);
for (const error of result.errors) {
console.error(` ${error.path} ${error.message}`);
}
}
}

console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
}

if (command === "extract-spec") {
const { target } = extractSpec({ file: values.file ?? "spec.md" });
console.log(`Spec extracted to ${target}`);
process.exit(0);
}

if (command === "extract-schema") {
const { target } = extractSchema({ folder: values.folder ?? process.cwd() });
console.log(`Schemas extracted to ${target}`);
process.exit(0);
}

console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
console.error(`Unknown command: ${command}`);
process.exit(1);
13 changes: 11 additions & 2 deletions core-spec/v1/schemas/common/parameter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ required:
properties:
id:
type: string
description: UUID identifier
format: uuid
minLength: 1
description: >
Unique identifier within the dashboard or card. UUIDs are recommended,
but Metabase accepts any unique non-empty string.
name:
type: string
description: Display name
Expand Down Expand Up @@ -135,3 +137,10 @@ $defs:
maxItems: 2
minItems: 2
maxItems: 2
- if: { prefixItems: [{ const: text-tag }] }
then:
prefixItems:
- const: text-tag
- type: string
minItems: 2
maxItems: 2
13 changes: 3 additions & 10 deletions core-spec/v1/schemas/common/query.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,6 @@ $defs:
properties:
include-current: { type: boolean }

case_if_options:
description: Options for case/if operators.
allOf:
- $ref: "#/$defs/options"
- type: object
properties:
default: { $ref: "#/$defs/expression" }

datetime_parse_options:
description: Options for the datetime parsing operator.
allOf:
Expand Down Expand Up @@ -474,15 +466,16 @@ $defs:
then:
prefixItems:
- enum: [case, if]
- $ref: "#/$defs/case_if_options"
- $ref: "#/$defs/options"
- type: array
items:
type: array
prefixItems: [{ $ref: "#/$defs/expression" }, { $ref: "#/$defs/expression" }]
minItems: 2
maxItems: 2
- $ref: "#/$defs/expression"
minItems: 3
maxItems: 3
maxItems: 4
- if: { prefixItems: [{ const: offset }] }
then:
prefixItems:
Expand Down
1 change: 0 additions & 1 deletion core-spec/v1/schemas/dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,6 @@ $defs:
description: >
Connects a dashboard parameter to a specific card column or variable.
required:
- card_id
- parameter_id
- target
properties:
Expand Down
30 changes: 24 additions & 6 deletions core-spec/v1/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ Extraction units for `temporal-extract`: `year-of-era`, `quarter-of-year`, `mont

| Operator | Arguments | Returns | Description |
|----------|-----------|---------|-------------|
| `case` | pairs of [condition, value] (default in options) | value type | Conditional expression (if/then/else) |
| `case` | pairs of [condition, value], optional default expression | value type | Conditional expression (if/then/else). The default value is supplied as a 4th positional argument. |
| `if` | same as `case` | value type | Alias for `case` |
| `coalesce` | 2+ expressions | first non-null type | First non-null value |

Expand All @@ -1232,7 +1232,6 @@ Extraction units for `temporal-extract`: `year-of-era`, `quarter-of-year`, `mont
expressions:
- - case
- "lib/expression-name": Price Tier
default: Standard
- - - - ">"
- {}
- - field
Expand All @@ -1247,6 +1246,7 @@ expressions:
- [Sample Database, PUBLIC, PRODUCTS, PRICE]
- 20
- Budget
- Standard # default (4th positional arg)

# Coalesce
- coalesce
Expand Down Expand Up @@ -1857,6 +1857,15 @@ visualization_settings:
display: text
text: "**Bold** and _italic_ markdown content"

# Text with parameter placeholders. Each `{{name}}` is wired to a dashboard
# parameter through `parameter_mappings` on the dashcard, with target
# `[text-tag, name]`. At render time the placeholder is replaced with the
# parameter's current value.
visualization_settings:
virtual_card:
display: text
text: "Showing results for {{product_category}}"

# Link (URL)
visualization_settings:
virtual_card:
Expand Down Expand Up @@ -2013,7 +2022,7 @@ On **cards**, parameters are typically empty `[]` for MBQL queries. For native q

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | UUID identifier |
| `id` | string | Yes | Unique identifier within the dashboard or card. UUIDs are recommended, but any unique non-empty string is accepted (e.g., `9d9cddd4`). |
| `name` | string | Yes | Display name |
| `slug` | string | Yes | URL-friendly identifier |
| `type` | string | Yes | Filter widget type (see below) |
Expand Down Expand Up @@ -2109,10 +2118,11 @@ Use `sectionId: id` to make a `number/=` or `string/=` parameter map only to pri

### Parameter Targets

Parameter targets specify which column or variable a parameter maps to. The outer wrapper is either `dimension` or `variable`:
Parameter targets specify which column or variable a parameter maps to. The outer wrapper is `dimension`, `variable`, or `text-tag`:

- **`dimension`** — for MBQL column references (`field`, `expression`) and for native template tags of type `dimension` or `temporal-unit`
- **`variable`** — for native template tags of type `text`, `number`, `date`, or `boolean`
- **`text-tag`** — for placeholders inside text/heading virtual cards (see [Virtual Card Settings](#virtual-card-settings))

An optional third element `{stage-number: N}` or `null` can specify which query stage the target belongs to (0 = first stage).

Expand Down Expand Up @@ -2169,6 +2179,14 @@ target:
- min_price
```

**Text card placeholder:**

```yaml
target:
- text-tag
- product_category
```

---

## Collection
Expand Down Expand Up @@ -2400,8 +2418,8 @@ Connects a dashboard parameter to a specific card column or variable. Each mappi

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card FK (entity_id) |
| `parameter_id` | string | Yes | UUID matching a dashboard parameter's `id` |
| `card_id` | string | No | Card FK (entity_id). Omit for mappings on virtual cards (e.g., text-tag placeholders). |
| `parameter_id` | string | Yes | Matches a dashboard parameter's `id` |
| `target` | array | Yes | Parameter target |

### DashboardCardSeries
Expand Down
15 changes: 12 additions & 3 deletions examples/v1/collections/main/dashboards/virtual_cards.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ name: Virtual Cards Dashboard
entity_id: Ylbpm_iPeEYav_o0kpUVD
creator_id: admin@example.com
collection_id: cOlDaShBoArDs0ExAmPlx
parameters: []
parameters:
- id: 9d9cddd4
name: Product Category
slug: product_category
type: string/=
sectionId: string
tabs: []
dashcards:
- entity_id: WcNpxqVEO_rIeWZwjYv7Z
Expand All @@ -28,13 +33,17 @@ dashcards:
col: 0
size_x: 12
size_y: 3
parameter_mappings: []
parameter_mappings:
- parameter_id: 9d9cddd4
target:
- text-tag
- product_category
series: []
visualization_settings:
virtual_card:
display: text
text: |-
This dashboard provides an overview of sales metrics.
This dashboard provides an overview of sales metrics for {{product_category}}.
**Key metrics** include revenue, order count, and average order value.
Use the filters above to narrow down the data.
serdes/meta:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ dataset_query:
- PRODUCTS
expressions:
- - case
- default: Budget
"lib/expression-name": Price Tier
- "lib/expression-name": Price Tier
- - - - ">"
- {}
- - field
Expand All @@ -37,9 +36,9 @@ dataset_query:
- PRICE
- 30
- Standard
- Budget
- - if
- default: false
"lib/expression-name": Is Premium
- "lib/expression-name": Is Premium
- - - - ">"
- {}
- - field
Expand All @@ -50,6 +49,7 @@ dataset_query:
- PRICE
- 100
- true
- false
- - case
- "lib/expression-name": Simple Category
- - - - ">"
Expand Down
Loading
Loading