@report-platform/sdk
v0.2.0
Published
Widget embutível de relatórios/propostas da Report Platform (editor de documentos orientado a blocos).
Readme
@report-platform/sdk
Widget embutível de relatórios e propostas da Report Platform: um editor
de documentos orientado a blocos que empresas de construção usam para compor,
revisar e exportar propostas a partir dos dados estruturados de clientes,
obras e orçamentos. O host embute o editor como biblioteca — sem router,
login ou navegação próprios — passando apenas um token temporário, o
reportType e os data; o usuário compõe o documento visualmente,
reorganiza blocos, controla campos e colunas, salva modelos reutilizáveis e
exporta o PDF.
A arquitetura é híbrida e declarativa: ProseMirror gerencia a árvore editável (seleção, transações, undo/redo), JSON Schema valida dados e configurações e uma DSL JSON descreve a composição visual dos blocos. Regras sensíveis, autorização, uploads e geração de PDF ficam no backend multiempresa da plataforma. Nenhum JSON vindo de cadastro executa código, e editor, prévia e PDF compartilham a mesma árvore de renderização.
Instalação
npm install @report-platform/sdk
# ou
pnpm add @report-platform/sdkO pacote traz duas entradas, com o mesmo contrato de opções, callbacks e capacidades:
| Entrada | Para quem | Runtime |
| --------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
| @report-platform/sdk | aplicações React (18 ou 19) | react e react-dom são peer dependencies do host |
| @report-platform/sdk/standalone | qualquer página/host que não é uma app React | React, ReactDOM e ProseMirror vêm embutidos no bundle |
Ambas são distribuídas em ESM (.mjs) e CommonJS (.cjs), com declarações
TypeScript autocontidas (.d.ts/.d.cts) — nenhum declare module é
necessário, em qualquer moduleResolution (bundler, node16/nodenext ou
node). O manifesto declara react/react-dom como peer dependencies do
pacote inteiro; a entrada standalone não os usa em runtime (traz o próprio
React), mas gerenciadores que instalam peers automaticamente (npm 7+) vão
baixá-los mesmo num host sem React — é inofensivo.
Pré-requisito: o token vem do SEU backend
O widget nunca conhece o clientSecret da sua empresa. Seu backend troca a
credencial por um JWT temporário (TTL ≤ 15 min, sem refresh token) e o
entrega ao navegador; é esse token que o widget recebe. A troca é
servidor-a-servidor — o endpoint não responde a CORS de navegador, por
construção:
curl -sS -X POST "https://api-production-7db79.up.railway.app/v1/auth/editor-tokens" \
-H "Authorization: Basic $(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64)" \
-H "Content-Type: application/json" \
-d '{
"userId": "usuario-42",
"reports": [
{ "reportType": "maiscontrole.proposal",
"scopes": ["editor:read", "templates:read", "templates:create"] }
]
}'Peça o mínimo de escopos que a tela precisa. Lista fechada:
editor:read, templates:read, templates:create, templates:update-own,
templates:set-default, assets:create, export:pdf.
O data do relatório também é derivado no seu backend e validado pela
plataforma contra o dataSchema publicado do reportType. A sessão do editor
guarda apenas o hash do data e o documento de trabalho persistido nunca
contém dados; somente os snapshots de prévia/exportação congelam os dados
validados (para gerar o PDF) e são purgados pela plataforma.
Uso — host sem React (mountReportEditor)
import { mountReportEditor } from "@report-platform/sdk/standalone";
const handle = mountReportEditor(document.getElementById("editor"), {
token, // JWT temporário vindo do SEU backend
reportType: "maiscontrole.proposal",
data, // derivado no SEU backend
documentKey: "orcamento-18", // opcional: a plataforma persiste e restaura o documento
onReady(context) {
/* sessionId, permissões efetivas, modelos disponíveis, avisos… */
},
onAuthRequired() {
renovarToken().then((novo) => handle.setToken(novo));
},
onConflict(conflict) {
// Três variantes, discriminadas por conflict.kind:
// "data-update" → keepLocal() | acceptIncoming()
// "working-document-save" → keepLocal() | acceptRemote()
// "template-save" → informativo (currentVersion, lastAuthor)
// Sem resposta, o documento local é preservado.
},
onError(error) {
console.warn(error.code, error.message); // código estável + mensagem pt-BR
},
});
// mais tarde
const documento = handle.getDocument();
handle.unmount(); // limpo e idempotenteTodo o DOM do widget — inclusive o <style> com escopo e os overlays — é
renderizado dentro do filho que o SDK cria no seu container. Nada é
injetado em document.head; o único toque em document.body é o
<a download> oculto, imediatamente removido, que dispara o download do PDF
exportado.
Uso — aplicação React
import { useMemo, useRef } from "react";
import {
ReportEditor,
ReportSdkProvider,
createReportClient,
type ReportEditorHandle,
type ReportEditorProps,
} from "@report-platform/sdk";
type Props = { token: string; data: ReportEditorProps["data"] };
export function PropostaEditor({ token, data }: Props) {
const client = useMemo(() => createReportClient(), []); // API de produção por padrão
const handleRef = useRef<ReportEditorHandle | null>(null);
return (
<ReportSdkProvider client={client}>
<ReportEditor
ref={handleRef}
token={token}
reportType="maiscontrole.proposal"
data={data}
documentKey="orcamento-18"
onReady={(ctx) => console.info("sessão", ctx.sessionId)}
onAuthRequired={() => renovarToken().then((novo) => handleRef.current?.setToken(novo))}
onError={(error) => console.warn(error.code, error.message)}
/>
</ReportSdkProvider>
);
}createReportClient é chamado uma vez por host; o provider carrega
apenas o client — nenhum token, dado ou estado de sessão vive nele.
apiBaseUrl — opcional, produção por padrão
Sem apiBaseUrl, o SDK fala com a API de produção da plataforma:
import { DEFAULT_API_BASE_URL } from "@report-platform/sdk"; // também exportado de /standalone
// "https://api-production-7db79.up.railway.app"Esse é hoje o domínio de andaime do Railway; quando o domínio definitivo da
plataforma entrar no ar, o default muda em nova versão do SDK e o domínio
anterior continua respondendo durante a janela de suporte. Instalações que
precisam de estabilidade absoluta devem passar apiBaseUrl explicitamente.
Para outro ambiente (desenvolvimento local, homologação, instalação própria), informe a base explicitamente — em qualquer uma das entradas:
createReportClient({ apiBaseUrl: "http://localhost:3001" });
mountReportEditor(el, { apiBaseUrl: "http://localhost:3001", token, reportType, data });Um valor em branco é rejeitado com TypeError (configuração errada falha
alto; nunca há fallback silencioso). O SDK nunca deriva a URL de
window.location.
Opções, callbacks e handle
Opções de mountReportEditor. As props de ReportEditor (React) são as
mesmas, com três diferenças: apiBaseUrl, fetch e validationLimits
pertencem a createReportClient na entrada React; previewDebounceMs
(padrão 2000 ms) e templatesToolbarTarget existem só na entrada React:
| Opção | Tipo | Descrição |
| -------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| token | string | JWT temporário — só em memória, enviado como Authorization: Bearer. Trocá-lo segue o fluxo de setToken |
| reportType | string | Chave estável da definição do relatório (ex.: maiscontrole.proposal) |
| data | JsonObject | Dados do relatório, validados contra o schema publicado. Trocá-los (por referência) segue o fluxo updateData |
| documentKey | string? | Identidade do documento de negócio (1–200 caracteres). Presente → a plataforma persiste, restaura e faz autosave |
| autosaveDebounceMs | number? | Debounce do autosave quando documentKey está presente (padrão 3000 ms) |
| apiBaseUrl | string? | Base da API (padrão: produção — acima) |
| fetch | FetchLike? | Implementação de fetch injetável (testes, proxies) |
| validationLimits | Partial<ValidationLimits>? | Ajustes dos limites estruturais de validação (bytes/profundidade/nós) |
Callbacks (ReportEditorCallbacks): onReady, onChange ({ revision,
dirty }, com throttle — nunca carrega o documento, puxe com
getDocument()), onDirtyChange, onTemplateSaved, onAuthRequired,
onConflict, onWarning, onError, onTelemetry (100% opt-in; sem token,
dado ou conteúdo de documento).
Handle (ReportEditorMountHandle no standalone; ReportEditorHandle via
ref no React): getDocument(), undo(), redo(), setToken(token),
updateData(data); no standalone ainda update(options) e unmount(); no
React ainda getTemplateActions(), setPreviewOpen(open) e
getExportController() para hosts que dirigem Prévia/Exportar do próprio
toolbar.
Erros chegam por onError como ReportEditorError — code estável
(AUTH_REQUIRED, FORBIDDEN_REPORT, INVALID_DATA, INVALID_CONFIG,
INVALID_DOCUMENT, PROTOCOL_INCOMPATIBLE, PRIMITIVES_UNSUPPORTED,
CONFLICT, RATE_LIMITED, INTERNAL), message em pt-BR, issues[] com
JSON Pointer por campo e requestId. AUTH_REQUIRED no bootstrap é roteado
para onAuthRequired.
CSP da página do host
| Diretiva | Valor necessário | Por quê |
| ------------- | ----------------------------------------------------- | --------------------------------------------------------------------- |
| connect-src | origem da API | bootstrap, modelos, uploads e exportação via fetch/XHR |
| frame-src | origem da API | a prévia paginada é um iframe da rota SSR de impressão |
| img-src | origem da API + origem do storage assinado (S3/MinIO) | imagens da galeria são servidas por URLs assinadas de curta duração |
| style-src | 'unsafe-inline' | o widget injeta seu stylesheet com escopo dentro do próprio container |
Nenhum cookie é usado; o SDK não carrega script remoto.
Compatibilidade
- Navegadores evergreen (bundle
es2022; o standalone não depende deprocess.env). - React 18 ou 19 (só na entrada React).
- O SDK negocia protocolo, schema de documento e conjunto de primitivas com a
API a cada bootstrap; incompatibilidades chegam tipadas
(
PROTOCOL_INCOMPATIBLE,INVALID_DOCUMENT,PRIMITIVES_UNSUPPORTED) e nunca renderizam um documento pela metade — bloco desconhecido abre em fallback somente leitura com os atributos preservados. - Versionamento SemVer; a superfície pública tipada (
.d.ts) é o contrato.
Licença
UNLICENSED — todos os direitos reservados. Uso mediante credencial emitida
pela Report Platform.
