Modelo de conteúdo

Nesta página

O EmDash usa um modelo de conteúdo orientado ao banco de dados, em que as definições de esquema ficam no banco, não no código. É uma decisão de desenho fundamental que permite alterar o esquema em tempo de execução e um fluxo acessível sem papel de desenvolvedor.

Esquema como dados

CMSs tradicionais como Strapi ou Keystatic exigem definir o esquema em código:

// Abordagem tradicional: esquema no código
const posts = collection({
	fields: {
		title: text({ required: true }),
		content: richText(),
	},
});

O EmDash guarda a mesma informação em tabelas:

-- tabela _emdash_collections
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');

-- tabela _emdash_fields
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
  ('coll_abc', 'title', 'string', true),
  ('coll_abc', 'content', 'portableText', false);

Ambas as abordagens definem a mesma estrutura. A diferença é onde essa estrutura vive e como pode ser alterada.

Por que o banco primeiro?

Alteração em tempo de execução

Criar e editar tipos de conteúdo sem mudanças de código nem rebuilds. Sem papel de desenvolvedor, o modelo de dados pode ser desenhado pela UI de administração.

Colunas SQL reais

Ao contrário do modelo EAV do WordPress, cada campo tem uma coluna real. Índices, chaves estrangeiras e otimização de consultas.

Autodocumentado

Ferramentas de banco podem inspecionar o esquema diretamente. Não é preciso analisar código.

Caminho de migração

Exportar o esquema como JSON para controlo de versões. Importar em novos ambientes.

Tabelas de esquema

Duas tabelas de sistema definem a estrutura:

Tabela de coleções

CREATE TABLE _emdash_collections (
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE NOT NULL,        -- "posts", "products"
  label TEXT NOT NULL,              -- "Blog Posts"
  label_singular TEXT,              -- "Post"
  description TEXT,
  icon TEXT,                        -- Nome do ícone Lucide
  supports JSON,                    -- ["drafts", "revisions", "preview"]
  source TEXT,                      -- Como foi criada
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT
);

O campo source indica como a coleção foi criada:

ValorDescrição
manualCriada pelo admin
template:blogPelo seed de um template
import:wordpressImportada do WordPress
discoveredDetetada automaticamente

Tabela de campos

CREATE TABLE _emdash_fields (
  id TEXT PRIMARY KEY,
  collection_id TEXT REFERENCES _emdash_collections(id),
  slug TEXT NOT NULL,               -- Nome da coluna: "title", "price"
  label TEXT NOT NULL,              -- Rótulo de exibição
  type TEXT NOT NULL,               -- Tipo de campo
  column_type TEXT NOT NULL,        -- Tipo SQLite: TEXT, REAL, INTEGER, JSON
  required INTEGER DEFAULT 0,
  unique_field INTEGER DEFAULT 0,
  default_value TEXT,               -- Valor padrão codificado em JSON
  validation JSON,                  -- Regras de validação
  widget TEXT,                      -- Identificador de widget personalizado
  options JSON,                     -- Opções do widget
  sort_order INTEGER,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(collection_id, slug)
);

Tabelas de conteúdo

Cada coleção tem a sua própria tabela com o prefixo ec_. Ao criar uma coleção «products» com campos title e price:

CREATE TABLE ec_products (
  -- Colunas de sistema (sempre presentes)
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE,
  status TEXT DEFAULT 'draft',
  author_id TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  published_at TEXT,
  deleted_at TEXT,                  -- Exclusão lógica
  version INTEGER DEFAULT 1,        -- Bloqueio otimista

  -- Colunas de conteúdo (das definições de campos)
  title TEXT NOT NULL,
  price REAL
);

Alterações de esquema em tempo de execução

Ao adicionar um campo pelo admin, o EmDash:

  1. Insere um registo em _emdash_fields 2. Executa ALTER TABLE ec_collection ADD COLUMN column_name TYPE 3. Regenera o esquema Zod para validação

Operações ALTER TABLE suportadas em tempo de execução no SQLite:

OperaçãoSuportado
Adicionar colunaSim
Renomear colunaSim
Remover colunaSim (SQLite 3.35+)
Alterar tipoNão (reconstrução da tabela)

Para mudanças de tipo, o EmDash trata a reconstrução da tabela de forma transparente: cria nova tabela → copia dados → elimina tabela antiga → renomeia nova tabela.

Separação esquema / conteúdo

O EmDash mantém uma separação clara:

PreocupaçãoLocalTabelas
EsquemaTabelas de sistema_emdash_collections, _emdash_fields
ConteúdoTabelas por coleçãoec_posts, ec_products, etc.
MídiaTabela separada + armazenamentomedia + R2/S3
ConfiguraçõesTabela de opçõesoptions com prefixo site:

Esta separação implica:

  • O esquema pode ser exportado sem o conteúdo
  • O conteúdo pode ser migrado entre esquemas
  • As tabelas de sistema nunca ficam poluídas com dados de utilizador

Validação em tempo de execução

O EmDash constrói esquemas Zod a partir das definições de campos no arranque:

// Exemplo simplificado
function buildSchema(fields: Field[]): ZodSchema {
	const shape: Record<string, ZodType> = {};

	for (const field of fields) {
		let zodType = fieldTypeToZod(field.type);

		if (field.required) {
			zodType = zodType.required();
		}

		if (field.validation?.min !== undefined) {
			zodType = zodType.min(field.validation.min);
		}

		shape[field.slug] = zodType;
	}

	return z.object(shape);
}

O conteúdo é validado contra estes esquemas em tempo de execução em cada operação de criação e atualização.

Integração TypeScript

Gere tipos TypeScript a partir do esquema no banco:

# Obter esquema do banco, gerar tipos
npx emdash types

Isto gera .emdash/types.ts:

// .emdash/types.ts (gerado)
export interface Post {
	title: string;
	content: PortableTextBlock[];
	excerpt?: string;
	featuredImage?: string;
}

export interface Product {
	title: string;
	price: number;
	quantity: number;
}

// Overloads tipados para funções de consulta
declare module "emdash" {
	export function getEmDashCollection(
		type: "posts",
	): Promise<{ entries: ContentEntry<Post>[]; error?: Error }>;

	export function getEmDashEntry(
		type: "products",
		id: string,
	): Promise<{ entry: ContentEntry<Product> | null; error?: Error; isPreview: boolean }>;
}

Fluxos desenvolvedor / não desenvolvedor

Desenvolvedores podem usar o CLI:

# Obter esquema, gerar tipos
npx emdash types

# Exportar esquema como JSON
npx emdash export-seed > seed.json

Não desenvolvedores usam exclusivamente o admin:

  1. Abrir Tipos de conteúdo no painel de administração
  2. Clicar em Adicionar coleção
  3. Definir campos pelo construtor visual
  4. Começar a criar conteúdo imediatamente

Ambos alteram as mesmas tabelas no banco.

Ficheiros seed

Templates e exportações usam ficheiros seed JSON para definições de esquema portáveis:

{
	"version": "1",
	"collections": [
		{
			"slug": "posts",
			"label": "Blog Posts",
			"labelSingular": "Post",
			"supports": ["drafts", "revisions", "preview"],
			"fields": [
				{ "slug": "title", "type": "string", "required": true },
				{ "slug": "content", "type": "portableText" },
				{ "slug": "featuredImage", "type": "image" }
			]
		}
	],
	"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
	"menus": [{ "name": "primary", "label": "Primary Navigation" }]
}

Aplicar ficheiros seed programaticamente:

import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";

// Validar primeiro
const { valid, errors } = validateSeed(seedData);

// Aplicar (idempotente — seguro reexecutar)
await applySeed(db, seedData, {
	includeContent: true,
	onConflict: "skip", // 'skip' | 'update' | 'error'
});

Comparação com outras abordagens

AbordagemLocal do esquemaAlteração em runtimeSegurança de tipos
EmDashBanco de dadosSim (completa)Gerada da BD
WordPressPHP + EAVLimitada (meta fields)Nenhuma
StrapiFicheiros de códigoNão (rebuild necessário)Gerada no build
SanityFicheiros de códigoNão (esquema precisa deploy)Integrada
DirectusBanco de dadosSim (completa)Gerada da BD

O EmDash segue o modelo Directus: banco primeiro com geração de tipos opcional. Isto proporciona máxima flexibilidade mantendo o suporte a desenvolvimento tipado quando desejado.

Próximos passos