nelcota-client
SDK Rust assíncrono para Nelcota: tabelas/views REST e RPC, sessões de autenticação e OAuth e armazenamento de arquivos sob RLS do PostgreSQL. Para aplicativos nativos usando Tokio.
[dependencies]
nelcota-client = "0.1.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Requer Rust 1.88+ (verificado na CI) e servidor Nelcota 0.3.0 ou posterior.
| SDK | Versão-base do servidor | Suporte |
|---|---|---|
| 0.1.x | Servidor v0.3.0 ou posterior |
Suíte completa de contratos HTTP/Postgres, OAuth, update_user e modelos Rust gerados |
| 0.1.x | Servidor v0.2.0 |
Não suportado: faltam as rotas OAuth, PUT /auth/v1/user, o contrato JSON de erros e o gerador Rust |
use nelcota_client::{Client, rest::Order};
use serde::Deserialize;
#[derive(Deserialize)]
struct Note { id: i64, body: String }
# async fn example() -> nelcota_client::Result<()> {
let client = Client::builder("https://api.example.com").build()?;
client.auth().sign_in_with_password("[email protected]", "password").await?;
let response = client.from("notes").select("id,body")
.order("id", Order::descending()).range(0, 19).count_exact()
.execute::<Vec<Note>>().await?;
for note in response.data { println!("{}: {}", note.id, note.body); }
# Ok(()) }Escopo de versão
Esta referência descreve a fonte atual. O pacote publicado é 0.1.2; correções não publicadas de margem de renovação/entrada finita e verificações ampliadas de ciclo de vida exigem novo build/pacote do SDK. Instalar uma versão do servidor não atualiza clientes. Use o registro para comportamento publicado e dependências path/arquivo para desenvolvimento.
REST
Builders são locais e consomem self; clone uma consulta-base para reutilizá-la. Apenas execute envia HTTP. Resultados são Result<Response<T>, Error>; metadados de resposta incluem status HTTP, contagem e cabeçalhos. Decodifique para suas próprias structs Deserialize ou serde_json::Value. Nomes de colunas e expressões select são verificados pelo servidor com seu catálogo atual; essas strings não validam o esquema em tempo de compilação.
# async fn example(client: nelcota_client::Client) -> nelcota_client::Result<()> {
use serde_json::{Value, json};
use nelcota_client::rest::{Condition, IsValue, UpsertOptions};
let note = client.from("notes").insert(&json!({"body":"hello"}))
.select("id,body").single().execute::<Value>().await?;
client.from("notes").update(&json!({"body":"edited"}))
.eq("id", ¬e.data["id"]).execute::<()>().await?;
client.from("notes").delete().eq("id", ¬e.data["id"])
.execute::<()>().await?;
client.from("products").upsert(&json!({"slug":"pen","name":"Pen"}),
UpsertOptions { on_conflict:vec!["slug".into()], ..Default::default() })
.execute::<()>().await?;
client.from("orders").select("id,customer:customers(name),items(product,qty)")
.gte("items.qty", 2)
.or([Condition::eq("status", "paid"),
Condition::all([Condition::gt("total", 100), !Condition::is("archived", IsValue::True)])])
.execute::<Vec<Value>>().await?;
let total = client.rpc("cart_total", &json!({"cart_id":7}))
.execute::<Value>().await?;
# Ok(()) }Filtros: eq, neq, gt, gte, lt, lte, like, ilike, in_values, not_in, is/is_null, not_is, not, matches, and, or. Condições são construídas, nunca montadas a partir de strings de sintaxe não confiáveis. Filtros de relacionamentos embutidos usam caminhos como items.qty; order_on, limit_on, range_on, and_on, or_on atuam sobre linhas embutidas. Chamadas de order se acumulam.
Escritas retornam corpo vazio por padrão (execute::<()>()); chame select para obter representação. single exige exatamente uma linha; maybe_single retorna null para zero linhas (decodifique para Option<T>). Use head().count_exact().execute::<()>() para leituras apenas de contagem. Atualizações/exclusões exigem filtro no recurso pai antes de enviar qualquer HTTP. Contagens exatas varrem todas as linhas correspondentes; solicite-as apenas quando necessário.
Cada leitura retorna no máximo NELCOTA_MAX_ROWS linhas (1000 por padrão), mesmo sem limit. Pagine com range(start, end) ou limit + offset, ordenando por uma coluna única, até uma página voltar vazia; count_exact() fornece o total quando necessário.
Autenticação
# async fn example(client: nelcota_client::Client) -> nelcota_client::Result<()> {
let signup = client.auth().sign_up("[email protected]", "strong-password-123", None).await?;
// signup.session is None while email confirmation is pending.
let session = client.auth().get_session().await?;
let user = client.auth().get_user().await?;
// Merged into user_metadata; a null value removes the key.
client.auth().update_user(nelcota_client::auth::UserUpdate {
data: Some(serde_json::json!({ "plan": "pro" })),
..Default::default()
}).await?;
let mut events = client.auth().subscribe();
let background = client.auth().start_auto_refresh(); // opt-in; retain this handle
client.auth().request_password_reset("[email protected]").await?;
client.auth().send_magic_link("[email protected]").await?;
client.auth().resend_confirmation("[email protected]").await?;
client.auth().sign_out().await?;
drop(background);
# Ok(()) }O armazenamento de sessões usa memória por padrão. Para uma CLI ou aplicativo desktop, auth::FileStorage::new(path) mantém a sessão em arquivo JSON, escrito atomicamente e legível apenas pelo proprietário (0600 no Unix). Implemente auth::SessionStorage para um chaveiro, arquivo criptografado ou armazenamento de cookies por requisição.
# fn example() -> nelcota_client::Result<()> {
use std::sync::Arc;
use nelcota_client::{Client, auth::FileStorage};
let client = Client::builder("https://api.example.com")
.session_storage(Arc::new(FileStorage::new("/home/ana/.config/notes/session.json")))
.build()?;
# Ok(()) }Clones compartilham uma sessão, um bloqueio de ciclo de vida e o resultado da renovação. Clientes/processos independentes compartilhando armazenamento persistente precisam de coordenação externa: a interface de armazenamento, por si só, não serializa renovações. Um token de renovação já usado encerra sua sessão no servidor.
A renovação ocorre antes da expiração quando uma requisição precisa de um token. A renovação opcional em segundo plano para quando seu handle é descartado. Ambos os caminhos limitam a margem configurada de renovação à metade da validade do token, evitando rotações repetidas de tokens novos de curta duração. A rotação continua se um chamador for cancelado ou descartado, para que um token de renovação consumido ainda possa ser substituído localmente. Uma falha transitória mantém a identidade anterior; uma concessão inválida limpa a sessão e retorna a falha à requisição que a iniciou. O logout aguarda a rotação e limpa a sessão local mesmo se a revogação no servidor falhar. Tokens de acesso existentes podem continuar válidos até a expiração.
Em um backend, restrinja um token do chamador a um clone; isso não altera a sessão compartilhada nem renova o token fornecido:
# fn example(client: nelcota_client::Client, caller_token: String) {
let caller = client.with_access_token(caller_token);
let visitor = client.anonymous();
# }OAuth usa PKCE S256 com Google/GitHub. begin_oauth retorna PkceFlow { url, verifier }; mantenha-o privado, abra sua URL no fluxo de navegador do aplicativo e consuma-o com handle_redirect(&mut callback_url, flow) ou troque explicitamente o código/verificador. O SDK não abre um navegador nem cria um servidor de callback. redirect_to deve estar em NELCOTA_OAUTH_REDIRECT_URLS.
Links de e-mail contêm #type=...&token=.... EmailLink::parse(&mut url) extrai o token e limpa o fragmento. Passe links de cadastro/mágicos para verify_email_link; use reset_password(token, password) para recuperação. As implementações Debug de sessão, fluxo e link omitem tokens secretos.
Armazenamento
# async fn example(client: nelcota_client::Client, user_id: String) -> nelcota_client::Result<()> {
use nelcota_client::storage::{UploadOptions, OpenOptions, ListOptions};
let files = client.storage().from("avatars")?;
let name = format!("{user_id}/me.txt");
files.upload(&name, "hello", UploadOptions::default()).await?; // text/plain, from .txt
files.upload("logo", "<svg/>", UploadOptions {
content_type: Some("image/svg+xml".into()), upsert: true,
}).await?;
let bytes = files.download(&name).await?;
let response = files.open(&name, OpenOptions { range:Some((0,Some(3))), ..Default::default() }).await?;
let listing = files.list(ListOptions { prefix:format!("{user_id}/"), ..Default::default() }).await?;
let signed_url = files.create_signed_url(&name, 3600).await?;
let public_url = files.public_url(&name)?;
files.remove(&name).await?;
# Ok(()) }Sem content_type explícito, uploads inferem o tipo pela extensão do nome (json, csv, txt, html, css, js, svg, png, jpg, gif, webp, pdf, mp4 e outros tipos comuns; veja storage::guess_content_type) e usam application/octet-stream como alternativa. upload_reader transmite um leitor Tokio. open retorna uma reqwest::Response em streaming; trate 206, 304, ETag e falhas do stream por conta própria após o retorno. download carrega o arquivo em memória e informa falhas no corpo em seu Result. Uploads/downloads não têm prazo por padrão; restrinja um timeout com RequestOptions quando necessário. Redirecionamentos S3 são seguidos; tokens bearer são removidos ao mudar de origem. Nomes de objetos são normalizados em NFC e codificados por segmento. Segmentos vazios/ponto, barras invertidas e caracteres de controle são rejeitados.
Buckets suportam list/get/create/update/delete, geralmente com service_role. update_bucket substitui todas as configurações, então recebe um BucketUpdate, que não tem Default: cada campo é explícito, e None remove um limite intencionalmente. Parta do bucket atual para alterar uma configuração:
# async fn example(client: nelcota_client::Client) -> nelcota_client::Result<()> {
use nelcota_client::storage::BucketUpdate;
let current = client.storage().get_bucket("avatars").await?;
client.storage().update_bucket("avatars", BucketUpdate { public: true, ..current.into() }).await?;
# Ok(()) }A autorização pertence à RLS do servidor; um token service_role a ignora e pertence apenas a um aplicativo confiável.
Tipos Rust do esquema
nelcota types --lang rust -o src/database.rsMódulos gerados expõem schema::table::{Row, Insert, Update, NAME, PRIMARY_KEY} e schema::rpc::function::{Args, Returns, NAME}. Colunas enum têm enums renomeados pelo serde. Colunas geradas/identity-always são omitidas dos modelos de escrita; views têm modelos de escrita, exceto views materializadas. Projeções e relacionamentos embutidos precisam de structs que correspondam ao formato selecionado.
Field::Omit omite um campo de escrita, mantendo seu valor padrão/atual; Field::Value(None) envia null explícito para colunas anuláveis. UUID/data/hora e tipos escalares desconhecidos usam strings. Bigint usa i64, numeric usa serde_json::Number com precisão arbitrária e arrays permitem elementos null. Aliases de retorno RPC permitem null; argumentos opcionais distinguem omissão de null explícito. Tipos são gerados a partir do esquema exposto configurado, sem exigir conexão com o banco ao compilar aplicativos consumidores.
Erros, prazos e novas tentativas
Error::code(), status() e retry_after() preservam informações do servidor. Outras variantes distinguem entrada inválida, rede, timeout, cancelamento, resposta inválida, cardinalidade e falhas de armazenamento de sessão. Não há panic para configuração do chamador ou falhas HTTP. URLs assinadas de requisição são omitidas do texto de erro de transporte. Uma resposta sem corpo JSON (contagem com head(), página de proxy) recebe unauthorized, forbidden, not_found, rate_limited, unavailable ou http_<status> como código.
Quando o servidor os informa, sqlstate(), details(), hint() e constraint() expõem o que o Postgres disse sobre um db_error; servidores antigos não enviam esses campos. Funções auxiliares classificam casos comuns: is_not_found(), is_conflict(), is_unique_violation(), is_rate_limited(), is_unauthorized(), is_forbidden() e is_retryable().
# async fn example(client: nelcota_client::Client) -> nelcota_client::Result<()> {
use serde_json::json;
match client.from("notes").insert(&json!({"slug":"a"})).execute::<()>().await {
Err(error) if error.is_unique_violation() => {
println!("taken ({:?})", error.constraint());
}
other => { other?; }
}
# Ok(()) }Apenas GET/HEAD repetem tentativas após falhas de rede/leitura do corpo e 429/503, com duas novas tentativas por padrão. A espera progressiva tem variação aleatória e respeita Retry-After (segundos ou data HTTP); um atraso superior a 30 segundos retorna imediatamente. Mutações, troca de autenticação, RPC e listagens POST de armazenamento nunca se repetem automaticamente. Timeouts cobrem cada tentativa e o corpo carregado em memória; renovação e espera progressiva são separadas. Descarte o future da requisição ou use CancellationToken para cancelar; uma escrita já aceita pelo servidor não pode ser desfeita pelo cancelamento do cliente. O ciclo de vida do stream bruto é gerenciado pelo reqwest/chamador.
Client::builder(url).header("x-app", "notes") adiciona um cabeçalho a cada requisição; build recusa cabeçalhos inválidos e Authorization, que vem de access_token ou da sessão. Instâncias personalizadas de http_client devem desativar suas próprias novas tentativas de autenticação/mutação. O cliente padrão usa rustls/ring explícitos e raízes Mozilla sem inicialização global de provedor criptográfico. Pode ser reutilizado entre tarefas.
Comece pelo início rápido (servidor local, migração, tipos gerados). Os exemplos em examples/ usam o esquema compartilhado examples/notes.sql. Resultados vazios, 401/403 ou limite de 1000 linhas? Veja solução de problemas. Guia em português: Começando com Rust. Contrato: Contrato do SDK Rust. Publicação: releasing.md.