@ederzadravec/firestore-migrations
v1.1.0
Published
Sistema de migrations para Firestore - Framework agnostic e reutilizável
Downloads
217
Maintainers
Readme
🔥 Firestore Migrations
Sistema de migrations e seeds para Firestore com zero configuração - Funciona como Prisma, TypeORM, Sequelize
✨ Por que usar?
- ✅ Zero Config - Funciona sem nenhum arquivo de configuração
- ✅ Auto-Discovery - Descobre Firebase e migrations automaticamente
- ✅ CLI Simples - Comandos diretos como
npx firestore-migrations up - ✅ TypeScript First - Suporte completo e types incluídos
- ✅ Rollback - Reverta migrations com segurança
- ✅ Seeds - Popule dados iniciais facilmente
- ✅ Checksum Validation - Detecta mudanças em migrations já executadas
⚠️ Erro "Unexpected token '{'?"
Se você receber este erro ao executar npx firestore-migrations up:
Erro: Unexpected token '{'
error Command failed with exit code 1.Solução rápida:
# Opção 1: Instalar ts-node (desenvolvimento)
npm install --save-dev ts-node
npx firestore-migrations up
# Opção 2: Compilar migrations (produção)
npx tsc migrations/**/*.ts --outDir migrations --module commonjs
npx firestore-migrations up📖 Ver Guia Completo de Solução
🚀 Quick Start (3 minutos)
1️⃣ Instalar
npm install @ederzadravec/firestore-migrations firebase-admin
# ou
yarn add @ederzadravec/firestore-migrations firebase-admin2️⃣ Configurar Firebase (escolha UMA opção)
Opção A: Variável de ambiente (mais fácil)
export GOOGLE_APPLICATION_CREDENTIALS="./serviceAccountKey.json"Opção B: Arquivo de config (mais controle)
Crie firestore-migrations.config.js:
module.exports = {
firebase: {
serviceAccountPath: './serviceAccountKey.json'
}
}Opção C: Application Default Credentials
gcloud auth application-default login3️⃣ Inicializar estrutura
npx firestore-migrations initIsso cria:
seu-projeto/
├── migrations/ ← suas migrations aqui
└── seeds/ ← seus seeds aqui4️⃣ Criar e executar migration
# Criar
npx firestore-migrations create migration add-user-email
# Editar migrations/TIMESTAMP_add-user-email.ts
# Implementar up() e down()
# Executar
npx firestore-migrations up🎯 Uso Completo
Comandos Disponíveis
# CRIAR
npx firestore-migrations create migration <name> # Nova migration
npx firestore-migrations create seed <name> # Novo seed
# EXECUTAR
npx firestore-migrations up # Rodar migrations pendentes
npx firestore-migrations down # Reverter última migration
npx firestore-migrations seed # Popular dados (seeds)
# INFORMAÇÃO
npx firestore-migrations status # Ver status
npx firestore-migrations list # Listar todas migrations⚡ TypeScript vs JavaScript
As migrations são criadas em TypeScript por padrão. Para executá-las em produção, você tem 3 opções:
Opção 1: Usar ts-node (desenvolvimento) ✅ Recomendado para dev
npm install --save-dev ts-node
npx firestore-migrations upOpção 2: Compilar migrations (produção) ✅ Recomendado para produção
# Compile suas migrations
npx tsc migrations/**/*.ts --outDir migrations --module commonjs
# Execute normalmente
npx firestore-migrations upOpção 3: Scripts do package.json
{
"scripts": {
"migrate:build": "tsc migrations/**/*.ts --outDir migrations --module commonjs",
"migrate:up": "npm run migrate:build && firestore-migrations up",
"migrate:up:dev": "ts-node -r tsconfig-paths/register ./node_modules/.bin/firestore-migrations up"
}
}💡 O CLI detecta automaticamente se você tem arquivos
.jsou.tse usa a versão apropriada.
📖 Erro "Unexpected token '{'?" Veja o Guia Completo TypeScript/JavaScript
Scripts no package.json (recomendado)
Adicione ao seu package.json:
{
"scripts": {
"migrate:create": "firestore-migrations create migration",
"migrate:up": "firestore-migrations up",
"migrate:down": "firestore-migrations down",
"migrate:status": "firestore-migrations status",
"seed:create": "firestore-migrations create seed",
"seed:run": "firestore-migrations seed"
}
}Agora use:
yarn migrate:create add-user-role
yarn migrate:up
yarn migrate:status📝 Exemplo de Migration
Arquivo gerado automaticamente em migrations/20231121120000_add_user_email.ts:
import type { IFirestoreAdapter } from '@ederzadravec/firestore-migrations'
export const migration = {
id: '20231121120000_add_user_email',
name: 'add user email',
description: 'Adiciona campo email em todos users',
async up(adapter: IFirestoreAdapter) {
console.log('▶️ Adicionando campo email...')
const users = await adapter.getItems('users')
for (const user of users) {
await adapter.updateItemById('users', user.id, {
email: user.email || `${user.username}@example.com`,
emailVerified: false
})
}
console.log(`✅ ${users.length} usuários atualizados`)
},
async down(adapter: IFirestoreAdapter) {
console.log('◀️ Removendo campo email...')
const users = await adapter.getItems('users')
for (const user of users) {
await adapter.deleteItemProperty('users', user.id, 'email')
await adapter.deleteItemProperty('users', user.id, 'emailVerified')
}
console.log('✅ Rollback completo')
}
}
export default migration🌱 Exemplo de Seed
Arquivo gerado automaticamente em seeds/20231121120000_initial_users.ts:
import type { IFirestoreAdapter } from '@ederzadravec/firestore-migrations'
export const seed = {
id: '20231121120000_initial_users',
name: 'initial users',
description: 'Cria usuários iniciais do sistema',
environments: ['all'], // 'all' para todos ambientes, ou ['development', 'production']
async run(adapter: IFirestoreAdapter) {
console.log('🌱 Criando usuários iniciais...')
await adapter.createItemWithId('users', 'admin', {
name: 'Admin User',
email: '[email protected]',
role: 'admin',
createdAt: new Date()
})
await adapter.createItemWithId('users', 'guest', {
name: 'Guest User',
email: '[email protected]',
role: 'guest',
createdAt: new Date()
})
console.log('✅ 2 usuários criados')
},
async validate() {
// Opcional: validações antes de executar
return true
}
}
export default seed💡 Campo
environments: Use['all']para executar em todos ambientes, ou especifique ambientes como['development'],['production'], ou['development', 'staging'].
⚙️ Configuração (Opcional)
A biblioteca funciona sem configuração, mas você pode customizar criando firestore-migrations.config.js:
module.exports = {
// Pasta de migrations (padrão: './migrations')
migrationsPath: './database/migrations',
// Pasta de seeds (padrão: './seeds')
seedsPath: './database/seeds',
// Collection de controle (padrão: '_migrations')
migrationsCollection: '_migrations',
// Firebase config (opcional - auto-discovery se não fornecido)
firebase: {
// Opção 1: Path para service account
serviceAccountPath: './config/firebase-adminsdk.json',
// Opção 2: Service account object
// serviceAccount: require('./serviceAccountKey.json'),
}
}🔐 Credenciais Firebase
A biblioteca descobre automaticamente suas credenciais Firebase nesta ordem:
- ✅ Se Firebase já está inicializado → usa instância existente
- ✅ Se tem
firebase.serviceAccountPathno config → carrega e usa - ✅ Se tem
GOOGLE_APPLICATION_CREDENTIALSenv var → usa - ✅ Application Default Credentials (gcloud auth)
Você não precisa fazer nada! Apenas garanta que uma dessas opções está configurada.
Exemplo: Service Account
- Baixe service account do Firebase Console
- Salve como
serviceAccountKey.jsonna raiz do projeto - Configure env var:
# .env
GOOGLE_APPLICATION_CREDENTIALS=./serviceAccountKey.json- Adicione ao
.gitignore:
serviceAccountKey.json
.envPronto! A lib vai descobrir e usar automaticamente.
📚 API do Adapter
Todas migrations e seeds recebem um adapter com estes métodos:
// Tipos de filtros suportados
type FirestoreFilterOperator =
| '==' | '!=' | '<' | '<=' | '>' | '>='
| 'array-contains' | 'array-contains-any' | 'in' | 'not-in'
interface IFirestoreFilter {
field: string
operator: FirestoreFilterOperator
value: any
}
interface IQueryOptions {
filters?: IFirestoreFilter[]
limit?: number
orderBy?: { field: string; direction?: 'asc' | 'desc' }
}
interface IFirestoreAdapter {
// Buscar
getItems<T>(collection: string, options?: IQueryOptions): Promise<T[]>
getItemById<T>(collection: string, id: string): Promise<T>
// Criar
createItemWithId(collection: string, id: string, data: any): Promise<string>
// Atualizar
updateItemById(collection: string, id: string, data: any): Promise<void>
updateItems(collection: string, data: any, options: IQueryOptions): Promise<number>
// Deletar
deleteItemById(collection: string, id: string): Promise<any>
deleteItems(collection: string, options: IQueryOptions): Promise<number>
}Exemplos de uso com filtros
// Buscar usuários ativos maiores de 18 anos
const users = await adapter.getItems('users', {
filters: [
{ field: 'status', operator: '==', value: 'active' },
{ field: 'age', operator: '>=', value: 18 }
],
orderBy: { field: 'createdAt', direction: 'desc' },
limit: 100
})
// Atualizar múltiplos documentos de uma vez
const updatedCount = await adapter.updateItems(
'users',
{ notifiedAt: new Date() },
{ filters: [{ field: 'status', operator: '==', value: 'active' }] }
)
console.log(`${updatedCount} usuários atualizados`)
// Deletar documentos expirados
const deletedCount = await adapter.deleteItems('sessions', {
filters: [{ field: 'expiresAt', operator: '<', value: new Date() }]
})
console.log(`${deletedCount} sessões removidas`)🎓 Casos de Uso
Adicionar campo em documentos existentes (bulk)
async up(adapter) {
// Usando updateItems para atualizar em lote (mais eficiente)
const count = await adapter.updateItems(
'users',
{ status: 'active', updatedAt: new Date() },
{ filters: [] } // filtros vazios = todos os documentos
)
console.log(`${count} usuários atualizados`)
}Adicionar campo apenas em documentos específicos
async up(adapter) {
// Atualiza apenas usuários pendentes
const count = await adapter.updateItems(
'users',
{ status: 'active', updatedAt: new Date() },
{ filters: [{ field: 'status', operator: '==', value: 'pending' }] }
)
console.log(`${count} usuários pendentes atualizados`)
}Migrar dados entre collections
async up(adapter) {
const oldUsers = await adapter.getItems('users_old')
for (const user of oldUsers) {
await adapter.createItemWithId('users', user.id, {
name: user.full_name, // rename
email: user.email,
active: user.status === 'enabled' // transform
})
}
}Criar estrutura inicial
async up(adapter) {
await adapter.createItemWithId('settings', 'app', {
version: '1.0.0',
maintenanceMode: false,
features: {
auth: true,
payments: false
}
})
}Limpar dados antigos
async up(adapter) {
// Remove todas as sessões expiradas
const deleted = await adapter.deleteItems('sessions', {
filters: [{ field: 'expiresAt', operator: '<', value: new Date() }]
})
console.log(`${deleted} sessões expiradas removidas`)
}Buscar e processar com filtros complexos
async up(adapter) {
// Busca usuários premium inativos há mais de 30 dias
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const inactiveUsers = await adapter.getItems('users', {
filters: [
{ field: 'plan', operator: '==', value: 'premium' },
{ field: 'lastLogin', operator: '<', value: thirtyDaysAgo }
],
orderBy: { field: 'lastLogin', direction: 'asc' }
})
// Processa cada usuário
for (const user of inactiveUsers) {
await adapter.updateItemById('users', user.id, {
status: 'inactive',
inactiveSince: new Date()
})
}
}🛡️ Segurança
- ✅ Checksum Validation - Detecta alterações em migrations já executadas
- ✅ Transaction Support - Opera\u00e7\u00f5es atômicas quando possível
- ✅ Rollback - Sempre implemente
down()para reverter - ✅ Dry-run - Teste com
validate()antes de executar
🤝 Contribuindo
Contribuições são bem-vindas! Veja CONTRIBUTING.md
Testando Localmente
# Setup rápido
./dev-setup.sh
# Testar comandos
cd test-project
npx ts-node ../cli/index.ts init
npx ts-node ../cli/index.ts create migration test
npx ts-node ../cli/index.ts listVer DEVELOPMENT.md para mais opções de teste.
🧪 Testes
A biblioteca possui uma suíte completa de testes automatizados com Jest:
# Rodar todos os testes
npm test
# Testes com cobertura
npm run test:coverage
# Rodar testes específicos
npm test -- tests/unit/MigrationService.test.ts
# Watch mode para desenvolvimento
npm run test:watchCobertura de Testes
- ✅ 22 testes passando
- ✅ 57% de cobertura geral
- ✅ Testes unitários para
MigrationService(70% cobertura) - ✅ Testes unitários para
MockFirestoreAdapter(100% cobertura) - ✅ Testes de integração para
ConfigLoader - ✅ Mock completo do Firestore para testes rápidos
Ver TESTING.md para guia completo de testes.
📄 Licença
MIT © Eder Zadravec
