Continuação da leva "pelo menos mais 20 etapas" — 2 subagentes em paralelo.
## Documents (apps/api/src/documents/ + migration documents)
Metade "templates + geração" do eden.md módulo 4 (§1.1-1.3): document_templates,
document_template_versions (ciclo draft→publish, no máximo 1 rascunho por
template via índice único parcial, versão publicada imutável), document_generations
(idempotente por (template_version_id, origem), CHECK de origem única). Motor de
merge de variáveis ({{chave}}→valor, nunca lança exceção) como função pura
testável. A metade de Assinatura Eletrônica (envelopes, signatários, OTP,
cadeia de auditoria hash-chain — eden.md §1.4-1.9/§5-7, 9 tabelas satélite)
fica deliberadamente fora — grande e sensível demais para dividir espaço
com esta leva, fica para uma leva própria. PDF real via Chromium também
deferido; document_generations grava rendered_html, pdf_* ficam NULL.
## EDEN Parceiros / EDEN Assinante (apps/reseller-web, apps/subscriber-web)
Replicam o shell autenticado do core-web (leva anterior): login real com
suporte a MFA, sessão, AppShell do Design System — trocando só
applicationKey ("reseller"/"subscriber"), título e chave de localStorage
da sessão. As duas telas "Bootstrap da Fase 1" saem de circulação.
Testado ao vivo contra os containers reais: ciclo completo de template
(criar→draft→409 no segundo draft→editar→publicar→gerar documento com
merge de variáveis funcionando→regeneração idempotente reaproveita o
registro), e login via /v1/auth/login com applicationKey "reseller"/
"subscriber" retornando tokens válidos nos 2 portais reconstruídos.
pnpm turbo run typecheck build (28/28), pnpm --filter @eden/api test
(30/30, sem regressão), trilha de auditoria conferida no Postgres.
Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
/**
|
|
* Stand-in SIMPLIFICADO para o `renderToHtml()`/`renderEditorContent.js`
|
|
* do legado (eden.md §2.2) — aquele renderer conhece toda a árvore
|
|
* Tiptap (paragraph/heading/table/mergeField/conditional/...) e escapa
|
|
* HTML nó a nó. Esta leva de backend não tem o editor visual real ainda
|
|
* (`editor_content` é aceito como JSONB genérico, não validado), então
|
|
* em vez de reimplementar aquele renderer pela metade, este módulo faz
|
|
* só o mínimo necessário para `document_template_versions.html_content`
|
|
* nunca ficar dessincronizado do `editor_content` salvo:
|
|
*
|
|
* - se `editor_content` tiver uma propriedade de nível raiz
|
|
* `plainText` (string), ela é usada como o texto do documento;
|
|
* - caso contrário, um placeholder determinístico é usado.
|
|
*
|
|
* NÃO deve ser confundido com o merge-engine (merge-engine.ts), que roda
|
|
* DEPOIS, sobre o texto aqui extraído, para resolver `{{variavel}}`.
|
|
*/
|
|
|
|
export const EDITOR_CONTENT_PLACEHOLDER_TEXT =
|
|
"[Conteúdo gerado pelo editor visual — renderização completa pendente de implementação]";
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Returns `editor_content.plainText` when present as a root-level string
|
|
* property, or `null` otherwise. Never throws on a malformed/unexpected
|
|
* shape — this leva doesn't validate the Tiptap tree at all (that's the
|
|
* visual editor's job once it exists).
|
|
*/
|
|
export function extractPlainText(editorContent: unknown): string | null {
|
|
if (
|
|
editorContent !== null &&
|
|
typeof editorContent === "object" &&
|
|
"plainText" in (editorContent as Record<string, unknown>) &&
|
|
typeof (editorContent as Record<string, unknown>).plainText === "string"
|
|
) {
|
|
return (editorContent as { plainText: string }).plainText;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* `extractPlainText` with the deterministic placeholder as fallback —
|
|
* this is the actual text fed to the merge engine at generation time
|
|
* (documents.service.ts), never `null`/empty on that path.
|
|
*/
|
|
export function resolveSourceText(editorContent: unknown): string {
|
|
const plainText = extractPlainText(editorContent);
|
|
return plainText && plainText.length > 0 ? plainText : EDITOR_CONTENT_PLACEHOLDER_TEXT;
|
|
}
|
|
|
|
/** Recomputes `document_template_versions.html_content` (eden.md §1.2 step 2). */
|
|
export function renderEditorContentToHtml(editorContent: unknown): string {
|
|
return `<p>${escapeHtml(resolveSourceText(editorContent))}</p>`;
|
|
}
|