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 é:
- Varre
libraries/<library>/hedhog/table/em busca de definições de schema em YAML — um arquivo por tabela. - Pareia cada schema de tabela com seus dados de seed de
libraries/<library>/hedhog/data/(correspondidos pelo nome do arquivo). - Gera uma migration PostgreSQL (
CREATE TABLE, índices, constraints de FK,INSERTs) emapps/api/prisma/migrations/. - 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 executehedhog dev apply, nunca edite manualmenteapps/api/prisma/schema.prismae nunca resete/recrie o projeto, o banco de dados ou as migrations existentes. Quando um YAML detable/oudata/mudar, adicione uma nova migration SQL emapps/api/prisma/migrations/que espelhe a mudança e, então, executepnpm prisma:deploya partir deapps/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:
| Chave | Usada na prática | Observações |
|---|---|---|
columns | em todo lugar | Lista 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. |
table | apenas em address e crm | Redundante — 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.
type | Nome de coluna automático | SQL gerado | NOT NULL | Observações |
|---|---|---|---|---|
pk | id | SERIAL PRIMARY KEY | ✓ | Sempre use isto para a chave primária, não isPrimaryKey. |
fk | (você fornece) | INTEGER + constraint de FK | ✓ | Requer references: — veja §2.5. |
slug | slug | VARCHAR(255) UNIQUE NOT NULL | ✓ | Sobrescreva o tamanho com length:. |
order | order | INTEGER NOT NULL DEFAULT 0 | ✓ | Muitas tabelas definem manualmente sua própria coluna int order/sort_order com um default customizado — ambos aparecem no repositório. |
created_at | created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | ✓ | |
updated_at | updated_at | TIMESTAMPTZ 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) | VARCHAR | — | Roteada para a tabela complementar <table>_locale gerada automaticamente, não para a tabela principal. Veja §2.7. |
locale_text | (você fornece) | TEXT | — | Mesmo 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):
type | SQL gerado |
|---|---|
int | INTEGER |
datetime | TIMESTAMPTZ |
char | CHAR(<length || 1>) |
bytes | BYTEA |
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
| Propriedade | Tipo | Aplica-se a | Descrição |
|---|---|---|---|
name | string | todos, exceto aliases de nome forçado | Nome da coluna, snake_case. |
type | string | todos | Alias especial, alias curto ou tipo nativo do PostgreSQL. |
isNullable | boolean | todos | Padrão false — toda coluna é NOT NULL a menos que indicado o contrário. |
isUnique | boolean | todos | Adiciona um UNIQUE INDEX. |
isPrimaryKey | boolean | colunas nativas | Prefira type: pk para tabelas novas. |
default | qualquer | todos | Para TIMESTAMPTZ, use a string literal "now()". |
length | integer | varchar, char, slug, locale_varchar | Limite de caracteres. slug/locale_varchar têm padrão 255; char tem padrão 1. |
precision / scale | integer | decimal / numeric | Não está no schema JSON formal, mas é usado em todo o repositório para campos de dinheiro/quantidade, por exemplo precision: 12, scale: 6. |
references | object | type: fk | Veja abaixo. |
enum / values | string[] | type: enum | Lista 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, umagentreferenciado por seu históricoagent_run, ousettlement.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:
| Coluna | Tipo | Constraints |
|---|---|---|
id | SERIAL | NOT NULL PRIMARY KEY |
locale_id | INTEGER | NOT NULL, FK → locale.id CASCADE |
<table>_id | INTEGER | NOT NULL, FK → <table>.id CASCADE |
| (colunas locale) | VARCHAR/TEXT | conforme declarado na tabela pai |
created_at / updated_at | TIMESTAMPTZ | defaults 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_atanulável por convenção (não há um aliastype: deleted_atdedicado) — sempre acompanhada de um índice. - Dinheiro como inteiro: valores monetários são modelados como
*_amount_cents/amount_cents(int/bigint), nãonumeric, 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/*.sqlsão anexados literalmente após todas as instruçõesCREATE TABLE/INSERTgeradas, 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 comlibraries/<lib>/hedhog/query/triggers.sqlpara 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.
| Operador | Exemplo | SQL gerado |
|---|---|---|
| (simples) | code: abc | "code" = 'abc' |
equals | equals: abc | "col" = 'abc' |
notEquals / not | not: abc | "col" <> 'abc' |
in | in: [a, b, c] | "col" IN ('a','b','c') |
notIn | notIn: [a, b] | "col" NOT IN ('a','b') |
like / notLike | like: "foo%" | "col" LIKE 'foo%' |
contains / notContains | contains: foo | "col" LIKE '%foo%' |
gt / gte / lt / lte | gt: 5 | "col" > '5' |
isNull / isNotNull | isNull: 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 umUPDATE ... WHERE <key>seguido de umINSERT ... WHERE NOT EXISTS (...)— nunca umINSERTpuro que dependa deON 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 empackage.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 buscawhere: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]