Exemplos¶
Operações reais contra o schema. A referência completa é o SDL gerado.
Playground (GraphiQL)¶
A API serve o GraphiQL nativo (Strawberry) em GET /graphql — um playground
no browser com introspecção do schema, autocomplete e execução de queries. É a
forma mais rápida de explorar a API.
| Ambiente | URL do playground |
|---|---|
| Staging | …/graphql |
| Dev local | http://localhost:8000/graphql (após make dev) |
Como autenticar no GraphiQL
Queries públicas (notícias, temas, busca, widgets) rodam sem nada. Para queries/mutations autenticadas (clippings, marketplace, push), adicione o header na aba Headers do GraphiQL:
{ "Authorization": "Bearer <SEU_JWT_DO_KEYCLOAK>" }
O JWT é o access_token da sua sessão no portal (visível em
/api/auth/session). Sem ele, campos guardados por IsAuthenticated falham.
Subscriptions não rodam no GraphiQL
O GraphiQL fala com /graphql (queries/mutations). A subscription
generateRecortes é SSE em /graphql/stream — ver
Subscriptions & SSE.
Queries públicas (sem auth)¶
query Artigos {
articles(page: 1, limit: 5, filter: { agencies: ["ms"], themes: [] }) {
articles {
uniqueId title url agency publishedAt
# hierarquia de temas (L1→L3) + o tema mais específico atribuído
theme1Level1Code theme1Level1Label
theme1Level2Code theme1Level2Label
theme1Level3Code theme1Level3Label
mostSpecificThemeCode mostSpecificThemeLabel
}
page
found
}
}
filter.themes faz OR entre níveis
Os codes em themes casam contra L1 ou L2 ou L3 — não só o nível
topo. Use themeLabel (label de L1) para filtrar por rótulo legível, e
dedup: true para agrupar variações do mesmo conteúdo por content_hash.
query Busca {
# alpha = peso do vetor no ranking híbrido (0=keyword puro, 1=vetor puro)
search(query: "vacinação", page: 1, semantic: true, alpha: 0.3, dedup: true) {
articles { uniqueId title }
found
}
}
query Metadados {
themes { code label }
agencies { code label }
popularTags(limit: 10) { label count }
# contagem de artigos por tema, no nível e janela escolhidos
themeArticleCounts(days: 30, level: 1) { code label count }
}
query Relacionados($id: String!) {
# similares por theme-code (Typesense), excluindo o próprio artigo
relatedArticles(uniqueId: $id, limit: 4) {
uniqueId title url mostSpecificThemeLabel
}
}
relatedArticles ≠ similarArticles
relatedArticles é público e similaridade por theme-code no
Typesense (usa mostSpecificThemeCode, senão theme1Level1Code). O
similarArticles é interno (IsInternal) e baseado em embeddings
no Postgres — ver Queries internas.
query Painel {
analyticsKpis(range: { days: 30 }) { total activeThemes activeAgencies dailyAverage }
topThemes(range: { days: 30 }, limit: 8) { label count }
articlesTimeline(range: { days: 30 }) { date count }
}
Queries e mutations autenticadas¶
Exigem Authorization: Bearer <JWT do Keycloak>.
query MeusClippings {
clippings { # autorados + inscritos do usuário autenticado
id
name
isAuthor
publishedToMarketplace
mySubscription { role deliveryChannels { email telegram push webhook } }
}
}
mutation Criar($input: ClippingInput!) {
createClipping(input: $input) { id name recortes { title themes } }
}
{
"input": {
"name": "Saúde — vacinação",
"schedule": "0 8 * * *",
"recortes": [
{ "title": "Vacinas", "themes": [], "agencies": ["ms"], "keywords": ["vacinação"] }
],
"deliveryChannels": { "email": true, "telegram": false, "push": false, "webhook": false }
}
}
query Estimar {
# contagem REAL por recorte nas últimas sinceHours; substitui o mock clippingEstimate
estimateRecorteCount(
themes: [], agencies: ["ms"], keywords: ["vacinação"], sinceHours: 24
)
}
estimateRecorteCount é o caminho atual
Retorna um Int! (contagem real no Typesense), não o { totalEstimate }
mock do antigo clippingEstimate. Para keywords, conta por keyword e
devolve o MAX; sem keywords, uma única contagem filtro-only. É público.
query MeusSeguidos {
# substitui o getFollows do portal; campos do listing + canais de entrega
myFollowedListings {
id name authorDisplayName followedAt
deliveryChannels { email telegram push webhook }
extraEmails webhookUrl
}
}
query Telegram {
# substitui o getHasTelegram; lê users/{id}/telegramLink/account
currentUserHasTelegramLinked
}
mutation Publicar($clippingId: String!, $input: PublishInput!) {
publishToMarketplace(clippingId: $clippingId, input: $input) { id name }
}
mutation Clonar($listingId: String!) {
cloneMarketplaceListing(listingId: $listingId) { id name } # retorna o Clipping novo
}
Releases (entregas)¶
Um release é uma entrega histórica de um clipping. A autorização é mista: pública se o listing-fonte estiver ativo (conteúdo já é público), senão restrita ao autor ou subscriber — ver auth › Releases.
query Release($id: String!) {
# busca por id; recortes/marketplaceListingId/digestPreview só são populados aqui
release(id: $id) {
id clippingName createdAt articlesCount
digestPreview # resumo ≤150 chars, computado
marketplaceListingId # id do listing ativo, ou null
recortes { title themes agencies keywords }
}
}
query ArtigosDoRelease($id: String!) {
# OR dos recortes (com keywords), dedup por uniqueId; auth espelha release(id)
releaseArticles(id: $id) {
uniqueId title url publishedAt mostSpecificThemeLabel
}
}
Subscription (SSE)¶
Via /graphql/stream — ver Subscriptions & SSE.
subscription Gerar($prompt: String!) {
generateRecortes(prompt: $prompt) {
__typename
... on AgentEventThinking { message }
... on AgentEventDone { suggestedName recortes { title themes agencies keywords } }
... on AgentEventError { message }
}
}
Queries internas (service account)¶
Usadas pelos workers de dados com OIDC do Google. Não são para o portal.
query ParaTypesense($id: String!) { newsForTypesense(uniqueId: $id) { uniqueId title } }
query ParaBigquery { newsBatchForBigquery(startDate: "2026-01-01", endDate: "2026-01-31") { uniqueId } }
mutation Features($id: String!, $f: JSON!) { upsertFeatures(uniqueId: $id, features: $f) }
Estes endpoints existem hoje
O schema já expõe a superfície interna que os workers precisam
(newsById, newsBatch, newsForTypesense, newsBatchForBigquery,
upsertFeatures, batchUpsertFeatures, updateTypesenseField). A migração
dos workers (remover acesso direto ao Postgres) é o próximo passo de
desacoplamento — ver a documentação central / blog.
Gotchas¶
Compilado de armadilhas (todas vieram do drift da R1)
- Scalar de IDs é
String, nãoID.clipping(id: String!),article(uniqueId: String!). UsarID!→Unknown type 'ID'. - Subscriptions em
/graphql/stream, não/graphql. URL errada → 404 / "Failed to fetch". - CSP do portal precisa listar a origin deste serviço em
connect-src, senão o browser bloqueia (CORS aqui não basta). Agencyé{ code, label }— não{ key, name, type }.estimateRecorteCounté a estimativa real (Int!) e substitui o mockclippingEstimate(que retorna{ totalEstimate }e é deprecated). O novo recebethemes/agencies/keywords/sinceHours.sendClippingsegue mock.- IDs de tema fazem OR entre L1/L2/L3.
ArticleFilter.themescasa contra qualquer nível;relatedArticles/releaseArticlesusammostSpecificThemeCode. relatedArticles(público, theme-code) ≠similarArticles(interno, embeddings).cloneMarketplaceListingretornaClipping!(precisa de selection set, ex.:{ id name }) — eraBoolean!antes de um gap-fix pré-rollout.clippings(nãomyClippings) é a query de listagem do usuário.