@attrx/core
v0.1.0
Published
Semantic attribute metadata system - schema, validation, presentation, and transformations for data keys
Downloads
14
Maintainers
Readme
@attrx/core
Sistema de atributos semânticos para chaves de dados.
Transforme chaves JSON em metadados ricos com tipo, semântica, validação e apresentação.
Instalação
pnpm add @attrx/core @attrx/role-morphicPor que @attrx/core?
// Sem attrx: só sei que é número
const data = { price: 99.90 };
// Com attrx: sei que é DINHEIRO em BRL
const attrs = new AttrCollection([
{ key: 'price', type: 'number', role: 'currency:brl', label: 'Preço' }
]);O que você ganha:
- Semântica:
type+role= significado completo - Slots: organização hierárquica (pictures, edges, address fields)
- 4 Pilares: cast, validate, convert, format
- State Tracking: dirty, pristine, commit, reset (por slot)
- Eventos: reatividade para UI
- JSON Schema: validação automática
Quick Start
import { AttrCollection } from '@attrx/core';
import { RoleMorphic, areaRole, colorRole } from '@attrx/role-morphic';
// Setup RoleMorphic
const morph = new RoleMorphic();
morph.register('area', areaRole.toSpec());
morph.register('color', colorRole.toSpec());
// Cria collection
const attrs = new AttrCollection([
{ key: 'name', type: 'string', label: 'Nome' },
{ key: 'area', type: 'number', role: 'area:hectare' },
{ key: 'color', type: 'string', role: 'color:hex' },
], morph);
// Define valores
attrs.setValue('name', 'Fazenda Sol');
attrs.setValue('area', 100);
attrs.setValue('color', '#ff0000');
// 4 Pilares
attrs.cast('area', '150 ha'); // Parse: "150 ha" → 150
attrs.validate('area'); // { valid: true, errors: [] }
attrs.convert('color', 'color:rgb_object'); // "#ff0000" → { r: 255, g: 0, b: 0 }
attrs.format('area'); // "100 ha"API
CRUD
// Criar
attrs.add({ key: 'email', type: 'string', role: 'email' });
// Ler
attrs.get('email'); // Attribute | undefined
attrs.getOrThrow('email'); // Attribute (throws se não existe)
attrs.has('email'); // boolean
attrs.getAll(); // Attribute[] (só slot '#' por padrão)
attrs.getAll({ slot: '*' }); // Attribute[] (todos os slots)
attrs.keys(); // string[]
attrs.length; // number
// Atualizar
attrs.update('email', { label: 'E-mail corporativo' });
// Deletar (virtual até commit)
attrs.remove('email');
attrs.remove('fotos', { cascade: true }); // Remove filhos tambémValues
// Individual
attrs.setValue('name', 'João');
attrs.getValue('name'); // 'João'
// Múltiplos
attrs.setValues({ name: 'João', age: 30 });
attrs.getValues(); // { name: 'João', age: 30 }4 Pilares
// CAST - Normaliza input sujo
attrs.cast('area', '100 hectares'); // → 100 (number)
attrs.cast('area', '100 ha'); // → 100
attrs.tryCast('area', 'invalid'); // { ok: false, error: '...' }
// VALIDATE - Verifica regras
attrs.validate('area'); // { valid: true, errors: [] }
attrs.validate('area', -5); // { valid: false, errors: ['...'] }
attrs.isValid('area'); // boolean
attrs.isAllValid(); // boolean
// CONVERT - Transforma variantes
attrs.convert('color', 'color:rgb_object'); // { r: 255, g: 0, b: 0 }
attrs.convertVariant('color', 'hsl_object'); // { h: 0, s: 100, l: 50 }
attrs.getConvertibleVariants('color'); // ['color:rgb_object', ...]
// FORMAT - Apresentação
attrs.format('area'); // "100 ha"
attrs.format('area', { verbose: true }); // "100 hectares"
attrs.format('area', { locale: 'pt-BR' }); // "1.000 ha"State Tracking
// Status de campos
attrs.getStatus('name'); // 'pristine' | 'dirty' | 'added' | 'removed'
attrs.getState('name'); // { status, original? }
// Checks (suporta slots)
attrs.isDirty(); // Algum campo alterado no slot '#'?
attrs.isDirty('name'); // Este campo alterado?
attrs.isDirty({ slot: 'pictures' }); // Slot pictures alterado?
attrs.isDirty({ slot: '*' }); // Qualquer slot alterado?
attrs.isPristine(); // Todos pristine?
attrs.isClean(); // Sem mudanças pendentes?
attrs.hasChanges({ slot: '*' }); // Mudanças em qualquer slot?
// Checks específicos
attrs.isAdded('newField'); // Foi adicionado?
attrs.isRemoved('oldField'); // Foi removido?
// Listas (suporta slots)
attrs.getDirtyKeys(); // Alterados no slot '#'
attrs.getDirtyKeys({ slot: 'pictures' }); // Alterados no slot pictures
attrs.getDirtyKeys({ slot: '*' }); // Alterados em qualquer slot
attrs.getModifiedKeys(); // Só dirty (não added/removed)
attrs.getAddedKeys(); // Só added
attrs.getRemovedKeys(); // Só removed
attrs.getPristineKeys(); // Não alteradosCommit & Reset
// Edita
attrs.setValue('name', 'João');
attrs.add({ key: 'phone', type: 'string' });
attrs.remove('oldField');
// Commit: salva como novo baseline
attrs.commit();
// - dirty → pristine
// - added → pristine (persiste)
// - removed → deletado de verdade
// Commit por slot
attrs.commit({ slot: 'pictures' }); // Só slot pictures
attrs.commit({ slot: '*' }); // Todos os slots
// Edita novamente
attrs.setValue('name', 'Maria');
// Reset: volta ao último commit
attrs.reset();
attrs.getValue('name'); // 'João'
// Reset parcial
attrs.reset('name'); // Só este campo
attrs.reset(['name', 'age']); // Só estes campos
attrs.reset({ slot: 'pictures' }); // Só slot picturesBatch Operations
// Agrupa operações (1 evento no final)
attrs.beginBatch();
attrs.setValue('a', 1);
attrs.setValue('b', 2);
attrs.setValue('c', 3);
attrs.endBatch(); // Emite batchEnd
// Ou com transaction
attrs.transaction(() => {
attrs.setValue('a', 1);
attrs.setValue('b', 2);
attrs.setValue('c', 3);
});Snapshot
// Salva estado completo
const snapshot = attrs.toSnapshot();
// Faz alterações
attrs.setValue('name', 'Outro');
attrs.add({ key: 'temp', type: 'string' });
// Restaura
attrs.restore(snapshot);Eventos
attrs.on('valueChanged', ({ key, value, previous }) => {
console.log(`${key}: ${previous} → ${value}`);
});
attrs.on('stateChanged', ({ key, status, previous }) => {
console.log(`${key}: ${previous} → ${status}`);
});
attrs.on('committed', ({ keys }) => {
console.log('Committed:', keys);
// Salvar no backend...
});
attrs.on('reset', ({ keys }) => {
console.log('Reset:', keys);
});
// Todos os eventos
// added, updated, removed, valueChanged, converted,
// stateChanged, committed, reset, batchStart, batchEndFilter & Find
attrs.filter(a => a.type === 'number'); // Attribute[]
attrs.find(a => a.key === 'name'); // Attribute | undefined
attrs.filterByType('number'); // Attribute[]
attrs.filterByRole('area'); // Attribute[]Slots
Slots são namespaces lógicos para organizar atributos hierarquicamente:
const property = new AttrCollection([
// Root (#) - dados principais
{ key: 'titulo', type: 'string', value: 'Apartamento Centro' },
{ key: 'preco', type: 'number', role: 'currency', value: 450000 },
// Parent com children (aponta para slot)
{ key: 'fotos', type: 'array', children: 'pictures' },
// Slot pictures (filho de 'fotos')
{ key: 'pictures:1', slot: 'pictures', type: 'object', value: { url: '/sala.jpg' } },
{ key: 'pictures:2', slot: 'pictures', type: 'object', value: { url: '/quarto.jpg' } },
// Slot orphan (sem parent) - dados auxiliares
{ key: 'edges:a->b', slot: 'edges', type: 'object', value: { from: 'a', to: 'b' } },
]);
// Query por slot
property.getAll(); // só '#' (root) - 3 items
property.getAll({ slot: '*' }); // todos - 6 items
property.getSlot('pictures'); // 2 items
// Children API
property.getChildren('fotos'); // [pictures:1, pictures:2]
property.getChildrenCount('fotos'); // 2
property.hasChildren('fotos'); // true
// Slot metadata
property.getSlots(); // ['#', 'pictures', 'edges']
property.getSlotParent('pictures'); // 'fotos'
property.getOrphanSlots(); // ['#', 'edges']
// Collection helpers (auto-ID)
const key = property.addToSlot('pictures', {
type: 'object',
value: { url: '/cozinha.jpg' }
});
// key = 'pictures:pic_a7f3b2x9'
// Reorder
property.reorderSlot('pictures', ['pictures:2', 'pictures:1']);
// Move entre slots
property.moveToSlot('pictures:1', 'archived');
// Clear slot inteiro
property.clearSlot('pictures');
// Remove com cascade (deleta children)
property.remove('fotos', { cascade: true });Convenções de key:
- Collection items:
slot:id→pictures:a7f3b2 - Structured fields:
parent.field→address.street
JSON Schema
import { toJsonSchema } from '@attrx/core';
const schema = toJsonSchema(attrs.getAll(), morph);
// {
// $schema: "http://json-schema.org/draft-07/schema#",
// type: "object",
// properties: {
// name: { type: "string" },
// area: { type: "number", minimum: 0 }
// },
// required: ["name"]
// }Form Workflow
// 1. Load
const attrs = new AttrCollection(dataFromApi, morph);
// Todos começam pristine
// 2. User edits
attrs.setValue('name', 'João');
attrs.setValue('age', 30);
// → dirty, eventos emitidos
// 3. Validate on blur
attrs.validate('age');
// { valid: true, errors: [] }
// 4. Check before submit
if (!attrs.isAllValid()) {
// Show errors
return;
}
// 5. Save
await api.save(attrs.getValues());
attrs.commit();
// → pristine, novo baseline
// 6. Cancel (em outro momento)
attrs.reset();
// → volta ao commitAdd/Remove Workflow
// Virtual delete
attrs.remove('email');
attrs.has('email'); // false (não aparece)
attrs.isRemoved('email'); // true (ainda existe internamente)
// Cancelar remoção
attrs.reset('email');
attrs.has('email'); // true (restaurado)
// Confirmar remoção
attrs.remove('email');
attrs.commit();
attrs.has('email'); // false (deletado de verdade)Types
import type {
Attribute,
AttributeInput,
AttributeType,
FieldState,
FieldStatus,
AttrEvents,
ValidationResult,
SchemaProps,
SlotOptions,
SlotInfo,
} from '@attrx/core';
import { DEFAULT_SLOT } from '@attrx/core'; // '#'Integração com React
function useAttrValue(attrs: AttrCollection, key: string) {
const [value, setValue] = useState(attrs.getValue(key));
useEffect(() => {
const handler = ({ key: k, value }: { key: string; value: unknown }) => {
if (k === key) setValue(value);
};
attrs.on('valueChanged', handler);
return () => attrs.off('valueChanged', handler);
}, [attrs, key]);
return value;
}
function useAttrStatus(attrs: AttrCollection, key: string) {
const [status, setStatus] = useState(attrs.getStatus(key));
useEffect(() => {
const handler = ({ key: k, status }: { key: string; status: FieldStatus }) => {
if (k === key) setStatus(status);
};
attrs.on('stateChanged', handler);
return () => attrs.off('stateChanged', handler);
}, [attrs, key]);
return status;
}Performance
- O(1) para operações por key (setValue, getStatus, etc)
- O(1) para lookup de slot (índice interno
bySlot) - O(n do slot) para listar attrs de um slot (não O(n total))
- O(dirty) para listas de alterados (getDirtyKeys, commit, reset)
- Lazy original: só guarda snapshot se campo foi alterado
- Batch: agrupa eventos para evitar re-renders
Métricas
- 192 testes passando
- ~28KB bundle size
- 0 dependências runtime extras
Licença
MIT
