@maykonpaulo/maestro-admin
v0.3.0
Published
Generic, metadata-driven admin UI for @maykonpaulo/maestro-core (via @maykonpaulo/maestro-server). Reads GET /metadata and auto-builds navigation, tables, detail views and create/update forms for every entity/collection — no per-entity code. Ships as a ru
Readme
@maykonpaulo/maestro-admin
Este guia leva um programador do zero a um painel administrativo governado (listar, filtrar, criar, editar, excluir, exportar, com RBAC e auditoria) sobre o banco de dados de qualquer sistema — sem escrever CRUD por entidade.
O que é o Maestro? Uma engine metadata-driven em TypeScript que você conecta a um banco existente para operá-lo com segurança, sem acesso direto ao banco. Você aponta para o banco, o Maestro descobre o schema (ou você declara), e ele entrega uma API REST + uma UI de admin genérica que se auto-monta. É agnóstico de framework, de banco e de domínio.
Índice
- Pré-requisitos
- Escolha seu banco (providers)
- Instalação
- Caminho A — Turnkey (recomendado)
- Caminho B — Declarativo/curado
- Autenticação (
actorResolver) - Autorização (RBAC)
- Auditoria
- Operações customizadas (ações de negócio)
- Campos: tipos, sensíveis, soft delete, relações
- A UI de admin (
maestro-admin) - A API HTTP (para frontends próprios)
- Referência rápida
- Produção: checklist e ressalvas
- Solução de problemas
- Pacotes e versões
1. Pré-requisitos
- Node.js 18+ e um gerenciador de pacotes (
npm,pnpmouyarn). - Acesso ao banco de dados do sistema que você quer administrar (string de conexão).
- Um mecanismo de autenticação seu (JWT, cookie de sessão, etc.) — o Maestro não faz auth; ele consome o "ator" que você já autenticou.
2. Escolha seu banco (providers)
O Maestro fala com o banco através de um provider. Existem 9 pacotes oficiais publicados — cada um implementa leitura de schema (introspecção), CRUD real e criação de coleção/tabela:
| Banco | Pacote |
|---|---|
| PostgreSQL · MySQL · SQLite · SQL Server | @maykonpaulo/maestro-provider-sql |
| MongoDB | @maykonpaulo/maestro-provider-mongodb |
| Redis | @maykonpaulo/maestro-provider-redis |
| Elasticsearch | @maykonpaulo/maestro-provider-elasticsearch |
| DynamoDB | @maykonpaulo/maestro-provider-dynamodb |
| Cassandra | @maykonpaulo/maestro-provider-cassandra |
| Couchbase | @maykonpaulo/maestro-provider-couchbase |
| Firestore | @maykonpaulo/maestro-provider-firestore |
| Neo4j | @maykonpaulo/maestro-provider-neo4j |
Banco fora dessa lista? Você pode implementar a interface
DatasourceProvider(6 métodos) — ver seção Solução de problemas. Mas para qualquer um acima, é só instalar.
3. Instalação
Instale a engine, o provider do seu banco, o servidor turnkey e a UI:
# engine + servidor + UI
npm i @maykonpaulo/maestro-core @maykonpaulo/maestro-server @maykonpaulo/maestro-admin
# provider do seu banco (exemplo: MongoDB)
npm i @maykonpaulo/maestro-provider-mongodbTroque o provider pelo do seu banco (ex.: @maykonpaulo/maestro-provider-sql).
4. Caminho A — Turnkey (recomendado)
Aponte para o banco e tenha um admin completo, sem declarar entidade nenhuma. O Maestro introspecta o banco, transforma cada coleção/tabela em uma entidade administrável e sobe um servidor REST. É o caminho mais rápido.
4.1 Conectar o provider
Escolha a fábrica do seu banco (confira as opções exatas de conexão no README do provider):
// MongoDB
import { createMongoProvider } from '@maykonpaulo/maestro-provider-mongodb';
const provider = createMongoProvider({ uri: process.env.MONGO_URL!, database: 'app' });
// PostgreSQL (mesma família: mysql/sqlite/mssql via `dialect`)
import { createSqlProvider } from '@maykonpaulo/maestro-provider-sql';
const provider = createSqlProvider({ dialect: 'postgres', connectionString: process.env.PG_URL! });
// sqlite: createSqlProvider({ dialect: 'sqlite', databasePath: './app.db' })
// mysql: createSqlProvider({ dialect: 'mysql', uri: process.env.MYSQL_URL! })
// Outros
import { createRedisProvider } from '@maykonpaulo/maestro-provider-redis';
const provider = createRedisProvider({ uri: 'redis://localhost:6379' });
// createElasticsearchProvider({ node, auth })
// createDynamoDbProvider({ region })
// createCassandraProvider({ contactPoints, localDataCenter, keyspace })
// createCouchbaseProvider({ connectionString, username, password, bucket })
// createFirestoreProvider({ projectId })
// createNeo4jProvider({ uri, username, password })4.2 Construir o engine por introspecção
import { createIntrospectedEngine } from '@maykonpaulo/maestro-server';
const engine = await createIntrospectedEngine({
provider,
access: 'full', // 'full' = list/detalhe/criar/editar/excluir/exportar
// 'readonly' = só navegar (sem risco de alterar)
});createIntrospectedEngine descobre toda coleção/tabela e habilita as capacidades de escrita
(o padrão do core seria só-leitura — este helper é o que liga o CRUD).
Opções:
| Opção | Descrição |
|---|---|
| provider | O provider conectado (obrigatório). |
| access | 'full' (padrão) ou 'readonly'. |
| datasourceId | id do datasource (padrão 'main'). |
| policies | Política RBAC (ver §7). Padrão: papel admin com ['*']. |
| overrides | Sobreposição curada (rótulos, campos, RBAC) mesclada sobre o schema descoberto. |
| operations | Operações customizadas (ver §9). |
| audit | Repositório de auditoria (ver §8). |
4.3 Subir o servidor
import { createMaestroServer } from '@maykonpaulo/maestro-server';
const server = createMaestroServer({ engine /*, actorResolver, cors, basePath */ });
await server.listen(3000);
// → REST admin para cada coleção em http://localhost:3000/entities/:colecao4.4 Autenticação
Sem actorResolver, o servidor usa um resolver só-dev que concede um ator admin a toda
requisição. Em produção, sempre passe o seu — ver §6.
4.5 Subir a UI
A UI genérica lê GET /metadata e se auto-monta (navegação, tabela, detalhe, formulários)
para todas as entidades. Rodando como app:
VITE_MAESTRO_API_URL=http://localhost:3000 \
npm --prefix node_modules/@maykonpaulo/maestro-admin run dev
# abra http://localhost:5173Ou embutida no seu app React — ver §11.
4.6 Servidor completo (copy-paste)
// server.ts
import { createMongoProvider } from '@maykonpaulo/maestro-provider-mongodb';
import { createMaestroServer, createIntrospectedEngine } from '@maykonpaulo/maestro-server';
import { InMemoryAuditRepository, type RbacPolicy } from '@maykonpaulo/maestro-core';
const provider = createMongoProvider({ uri: process.env.MONGO_URL!, database: 'app' });
const policies: RbacPolicy = {
roles: {
admin: { id: 'admin', name: 'Admin', permissions: ['*'] },
viewer: { id: 'viewer', name: 'Viewer', permissions: ['entity.*.list', 'entity.*.detail'] },
},
};
const engine = await createIntrospectedEngine({
provider,
access: 'full',
policies,
audit: new InMemoryAuditRepository(), // troque por seu repositório em produção (§8)
});
const server = createMaestroServer({
engine,
actorResolver: async (req) => {
// derive o ator da SUA autenticação (JWT/cookie)
const token = String(req.headers['authorization'] ?? '').replace('Bearer ', '');
const user = await verifyJwt(token); // sua função
return { actor: { id: user.sub, type: 'user', roles: user.roles } };
},
});
await server.listen(3000);
console.log('Admin API em http://localhost:3000');5. Caminho B — Declarativo/curado
Quando quiser rótulos bonitos, RBAC fino, tipos de campo explícitos e formulários controlados, declare as entidades. Você tem controle total; o Maestro monta o resto.
5.1 Declarar entidades
import { type DeclarativeConfig } from '@maykonpaulo/maestro-core';
const declarations: DeclarativeConfig = {
entities: [
{
entity: 'order', // = nome da tabela/coleção
label: 'Pedido',
pluralLabel: 'Pedidos',
fields: {
id: { type: 'string', primary: true },
code: { type: 'string', label: 'Código', required: true, searchable: true, sortable: true },
status: { type: 'enum', label: 'Status',
enumOptions: [
{ value: 'open', label: 'Aberto' },
{ value: 'paid', label: 'Pago' },
{ value: 'canceled', label: 'Cancelado' },
] },
total: { type: 'currency', label: 'Total', sortable: true },
paid: { type: 'boolean', label: 'Pago' },
createdAt: { type: 'datetime', label: 'Criado em', sortable: true },
notes: { type: 'text', label: 'Observações' },
},
// capacidades: liste o que este admin pode fazer nesta entidade
capabilities: { list: true, detail: true, create: true, update: true, delete: true, export: true },
},
],
};5.2 createMaestro + servir
import { createMaestro } from '@maykonpaulo/maestro-core';
import { createMaestroServer } from '@maykonpaulo/maestro-server';
const engine = createMaestro({
datasources: { main: provider }, // o provider do seu banco (§4.1)
declarations,
policies, // RBAC (§7)
audit: myAuditRepository, // (§8)
operations: [cancelOrder], // ações customizadas (§9)
});
await createMaestroServer({ engine, actorResolver }).listen(3000);5.3 A vs B (e híbrido)
- A (introspecção): o mais rápido; toda coleção vira entidade; schema inferido.
- B (declarativo): controle total; ideal para as entidades que os operadores mais usam.
- Híbrido: passe
overrides(config declarativa) paracreateIntrospectedEngine— o Maestro descobre tudo e aplica seus rótulos/regras por cima nas entidades que você curou.
6. Autenticação (actorResolver)
Autenticação é responsabilidade sua. O único ponto onde ela entra no Maestro é o
actorResolver, que converte cada requisição em um Actor:
const server = createMaestroServer({
engine,
actorResolver: async (req) => {
const token = String(req.headers['authorization'] ?? '').replace('Bearer ', '');
const user = await verifyJwt(token); // SUA autenticação
return {
actor: {
id: user.sub,
type: 'user', // 'user' | 'system' | 'job' | 'integration' | 'ai-agent'
name: user.name,
email: user.email,
roles: user.roles, // dão as permissões (§7)
},
correlationId: String(req.headers['x-correlation-id'] ?? ''), // opcional (rastreio na auditoria)
};
},
});- Omitir
actorResolver= rodar sem autenticação (o defaultdevActorResolverconcede admin a todos). Nunca faça isso em produção. - O
Actoré passado a toda operação; o RBAC decide o que ele pode fazer.
7. Autorização (RBAC)
Você define papéis e as permissões de cada um. O ator recebe permissões pelos seus roles.
import { type RbacPolicy } from '@maykonpaulo/maestro-core';
const policies: RbacPolicy = {
roles: {
admin: { id: 'admin', name: 'Admin', permissions: ['*'] },
operator: { id: 'operator', name: 'Operador', permissions: ['entity.order.*', 'operation.order.cancel'] },
viewer: { id: 'viewer', name: 'Leitura', permissions: ['entity.*.list', 'entity.*.detail', 'entity.*.export'] },
},
};Formato das permissões:
| Permissão | Concede |
|---|---|
| entity.<entidade>.<capacidade> | Uma ação em uma entidade. Capacidades: list, detail, create, update, clone, delete, softDelete, restore, export. |
| operation.<opId> | Executar uma operação customizada (§9). |
| entity.order.* | Tudo na entidade order. |
| entity.*.list | list em todas as entidades. |
| * | Tudo. |
Curinga: * (tudo) e prefixo.* (tudo abaixo do prefixo). Um 403 é retornado quando negado — a
UI trata isso como erro na tela, sem quebrar.
Importante: as capacidades (o que a entidade permite) e o RBAC (o que o ator pode) são checados juntos. No modo introspecção
access: 'full', todas as capacidades ficam ligadas; o RBAC é quem restringe por papel.
8. Auditoria
Toda operação emite um evento de auditoria quando você fornece um AuditRepository. Para começar,
use o InMemoryAuditRepository; em produção, implemente o contrato gravando no seu store:
import { type AuditRepository, type AuditEvent } from '@maykonpaulo/maestro-core';
class MyAuditRepository implements AuditRepository {
async record(event: AuditEvent): Promise<void> {
await db.collection('audit').insertOne(event); // grave onde quiser
}
async list(filter) { /* consulte por ator/ação/entidade/período/correlationId */ return []; }
}Cada evento inclui ação, ator, recurso, nível, before/after (em update/delete) e correlationId.
9. Operações customizadas (ações de negócio)
Além do CRUD, exponha ações (ex.: "cancelar pedido", "reenviar e-mail"). Elas aparecem como botões (por linha / em massa / globais) e executam sua lógica com RBAC + auditoria:
import { type OperationDef } from '@maykonpaulo/maestro-core';
const cancelOrder: OperationDef = {
id: 'order.cancel',
label: 'Cancelar pedido',
entity: 'order',
scope: 'record', // 'record' | 'bulk' | 'global'
requiredPermission: 'operation.order.cancel',
execute: async ({ record, input, actor }) => {
// sua lógica de negócio aqui
await paymentGateway.refund(record.id);
return { success: true, message: 'Pedido cancelado.' };
},
};
// registre no engine:
createMaestro({ /* ... */ operations: [cancelOrder] });
// ou createIntrospectedEngine({ /* ... */ operations: [cancelOrder] });Chamada HTTP: POST /operations/order.cancel com { entityId, record, input }.
10. Campos: tipos, sensíveis, soft delete, relações
Tipos de campo (type): string, text, number, integer, decimal, currency,
boolean, date, datetime, time, email, phone, url, document, uuid, enum,
json, relation, array. A UI escolhe o controle certo por tipo (checkbox, número, select,
data, textarea/JSON…).
Flags úteis por campo (no modo declarativo): required, readonly, primary, sensitive,
searchable, sortable, filterable, exportable, enumOptions (para enum), relationEntity
(para relation).
sensitive: true— mascara o valor (***) na exportação CSV e na UI. (Logging/before/aftercontinuam responsabilidade sua.)- Soft delete — configure um campo que marca "inativo" (ex.:
active/deletedAt). As operaçõessoftDelete/restorepassam a existir em vez de exclusão física. - Relações — o Maestro não infere FK em bancos NoSQL; declare relações à mão quando quiser abas de relacionamento no detalhe.
- Campos cifrados (E2EE / zero-knowledge) — se o seu backend cifra campos, eles chegam opacos; a UI os exibe como estão e nunca descriptografa. Comece o admin pelas coleções não cifradas.
11. A UI de admin (maestro-admin)
A UI (React + Vite + Tailwind) lê GET /metadata e monta sozinha: navegação por entidade, tabela
(com busca/ordenação/paginação/filtros vindos do metadata), detalhe, e formulários de criar/editar.
Ela respeita as capacidades (esconde New/Edit/Delete quando desabilitados) e o RBAC (403 vira
erro na tela). Uma tela, todas as entidades.
App standalone
VITE_MAESTRO_API_URL=http://localhost:3000 \
npm --prefix node_modules/@maykonpaulo/maestro-admin run dev # dev server
# ou build estático:
VITE_MAESTRO_API_URL=http://localhost:3000 \
npm --prefix node_modules/@maykonpaulo/maestro-admin run build:appVITE_MAESTRO_ROLE=viewer (ou ?role=viewer na URL) define o header X-Role — útil para testar RBAC.
Embutida no seu app React
import { MaestroAdmin } from '@maykonpaulo/maestro-admin';
import '@maykonpaulo/maestro-admin/src/styles.css';
export function AdminPage() {
return (
<MaestroAdmin
apiUrl="http://localhost:3000"
headers={() => ({ Authorization: `Bearer ${getToken()}` })} // pode ser função (token fresco a cada request)
title="Admin"
/>
);
}Também exportamos os blocos (EntityList, EntityDetail, EntityForm, Sidebar, useMetadata,
useEntityList, MaestroClient, …) para montar um layout próprio.
12. A API HTTP (para frontends próprios)
Se você quiser construir sua própria UI (ou integrar outro sistema), o servidor expõe:
GET /health
GET /metadata # descreve todas as entidades (campos, tipos, capacidades)
GET /metadata/:entidade
GET /entities/:entidade # list — query: page, pageSize, search, searchFields, sort, filter
POST /entities/:entidade # criar
GET /entities/:entidade/:id # detalhe
PATCH /entities/:entidade/:id # editar
DELETE /entities/:entidade/:id # excluir (hard delete)
POST /entities/:entidade/:id/clone
POST /entities/:entidade/:id/soft-delete
POST /entities/:entidade/:id/restore
GET /entities/:entidade/export # ?format=csv|json
POST /operations/:operationId # { entityId, record, input }Query string do list:
| Param | Exemplo | Significado |
|---|---|---|
| page, pageSize | ?page=1&pageSize=20 | Paginação. |
| search, searchFields | ?search=ada&searchFields=name,email | Busca textual. |
| sort | ?sort=name:asc (repetível) | Ordenação. |
| filter | ?filter=status:equals:open (repetível) | Filtro campo:operador:valor. |
Todo endpoint checa RBAC e emite auditoria. O GET /metadata é o contrato para gerar qualquer UI.
13. Referência rápida
Operadores de filtro: equals, notEquals, contains, startsWith, endsWith, in,
notIn, gt, gte, lt, lte, between, isNull, isNotNull, isTrue, isFalse.
Capacidades de entidade: list, detail, create, update, clone, delete, softDelete,
export, bulkActions. Padrão do core: list/detail ligadas, o resto desligado (por segurança).
No modo createIntrospectedEngine({ access: 'full' }), todas ligadas.
Métodos da engine (se você usar o core direto, sem o servidor): list, findById, create,
update, clone, softDelete, restore, hardDelete, export, executeOperation, getMetadata
— todos recebem o actor e checam RBAC + auditoria.
Actor: { id, type: 'user'|'system'|'job'|'integration'|'ai-agent', name?, email?, roles?, permissions?, metadata? }.
14. Produção: checklist e ressalvas
- [ ] Sempre forneça um
actorResolverreal (JWT/cookie). Sem ele = sem auth. - [ ] Defina papéis RBAC de verdade (não deixe todo mundo
admin/['*']). - [ ] Comece com
access: 'readonly'para validar a leitura do banco de produção sem risco; suba para'full'quando confiar. - [ ] Implemente um
AuditRepositoryque persista (não o InMemory). - [ ] Configure CORS conforme seu frontend (
createMaestroServer({ cors: { origin, credentials } })). - [ ] Comece o admin pelas coleções operacionais não cifradas (usuários, configs, etc.). Campos E2EE aparecem opacos.
- [ ] Rode o servidor atrás do seu gateway/HTTPS; o Maestro é só a camada de operação.
15. Solução de problemas
- "Nome da coleção não bate" (MongoDB/Mongoose). No modo introspecção, cada entidade é a coleção
real (ex.:
users). No modo declarativo, oentityda declaração é o nome da coleção — o Mongoose costuma pluralizar (User→users); declare o nome real, ou use oEntitySchemade baixo nível (source.table) para mapear um id amigável a uma coleção diferente. _id(MongoDB). O provider trata_idcomoObjectIdnativo ou string de forma transparente — funciona sobre coleções existentes sem migração.- Campos
currency/decimalaparecem como texto? Use a versão mais recente domaestro-admin. - Banco não coberto pelos 9 providers. Implemente
DatasourceProvider(6 métodos:list,findById,create,update,delete,count) e passe emdatasources: { main: seuProvider }. - Publicou no
@latestpor engano? Auth e canais de release não fazem parte deste guia — o admin em si só precisa dos pacotes instalados e do servidor no ar.
16. Pacotes e versões
Instale pela tag latest (estável):
npm i @maykonpaulo/maestro-core \
@maykonpaulo/maestro-server \
@maykonpaulo/maestro-admin \
@maykonpaulo/maestro-provider-<seu-banco>| Pacote | O que é |
|---|---|
| @maykonpaulo/maestro-core | Engine, contratos, metadata, RBAC, auditoria, operações, export. |
| @maykonpaulo/maestro-server | Servidor HTTP turnkey (createMaestroServer, createIntrospectedEngine). |
| @maykonpaulo/maestro-admin | UI genérica metadata-driven (app + componentes React). |
| @maykonpaulo/maestro-provider-* | Provider do seu banco (9 opções). |
| @maykonpaulo/maestro-cli | CLI opcional (introspect/generate/validate/diff/snapshot). |
Resumo em 6 linhas
const provider = createMongoProvider({ uri, database }); // 1. conecte o banco
const engine = await createIntrospectedEngine({ provider, access: 'full', policies, audit }); // 2. engine
const server = createMaestroServer({ engine, actorResolver }); // 3. auth + servidor
await server.listen(3000); // 4. API no ar
// 5. rode @maykonpaulo/maestro-admin apontando VITE_MAESTRO_API_URL para :3000
// 6. pronto: admin de todo o sistema, com RBAC e auditoriaReferência do pacote @maykonpaulo/maestro-admin
The generic, metadata-driven admin UI for Maestro, per ADR 0008 — Camada Turnkey.
Point it at a running @maykonpaulo/maestro-server and it reads GET /metadata and auto-builds the whole admin: a navigation sidebar, a list table, a detail view and create/update forms for every entity/collection — no per-entity code. It respects each entity's capabilities (a read-only collection shows no New/Edit/Delete) and the server's RBAC (a 403 surfaces as an error, never a broken screen).
Built with React 19 + Vite + Tailwind. It ships two ways:
- a runnable static app (
vite build) you configure with an env var, and - exported React components (
import { MaestroAdmin } from '@maykonpaulo/maestro-admin') to embed in your own app.
It depends only on the Maestro HTTP contract — never on @maykonpaulo/maestro-core — so it stays fully decoupled from the engine.
Run it as a standalone admin
# point it at your maestro-server and start it
VITE_MAESTRO_API_URL=http://localhost:3000 pnpm --filter @maykonpaulo/maestro-admin dev
# or build the static bundle
VITE_MAESTRO_API_URL=http://localhost:3000 pnpm --filter @maykonpaulo/maestro-admin build:appVITE_MAESTRO_API_URL is the base URL of the server (defaults to same-origin). VITE_MAESTRO_ROLE optionally sets an X-Role header — handy against a dev server's actorResolver.
Embed the component
import { MaestroAdmin } from '@maykonpaulo/maestro-admin';
import '@maykonpaulo/maestro-admin/src/styles.css'; // or include the package in your Tailwind content
export function AdminPage() {
return (
<MaestroAdmin
apiUrl="http://localhost:3000"
headers={() => ({ Authorization: `Bearer ${getToken()}` })}
title="Admin"
/>
);
}headers may be a function so a fresh auth token is read on every request. Pass a pre-built client (a MaestroClient) instead of apiUrl/headers for full control (custom fetch, SSR).
Styling
Tailwind v4. Either import the package's source stylesheet (@maykonpaulo/maestro-admin/src/styles.css) or, if you already run Tailwind, add the package to your content so its class names are scanned:
/* your app.css */
@import 'tailwindcss';
@source '../node_modules/@maykonpaulo/maestro-admin/dist';Compose your own layout
Beyond the all-in-one <MaestroAdmin>, the building blocks are exported so you can assemble a custom shell:
- Components:
EntityList,EntityDetail,EntityForm,Sidebar,FieldValue,FieldInput - Hooks:
useMetadata,useEntityList,useEntityRecord,useAsync, andlistFields/detailFields/formFields - Client:
MaestroClient,MaestroApiError,AdminClientProvider,useClient
All of them talk to the server through a single MaestroClient provided via AdminClientProvider.
What it renders from metadata
| Metadata | Drives |
|---|---|
| entities[] | the sidebar navigation |
| field.list.visible / order | table columns |
| field.detail.visible / order | the detail view |
| field.form.creatable / editable / order | the create/update form fields |
| field.type | the right table cell and form control (checkbox, number, select, date, json, …) |
| field.enumOptions | select options |
| field.sensitive | masked values |
| capabilities | which of New / Edit / Delete appear |
Fields the server marks as encrypted (as in a zero-knowledge / E2EE system) come across as opaque values — the admin shows them as-is; it never decrypts.
