UNPKG

@ferjssilva/fast-crud-api

Version:

A complete and fast crud API generator

736 lines (606 loc) 21.7 kB
# Exemplos de Lógica de Negócios com Fast CRUD API Este documento demonstra como adicionar lógica de negócios personalizada aos endpoints gerados pelo `fast-crud-api` sem modificar o código da biblioteca. Utilizamos os recursos nativos do Fastify para estender a funcionalidade. ## Índice 1. [Hooks Globais e Específicos](#hooks-globais-e-específicos) 2. [Autenticação e Autorização](#autenticação-e-autorização) 3. [Recursos User-Scoped](#recursos-user-scoped) 4. [Validação Personalizada](#validação-personalizada) 5. [Processamento de Dados](#processamento-de-dados) 6. [Filtros Avançados](#filtros-avançados) 7. [Exemplo Completo](#exemplo-completo) ## Hooks Globais e Específicos O Fastify permite adicionar hooks que executam lógica antes ou depois de uma requisição ser processada. ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); const User = require('./models/User'); // Hook global para todas as rotas fastify.addHook('preHandler', (request, reply, done) => { console.log(`${request.method} ${request.url} - ${new Date().toISOString()}`); done(); }); // Hook específico para rotas de usuário fastify.register(async (userRoutes) => { userRoutes.addHook('preHandler', (request, reply, done) => { console.log('Acessando rota de usuário'); done(); }); }, { prefix: '/api/users' }); // Registrar o plugin após configurar os hooks fastify.register(fastCrudApi, { prefix: '/api', models: [User] }); ``` ## Autenticação e Autorização Controle de acesso para rotas específicas usando hooks: ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); const User = require('./models/User'); const Post = require('./models/Post'); // Função para verificar token JWT const verifyToken = async (request) => { const token = request.headers.authorization?.split(' ')[1]; // Implemente sua lógica de verificação de token return token ? { id: 'user123', role: 'admin' } : null; }; // Hook de autenticação para todas as rotas fastify.addHook('preHandler', async (request, reply) => { if (request.url.startsWith('/api/admin')) { const user = await verifyToken(request); if (!user) { return reply.code(401).send({ error: 'Unauthorized', message: 'Token de autenticação inválido ou ausente' }); } if (user.role !== 'admin') { return reply.code(403).send({ error: 'Forbidden', message: 'Apenas administradores podem acessar este recurso' }); } // Armazenar usuário autenticado para uso posterior request.user = user; } }); // API pública fastify.register(fastCrudApi, { prefix: '/api', models: [Post], methods: { posts: ['GET'] // Apenas leitura pública } }); // API administrativa protegida fastify.register(async (adminApi) => { adminApi.register(fastCrudApi, { prefix: '', // Já estamos em /api/admin models: [User, Post], methods: { users: ['GET', 'POST', 'PUT', 'DELETE'], posts: ['POST', 'PUT', 'DELETE'] } }); }, { prefix: '/api/admin' }); ``` ## Recursos User-Scoped O `fast-crud-api` oferece suporte nativo para recursos isolados por usuário, onde cada usuário acessa apenas seus próprios dados. Esta funcionalidade é ideal para aplicações multi-tenant. ### Configuração Básica ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); const UserHabit = require('./models/UserHabit'); const UserProfile = require('./models/UserProfile'); // Middleware de autenticação - Define request.userId fastify.addHook('preHandler', async (request, reply) => { const token = request.headers.authorization?.split(' ')[1]; if (!token) { return; // Permitir rotas públicas continuarem } try { // Verificar e decodificar o token (use sua lógica de autenticação) const user = await verifyJWT(token); request.userId = user.id; // Definir userId no request } catch (error) { // Token inválido - não definir userId console.error('Token inválido:', error); } }); // Registrar recursos user-scoped fastify.register(fastCrudApi, { prefix: '/api', models: [UserHabit, UserProfile], userScoped: ['user-habits', 'user-profiles'] // Recursos isolados por usuário }); ``` ### Como Funciona Quando um recurso é marcado como `userScoped`: 1. **Autenticação Obrigatória**: Todas as operações requerem `request.userId` 2. **Filtragem Automática**: - `GET` - Filtra automaticamente por `userId` - `POST` - Injeta `userId` nos novos documentos - `PUT/DELETE` - Verifica propriedade atomicamente 3. **Segurança**: - Usuários não podem acessar dados de outros usuários - Usuários não podem modificar o `userId` dos seus recursos - Verificações atômicas previnem vulnerabilidades TOCTOU ### Exemplo de Modelo User-Scoped ```javascript // models/UserHabit.js const mongoose = require('mongoose'); const userHabitSchema = new mongoose.Schema({ userId: { type: String, required: true, index: true // Importante para performance }, habitId: { type: String, required: true }, frequency: { type: String, enum: ['daily', 'weekly', 'monthly'], default: 'daily' }, completedDates: [Date], createdAt: { type: Date, default: Date.now } }); // Índice composto para queries eficientes userHabitSchema.index({ userId: 1, habitId: 1 }, { unique: true }); module.exports = mongoose.model('UserHabit', userHabitSchema); ``` ### Uso da API ```javascript // Cliente fazendo requisições // 1. Listar hábitos do usuário autenticado // GET /api/user-habits // Authorization: Bearer <token> // Resposta: Apenas hábitos do usuário logado // 2. Criar novo hábito // POST /api/user-habits // Authorization: Bearer <token> // Body: { "habitId": "exercise", "frequency": "daily" } // O userId é injetado automaticamente // 3. Tentar acessar dados de outro usuário (bloqueado) // GET /api/user-habits?userId=outro-usuario-id // Resposta: 403 Forbidden - Cannot access other users' data // 4. Atualizar hábito (apenas se pertencer ao usuário) // PUT /api/user-habits/:id // Authorization: Bearer <token> // Body: { "frequency": "weekly" } // Retorna 404 se o hábito não pertencer ao usuário // 5. Deletar hábito (apenas se pertencer ao usuário) // DELETE /api/user-habits/:id // Authorization: Bearer <token> // Retorna 404 se o hábito não pertencer ao usuário ``` ### Combinando User-Scoped com Outros Recursos ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); // Modelos const User = require('./models/User'); const Post = require('./models/Post'); // Público const UserHabit = require('./models/UserHabit'); // User-scoped const UserProfile = require('./models/UserProfile'); // User-scoped // Middleware de autenticação fastify.addHook('preHandler', async (request, reply) => { const token = request.headers.authorization?.split(' ')[1]; if (token) { try { const user = await verifyJWT(token); request.userId = user.id; request.user = user; } catch (error) { // Token inválido } } }); // Recursos públicos (sem user-scoped) fastify.register(fastCrudApi, { prefix: '/api/public', models: [Post], methods: { posts: ['GET'] // Apenas leitura pública } }); // Recursos user-scoped (requerem autenticação) fastify.register(fastCrudApi, { prefix: '/api/user', models: [UserHabit, UserProfile], userScoped: ['user-habits', 'user-profiles'], methods: { 'user-habits': ['GET', 'POST', 'PUT', 'DELETE'], 'user-profiles': ['GET', 'PUT'] // Sem POST/DELETE } }); // Recursos administrativos (requer role de admin) fastify.register(async (adminApi) => { // Hook adicional para verificar role de admin adminApi.addHook('preHandler', async (request, reply) => { if (!request.user || request.user.role !== 'admin') { return reply.code(403).send({ error: 'Forbidden', message: 'Acesso restrito a administradores' }); } }); adminApi.register(fastCrudApi, { prefix: '', models: [User, Post], methods: { users: ['GET', 'POST', 'PUT', 'DELETE'], posts: ['GET', 'POST', 'PUT', 'DELETE'] } }); }, { prefix: '/api/admin' }); ``` ### Boas Práticas 1. **Índices no Banco de Dados**: ```javascript // Sempre adicione índice no campo userId userHabitSchema.index({ userId: 1 }); // Considere índices compostos para queries comuns userHabitSchema.index({ userId: 1, createdAt: -1 }); ``` 2. **Middleware de Autenticação Robusto**: ```javascript fastify.addHook('preHandler', async (request, reply) => { const token = request.headers.authorization?.split(' ')[1]; if (!token) { return; // Permitir rotas públicas } try { const decoded = await verifyJWT(token); // Validar se o usuário ainda existe const user = await User.findById(decoded.id); if (!user) { throw new Error('Usuário não encontrado'); } // Verificar se o usuário está ativo if (!user.isActive) { throw new Error('Usuário inativo'); } request.userId = user._id.toString(); request.user = user; } catch (error) { // Log do erro mas não bloquear rotas públicas console.error('Erro de autenticação:', error.message); } }); ``` 3. **Modelo de Dados Consistente**: ```javascript // Sempre use o mesmo tipo para userId (String ou ObjectId) const schema = new mongoose.Schema({ userId: { type: String, // ou mongoose.Schema.Types.ObjectId required: true, index: true }, // outros campos... }); ``` 4. **Tratamento de Erros**: ```javascript // Os recursos user-scoped retornam: // - 401 Unauthorized: quando request.userId não está definido // - 403 Forbidden: quando tenta acessar dados de outro usuário // - 404 Not Found: quando tenta modificar/deletar recurso de outro usuário // (para não vazar informação sobre existência do recurso) ``` ### Cenários Avançados **Compartilhamento de Recursos entre Usuários**: ```javascript // Para recursos que podem ser compartilhados, // não use user-scoped. Em vez disso, use hooks personalizados: fastify.addHook('preHandler', async (request, reply) => { if (request.method === 'GET' && request.url.includes('/api/shared-docs')) { const docId = request.params.id; // Verificar se o usuário tem acesso ao documento const doc = await SharedDoc.findOne({ _id: docId, $or: [ { ownerId: request.userId }, { sharedWith: request.userId } ] }); if (!doc) { return reply.code(404).send({ error: 'NotFound', message: 'Documento não encontrado' }); } } }); ``` ## Validação Personalizada Adicione validação avançada além dos validadores do Mongoose: ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); const User = require('./models/User'); // Funções de validação personalizada const validateUser = (userData) => { const errors = []; // Validar formato de email if (userData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) { errors.push('Formato de email inválido'); } // Validar complexidade de senha if (userData.password && (userData.password.length < 8 || !/[A-Z]/.test(userData.password) || !/[0-9]/.test(userData.password))) { errors.push('A senha deve ter pelo menos 8 caracteres, uma letra maiúscula e um número'); } return { valid: errors.length === 0, errors }; }; // Hook para validar dados de usuário antes de salvar fastify.addHook('preHandler', (request, reply, done) => { if ((request.method === 'POST' || request.method === 'PUT') && request.url.includes('/api/users')) { const validation = validateUser(request.body); if (!validation.valid) { return reply.code(400).send({ error: 'ValidationError', message: 'Erro de validação', details: validation.errors }); } } done(); }); // Registrar o plugin após configurar validação fastify.register(fastCrudApi, { prefix: '/api', models: [User] }); ``` ## Processamento de Dados Manipulação de dados antes ou depois de operações CRUD: ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); const User = require('./models/User'); const Post = require('./models/Post'); const bcrypt = require('bcrypt'); // Pre-processamento - Hash de senha antes de salvar fastify.addHook('preHandler', async (request, reply) => { if ((request.method === 'POST' || request.method === 'PUT') && request.url.includes('/api/users') && request.body.password) { // Hash de senha request.body.password = await bcrypt.hash(request.body.password, 10); } }); // Pós-processamento - Limpeza de dados sensíveis fastify.addHook('onSend', (request, reply, payload, done) => { if (request.url.includes('/api/users')) { try { const data = JSON.parse(payload); // Remover campos sensíveis da resposta if (data.password) delete data.password; if (data.secretKey) delete data.secretKey; done(null, JSON.stringify(data)); } catch (err) { done(null, payload); } } else { done(); } }); // Registrar o plugin fastify.register(fastCrudApi, { prefix: '/api', models: [User, Post] }); ``` ## Filtros Avançados Adicione lógica de filtragem personalizada além dos filtros básicos: ```javascript const fastify = require('fastify')(); const fastCrudApi = require('@ferjssilva/fast-crud-api'); const User = require('./models/User'); const Post = require('./models/Post'); // Hooks para manipulação de parâmetros de consulta fastify.addHook('preHandler', (request, reply, done) => { if (request.method === 'GET' && request.url.startsWith('/api/posts')) { // Filtro por intervalo de datas if (request.query.dateFrom || request.query.dateTo) { request.query.createdAt = {}; if (request.query.dateFrom) { request.query.createdAt.$gte = new Date(request.query.dateFrom); delete request.query.dateFrom; } if (request.query.dateTo) { request.query.createdAt.$lte = new Date(request.query.dateTo); delete request.query.dateTo; } } // Filtro por status com valores personalizados if (request.query.status === 'active') { request.query.isPublished = true; request.query.isArchived = false; delete request.query.status; } else if (request.query.status === 'archived') { request.query.isArchived = true; delete request.query.status; } } done(); }); // Registrar o plugin fastify.register(fastCrudApi, { prefix: '/api', models: [User, Post] }); ``` ## Exemplo Completo Um exemplo completo integrando várias técnicas: ```javascript const fastify = require('fastify')(); const mongoose = require('mongoose'); const fastCrudApi = require('@ferjssilva/fast-crud-api'); // Modelos const User = require('./models/User'); const Post = require('./models/Post'); const Order = require('./models/Order'); const UserHabit = require('./models/UserHabit'); const UserProfile = require('./models/UserProfile'); // Utilitários const { verifyToken, isAdmin } = require('./utils/auth'); const { validateUser, validatePost } = require('./utils/validators'); const { sanitizeData, enrichData } = require('./utils/dataProcessors'); const { applyBusinessRules } = require('./utils/businessRules'); // Registrar decoradores para lógica de negócios fastify.decorateRequest('applyBusinessRules', applyBusinessRules); // 1. Hook global para logging e rastreamento fastify.addHook('preHandler', (request, reply, done) => { request.requestId = Date.now().toString(); console.log(`[${request.requestId}] ${request.method} ${request.url}`); // Adicionar headers de rastreamento reply.header('X-Request-ID', request.requestId); done(); }); // 2. Hook de autenticação fastify.addHook('preHandler', async (request, reply) => { // Ignorar rotas públicas if (request.url.match(/^\/api\/(login|register|public)/)) { return; } try { // Verificar token de autenticação const user = await verifyToken(request); if (!user) { return reply.code(401).send({ error: 'Unauthorized', message: 'Autenticação necessária' }); } // Verificar permissões para rotas administrativas if (request.url.includes('/api/admin') && !isAdmin(user)) { return reply.code(403).send({ error: 'Forbidden', message: 'Acesso negado' }); } // Armazenar usuário no contexto request.user = user; } catch (error) { return reply.code(401).send({ error: 'AuthError', message: error.message }); } }); // 3. Validação personalizada para cada modelo fastify.addHook('preHandler', (request, reply, done) => { if (request.method !== 'POST' && request.method !== 'PUT') { return done(); } let validation = { valid: true }; // Selecionar validador com base na URL if (request.url.includes('/api/users')) { validation = validateUser(request.body); } else if (request.url.includes('/api/posts')) { validation = validatePost(request.body); } if (!validation.valid) { return reply.code(400).send({ error: 'ValidationError', message: 'Dados inválidos', details: validation.errors }); } done(); }); // 4. Pré-processamento de dados fastify.addHook('preHandler', (request, reply, done) => { if (request.method === 'POST' || request.method === 'PUT') { // Enriquecer dados com metadados enrichData(request.body, request.user); // Aplicar regras de negócios específicas request.applyBusinessRules(request.body, request.url); } done(); }); // 5. Pós-processamento de resposta fastify.addHook('onSend', (request, reply, payload, done) => { if (payload) { try { const data = JSON.parse(payload); // Sanitizar dados sensíveis const sanitized = sanitizeData(data, request.url); done(null, JSON.stringify(sanitized)); } catch (err) { done(null, payload); } } else { done(null, payload); } }); // Rotas públicas - sem autenticação fastify.register(async (publicApi) => { publicApi.register(fastCrudApi, { prefix: '', // Já estamos em /api/public models: [User, Post], methods: { users: ['POST'], // Apenas para registro posts: ['GET'] // Leitura pública } }); }, { prefix: '/api/public' }); // API principal - requer autenticação fastify.register(fastCrudApi, { prefix: '/api', models: [User, Post, Order], methods: { users: ['GET', 'PUT'], // Usuários podem ler e atualizar seus dados posts: ['GET', 'POST', 'PUT', 'DELETE'], orders: ['GET', 'POST', 'PUT'] } }); // Recursos user-scoped - isolamento automático por usuário fastify.register(fastCrudApi, { prefix: '/api/user', models: [UserHabit, UserProfile], userScoped: ['user-habits', 'user-profiles'], // Isolamento automático methods: { 'user-habits': ['GET', 'POST', 'PUT', 'DELETE'], 'user-profiles': ['GET', 'PUT'] } }); // API administrativa - requer permissões de admin fastify.register(async (adminApi) => { adminApi.register(fastCrudApi, { prefix: '', // Já estamos em /api/admin models: [User, Post, Order], methods: { users: ['GET', 'POST', 'PUT', 'DELETE'], posts: ['GET', 'POST', 'PUT', 'DELETE'], orders: ['GET', 'POST', 'PUT', 'DELETE'] } }); }, { prefix: '/api/admin' }); // Iniciar servidor fastify.listen(3000, (err) => { if (err) throw err; console.log('Servidor rodando na porta 3000'); }); ``` Este exemplo demonstra: 1. **Logging e rastreamento** para todas as requisições 2. **Autenticação e autorização** com rotas protegidas 3. **Isolamento automático por usuário** com recursos user-scoped 4. **Validação personalizada** para diferentes modelos 5. **Processamento de dados** antes e depois das operações CRUD 6. **Regras de negócios** aplicadas via decoradores 7. **Múltiplos contextos de API** (público, autenticado, user-scoped, administrativo) Ao usar estas técnicas, você pode estender significativamente as funcionalidades do `fast-crud-api` sem modificar seu código-fonte, mantendo a separação de responsabilidades e facilitando a manutenção da sua aplicação.