From 47d8199c3df3a286b6a9e558d8bd5a6c131607a1 Mon Sep 17 00:00:00 2001 From: Yannick Utard Date: Mon, 12 Oct 2026 11:39:21 +0200 Subject: [PATCH 1/3] fix(apps): stop sending --queries when publishing data apps --- COMMANDS.md | 31 ++++++++--------- cli-reference.json | 52 ++++++--------------------- cli/src/commands/app/index.ts | 4 +-- cli/src/commands/app/lib/remote.ts | 40 --------------------- cli/src/commands/app/publish.ts | 17 +++------ cli/src/commands/app/remote.test.ts | 54 ++++++++++------------------- cli/src/commands/app/update.ts | 17 +++------ cli/src/commands/app/validate.ts | 16 +++------ 8 files changed, 59 insertions(+), 172 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 3943611..42bc059 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -426,9 +426,9 @@ altertable app create|dev|build|lint|upgrade|validate|publish|update - `build` — Typecheck and build a local data app without Altertable credentials. - `lint` — Lint a local data app project: format, lint rules, types, contracts, build, and client credential boundary. - `upgrade` — Update a local data app's package to the CLI's tested version. -- `validate` — Type-check and bundle a data app from files without saving. -- `publish` — Create a data app from files. -- `update` — Update a data app from files. Omitted fields are left unchanged. +- `validate` — Type-check and bundle a data app from source without saving. +- `publish` — Create a data app from source. +- `update` — Update a data app from source. Omitted fields are left unchanged. **Examples** @@ -438,8 +438,8 @@ altertable app dev altertable app lint altertable app build altertable app upgrade -altertable app validate --file index.tsx --queries queries.json -altertable app publish --title 'Revenue explorer' --file index.tsx --queries queries.json +altertable app validate --file index.tsx +altertable app publish --title 'Revenue explorer' --file index.tsx altertable app update APP-1 --file index.tsx ``` @@ -571,7 +571,7 @@ altertable app upgrade --dir ./my-app #### `altertable app validate` -Type-check and bundle a data app from files without saving. +Type-check and bundle a data app from source without saving. **Usage** @@ -583,18 +583,17 @@ altertable app validate [options] | Option | Description | | --- | --- | -| `--file ` | Path to the single-file React source (index.tsx). Required. | -| `--queries ` | Path to queries JSON (object of id → SQL, or [{id, sql}]). Required. | +| `--file ` | Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp. Required. | **Examples** ```bash -altertable app validate --file index.tsx --queries queries.json +altertable app validate --file index.tsx ``` #### `altertable app publish` -Create a data app from files. +Create a data app from source. **Usage** @@ -607,19 +606,18 @@ altertable app publish [options] | Option | Description | | --- | --- | | `--title ` | Title of the data app. Required. | -| `--file <FILE>` | Path to the single-file React source (index.tsx). Required. | -| `--queries <QUERIES>` | Path to queries JSON (object of id → SQL, or [{id, sql}]). Required. | +| `--file <FILE>` | Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp. Required. | | `--description <DESCRIPTION>` | Optional description of the data app. | **Examples** ```bash -altertable app publish --title 'Revenue explorer' --file index.tsx --queries queries.json +altertable app publish --title 'Revenue explorer' --file index.tsx ``` #### `altertable app update` -Update a data app from files. Omitted fields are left unchanged. +Update a data app from source. Omitted fields are left unchanged. **Usage** @@ -638,15 +636,14 @@ altertable app update [options] <SLUG> | Option | Description | | --- | --- | | `--title <TITLE>` | New title of the data app. | -| `--file <FILE>` | Path to replacement single-file React source (index.tsx). | -| `--queries <QUERIES>` | Path to queries JSON. Replaces the full set when provided. | +| `--file <FILE>` | Path to replacement single-file React source (index.tsx). Replaces the queries with those the file declares. | | `--description <DESCRIPTION>` | New description of the data app. | **Examples** ```bash altertable app update APP-1 --file index.tsx -altertable app update APP-1 --title 'Revenue explorer' --file index.tsx --queries queries.json +altertable app update APP-1 --title 'Revenue explorer' --file index.tsx ``` ### `altertable update` diff --git a/cli-reference.json b/cli-reference.json index ab3f8ac..940d821 100644 --- a/cli-reference.json +++ b/cli-reference.json @@ -816,8 +816,8 @@ "altertable app lint", "altertable app build", "altertable app upgrade", - "altertable app validate --file index.tsx --queries queries.json", - "altertable app publish --title 'Revenue explorer' --file index.tsx --queries queries.json", + "altertable app validate --file index.tsx", + "altertable app publish --title 'Revenue explorer' --file index.tsx", "altertable app update APP-1 --file index.tsx" ], "subcommands": [ @@ -1003,7 +1003,7 @@ { "id": "altertable-app-validate", "command": "altertable app validate", - "description": "Type-check and bundle a data app from files without saving.", + "description": "Type-check and bundle a data app from source without saving.", "usage": [ "altertable app validate [options]" ], @@ -1014,17 +1014,7 @@ "name": "file", "aliases": [], "type": "string", - "description": "Path to the single-file React source (index.tsx).", - "required": true, - "repeatable": false, - "scope": "command", - "values": [] - }, - { - "name": "queries", - "aliases": [], - "type": "string", - "description": "Path to queries JSON (object of id → SQL, or [{id, sql}]).", + "description": "Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp.", "required": true, "repeatable": false, "scope": "command", @@ -1032,14 +1022,14 @@ } ], "examples": [ - "altertable app validate --file index.tsx --queries queries.json" + "altertable app validate --file index.tsx" ], "subcommands": [] }, { "id": "altertable-app-publish", "command": "altertable app publish", - "description": "Create a data app from files.", + "description": "Create a data app from source.", "usage": [ "altertable app publish [options]" ], @@ -1060,17 +1050,7 @@ "name": "file", "aliases": [], "type": "string", - "description": "Path to the single-file React source (index.tsx).", - "required": true, - "repeatable": false, - "scope": "command", - "values": [] - }, - { - "name": "queries", - "aliases": [], - "type": "string", - "description": "Path to queries JSON (object of id → SQL, or [{id, sql}]).", + "description": "Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp.", "required": true, "repeatable": false, "scope": "command", @@ -1088,14 +1068,14 @@ } ], "examples": [ - "altertable app publish --title 'Revenue explorer' --file index.tsx --queries queries.json" + "altertable app publish --title 'Revenue explorer' --file index.tsx" ], "subcommands": [] }, { "id": "altertable-app-update", "command": "altertable app update", - "description": "Update a data app from files. Omitted fields are left unchanged.", + "description": "Update a data app from source. Omitted fields are left unchanged.", "usage": [ "altertable app update [options] <SLUG>" ], @@ -1128,17 +1108,7 @@ "name": "file", "aliases": [], "type": "string", - "description": "Path to replacement single-file React source (index.tsx).", - "required": false, - "repeatable": false, - "scope": "command", - "values": [] - }, - { - "name": "queries", - "aliases": [], - "type": "string", - "description": "Path to queries JSON. Replaces the full set when provided.", + "description": "Path to replacement single-file React source (index.tsx). Replaces the queries with those the file declares.", "required": false, "repeatable": false, "scope": "command", @@ -1157,7 +1127,7 @@ ], "examples": [ "altertable app update APP-1 --file index.tsx", - "altertable app update APP-1 --title 'Revenue explorer' --file index.tsx --queries queries.json" + "altertable app update APP-1 --title 'Revenue explorer' --file index.tsx" ], "subcommands": [] } diff --git a/cli/src/commands/app/index.ts b/cli/src/commands/app/index.ts index 684dd65..e68e29d 100644 --- a/cli/src/commands/app/index.ts +++ b/cli/src/commands/app/index.ts @@ -20,8 +20,8 @@ export const appCommand = defineCommand({ "altertable app lint", "altertable app build", "altertable app upgrade", - "altertable app validate --file index.tsx --queries queries.json", - "altertable app publish --title 'Revenue explorer' --file index.tsx --queries queries.json", + "altertable app validate --file index.tsx", + "altertable app publish --title 'Revenue explorer' --file index.tsx", "altertable app update APP-1 --file index.tsx", ], }, diff --git a/cli/src/commands/app/lib/remote.ts b/cli/src/commands/app/lib/remote.ts index d5c82b6..436be39 100644 --- a/cli/src/commands/app/lib/remote.ts +++ b/cli/src/commands/app/lib/remote.ts @@ -2,13 +2,10 @@ import { readFile } from "node:fs/promises"; import { CliError, EXIT_VALIDATION } from "@/lib/errors.ts"; import type { HttpRequest } from "@/lib/http-request.ts"; -export type DataAppQuery = { id: string; sql: string }; - export type RemoteDataAppPayload = { title?: string; description?: string; index_tsx?: string; - queries?: DataAppQuery[]; }; export type ValidationDiagnostic = { @@ -37,43 +34,6 @@ export async function readRequiredFile(path: string, label: string): Promise<str } } -export async function readJsonFile(path: string, label: string): Promise<unknown> { - const text = await readRequiredFile(path, label); - try { - return JSON.parse(text) as unknown; - } catch (error) { - throw new CliError(`${label} is not valid JSON: ${path}`, { cause: error }); - } -} - -export function parseQueries(value: unknown): DataAppQuery[] { - if (Array.isArray(value)) { - return value.map((entry, index) => { - if (typeof entry !== "object" || entry === null) { - throw new CliError(`queries[${index}] must be an object with id and sql.`); - } - const { id, sql } = entry as { id?: unknown; sql?: unknown }; - if (typeof id !== "string" || typeof sql !== "string") { - throw new CliError(`queries[${index}] id and sql must be strings.`); - } - return { id, sql }; - }); - } - if (typeof value === "object" && value !== null) { - return Object.entries(value as Record<string, unknown>).map(([id, sql]) => { - if (typeof sql !== "string") { - throw new CliError(`queries.${id} must be a SQL string.`); - } - return { id, sql }; - }); - } - throw new CliError("queries must be a JSON object or an array of {id, sql}."); -} - -export async function readQueriesFile(path: string): Promise<DataAppQuery[]> { - return parseQueries(await readJsonFile(path, "queries")); -} - export function buildDataAppHttpRequest( env: string, method: string, diff --git a/cli/src/commands/app/publish.ts b/cli/src/commands/app/publish.ts index 8d0b9e9..b0d038a 100644 --- a/cli/src/commands/app/publish.ts +++ b/cli/src/commands/app/publish.ts @@ -6,7 +6,6 @@ import { sendHttp } from "@/lib/http-request.ts"; import { parseApiJson } from "@/lib/parse-api-json.ts"; import { buildDataAppHttpRequest, - readQueriesFile, readRequiredFile, type PublishedDataApp, } from "@/commands/app/lib/remote.ts"; @@ -14,22 +13,15 @@ import { export const appPublishCommand = defineCommand({ metadata: { name: "publish", - description: "Create a data app from files.", - examples: [ - "altertable app publish --title 'Revenue explorer' --file index.tsx --queries queries.json", - ], + description: "Create a data app from source.", + examples: ["altertable app publish --title 'Revenue explorer' --file index.tsx"], }, args: { title: { type: "string", description: "Title of the data app.", required: true }, file: { type: "string", - description: "Path to the single-file React source (index.tsx).", - required: true, - completion: "file", - }, - queries: { - type: "string", - description: "Path to queries JSON (object of id → SQL, or [{id, sql}]).", + description: + "Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp.", required: true, completion: "file", }, @@ -45,7 +37,6 @@ export const appPublishCommand = defineCommand({ buildDataAppHttpRequest(env, "POST", "/data_apps", { title, index_tsx: await readRequiredFile(stringArg(args, "file"), "source"), - queries: await readQueriesFile(stringArg(args, "queries")), description: optionalStringArg(args, "description"), }), execution, diff --git a/cli/src/commands/app/remote.test.ts b/cli/src/commands/app/remote.test.ts index d65188c..eacbcdc 100644 --- a/cli/src/commands/app/remote.test.ts +++ b/cli/src/commands/app/remote.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { parseQueries } from "@/commands/app/lib/remote.ts"; import { runCommandWithTestRuntime } from "@/test-utils/cli.ts"; import { createLakehouseTestWorkspace, @@ -24,29 +23,13 @@ afterEach(() => { const INDEX_TSX = "export default function App() { return <div>Hello</div>; }\n"; -function writeAppFiles() { - const file = workspace.writeFile("index.tsx", INDEX_TSX); - const queries = workspace.writeFile( - "queries.json", - JSON.stringify({ "top-stories": "SELECT id FROM stories" }), - ); - return { file, queries }; +function writeAppFile() { + return workspace.writeFile("index.tsx", INDEX_TSX); } -describe("remote query files", () => { - test("accepts an id-to-sql object and a REST query array", () => { - expect(parseQueries({ "top-stories": "SELECT 1" })).toEqual([ - { id: "top-stories", sql: "SELECT 1" }, - ]); - expect(parseQueries([{ id: "top-stories", sql: "SELECT 1" }])).toEqual([ - { id: "top-stories", sql: "SELECT 1" }, - ]); - }); -}); - describe("app validate", () => { test("posts the source from disk and exits non-zero when the API returns errors", async () => { - const { file, queries } = writeAppFiles(); + const file = writeAppFile(); workspace.writeMocks([ { urlPattern: "/data_apps/validate", @@ -58,10 +41,11 @@ describe("app validate", () => { }, ]); - const result = await runCommandWithTestRuntime( - ["app", "validate", "--file", file, "--queries", queries], - { debug: false, json: true, agent: false }, - ); + const result = await runCommandWithTestRuntime(["app", "validate", "--file", file], { + debug: false, + json: true, + agent: false, + }); expect(result.exitCode).toBe(6); expect(JSON.parse(result.stdout[0]!)).toEqual({ @@ -70,12 +54,11 @@ describe("app validate", () => { }); expect(JSON.parse(workspace.readPayloads()[0]!)).toEqual({ index_tsx: INDEX_TSX, - queries: [{ id: "top-stories", sql: "SELECT id FROM stories" }], }); }); test("reports a valid app", async () => { - const { file, queries } = writeAppFiles(); + const file = writeAppFile(); workspace.writeMocks([ { urlPattern: "/data_apps/validate", @@ -84,10 +67,11 @@ describe("app validate", () => { }, ]); - const result = await runCommandWithTestRuntime( - ["app", "validate", "--file", file, "--queries", queries], - { debug: false, json: false, agent: false }, - ); + const result = await runCommandWithTestRuntime(["app", "validate", "--file", file], { + debug: false, + json: false, + agent: false, + }); expect(result.exitCode).toBe(0); expect(result.stdout.join("\n")).toContain("Data app is valid."); @@ -95,8 +79,8 @@ describe("app validate", () => { }); describe("app publish", () => { - test("creates a data app from files", async () => { - const { file, queries } = writeAppFiles(); + test("creates a data app from source", async () => { + const file = writeAppFile(); workspace.writeMocks([ { urlPattern: "/environments/env-1/data_apps", @@ -112,7 +96,7 @@ describe("app publish", () => { ]); const result = await runCommandWithTestRuntime( - ["app", "publish", "--title", "Revenue explorer", "--file", file, "--queries", queries], + ["app", "publish", "--title", "Revenue explorer", "--file", file], { debug: false, json: true, agent: false }, ); @@ -130,7 +114,7 @@ describe("app publish", () => { describe("app update", () => { test("patches only the provided fields", async () => { - const { file } = writeAppFiles(); + const file = writeAppFile(); workspace.writeMocks([ { urlPattern: "/data_apps/APP-1", @@ -161,7 +145,7 @@ describe("app update", () => { ); expect(error).toBeInstanceOf(Error); expect((error as Error).message).toBe( - "Provide at least one of --title, --description, --file, or --queries.", + "Provide at least one of --title, --description, or --file.", ); }); }); diff --git a/cli/src/commands/app/update.ts b/cli/src/commands/app/update.ts index aa5897c..313c653 100644 --- a/cli/src/commands/app/update.ts +++ b/cli/src/commands/app/update.ts @@ -7,7 +7,6 @@ import { sendHttp } from "@/lib/http-request.ts"; import { parseApiJson } from "@/lib/parse-api-json.ts"; import { buildDataAppHttpRequest, - readQueriesFile, readRequiredFile, type RemoteDataAppPayload, type PublishedDataApp, @@ -16,10 +15,10 @@ import { export const appUpdateCommand = defineCommand({ metadata: { name: "update", - description: "Update a data app from files. Omitted fields are left unchanged.", + description: "Update a data app from source. Omitted fields are left unchanged.", examples: [ "altertable app update APP-1 --file index.tsx", - "altertable app update APP-1 --title 'Revenue explorer' --file index.tsx --queries queries.json", + "altertable app update APP-1 --title 'Revenue explorer' --file index.tsx", ], }, args: { @@ -31,12 +30,8 @@ export const appUpdateCommand = defineCommand({ title: { type: "string", description: "New title of the data app." }, file: { type: "string", - description: "Path to replacement single-file React source (index.tsx).", - completion: "file", - }, - queries: { - type: "string", - description: "Path to queries JSON. Replaces the full set when provided.", + description: + "Path to replacement single-file React source (index.tsx). Replaces the queries with those the file declares.", completion: "file", }, description: { type: "string", description: "New description of the data app." }, @@ -48,7 +43,7 @@ export const appUpdateCommand = defineCommand({ const slug = stringArg(args, "slug"); const body = await updatePayload(args); if (Object.keys(body).length === 0) { - throw new CliError("Provide at least one of --title, --description, --file, or --queries."); + throw new CliError("Provide at least one of --title, --description, or --file."); } const response = parseApiJson( await sendHttp(buildDataAppHttpRequest(env, "PATCH", `/data_apps/${slug}`, body), execution), @@ -70,10 +65,8 @@ async function updatePayload(args: Record<string, unknown>): Promise<RemoteDataA const title = optionalStringArg(args, "title"); const description = optionalStringArg(args, "description"); const file = optionalStringArg(args, "file"); - const queries = optionalStringArg(args, "queries"); if (title !== undefined) body.title = title; if (description !== undefined) body.description = description; if (file !== undefined) body.index_tsx = await readRequiredFile(file, "source"); - if (queries !== undefined) body.queries = await readQueriesFile(queries); return body; } diff --git a/cli/src/commands/app/validate.ts b/cli/src/commands/app/validate.ts index 4a90a60..2db35ec 100644 --- a/cli/src/commands/app/validate.ts +++ b/cli/src/commands/app/validate.ts @@ -7,7 +7,6 @@ import { parseApiJson } from "@/lib/parse-api-json.ts"; import { buildDataAppHttpRequest, formatValidationSummary, - readQueriesFile, readRequiredFile, validationExitCode, type ValidateDataAppResponse, @@ -16,19 +15,14 @@ import { export const appValidateCommand = defineCommand({ metadata: { name: "validate", - description: "Type-check and bundle a data app from files without saving.", - examples: ["altertable app validate --file index.tsx --queries queries.json"], + description: "Type-check and bundle a data app from source without saving.", + examples: ["altertable app validate --file index.tsx"], }, args: { file: { type: "string", - description: "Path to the single-file React source (index.tsx).", - required: true, - completion: "file", - }, - queries: { - type: "string", - description: "Path to queries JSON (object of id → SQL, or [{id, sql}]).", + description: + "Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp.", required: true, completion: "file", }, @@ -38,12 +32,10 @@ export const appValidateCommand = defineCommand({ requirement: "Validating a data app requires the management API", }); const indexTsx = await readRequiredFile(stringArg(args, "file"), "source"); - const queries = await readQueriesFile(stringArg(args, "queries")); const response = parseApiJson( await sendHttp( buildDataAppHttpRequest(env, "POST", "/data_apps/validate", { index_tsx: indexTsx, - queries, }), execution, ), From 35695dba599f5fcc6a94d2e1feb6508b67e06481 Mon Sep 17 00:00:00 2001 From: Yannick Utard <yannickutard@gmail.com> Date: Mon, 12 Oct 2026 11:50:25 +0200 Subject: [PATCH 2/3] Address feedback --- COMMANDS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 42bc059..8d1f33f 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -583,7 +583,7 @@ altertable app validate [options] | Option | Description | | --- | --- | -| `--file <FILE>` | Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp. Required. | +| `--file <FILE>` | Path to the single-file React source (index.tsx). Required. | **Examples** @@ -606,7 +606,7 @@ altertable app publish [options] | Option | Description | | --- | --- | | `--title <TITLE>` | Title of the data app. Required. | -| `--file <FILE>` | Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp. Required. | +| `--file <FILE>` | Path to the single-file React source (index.tsx). Required. | | `--description <DESCRIPTION>` | Optional description of the data app. | **Examples** @@ -636,7 +636,7 @@ altertable app update [options] <SLUG> | Option | Description | | --- | --- | | `--title <TITLE>` | New title of the data app. | -| `--file <FILE>` | Path to replacement single-file React source (index.tsx). Replaces the queries with those the file declares. | +| `--file <FILE>` | Path to replacement single-file React source (index.tsx). | | `--description <DESCRIPTION>` | New description of the data app. | **Examples** From e1ee68cb09ade5f804724f5e66d39f9547bc7fe6 Mon Sep 17 00:00:00 2001 From: Albert <albert20260301@gmail.com> Date: Mon, 12 Oct 2026 12:32:06 +0200 Subject: [PATCH 3/3] docs(cli): regenerate command reference (#138) ## Summary - regenerate `COMMANDS.md` from PR #137's updated command metadata - restore the generated description text required by the repository-integrity CI check ## Validation - `git diff --check` - `bun run generate:commands` - `bun run generate:check` *(blocked locally because the checkout lacks the `typescript` dependency required by `openapi-typescript`; CI installs the locked dependency set and will verify the generated artifacts)* This is a stacked PR targeting `yu/fix-data-apps-without-queries`; merging it restores PR #137's required generated-artifact check. --- COMMANDS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/COMMANDS.md b/COMMANDS.md index 8d1f33f..42bc059 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -583,7 +583,7 @@ altertable app validate [options] | Option | Description | | --- | --- | -| `--file <FILE>` | Path to the single-file React source (index.tsx). Required. | +| `--file <FILE>` | Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp. Required. | **Examples** @@ -606,7 +606,7 @@ altertable app publish [options] | Option | Description | | --- | --- | | `--title <TITLE>` | Title of the data app. Required. | -| `--file <FILE>` | Path to the single-file React source (index.tsx). Required. | +| `--file <FILE>` | Path to the single-file React source (index.tsx). Queries are declared in the file via defineDataApp. Required. | | `--description <DESCRIPTION>` | Optional description of the data app. | **Examples** @@ -636,7 +636,7 @@ altertable app update [options] <SLUG> | Option | Description | | --- | --- | | `--title <TITLE>` | New title of the data app. | -| `--file <FILE>` | Path to replacement single-file React source (index.tsx). | +| `--file <FILE>` | Path to replacement single-file React source (index.tsx). Replaces the queries with those the file declares. | | `--description <DESCRIPTION>` | New description of the data app. | **Examples**