# Convex Schema Parser & Client Generator `convex-schema-parser` is a simple command-line tool designed to parse your Convex project's schema and function definitions, generating strongly-typed API clients for both Rust and Python. It offers two primary modes of operation: 1. A one-shot `generate` command for manual client generation. 2. A persistent `dev` mode that watches your Convex project for changes and automatically regenerates your clients, providing a seamless development experience. > [!IMPORTANT] > At the bottom you will find a USAGE section ## Installation The easiest way to use `convex-schema-parser` is currently through the Cabal package manager. We provide prebuilt binaries for `linux` & `macOS` that you can download and run directly, but `macOS` users have to allow the binary to run first since we do not sign it (yet). A `npm` package `@parsonosai/convex-schema-parser` is also on its way and supported as soon as we get code-signing ready, we currently use a placeholder. Installing `cabal` & `ghc` is best done using [`ghcup`](https://www.haskell.org/ghcup/). As soon as it is installed: ```bash ghcup install ghc 9.10.1 ghcup install cabal 3.10.3.0 ``` You can then build/run the tool from source. ```bash cabal update cabal install convex-schema-parser # If you have $HOME/.cabal/bin in your PATH. cabal run convex-schema-parser -- --help # If you do not want to install it globally and just run it. ``` ## Prerequisites > [!NOTE] > Everything here is also explained when you issue `conves-schema-parser init`. Before using the tool, please ensure your environment meets the following requirements: ### 1. `pnpm` or `npm` Installation The tool shells out to your package manager to generate TypeScript declaration files (`.d.ts`). You must have `pnpm` or `npm` installed and available in your system's PATH. ### 2. `package.json` Script Your Convex project's `package.json` must contain a script named `declarations`. This script is responsible for running all the necessary steps to generate the `.d.ts` files for the parser to read. This often involves cleaning old artifacts, running the TypeScript compiler, and copying over pre-generated files. Everything here will also be explained when you issue `convex-schema-parser init`, this command will also create a template `convex-parser.yaml` file. **Example `package.json`:** ```json { "scripts": { "declarations:clean": "rm -rf tmp", "declarations:build": "tsc -p tsconfig.declarations.json", "declarations:copy-generated": "cp -r convex/_generated tmp/declarations/", "declarations": "npm run declarations:clean && npm run declarations:build && npm run declarations:copy-generated", "test": "echo \"Error: no test specified\" && exit 1" } } ``` ### 3. tsconfig.declarations.json The tool assumes a specific `tsconfig.json` file exists to guide the declaration generation process. This file should be configured to only emit declaration files into a designated output directory (e.g., `tmp/declarations`) from your source `convex/` directory. Example `tsconfig.declarations.json`: ```json { "compilerOptions": { // --- Output Configuration --- "outDir": "./tmp/declarations", // Output all files to a temporary directory "rootDir": "./convex", // The root of the source files we care about // --- Generation Settings --- "declaration": true, // Generate .d.ts files "emitDeclarationOnly": true, // Don't generate any .js files "skipLibCheck": true, // Speeds up compilation by not checking library files // --- Module Settings (to match Convex) --- "module": "commonjs", "target": "es2020" }, // Tell tsc where to find the files to compile "include": ["convex/**/*.ts"] } ``` # Usage The tool is run from the command line and has two main subcommands: generate and dev. ## `generate` Command (One-Shot) This command parses your project once, generates the specified client, and then exits. It's useful for CI/CD pipelines or manual updates. ```bash convex-schema-parser generate --schema --declarations --target [-o ] # Or if you are not using the installed binary but via cabal (same for the other commands): cabal run convex-schema-parser -- generate --schema --declarations --target [-o ] ``` ### Arguments: * `--schema`: (Required) Path to your main `schema.ts` file. * `--declarations`: (Required) Path to the root directory where `tsc` generated the `.d.ts` files (the `outDir` from your `tsconfig`). * `--target`: (Required) The target language. Can be `Python` or `Rust`. * `-o, --output`: (Optional) The file to write the generated code to. If omitted, the code will be printed to standard output. ### Example: ```bash cabal run convex-schema-parser -- generate \ --schema /path/to/my-project/convex/schema.ts \ --declarations /path/to/my-project/tmp/declarations \ --target Rust \ --output /path/to/my-rust-app/src/convex_api.rs ``` ## `dev` Command (Watch Mode) This is the recommended mode for local development. It starts a persistent process that watches your `convex/` directory for any file changes. When a change is detected, it automatically runs the `pnpm declarations` (or `npm run declarations`) script and regenerates all clients defined in your configuration file. ```bash convex-schema-parser dev [--config ] ``` ### Arguments: * `--config`: (Optional) The path to your YAML configuration file. Defaults to `convex-parser.yaml` in the current working directory. # Configuration (`convex-parser.yaml`) The `dev` mode is driven by a YAML configuration file. This file allows you to define multiple generation targets, enabling you to generate clients for different languages and output them to multiple locations simultaneously. ### Example `convex-parser.yaml`: ```yaml # Configuration for the Convex Client Generator. # (Required) The absolute path to the root of your Convex project. # This is the directory that contains the `convex/` folder and `package.json`. project_path: /path/to/your/convex/project # (Required) The absolute path to the generated TypeScript declarations, relative to `project_path`. declarations_dir: /path/to/your/tmp/declarations # (Optional) The path where validation sandbox projects will be created. # Defaults to `~/.config/convex-schema-parser` if omitted. validation_path: "~/.config/convex-schema-parser" # (Required) A list of generation targets. You can have one or more. targets: # Example 1: Generate a Rust client for a backend service. - lang: Rust # A list of one or more output files for this target. output: - ../my-rust-app/src/convex_api.rs - ../my-other-app/src/convex_api.rs # Example 2: Generate a Python client for data scripts. - lang: Python output: - ../scripts/lib/convex_client.py ``` ### Configuration Schema: * `project_path`: The absolute path to your Convex project root directory. This directory should contain the `convex/` folder and a `package.json` file. * `declarations_dir`: The absolute path to the directory where your TypeScript declaration files (`.d.ts`) are generated. This should be the output directory specified in your `tsconfig.declarations.json`. * `validation_path`: (Optional) The path where validation sandbox projects will be created. If omitted, defaults to `~/.config/convex-schema-parser`. * `targets`: A opt-level key holding a list `[]` of target configurations. * `lang`: The target language for the client. **Must** be `Python` or `Rust`. * `output`: A list `[]` of file paths where the generated client code will be written. Each target can have multiple output paths. # API Usage Examples Once you have generated your client code, you can use it in your projects. ## Python Client Example The generated Python client uses nested classes to mirror your Convex project's file structure. ### Queries, Mutations and Actions ```python import os from convex import ConvexClient # Import the generated API module (e.g., convex_api.py) import convex_api # 1. Instantiate the official ConvexClient with your deployment URL. deployment_url = os.environ.get("CONVEX_URL") client = ConvexClient(deployment_url) # 2. Instantiate your generated API, wrapping the client. auth_key = get_auth_key() # Replace with your method to get the auth/api/jwt key if required client.set_auth(auth_key) api = convex_api.API(client) # 3. Call functions using the nested structure. # This corresponds to the function `getProject` in `convex/functions/projects.ts`. # The generated API reraises any exceptions from the Convex client, so you can handle them as needed. # Additionally, we use `pydantic` for type validation, so we raise these exceptions as well. try: project_id = convex_api.Id("prj_...") project = api.functions.projects.get_project(project_id) if project: print(f"Successfully fetched project: {project.project_name}") else: print("Project not found.") except Exception as e: print(f"An error occurred: {e}") ``` ### Subscriptions ```python from convex import ConvexError # ... (assuming `api` is already instantiated and authenticated) try: # 1. Call the generated `subscribe_*` method. This returns a generator instantly. tenant_id = convex_api.Id("tnt_...") project_subscription = api.functions.queries.subscribe_fetch_projects(tenant_id) print("Subscribed to projects. Waiting for updates... (Press Ctrl+C to stop)") # 2. The `for` loop starts the subscription and blocks until the first value # is received. The loop body will run again for each subsequent update. for updated_projects in project_subscription: # 3. `updated_projects` is already a fully validated Pydantic model # (e.g., list[FetchProjectsReturnObject]). print(f"Received update with {len(updated_projects)} projects:") for project in updated_projects: print(f" - ID: {project._id}, Name: {project.project_name}") except ConvexError as e: print(f"Subscription failed with an error: {e}") except KeyboardInterrupt: print("\nSubscription stopped by user.") ``` ## Rust Client Example The generated Rust client uses a method-based API which works with Rust's ownership and borrowing rules. ### Queries, Mutations and Actions ```rust // Assuming the generated module is named `convex_api`. use convex_api::{Api, Id, types::ProjectsDoc}; use convex::ConvexClient; use anyhow::Result; #[tokio::main] async fn main() -> anyhow::Result<()> { // 1. Instantiate and authenticate the official ConvexClient. let convex_url = std::env::var("CONVEX_URL")?; let auth_key = get_auth_key(); // Replace with your method to get the auth/api/jwt key if required let mut convex_client = ConvexClient::new(&convex_url).await?; convex_client.set_auth(&auth_key); // 2. Instantiate your generated API, wrapping the client. let mut api = Api::new(convex_client); // 3. Call functions using the nested, method-chaining API. let project_id = Id::::new("prj_...".to_string()); // This corresponds to the function `getProject` in `convex/functions/projects.ts`. match api.functions().projects().get_project(&project_id).await { Ok(Some(project)) => { println!("Successfully fetched project: {}", project.project_name.unwrap_or_default()); } Ok(None) => { println!("Project not found."); } Err(e) => { eprintln!("An error occurred: {}", e); } } Ok(()) } ``` ### Subscriptions ```rust use futures_util::stream::StreamExt; // ... (assuming `api` is already instantiated and authenticated) async fn run_subscription() -> anyhow::Result<()> { // 1. Call the generated `subscribe_*` method. let tenant_id = convex_api::Id::::new("tnt_...".to_string()); let mut project_subscription = api.functions().queries().subscribe_fetch_projects(&tenant_id).await?; println!("Subscribed to projects. Waiting for updates... (Press Ctrl+C to stop)"); // 2. The `while let` loop asynchronously polls the stream for new items. while let Some(result) = project_subscription.next().await { // 3. Each `result` is a `Result`, where T is your strongly-typed // return value (e.g., Vec). match result { Ok(updated_projects) => { println!("Received update with {} projects:", updated_projects.len()); for project in updated_projects { println!(" - ID: {}, Name: {}", project._id, project.project_name.unwrap_or_default()); } } Err(e) => { eprintln!("Received an error in the subscription stream: {}", e); } } } Ok(()) } ```