diff --git a/src/documentation/user_guides/publishing/connections.malloynb b/src/documentation/user_guides/publishing/connections.malloynb index 46041033..919073bb 100644 --- a/src/documentation/user_guides/publishing/connections.malloynb +++ b/src/documentation/user_guides/publishing/connections.malloynb @@ -7,15 +7,30 @@ Configure database connections for Malloy Publisher deployments. For an overview ## Configuration File -Publisher uses `publisher.config.json` for database connections. This file lives in your server root directory. +Publisher uses `publisher.config.json` for database connections. By default it is read from your server root, and `--config ` points at one anywhere else. Package locations inside it resolve against the directory holding the config, so a config kept next to its packages travels with them. -**Important:** Publisher does not support environment variable substitution. Put actual credential values directly in the config file. +**Keeping credentials out of the file:** any `${VAR}` inside a string value is replaced with that environment variable's value when the config loads, so passwords and keys can stay out of the file and out of version control: + +```json +"postgresConnection": { + "host": "${PG_HOST}", + "password": "${PG_PASSWORD}" +} +``` + +Substitution applies to every string in the config, including package `location` values. Only uppercase, braced names are matched: `$PG_PASSWORD` (no braces) and `${lowercase}` are left as literal text. + +This keeps credentials out of the file, not out of the server. Publisher resolves `${VAR}` at load time and then serves the resolved connection configuration, password included, from its API: `/api/v0/environments/{environment}/connections` returns it, and so do `/api/v0/environments` and `/api/v0/status`, the endpoint most people wire into a health check. + +Publisher ships with no authentication and binds all interfaces by default, so anyone who can reach the port can read those credentials, run SQL through any connection you have configured, and (unless you set `frozenConfig: true`) add connections of their own. This is not only a concern for servers that hold credentials: the DuckDB sandbox every package gets automatically will also read files off the server's filesystem, so even a Publisher with no connections configured at all exposes its host. Keep Publisher on a trusted network or behind your own authentication. + +Set every variable your config references before starting. If one is unset, the whole config fails to load and Publisher comes up serving nothing, which is easy to miss because `/api/v0/status` still reports `"operationalState": "serving"`. Check `/api/v0/environments` to confirm your environments actually loaded. A variable that is set but empty is not an error: it substitutes an empty string, so a blank `${PG_PASSWORD}` becomes a blank password rather than a failure. ### Basic Structure ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -53,7 +68,7 @@ For packages with embedded parquet/CSV files, **no connection configuration is n ```json { - "projects": [ + "environments": [ { "name": "default", "packages": [ @@ -88,16 +103,18 @@ my-analytics/ DuckDB can federate queries to external databases (BigQuery, Snowflake, PostgreSQL) using attached databases. This lets you query cloud data warehouses through DuckDB. +**Note:** the connection name `duckdb` is reserved for the per-package sandbox described above, so an environment-level DuckDB connection needs a different name. Using `duckdb` here does not stop the server: it logs the error, skips that whole environment, and carries on reporting `"operationalState": "serving"` with nothing loaded. These examples use `shared_duckdb`; reference that same name in your Malloy code. + **Attach BigQuery:** ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ { - "name": "duckdb", + "name": "shared_duckdb", "type": "duckdb", "duckdbConnection": { "attachedDatabases": [ @@ -121,19 +138,19 @@ DuckDB can federate queries to external databases (BigQuery, Snowflake, PostgreS In your Malloy model: ```malloy -source: events is duckdb.table('my_bq.my_dataset.events') +source: events is shared_duckdb.table('my_bq.my_dataset.events') ``` **Attach Snowflake:** ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ { - "name": "duckdb", + "name": "shared_duckdb", "type": "duckdb", "duckdbConnection": { "attachedDatabases": [ @@ -162,12 +179,12 @@ source: events is duckdb.table('my_bq.my_dataset.events') ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ { - "name": "duckdb", + "name": "shared_duckdb", "type": "duckdb", "duckdbConnection": { "attachedDatabases": [ @@ -219,7 +236,7 @@ source: events is duckdb.table('my_bq.my_dataset.events') ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -244,11 +261,11 @@ source: events is duckdb.table('my_bq.my_dataset.events') **With service account (recommended for production):** -Embed the service account JSON directly in the config: +The service account JSON goes in `serviceAccountKeyJson` as a string. To keep the key out of the file, put it in an environment variable and reference it as `${BIGQUERY_SA_JSON}`: ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -258,7 +275,7 @@ Embed the service account JSON directly in the config: "bigqueryConnection": { "defaultProjectId": "my-gcp-project", "location": "US", - "serviceAccountKeyJson": "{\n \"type\": \"service_account\",\n \"project_id\": \"my-gcp-project\",\n ...rest of service account JSON...\n}" + "serviceAccountKeyJson": "${BIGQUERY_SA_JSON}" } } ], @@ -268,11 +285,20 @@ Embed the service account JSON directly in the config: } ``` +Then start Publisher with the key in the environment: + +```bash +export BIGQUERY_SA_JSON="$(cat service-account.json)" +npx @malloy-publisher/server --server_root . +``` + +The whole key can also be pasted inline as a JSON string (`"{\n \"type\": \"service_account\", ...}"`) if you would rather not use an environment variable. + **With gcloud auth (development only):** ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -299,7 +325,7 @@ Embed the service account JSON directly in the config: ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -326,7 +352,7 @@ Embed the service account JSON directly in the config: ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -355,7 +381,7 @@ Embed the service account JSON directly in the config: ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -383,7 +409,7 @@ Embed the service account JSON directly in the config: ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -407,11 +433,11 @@ Embed the service account JSON directly in the config: --- -## Trino and Presto +## Trino ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -430,17 +456,15 @@ Embed the service account JSON directly in the config: } ``` -For Presto, use `"type": "presto"` with the same configuration options. - --- ## Multi-Environment Configuration -You can configure multiple projects for different environments: +You can configure multiple environments, for example one per deployment stage: ```json { - "projects": [ + "environments": [ { "name": "staging", "connections": [ diff --git a/src/documentation/user_guides/publishing/explorer.malloynb b/src/documentation/user_guides/publishing/explorer.malloynb index c3bb9cd0..6f14e989 100644 --- a/src/documentation/user_guides/publishing/explorer.malloynb +++ b/src/documentation/user_guides/publishing/explorer.malloynb @@ -19,7 +19,7 @@ Deploy Publisher ([see Publishing](publishing.malloynb)) and share the URL with Publisher model list -The URL format is: `http://localhost:4000/project/package/model` +The URL format is: `http://localhost:4000///`, where the model path includes the file extension, for example `http://localhost:4000/examples/storefront/storefront.malloy`. Anyone with access to Publisher can explore your semantic models—no Malloy knowledge required. @@ -61,9 +61,9 @@ The panel is organized into three sections: Attributes you can group by, filter on, or sort with. Dimensions are grouped by their source. For example, in a query centered around `order_items`, you might also see dimensions from joined models like `users` or `products`. Hovering over a dimension reveals actions: -- **Add as Group By** – Segment results by this dimension -- **Add as Filter** – Apply a filter based on the field -- **Add as Sort** – Sort results by this value +- **Add as group by** – Segment results by this dimension +- **Add as filter** – Apply a filter based on the field +- **Add as order by** – Sort results by this value (only available for fields already in the output) Dimension Hover Actions @@ -72,9 +72,9 @@ Hovering over a dimension reveals actions: Predefined metrics you can aggregate, filter on, or sort with. These include calculations such as totals, averages, counts, and ratios. Hovering over a measure provides actions: -- **Add as Aggregate** – Include the metric in results -- **Add as Filter** – Use the measure to restrict results -- **Add as Sort** – Sort results based on the metric value +- **Add as aggregate** – Include the metric in results +- **Add as filter** – Use the measure to restrict results +- **Add as order by** – Sort results based on the metric value (only available for fields already in the output) Measure Hover Actions @@ -83,8 +83,8 @@ Hovering over a measure provides actions: Saved queries defined in the underlying Malloy model. Views often represent curated KPIs, commonly-used explorations, or analytical building blocks. Hovering over a view provides actions: -- **Add to Query** – Load the view's query -- **Add as Nested Query** – Add the view as a nested subquery +- **Add view** – Load the view's query +- **Add as new nested query** – Add the view as a nested subquery View Hover Actions @@ -108,13 +108,13 @@ The Query Panel is where queries come together. It provides a structured, visual This menu lets you add fields by operation type: -- Add Group By -- Add Aggregate -- Add Filter -- Add View +- Add group by +- Add aggregate +- Add filter +- Add view - Limit -- Order By -- Add Blank Nested Query +- Order by +- Add blank nested query Add Query Element @@ -155,8 +155,8 @@ Hovering over a section shows additional ways to add elements. Malloy's nesting feature enables rich, multidimensional analysis—and Explorer gives you a no-code way to use it. To add a nested query: -- Select **Nest Query** from the Query Panel **More Actions** menu -- Or hover over a view in the Source Panel and select **Add as Nested Query** +- Select **Nest query** from the Query Panel **More Actions** menu +- Or hover over a view in the Source Panel and select **Add as new nested query** Nest Action diff --git a/src/documentation/user_guides/publishing/publisher_sdk.malloynb b/src/documentation/user_guides/publishing/publisher_sdk.malloynb index 9c77b9ca..ae8fbf1c 100644 --- a/src/documentation/user_guides/publishing/publisher_sdk.malloynb +++ b/src/documentation/user_guides/publishing/publisher_sdk.malloynb @@ -10,27 +10,29 @@ Build custom data applications with the Publisher SDK. Embed live, governed anal The fastest way to understand the SDK is to run the example data app: ```bash -# Clone the publisher repo +# Clone the publisher repo and install at the root. The example app resolves +# react and @emotion from the repo-root node_modules, so this step is required. git clone https://github.com/malloydata/publisher.git cd publisher +bun install -# Start the Publisher server -npx @malloy-publisher/server --server_root ./malloy-samples +# Start the Publisher server, serving the bundled example packages +npx @malloy-publisher/server --server_root packages/server -# In a new terminal, run the example app +# In a new terminal, install and run the example app. It is not part of the +# workspace, so it needs its own install as well as the one above. cd examples/data-app -cp .env.example .env -npm install -npm run dev +bun install +bun run dev ``` Open [http://localhost:5173](http://localhost:5173) to see a working dashboard with embedded Malloy visualizations. The example app demonstrates: -- **Malloy Samples Dashboard** – Pre-configured charts from the names dataset +- **Storefront dashboard** – A fixed grid of SDK tiles over the storefront model - **Single Embed** – Embedding a single query result -- **Dynamic Dashboard** – Adding charts at runtime -- **Interactive Dashboard** – Using raw data with custom Recharts visualizations +- **Dynamic Dashboard** – Adding and arranging widgets at runtime +- **Interactive** – Using raw data with custom Recharts visualizations --- @@ -55,34 +57,40 @@ function App() { } ``` -By default, connects to `http://localhost:4000`. For production: +By default the SDK talks to `/api/v0` on the same origin the page is served from. That is what you want when Publisher serves your app itself, and it is why the example app proxies `/api/v0` through Vite to port 4000. To point at a different server, pass `baseURL`: ```tsx - + ``` ### 2. EmbeddedQueryResult -Renders a Malloy query result. The component picks the visualization automatically—tables for flat data, bar charts for single-dimension aggregates, or whatever [renderer tag](../../language/tags.malloynb) you specify in your model (e.g., `# line_chart`). +Renders a Malloy query result. Results render as a table unless the query or the model carries a [renderer tag](../../language/tags.malloynb) (e.g. `# bar_chart`, `# line_chart`), which is what selects a visualization. ```tsx -import { EmbeddedQueryResult } from "@malloy-publisher/sdk"; +import { + EmbeddedQueryResult, + createEmbeddedQueryResult, + encodeResourceUri, +} from "@malloy-publisher/sdk"; function MyChart() { - const embeddedQuery = JSON.stringify({ - modelPath: "orders.malloy", - query: "run: orders -> { group_by: region; aggregate: total_revenue }", - optionalPackageName: "ecommerce", - optionalProjectName: "production" + const embeddedQuery = createEmbeddedQueryResult({ + query: "run: order_items -> { group_by: products.brand; aggregate: total_sales }", + resourceUri: encodeResourceUri({ + environmentName: "examples", + packageName: "storefront", + modelPath: "storefront.malloy", + }), }); return ; } ``` -The `optionalPackageName` and `optionalProjectName` parameters are required when your Publisher serves multiple projects or packages—they tell the SDK which model to use. +`resourceUri` is how you address a model: it names the environment, the package, and the model path. It is always required, not only when the server holds more than one package, and `createEmbeddedQueryResult` throws without it. Instead of writing the query inline, you can reference a view defined in the model by passing `queryName` together with `sourceName`. --- @@ -113,11 +121,13 @@ my-data-app/ │ │ └── widgets.json # Saved embed configurations │ └── components/ │ └── CustomChart.tsx -├── .env # VITE_PUBLISHER_API=http://localhost:4000 ├── package.json -└── vite.config.ts +└── vite.config.ts # Proxies /api/v0 to the Publisher server ``` +There is no environment variable that points the SDK at a Publisher server. Either proxy `/api/v0` in +`vite.config.ts` (below) or pass `baseURL` to `ServerProvider`. + --- ## CORS Configuration @@ -130,7 +140,7 @@ If your React app runs on a different port/domain than Publisher, configure CORS export default defineConfig({ server: { proxy: { - '/api': { + '/api/v0': { target: 'http://localhost:4000', changeOrigin: true, }, @@ -142,7 +152,7 @@ export default defineConfig({ Then use relative paths in ServerProvider: ```tsx - + ``` --- diff --git a/src/documentation/user_guides/publishing/publishing.malloynb b/src/documentation/user_guides/publishing/publishing.malloynb index 7f6b442c..9f9d6772 100644 --- a/src/documentation/user_guides/publishing/publishing.malloynb +++ b/src/documentation/user_guides/publishing/publishing.malloynb @@ -35,9 +35,28 @@ Create `publisher.json`: } ``` -### 2. Start Publisher +### 2. List It in a Config File -Navigate to the **parent directory** (the folder containing `my-analytics/`), then run: +Publisher only serves packages that `publisher.config.json` lists, so create one in the **parent +directory** (the folder containing `my-analytics/`): + +```json +{ + "frozenConfig": false, + "environments": [ + { + "name": "default", + "packages": [ + { "name": "my-analytics", "location": "./my-analytics" } + ] + } + ] +} +``` + +### 3. Start Publisher + +From that same parent directory, run: ```bash npx @malloy-publisher/server --server_root . @@ -46,7 +65,7 @@ npx @malloy-publisher/server --server_root . The `--server_root` should point to the directory that **contains** your package folder(s), not the package itself. ``` -projects/ ← Run npx from HERE (--server_root .) +my-workspace/ ← Run npx from HERE (--server_root .) ├── publisher.config.json ← Server configuration └── my-analytics/ ← This is your package ├── publisher.json @@ -65,19 +84,31 @@ Then open `http://localhost:4000` to explore the sample models. For alternative deployment methods (Docker, build from source), see [Deployment & Configuration](#deployment-configuration) below. -### 3. Open Browser +### 4. Open Browser + +Go to `http://localhost:4000` to browse and query your models. If the package list is empty, the +config did not load: check the startup log rather than the status endpoint, which reports `serving` +either way. -Go to `http://localhost:4000` to browse and query your models. +That's it for local files. If your models use DuckDB with `.parquet`, `.csv`, or `.json` files, no +connection configuration is needed on top of the config above, because every package gets its own +DuckDB sandbox automatically. -That's it for local files. If your models use DuckDB with `.parquet`, `.csv`, or `.json` files, you're done—no config file needed. +**Note:** Publisher copies your package when it loads it, so editing a `.malloy` file does not change +what a running server serves. Restart with `--init` to re-copy from source and pick the edit up. -**Note:** If you edit a `.malloy` file while Publisher is running, restart it to pick up changes. +While you are iterating on a model, restart with `--init --watch-env default` instead: that mounts +the package in place and recompiles it as you save. `--watch-env` needs `--init` here, because the +in-place mount is only set up when Publisher has not already copied the package into +`publisher_data/`. Adding `--watch-env` on its own to a server root you have already started does +nothing, and does so quietly: `/api/v0/watch-mode/status` still reports `"enabled": true` while your +edits are ignored. -**Want to connect to a database?** Create `publisher.config.json` in the same directory: +**Want to connect to a database?** Add a connection to the same `publisher.config.json`: ```json { - "projects": [ + "environments": [ { "name": "default", "connections": [ @@ -110,15 +141,24 @@ See [Publisher Connections](connections.malloynb) for BigQuery, Snowflake, and o ## Package Locations -Publisher can load packages from various sources: +A package's `location` can be any of these: **Local Filesystem** -- Relative paths: `./package`, `../package`, `~/package` +- Relative paths: `./package`, `../package`, resolved against the directory holding the config they appear in +- Home-relative paths: `~/package` - Absolute paths: `/absolute/path/to/package` +Packages do not have to live inside your server root. Keeping a config next to the packages it points at means the two move together. + **GitHub** - `https://github.com/owner/repo/tree/branch/package-path` +**Google Cloud Storage** +- `gs://bucket/path/to/package` + +**Amazon S3** +- `s3://bucket/path/to/package` + --- ## Deployment & Configuration @@ -144,12 +184,23 @@ bun run start Mount your config and packages into the container: ```bash -docker run -p 4000:4000 \ - -v ./publisher.config.json:/publisher/publisher.config.json \ - -v ./packages:/publisher/packages \ +docker run -p 4000:4000 -p 4040:4040 \ + -v ./publisher.config.json:/publisher/publisher.config.json:ro \ + -v ./packages:/publisher/my-packages \ ms2data/malloy-publisher ``` +Mount your packages anywhere except `/publisher/packages`: that path holds the server's own code +inside the image, and mounting over it breaks the container. Point each package's `location` at +wherever you mounted them (`./my-packages/...` above). + +Publisher has no built-in authentication and binds all interfaces. Anyone who can reach the port can +read your connection configuration (database passwords included), run SQL through any connection, and +by default add connections of their own, with those changes persisted to `publisher.db`. The DuckDB +sandbox each package gets automatically will read files off the server's filesystem, so this applies +even with no database configured. Publish Publisher only on a trusted network, or put your own +authentication in front of it. The MCP port (4040) is unauthenticated too. + ### Docker Compose ```yaml @@ -162,19 +213,27 @@ services: - "4000:4000" - "4040:4040" # MCP endpoint for AI agents volumes: - - ./publisher.config.json:/publisher/publisher.config.json - - ./packages:/publisher/packages + - ./publisher.config.json:/publisher/publisher.config.json:ro + - ./packages:/publisher/my-packages restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:4000/status"] + test: ["CMD", "curl", "-f", "http://localhost:4000/health"] interval: 30s timeout: 10s retries: 3 ``` +Use `/health` for the container healthcheck, not `/status` and not `/api/v0/status`. A bare `/status` +is not a route at all: the web app's catch-all answers it with `200` and an HTML page, so `curl -f` +passes even when the API is not serving. `/api/v0/status` is a real endpoint, but its response +includes your connection configuration, so a healthcheck pointed at it writes database passwords into +the container's health log every interval. `/health` reports liveness and readiness and nothing else, +with no configuration in it. Either way this checks that the server is up, not that your packages +loaded. + ### Environment Variables -Publisher reads runtime settings from environment variables. The most common ones are below; see the [Publisher README](https://github.com/malloydata/publisher#configuration) for the full list and matching CLI flags. +Publisher reads runtime settings from environment variables. The most common ones are below; see the [configuration reference](https://github.com/malloydata/publisher/blob/main/docs/configuration.md) for the full list and matching CLI flags. | Env var | Default | Meaning | |---|---|---| @@ -183,7 +242,6 @@ Publisher reads runtime settings from environment variables. The most common one | `SERVER_ROOT` | `.` (cwd) | Directory containing `publisher.config.json`. | | `LOG_LEVEL` | `debug` | One of `error`, `warn`, `info`, `verbose`, `debug`, `silly`. | | `GOOGLE_APPLICATION_CREDENTIALS` | _unset_ | Fallback path to a GCP service-account JSON for BigQuery connections that don't include inline auth. Ignored when the connection config provides its own credentials. | -| `PG_CONNECT_TIMEOUT_SECONDS` | `5` | Connection timeout (seconds) for Postgres-backed DuckLake manifest catalogs (`materializationStorage`). Bad credentials or an unreachable host return HTTP 422 in ~5s rather than hanging the publisher. No effect on user-facing Postgres connections or non-PG catalogs (SQLite, MySQL). | --- @@ -192,15 +250,24 @@ Publisher reads runtime settings from environment variables. The most common one ### Health Check ```bash -curl http://localhost:4000/status +curl http://localhost:4000/health ``` -### List Projects +### List Environments ```bash -curl http://localhost:4000/api/v1/projects +curl http://localhost:4000/api/v0/environments ``` +A `200` from either endpoint means the server is up, but it does not mean your packages loaded: a +config that fails to load leaves Publisher reporting `"operationalState": "serving"` with an empty +environment list. If a package is missing, count what `/api/v0/environments` returns before assuming +the server is fine, and check the startup log, where the cause is named: `Failed to load package` +for one bad package, `Error initializing environment` when a whole environment is dropped, +`Failed to parse` when `publisher.config.json` is not valid JSON, and +`Error reading publisher.config.json` when the config parsed but could not be processed, which is +usually a `${VAR}` you have not set. + See the [REST API](rest_api.malloynb) documentation for all available endpoints. ### Test in Browser @@ -216,13 +283,13 @@ See the [REST API](rest_api.malloynb) documentation for all available endpoints. ## State Persistence -Publisher persists configuration changes in a local DuckDB database (`publisher.db`). This means changes made via the REST API—adding projects, packages, or connections—survive server restarts. +Publisher persists configuration changes in a local DuckDB database (`publisher.db`). This means changes made via the REST API—adding environments, packages, or connections—survive server restarts. ### How It Works 1. **First start**: Publisher reads `publisher.config.json` and syncs it to `publisher.db` 2. **Subsequent starts**: Publisher loads from the database, ignoring config file changes -3. **API changes**: Adding/removing projects, packages, or connections updates the database +3. **API changes**: Adding/removing environments, packages, or connections updates the database ### Reinitializing from Config @@ -238,16 +305,25 @@ Use `--init` when: - You want to reset to the original configuration - You're troubleshooting configuration issues +`--init` deletes `publisher_data/` and rebuilds it from the config file, without prompting. Anything +that only exists in the database or in Publisher's copy of a package is discarded, including +environments, packages, and connections added through the API or the UI. Keep your source packages +outside `publisher_data/` (as the layout above does) and `--init` is safe to run. + **Note:** On first run, Publisher automatically creates the database and syncs from `publisher.config.json`—no `--init` needed. ### Mutable vs Frozen Configuration -By default, Publisher allows configuration changes via the API. To lock the configuration (e.g., in production): +By default, Publisher allows configuration changes via the API, and that default is unauthenticated: +any caller who can reach the port can add or change a connection, and the change survives restarts. +Set `frozenConfig: true` on any Publisher reachable beyond your own machine. It is the control that +closes config writes (they return `403`), though it does not stop queries, so it is not a substitute +for keeping the port off untrusted networks: ```json { "frozenConfig": true, - "projects": [...] + "environments": [...] } ``` diff --git a/src/documentation/user_guides/publishing/rest_api.malloynb b/src/documentation/user_guides/publishing/rest_api.malloynb index 894160aa..db10edbc 100644 --- a/src/documentation/user_guides/publishing/rest_api.malloynb +++ b/src/documentation/user_guides/publishing/rest_api.malloynb @@ -27,7 +27,7 @@ The REST API lets you: | Capability | Description | |------------|-------------| -| **Browse models** | List projects, packages, and models available on the server | +| **Browse models** | List environments, packages, and models available on the server | | **Get model schema** | Retrieve sources, measures, dimensions, and views defined in a model | | **Execute queries** | Run Malloy queries and get JSON results | | **Explore databases** | List schemas, tables, and columns from connected databases | @@ -37,38 +37,53 @@ The REST API lets you: ## Quick Example -Run a query against a model: +Run a query against a model. The environment and package below are the samples Publisher serves out of the box, so this works as-is against a server started with no arguments (`npx @malloy-publisher/server`). If you passed `--server_root` or `--config`, substitute your own environment, package and model names. ```bash -curl -X POST "http://localhost:4000/api/v0/projects/my-project/packages/ecommerce/queryResults/orders.malloy" \ +curl -X POST "http://localhost:4000/api/v0/environments/examples/packages/storefront/models/storefront.malloy/query" \ -H "Content-Type: application/json" \ - -d '{"query": "run: orders -> { aggregate: order_count, total_revenue }"}' + -d '{"query": "run: order_items -> { aggregate: order_count is count(), total_revenue is sum(sale_price) }", "compactJson": true}' ``` Response: ```json { - "result": [ - { "order_count": 12345, "total_revenue": 1234567.89 } - ] + "result": "[{\"order_count\":25356,\"total_revenue\":2098177.9700000403}]", + "resource": "/api/v0/environments/examples/packages/storefront/models/storefront.malloy/query" } ``` +Note that `result` is a **JSON string**, not an object, so parse it before use: +`JSON.parse(body.result)`. + +`compactJson: true` gives you the plain array of row objects shown above. Leave it out (the default) +and `result` contains the full Malloy result instead: the same rows plus the schema and per-cell type +metadata that the renderer needs to draw charts and tables. + --- ## Common Patterns ### Fetch and Display Data +The query goes in the request body, not the query string. The endpoint accepts POST only. + ```javascript async function fetchMetrics() { const response = await fetch( - 'http://localhost:4000/api/v0/projects/prod/packages/analytics/queryResults/orders.malloy?query=' + - encodeURIComponent('run: orders -> { aggregate: total_revenue }') + 'http://localhost:4000/api/v0/environments/examples/packages/storefront/models/storefront.malloy/query', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: 'run: order_items -> { aggregate: total_revenue is sum(sale_price) }', + compactJson: true, + }), + } ); - const data = await response.json(); - return data.result; + const body = await response.json(); + return JSON.parse(body.result); // -> [{ total_revenue: 2098177.9700000403 }] } ``` @@ -77,8 +92,8 @@ async function fetchMetrics() { Instead of writing queries inline, reference views defined in your model: ```javascript -// Model has: view: monthly_summary is { ... } -const query = 'run: orders -> monthly_summary'; +// storefront.malloy defines: view: business_overview is { ... } +const query = 'run: order_items -> business_overview'; ``` This keeps business logic in the model, not scattered across API calls.