Logotipo Hedhog

Receba novidades do Hedhog no seu e-mail

Novos lançamentos, receitas e breaking changes — sem spam.

Hedhog YAML

Cada biblioteca HedHog controla sua própria fatia do banco de dados por meio de dois diretórios: hedhog/table/ (schema/DDL) e hedhog/data/ (seed/DML). Esta página é a referência completa dos dois formatos: cada tipo de coluna, cada padrão de relacionamento e as convenções que este codebase realmente segue — incluindo algumas extensões reais que vão além do schema formal estrito.

1. Visão geral

Quando uma biblioteca é adicionada (hedhog add <library-name>), o pipeline é:

  1. Varre libraries/<library>/hedhog/table/ em busca de definições de schema em YAML — um arquivo por tabela.
  2. Pareia cada schema de tabela com seus dados de seed de libraries/<library>/hedhog/data/ (correspondidos pelo nome do arquivo).
  3. Gera uma migration PostgreSQL (CREATE TABLE, índices, constraints de FK, INSERTs) em apps/api/prisma/migrations/.
  4. Aplica a migration via Prisma.
libraries/
└── <library-name>/
    └── hedhog/
        ├── table/          ← DDL — one file per table (required)
        │   ├── my_table.yaml
        │   └── my_other_table.yaml
        ├── data/            ← seed data — one file per table (optional)
        │   ├── my_table.yaml
        │   └── my_other_table.yaml
        ├── query/           ← raw .sql, appended verbatim after the migration (optional)
        │   └── post_migration.sql
        └── frontend/        ← frontend page / message files (optional)

Política do repositório para o hub: nunca execute hedhog dev apply, nunca edite manualmente apps/api/prisma/schema.prisma e nunca resete/recrie o projeto, o banco de dados ou as migrations existentes. Quando um YAML de table/ ou data/ mudar, adicione uma nova migration SQL em apps/api/prisma/migrations/ que espelhe a mudança e, então, execute pnpm prisma:deploy a partir de apps/api.


2. YAML de tabela (hedhog/table/*.yaml)

O nome do arquivo (sem extensão), em snake_case, se torna o nome da tabela no PostgreSQL. user_profile.yaml → tabela "user_profile".

2.1 Estrutura de nível superior

columns:
  - type: pk
  - name: my_column
    type: varchar
    isNullable: false

columns é a única chave exigida pelo schema formal. Na prática, mais duas chaves de nível superior são usadas em todo o repositório e devem ser consideradas parte do contrato real:

ChaveUsada na práticaObservações
columnsem todo lugarLista ordenada de definições de coluna. Obrigatória.
indices / indexes~dezenas de arquivos (ambas as grafias aparecem, indices é mais comum)Definições de índice em nível de tabela — veja §2.6.
tableapenas em address e crmRedundante — sempre duplica o nome do arquivo. Resquício da escrita manual; pode ser omitida com segurança em arquivos novos.

2.2 Aliases de tipo especiais

Estes são atalhos do HedHog. Cada um se expande para um formato SQL fixo e, para a maioria deles, o name que você fornece é ignorado — a CLI força um nome de coluna específico.

typeNome de coluna automáticoSQL geradoNOT NULLObservações
pkidSERIAL PRIMARY KEYSempre use isto para a chave primária, não isPrimaryKey.
fk(você fornece)INTEGER + constraint de FKRequer references: — veja §2.5.
slugslugVARCHAR(255) UNIQUE NOT NULLSobrescreva o tamanho com length:.
orderorderINTEGER NOT NULL DEFAULT 0Muitas tabelas definem manualmente sua própria coluna int order/sort_order com um default customizado — ambos aparecem no repositório.
created_atcreated_atTIMESTAMPTZ NOT NULL DEFAULT now()
updated_atupdated_atTIMESTAMPTZ NOT NULL DEFAULT now()Também cria um trigger trg_touch_updated_at que atualiza automaticamente o valor a cada UPDATE.
locale_varchar(você fornece)VARCHARRoteada para a tabela complementar <table>_locale gerada automaticamente, não para a tabela principal. Veja §2.7.
locale_text(você fornece)TEXTMesmo roteamento acima.
enum(você fornece)CREATE TYPE <table>_<col>_enum AS ENUM (...)Requer enum: ou values: (intercambiáveis — use um, não os dois).

2.3 Aliases SQL curtos e tipos nativos

Aliases curtos são traduzidos diretamente no momento do CREATE TABLE (sem nome/constraints automáticos — name é obrigatório):

typeSQL gerado
intINTEGER
datetimeTIMESTAMPTZ
charCHAR(<length || 1>)
bytesBYTEA

Qualquer tipo padrão do PostgreSQL também é aceito literalmente (sem diferenciar maiúsculas de minúsculas): smallint, integer, bigint, serial, bigserial, decimal, numeric, real, double precision, money, varchar, char, text, bytea, timestamp[tz], date, time, interval, boolean, json, jsonb, uuid, inet, cidr, macaddr, tipos geométricos, tsvector/tsquery, xml. Tipos não reconhecidos fazem a migration falhar.

Colunas simples sem type: significam implicitamente VARCHAR. Esta é uma convenção muito comum em todo o repositório (mais de cem colunas a utilizam):

# libraries/core/hedhog/table/user.yaml
- name: name
- name: suspended_reason
  isNullable: true

2.4 Referência de propriedades de coluna

PropriedadeTipoAplica-se aDescrição
namestringtodos, exceto aliases de nome forçadoNome da coluna, snake_case.
typestringtodosAlias especial, alias curto ou tipo nativo do PostgreSQL.
isNullablebooleantodosPadrão false — toda coluna é NOT NULL a menos que indicado o contrário.
isUniquebooleantodosAdiciona um UNIQUE INDEX.
isPrimaryKeybooleancolunas nativasPrefira type: pk para tabelas novas.
defaultqualquertodosPara TIMESTAMPTZ, use a string literal "now()".
lengthintegervarchar, char, slug, locale_varcharLimite de caracteres. slug/locale_varchar têm padrão 255; char tem padrão 1.
precision / scaleintegerdecimal / numericNão está no schema JSON formal, mas é usado em todo o repositório para campos de dinheiro/quantidade, por exemplo precision: 12, scale: 6.
referencesobjecttype: fkVeja abaixo.
enum / valuesstring[]type: enumLista de valores permitidos. Ambas as chaves são intercambiáveis — usadas até em colunas irmãs no mesmo arquivo.

2.5 Referências de chave estrangeira

- name: parent_id
  type: fk
  references:
    table: parent_table   # required
    column: id            # required
    onDelete: CASCADE     # optional, default NO ACTION
    onUpdate: CASCADE     # optional, default NO ACTION

Ações referenciais aceitas: CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION (valores em minúsculas como cascade/set null também funcionam na prática, mas prefira maiúsculas para consistência com a maior parte do codebase). O SQL gerado:

ALTER TABLE "<table>"
  ADD CONSTRAINT "<fkTable>_<column>_fkey"
  FOREIGN KEY ("<column>")
  REFERENCES "<fkTable>" ("<fkColumn>")
  ON DELETE <onDelete> ON UPDATE <onUpdate>;

references.table pode apontar para uma tabela definida em uma biblioteca @hed-hog/* diferente, desde que essa biblioteca seja uma dependência declarada — o motor de migration a inclui no grafo de dependências sem recriá-la.

Escolha onDelete pela semântica, não por hábito — esta é uma convenção deliberada, válida para todo o repositório:

  • CASCADE — posse verdadeira (uma página é dona de seus componentes, uma proposta é dona de suas revisões).
  • SET NULL — referência opcional/fraca (created_by_user_id, photo_id).
  • RESTRICT — bloqueia a exclusão de linhas ainda referenciadas por dados de auditoria/histórico (por exemplo, um agent referenciado por seu histórico agent_run, ou settlement.reverses_settlement_id).

2.6 Índices

Os índices em nível de tabela ficam em um bloco indices: (preferido, mais comum) ou indexes:, ao lado de columns::

# libraries/lms/hedhog/table/xp_skill.yaml
indices:
  - columns: [slug]
    isUnique: true
  - columns: [xp_area_id]
  - columns: [status]

Índices compostos/de múltiplas colunas são a forma padrão de expressar unicidade composta (por exemplo, [proposal_id, revision_number], ou uma tupla de 4 colunas para nós de agente versionados). Tanto isUnique quanto unique são aceitos como a flag de "torná-lo um índice único" — você verá qualquer uma delas por aí.

Índices únicos parciais (filtrados) usam uma string de predicado SQL bruto em where: (este é um where diferente daquele de busca de FK usado no YAML de dados — aqui é um fragmento SQL literal):

# libraries/inbox/hedhog/table/inbox_account.yaml
indices:
  - columns: [user_id, is_default]
    isUnique: true
    where: is_default = true AND deleted_at IS NULL
# libraries/crm/hedhog/table/crm_activity.yaml
indices:
  - columns: [person_id, source_kind]
    isUnique: true
    where: source_kind = 'followup' AND completed_at IS NULL

2.7 Tabela locale gerada automaticamente

Qualquer coluna locale_varchar/locale_text é removida da tabela principal e colocada em uma tabela complementar <table>_locale sintetizada automaticamente — nenhum arquivo YAML de tabela extra é necessário para ela:

ColunaTipoConstraints
idSERIALNOT NULL PRIMARY KEY
locale_idINTEGERNOT NULL, FK → locale.id CASCADE
<table>_idINTEGERNOT NULL, FK → <table>.id CASCADE
(colunas locale)VARCHAR/TEXTconforme declarado na tabela pai
created_at / updated_atTIMESTAMPTZdefaults padrão + trigger
# libraries/lms/hedhog/table/xp_skill.yaml
- name: name
  type: locale_varchar
  locale: { pt: Nome, en: Name }
- name: description
  type: locale_text
  isNullable: true
  locale: { pt: Descrição, en: Description }

A chave locale: {...} vista acima não faz parte do motor de migration — é uma dica de rótulo padrão em tempo de design que algumas ferramentas leem (por exemplo, um gerador de CRUD do admin). É opcional e pode ser um objeto vazio (locale: {}).

2.8 Padrões de relacionamento

FK simples (muitos-para-um) — a esmagadora maioria dos relacionamentos:

- name: category_id
  type: fk
  isNullable: true
  references: { table: category, column: id, onDelete: SET NULL }

FK autorreferenciada (árvores, threads, hierarquias) — por exemplo, menu.yaml (árvore de menu), category.yaml (árvore de categorias), course_lesson_discussion_topic.yaml (threads de comentários), agent_run.yaml (parent_run_id):

# libraries/lms/hedhog/table/course_lesson_discussion_topic.yaml
- name: parent_topic_id
  type: fk
  isNullable: true
  references: { table: course_lesson_discussion_topic, column: id, onDelete: CASCADE }

Tabelas de junção (pivot) para muitos-para-muitos — uma tabela dedicada com exatamente duas colunas fk, geralmente com um índice único composto:

# libraries/core/hedhog/table/dashboard_component_role.yaml
columns:
  - type: pk
  - name: component_id
    type: fk
    references: { table: dashboard_component, column: id, onDelete: CASCADE }
  - name: role_id
    type: fk
    references: { table: role, column: id, onDelete: CASCADE }
  - type: created_at
  - type: updated_at

O motor de migration detecta automaticamente esse formato: qualquer tabela com duas colunas FK apontando para os dois lados de um relacionamento é tratada como sua tabela de junção quando um YAML de dados usa relations: entre essas duas entidades (veja §3.5) — você nunca referencia a tabela de junção pelo nome.

Relações quase polimórficas — FKs do Postgres não conseguem apontar para "uma de várias tabelas", então isso é modelado como um enum *_type mais um id inteiro simples, com um índice único composto para garantir integridade:

# libraries/operations/hedhog/table/operations_approval.yaml
- name: target_type
  type: enum
  values: [timesheet, time_off_request, schedule_adjustment_request, expense_reimbursement_request]
- name: target_id
  type: int
indices:
  - columns: [target_type, target_id]
    isUnique: true

2.9 Outras convenções do repositório

  • Soft delete: uma coluna timestamp deleted_at anulável por convenção (não há um alias type: deleted_at dedicado) — sempre acompanhada de um índice.
  • Dinheiro como inteiro: valores monetários são modelados como *_amount_cents / amount_cents (int/bigint), não numeric, para evitar desvios de ponto flutuante.
  • JSON/JSONB para payloads flexíveis: input/output/state (jsonb), snapshot_json (json).
  • SQL bruto pós-migration: arquivos em hedhog/query/*.sql são anexados literalmente após todas as instruções CREATE TABLE/INSERT geradas, na ordem do sistema de arquivos — usados para views, triggers e qualquer coisa mais fácil de expressar diretamente em SQL. Quando você adicionar um trigger/função dessa forma, mantenha-o sincronizado com libraries/<lib>/hedhog/query/triggers.sql para que instalações novas recebam os mesmos objetos.

3. YAML de dados (hedhog/data/*.yaml)

O nome do arquivo (sem extensão) deve corresponder ao nome da tabela correspondente. Uma tabela sem arquivo de dados simplesmente não recebe linhas de seed — isso é válido.

3.1 Estrutura raiz

Um arquivo YAML de dados deve ser interpretado como um array de nível raiz; caso contrário, ele é ignorado com um aviso e nada é inserido.

- id: 1
  name: "First Row"
- id: 2
  name: "Second Row"

3.2 Valores escalares

- id: 1
  slug: example-item
  order: 10
  is_active: true
  rating: 4.5
  description: "A description with 'single quotes' works fine"

3.3 Busca de FK (where:)

Nunca fixe manualmente o inteiro de uma chave estrangeira — em vez disso, busque a linha por uma chave estável:

- id: 1
  name: "Child Row"
  parent_id:
    where:
      slug: some-slug

Isso é compilado para um sub-select inline: (SELECT "id" FROM "parent_table" WHERE "slug" = 'some-slug'). Múltiplas chaves no mesmo where: são combinadas com AND.

OperadorExemploSQL gerado
(simples)code: abc"code" = 'abc'
equalsequals: abc"col" = 'abc'
notEquals / notnot: abc"col" <> 'abc'
inin: [a, b, c]"col" IN ('a','b','c')
notInnotIn: [a, b]"col" NOT IN ('a','b')
like / notLikelike: "foo%""col" LIKE 'foo%'
contains / notContainscontains: foo"col" LIKE '%foo%'
gt / gte / lt / ltegt: 5"col" > '5'
isNull / isNotNullisNull: true"col" IS NULL

3.4 Valores de locale

Para colunas locale_varchar/locale_text, forneça um objeto indexado por código de locale em vez de uma string simples — a linha se ramifica na tabela <table>_locale gerada automaticamente, um insert por locale fornecido:

- id: 1
  slug: dashboard
  title:
    en: "Dashboard"
    pt: "Painel"
  description:
    en: "Main control panel"
    pt: "Painel de controlo principal"

Códigos de locale suportados: en, pt, es, it, jp. Você não precisa fornecer todos os locales para cada linha. Uma tabela simples de lookup/enum que não se destina a ser traduzida deve apenas usar uma coluna simples (não-locale_*) com uma string bruta — não recorra a locale_varchar a menos que o valor seja genuinamente conteúdo voltado ao usuário e traduzível.

Atenção: o motor reconhece um objeto de locale apenas se todas as chaves forem um código de 2–3 letras do conjunto conhecido acima. Qualquer outro formato de chave é tratado como um valor bruto semelhante a JSON em vez de traduções.

3.5 Relações inline (relations:)

relations: dispara inserts em uma ou mais tabelas relacionadas na mesma transação que a linha pai:

- id: 1
  slug: admin
  relations:
    permission:
      - where:
          slug: view-dashboard
      - where:
          slug: manage-users

Regra de resolução: o motor procura por uma tabela de junção com duas colunas FK apontando para o pai e o alvo (§2.8) — se encontrada, ele insere na tabela de junção automaticamente. Se nenhuma tabela de junção existir, a coluna FK que aponta de volta para o pai é injetada automaticamente e a linha é inserida diretamente na tabela alvo (um filho direto, não um muitos-para-muitos).

Relações aninhadas podem ter profundidade arbitrária — cada nível aninhado resolve da mesma forma. Este exemplo faz o seed de uma árvore de configurações de 3 níveis, onde o vínculo de volta ao pai imediato (setting.group_id) é implícito pelo aninhamento, enquanto uma FK que cruza a árvore (setting.subgroup_id) ainda precisa de uma busca where: explícita:

# libraries/core/hedhog/data/setting_group.yaml
- slug: general
  name: { en: General, pt: Geral }
  relations:
    setting:
      - slug: pagination-page-sizes
        type: array
        component: checkbox
        value: '["6","12","24","48","96"]'
        subgroup_id:
          where:
            slug: general-system
        relations:
          setting_list:
            - value: '6'
              order: 0
            - value: '12'
              order: 1

3.6 Arquivos de dados de junção planos

Quando uma biblioteca precisa vincular suas próprias linhas a uma tabela pertencente a uma biblioteca diferente (de modo que aninhar sob o arquivo de dados daquela biblioteca não é uma opção), faça o seed da tabela de junção diretamente como seu próprio arquivo de dados plano, com ambos os lados resolvidos por where::

# libraries/finance/hedhog/data/role_route.yaml
- role_id:
    where:
      slug: admin-finance
  route_id:
    where:
      url: /dashboard-core/home
      method: GET

É assim que o finance vincula admin-finance a rotas de dashboard compartilhadas do core sem nunca tocar em core/hedhog/data/route.yaml.

3.7 Âncoras YAML para blocos de relação repetidos

Blocos relations: são valores YAML comuns, então âncoras/aliases nativos do YAML são uma forma prática de evitar repetir a mesma lista de papéis em dezenas de linhas — usados por todo o core/hedhog/data/route.yaml:

- url: /auth/verify
  method: GET
  type: HTTP
  relations: &core_user_roles
    role:
      - where: { slug: admin }
      - where: { slug: admin-access }
- url: /auth/roles
  method: GET
  type: HTTP
  relations: *core_user_roles
- url: /profile
  method: GET
  type: HTTP
  relations: *core_user_roles

3.8 Como o seeding funciona por baixo dos panos

  • Idempotente por design: para qualquer linha cuja chave de identidade possa ser resolvida (slug → uma coluna única → um índice único → o par [url, method] para tabelas no formato de rota), o motor emite um UPDATE ... WHERE <key> seguido de um INSERT ... WHERE NOT EXISTS (...) — nunca um INSERT puro que dependa de ON CONFLICT. Reexecutar uma migration que faz o seed de linhas existentes é seguro.

  • Relações de junção são envoltas em um bloco protegido DO $$ ... END $$ que lança um erro explícito se qualquer um dos lados da relação não puder ser resolvido, em vez de silenciosamente não inserir nada:

    DO $$
    DECLARE
      v_parent_id INTEGER;
      v_target_id INTEGER;
    BEGIN
      v_parent_id := (SELECT "id" FROM "menu" WHERE "slug" = '/core/management');
      v_target_id := (SELECT "id" FROM "role" WHERE "slug" = 'admin');
      IF v_parent_id IS NULL THEN
        RAISE EXCEPTION 'HedHog migration: cannot link "menu" to "role_menu" — parent row not found';
      END IF;
      INSERT INTO "role_menu" ("menu_id", "role_id")
      SELECT v_parent_id, v_target_id
      WHERE NOT EXISTS (SELECT 1 FROM "role_menu" WHERE "menu_id" = v_parent_id AND "role_id" = v_target_id);
    END $$;
    
  • Ordem de execução: a tabela locale é sempre criada primeiro, então as tabelas são ordenadas topologicamente por dependência de FK para que todo pai exista antes de seus filhos. Uma dependência circular faz a migration falhar imediatamente, listando as tabelas problemáticas. Dependências entre bibliotecas (declaradas em package.json) são incluídas na mesma ordenação.

  • Não existe sintaxe de seeding condicional/com escopo de ambiente (nenhuma chave env:/if:) — a única forma de tornar um seed condicional é uma busca where: que pode não resolver para nada, ou simplesmente não incluir o arquivo de dados.


4. Sincronização de permissões: route.yaml + role.yaml

Sempre que a URL, o método, a autenticação ou os papéis de um endpoint de backend mudarem, route.yaml e role.yaml na mesma biblioteca devem ser mantidos sincronizados (veja a regra "Endpoint Permission Sync" no CLAUDE.md raiz, ou execute a skill /sync-permissions).

4.1 role.yaml — catálogo plano de papéis

# libraries/core/hedhog/data/role.yaml
- slug: admin
  name: { en: Administrator, pt: Administrador }
  description: { en: System administrator with full access., pt: Administrador do sistema com acesso total. }
- slug: admin-access
  name: { en: Admin Access, pt: Acesso de Administrador }
  description: { en: Access to administrative features., pt: Acesso a funcionalidades administrativas. }
# libraries/finance/hedhog/data/role.yaml
- slug: admin-finance
  name: { en: Finance Administrator, pt: Administrador de Finanças }
- slug: admin-finance-operator
  name: { en: Finance Operator, pt: Operador de Finanças }
- slug: finance-statements-editor
  name: { en: Finance Statements Editor, pt: Editor de Extratos Bancários }

4.2 route.yaml — catálogo de endpoints com atribuição inline de papéis

Uma linha por endpoint HTTP (url + method) ou ferramenta MCP (tool_name). Os papéis são atribuídos inline via relations.role:

# libraries/core/hedhog/data/route.yaml
- url: /user/:userId/verify-identifier/:identifierId
  method: POST
  type: HTTP
  relations:
    role:
      - where: { slug: admin }
      - where: { slug: admin-access }
# libraries/finance/hedhog/data/route.yaml
- url: /finance/realtime/cursor
  method: GET
  relations:
    role:
      - where: { slug: admin }
      - where: { slug: admin-finance }
      - where: { slug: admin-finance-operator }
      - where: { slug: finance-statements-editor }

Sempre inclua admin mais o papel de admin específico da biblioteca (admin-<library>) em toda rota que sua biblioteca adicionar.

4.3 Vínculo papel↔rota entre bibliotecas

Quando uma biblioteca precisa que seu próprio papel enxergue rotas/menus pertencentes a uma biblioteca diferente (tipicamente rotas de dashboard compartilhadas do core), não edite o route.yaml daquela biblioteca — em vez disso, adicione linhas ao seu próprio role_route.yaml / role_menu.yaml plano, como mostrado em §3.6.


5. Exemplos completos

5.1 Tabela simples com aliases padrão

# hedhog/table/category.yaml
columns:
  - type: pk
  - name: slug
    type: slug
  - name: order
    type: order
  - type: created_at
  - type: updated_at
# hedhog/data/category.yaml
- id: 1
  slug: electronics
  order: 1
- id: 2
  slug: clothing
  order: 2

5.2 Tabela com localização

# hedhog/table/category.yaml
columns:
  - type: pk
  - name: slug
    type: slug
  - name: name
    type: locale_varchar   # → category_locale, not category
    length: 100
  - name: description
    type: locale_text      # → category_locale, not category
  - type: created_at
  - type: updated_at
# hedhog/data/category.yaml
- id: 1
  slug: electronics
  name: { en: Electronics, pt: Eletrônicos, es: Electrónica }
  description:
    en: Electronic products and gadgets
    pt: Produtos eletrônicos e gadgets

5.3 Coluna FK + muitos-para-muitos via tabela de junção

# hedhog/table/role_permission.yaml (junction table)
columns:
  - type: pk
  - name: role_id
    type: fk
    references: { table: role, column: id, onDelete: CASCADE, onUpdate: CASCADE }
  - name: permission_id
    type: fk
    references: { table: permission, column: id, onDelete: CASCADE, onUpdate: CASCADE }
# hedhog/data/role.yaml
- id: 1
  slug: admin
  relations:
    permission:
      - where: { slug: view-dashboard }
      - where: { slug: manage-users }
- id: 2
  slug: viewer
  relations:
    permission:
      - where: { slug: view-dashboard }

5.4 Coluna enum

# hedhog/table/article.yaml
columns:
  - type: pk
  - name: status
    type: enum
    enum: [draft, published, archived]
  - name: title
    type: varchar
    length: 255
  - type: created_at
  - type: updated_at
# hedhog/data/article.yaml
- id: 1
  status: draft
  title: "My First Article"
- id: 2
  status: published
  title: "Hello World"

5.5 Todos os recursos de coluna combinados

# hedhog/table/product.yaml
columns:
  - type: pk

  - name: category_id
    type: fk
    references: { table: category, column: id, onDelete: SET NULL, onUpdate: CASCADE }
    isNullable: true

  - name: slug
    type: slug

  - name: order
    type: order

  - name: status
    type: enum
    enum: [active, inactive, discontinued]

  - name: name
    type: locale_varchar   # → product_locale
    length: 200

  - name: description
    type: locale_text      # → product_locale

  - name: sku
    type: varchar
    length: 100
    isUnique: true

  - name: price_amount_cents
    type: bigint

  - name: metadata
    type: jsonb
    isNullable: true

  - name: deleted_at
    type: timestamptz
    isNullable: true

  - type: created_at
  - type: updated_at

indices:
  - columns: [deleted_at]