# CLI: capa-codegen and capa persist

Source: https://capacms.com/docs/sdk/cli

Generate types from your models with capa-codegen, and register persisted queries with capa persist.

## Typed select and codegen

`Select<T>` is exported from `@capacms/sdk/next`. `capa-codegen` keeps the legacy
`/v2/schema/types` shape for schemas without relations. When a schema has
relations, codegen wraps them in branded helpers, which describe the model;
a read's `fields` is typed from them as the API returns it (see Typed reads):

```ts
export type CapaRelation<T> = T & { readonly __capaRelation: "one"; readonly __capaRelationTarget: T };
export type CapaRelationList<T> = T[] & { readonly __capaRelation: "many"; readonly __capaRelationTarget: T };

export interface Article {
  title?: string;
  author?: CapaRelation<Author>;
  coauthors?: CapaRelationList<Author>;
}

export type ArticleSelect = import("@capacms/sdk/next").Select<Article>;
```

Those brands let TypeScript tell scalar fields from relation fields, so misspelled
fields and invalid nested selects fail in consumer typechecks. A relation named
alone (`"author"`) is read as a reference.

Every name is written so the module parses, whatever the namespace holds. A
field that is not an identifier is quoted (`"am/pm_indicator"?: string;`,
read as `fields["am/pm_indicator"]`), and a model whose name is not one is
named as GraphQL names it: `2024_events` is `_2024Events`, with
`_2024EventsSelect` beside it. Two models whose names give one interface name,
such as `twin_a` and `twin-a`, are each named from the whole namespace instead:
`Model_twin_a` and `Model_twin$2da`, each character that is not a letter, digit
or `_` written as `$` and its hex code. An enum's values are written as stored,
a quote or a backslash included.

A select names a field by its namespace as saved, whatever it holds:
`["price.usd", { "at.place": ["zip.code"] }]`. The client writes a name holding
`,` `(` `)` `:` `.` `"` `*` `[` `]` or a space, or starting with `-`, quoted, as
REST's grammar reads it: `select="price.usd","at.place"("zip.code")`. A string
select, `sort` and `where` take REST's own text, so write such a name quoted
there yourself: `sort: ['-"price.usd"']`.

A select also takes the entry's system keys by their `$` names, which mean the
system key even on a model with a field of the same plain name
(`SystemKey`): `["title", "$tags", { coauthors: { select: ["name"], sort: "-$createdAt" } }]`.
A `$` name that is no system key fails to compile. A field whose namespace
starts with `$` cannot be named, so `select: "*"` is how to read it.

## Codegen

```sh
CAPA_API_URL=... CAPA_KEY=pk_... CAPA_TENANT_ID=... capa-codegen --out src/capa-types.ts
```

Without `--graphql`, `capa-codegen` reads the legacy `/v2` schema, which
takes a `pk_` key and `CAPA_TENANT_ID`. A `cap_` key, which `/v2` refuses,
writes the same interfaces from the key's GraphQL schema, with no tenant id,
and so does `--schema <file>`, a schema `capa-codegen --graphql --save-schema`
wrote. GraphQL does not say three things, so those types say less: every
field is optional, an enum is a `string`, and a field GraphQL leaves out is
`unknown`. There is no `CAPA_SCHEMA_CHECKSUM`, since that is the `/v2`
schema's checksum. Where the API does not serve GraphQL, use a `pk_` key. It
reads `.env.local` and `.env` too, as `next dev` does.

Exit codes: `0` wrote or already current, `1` failed, `2` `--check` found a
diff. `--check` is the CI mode: it fails when the committed file is stale.

**Commit the output.** Generating at install needs credentials during
`npm install`, which breaks CI images and Docker builds, and generating at
build time makes every build depend on the network. A committed file also
turns "a model changed, so the build fails" into a reviewable diff instead of
a wall of `tsc` errors.

Re-running when nothing changed costs one conditional request that returns
no body: codegen stamps the schema's checksum in the file and fetches the
types only when the schema moved. Models are written in the order of their
interface names, so renaming a model's display name does not reorder the
file.

## Persisted queries

Register your documents at build time, with a development key:

```sh
CAPA_API_URL=https://cdn.capacms.com CAPA_DRAFT_KEY=cap_test_... capa persist --manifest persisted.json
```

Then send only their hash, with any key, including the production key your
site ships:

```ts
import { ArticlesPageDocument } from "./capa-graphql";

const { data } = await capa.graphql(ArticlesPageDocument, { first: 10 }, { persisted: true });
```

With `persisted: true` the client sends the document's sha256 as one small
GET, which the CDN and the API cache for a production key, and the document
never travels. Variables too long for a GET URL (8,192 bytes) go with the hash
in a POST instead, which works the same way but is not cached. `capa persist` prints `<sha256>  <operation>  stored, pinned` per
operation, hashing the same text `capa-codegen --graphql` exports, so run
both from the same documents. A document written as a literal is stored
twice, as its `<Name>Document` text and as written (`<Name> (literal)`),
since a call with the literal sends that text.

`capa persist` registers every document with `Capa-Persist: pin`. The API
drops unpinned documents first when a project's store is full, so the
documents developers register while trying queries in the Explorer or a
draft preview never push your site's documents out. A document the API
stores without its pin is reported as `stored, not pinned` and the command
exits 3, since it is exposed to exactly that. `--manifest` writes each
operation's `sha256`, `stored` and `pinned`.

Name the build too, so a run of preview deploys never pushes production's
documents out:

```sh
capa persist --release "$GIT_COMMIT" --env production
```

That sends `Capa-Persist: pin; release=<commit>; env=production`. A project
keeps up to 2,000 documents and 16 MiB, and when it is full the API keeps
the pins of the latest 3 production releases first, then what a production
key ran in the last 30 days, then other pins, such as a preview build's.
On Vercel and Netlify the command reads both from the build
(`VERCEL_GIT_COMMIT_SHA` and `VERCEL_ENV`, or `COMMIT_REF` and `CONTEXT`),
so there is nothing to pass. Elsewhere, pass the flags or set `CAPA_RELEASE`
and `CAPA_RELEASE_ENV`. A release is 1 to 64 letters, digits, dots, dashes
or underscores, such as a commit or a deploy id, and an environment is a
lowercase name such as `production` or `preview`. The two go together, and
the command checks them before it sends anything. The manifest records them
as `release` and `env`.

Two things decide whether a document is stored:

* The key. `capa persist` reads the development key from `CAPA_DRAFT_KEY`,
  the name `/nextjs` reads for drafts (`CAPA_KEY` when that is unset), and
  refuses a production key before it sends anything. A production key ships
  in your site's bundle, so it may run a document but never register one.
* The host. `capa persist` sends each document to `CAPA_API_URL`, the URL
  your site already reads. `https://cdn.capacms.com` passes every POST on to
  the host that stores documents. On a self-hosted stack whose read host
  stores nothing, set `CAPA_ADMIN_URL` to the API host your Capa admin uses;
  it wins over `CAPA_API_URL`. When a host stores nothing, the command says
  so and names the variable to set.

When a hash is not stored yet, a production client still gets its data, with
no error: the API answers the GET with `PersistedQueryNotFound`, the client
sends one POST carrying the document and the hash, and the API runs it and
answers `extensions.persistedQuery.registered: false`. The client then
remembers that hash for five minutes and sends POST straight away, so a
missing registration costs one extra GET per five minutes rather than one per
call. `registered: false` on a production read is how to spot a document
`capa persist` did not register. `graphql()` from `/nextjs` is not persisted
by default for the same reason: until `capa persist` has run, every hash
misses.

A builder read (`capa.graphql.query`) cannot be persisted, and
`persisted: true` there throws a `TypeError` before any request. Its
document is printed when it runs, so `capa persist` never sees it, and a
production key never stores one. It is already a cacheable GET.
