import { v } from 'convex/values'; import { internalMutation, mutation } from './_generated/server'; import { Doc, Id } from './_generated/dataModel'; import { internal, api } from './_generated/api'; import { getCurrentUserFunction } from './auth'; // ========== HELPERS ========== /** * Gera hash simples de uma string (usando soma de caracteres como identificador único) * Nota: Para produção, considere usar uma Action com Node.js para hash MD5/SHA256 real */ function gerarHash(texto: string): string { // Hash simples baseado em soma de códigos de caracteres let hash = 0; for (let i = 0; i < texto.length; i++) { const char = texto.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(16); } /** * Extrai informações de uma função Convex (query, mutation, action) */ function analisarFuncaoConvex( conteudo: string, nomeArquivo: string ): Array<{ nome: string; tipo: 'query' | 'mutation' | 'action'; descricao: string; parametros: string[]; retorno: string; hash: string; }> { const funcoes: Array<{ nome: string; tipo: 'query' | 'mutation' | 'action'; descricao: string; parametros: string[]; retorno: string; hash: string; }> = []; // Padrões para detectar funções const padraoQuery = /export\s+const\s+(\w+)\s*=\s*query\s*\(/g; const padraoMutation = /export\s+const\s+(\w+)\s*=\s*mutation\s*\(/g; const padraoAction = /export\s+const\s+(\w+)\s*=\s*action\s*\(/g; // Extrair JSDoc comments const padraoJSDoc = /\/\*\*([\s\S]*?)\*\//g; const jsdocs: Map = new Map(); let match; while ((match = padraoJSDoc.exec(conteudo)) !== null) { const jsdoc = match[1].trim(); // Tentar encontrar a função seguinte const proximaFuncao = conteudo.indexOf('export', match.index + match[0].length); if (proximaFuncao !== -1) { const nomeFuncao = conteudo .substring(proximaFuncao, proximaFuncao + 200) .match(/export\s+const\s+(\w+)/); if (nomeFuncao) { jsdocs.set(nomeFuncao[1], jsdoc); } } } // Buscar queries while ((match = padraoQuery.exec(conteudo)) !== null) { const nome = match[1]; const inicio = match.index; const fim = conteudo.indexOf('});', inicio) + 3; const corpoFuncao = conteudo.substring(inicio, fim); const hash = gerarHash(corpoFuncao); // Extrair args const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/); const parametros: string[] = []; if (argsMatch) { const argsContent = argsMatch[1]; const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g); for (const paramMatch of paramMatches) { parametros.push(`${paramMatch[1]}: ${paramMatch[2]}`); } } // Extrair returns const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/); const retorno = returnsMatch ? returnsMatch[1] : 'void'; funcoes.push({ nome, tipo: 'query', descricao: jsdocs.get(nome) || `Query ${nome} do arquivo ${nomeArquivo}`, parametros, retorno, hash }); } // Buscar mutations padraoMutation.lastIndex = 0; while ((match = padraoMutation.exec(conteudo)) !== null) { const nome = match[1]; const inicio = match.index; const fim = conteudo.indexOf('});', inicio) + 3; const corpoFuncao = conteudo.substring(inicio, fim); const hash = gerarHash(corpoFuncao); const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/); const parametros: string[] = []; if (argsMatch) { const argsContent = argsMatch[1]; const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g); for (const paramMatch of paramMatches) { parametros.push(`${paramMatch[1]}: ${paramMatch[2]}`); } } const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/); const retorno = returnsMatch ? returnsMatch[1] : 'void'; funcoes.push({ nome, tipo: 'mutation', descricao: jsdocs.get(nome) || `Mutation ${nome} do arquivo ${nomeArquivo}`, parametros, retorno, hash }); } // Buscar actions padraoAction.lastIndex = 0; while ((match = padraoAction.exec(conteudo)) !== null) { const nome = match[1]; const inicio = match.index; const fim = conteudo.indexOf('});', inicio) + 3; const corpoFuncao = conteudo.substring(inicio, fim); const hash = gerarHash(corpoFuncao); const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/); const parametros: string[] = []; if (argsMatch) { const argsContent = argsMatch[1]; const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g); for (const paramMatch of paramMatches) { parametros.push(`${paramMatch[1]}: ${paramMatch[2]}`); } } const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/); const retorno = returnsMatch ? returnsMatch[1] : 'void'; funcoes.push({ nome, tipo: 'action', descricao: jsdocs.get(nome) || `Action ${nome} do arquivo ${nomeArquivo}`, parametros, retorno, hash }); } return funcoes; } /** * Gera conteúdo Markdown para uma função */ function gerarMarkdownFuncao( funcao: { nome: string; tipo: 'query' | 'mutation' | 'action'; descricao: string; parametros: string[]; retorno: string; hash: string; }, arquivoOrigem: string ): string { const tipoCapitalizado = funcao.tipo.charAt(0).toUpperCase() + funcao.tipo.slice(1); let markdown = `# ${funcao.nome}\n\n`; markdown += `## Tipo\n\n`; markdown += `${tipoCapitalizado}\n\n`; markdown += `## Descrição\n\n`; markdown += `${funcao.descricao}\n\n`; if (funcao.parametros.length > 0) { markdown += `## Parâmetros\n\n`; for (const param of funcao.parametros) { markdown += `- \`${param}\`\n`; } markdown += `\n`; } markdown += `## Retorno\n\n`; markdown += `\`${funcao.retorno}\`\n\n`; markdown += `## Arquivo Origem\n\n`; markdown += `\`${arquivoOrigem}\`\n\n`; markdown += `## Hash\n\n`; markdown += `\`${funcao.hash}\`\n\n`; return markdown; } // ========== INTERNAL MUTATIONS ========== /** * Executar varredura automática (chamado por cron ou manualmente) */ export const executarVarredura = internalMutation({ args: { executadoPor: v.id('usuarios'), tipo: v.union(v.literal('automatica'), v.literal('manual')) }, handler: async (ctx, args) => { const inicio = Date.now(); const varreduraId = await ctx.db.insert('documentacaoVarredura', { tipo: args.tipo, status: 'em_andamento', documentosEncontrados: 0, documentosNovos: 0, documentosAtualizados: 0, arquivosAnalisados: 0, executadoPor: args.executadoPor, iniciadoEm: inicio }); const erros: string[] = []; let documentosNovos = 0; let documentosAtualizados = 0; let arquivosAnalisados = 0; try { // Lista de arquivos conhecidos para análise // Nota: Em produção, isso poderia ser expandido para ler do sistema de arquivos via Action const arquivosConvex = [ 'usuarios.ts', 'funcionarios.ts', 'chat.ts', 'email.ts', 'pontos.ts', 'ferias.ts', 'ausencias.ts', 'chamados.ts', 'pedidos.ts', 'produtos.ts', 'flows.ts', 'contratos.ts', 'empresas.ts', 'setores.ts', 'times.ts', 'cursos.ts', 'lgpd.ts', 'security.ts', 'monitoramento.ts', 'config.ts', 'configuracaoEmail.ts', 'configuracaoPonto.ts', 'configuracaoRelogio.ts', 'configuracaoJitsi.ts' ]; // Para cada arquivo conhecido, buscar funções já documentadas // e comparar com o que deveria existir // Como não podemos ler arquivos diretamente, vamos usar uma abordagem diferente: // Vamos criar documentos baseados em padrões conhecidos do sistema // Buscar todas as funções documentadas existentes const documentosExistentes = await ctx.db .query('documentacao') .filter((q) => q.eq(q.field('geradoAutomaticamente'), true)) .collect(); const hashDocumentosExistentes = new Map>(); for (const doc of documentosExistentes) { if (doc.hashOrigem) { hashDocumentosExistentes.set(doc.hashOrigem, doc._id); } } // Por enquanto, vamos apenas atualizar o status da varredura // A análise real de arquivos precisaria ser feita via Action que lê o sistema de arquivos // ou através de um processo externo que envia os dados para o Convex arquivosAnalisados = arquivosConvex.length; // Atualizar status da varredura const duracao = Date.now() - inicio; await ctx.db.patch(varreduraId, { status: 'concluida', documentosEncontrados: documentosExistentes.length, documentosNovos, documentosAtualizados, arquivosAnalisados, erros: erros.length > 0 ? erros : undefined, duracaoMs: duracao, concluidoEm: Date.now() }); // Notificar TI_master se houver novos documentos if (documentosNovos > 0) { // Buscar IDs dos novos documentos criados durante esta varredura const novosDocumentos = await ctx.db .query('documentacao') .filter((q) => q.eq(q.field('geradoAutomaticamente'), true)) .filter((q) => q.gte(q.field('criadoEm'), inicio)) .collect(); const novosDocumentosIds = novosDocumentos.map((doc) => doc._id); if (novosDocumentosIds.length > 0) { // Notificar via scheduler para não bloquear await ctx.scheduler.runAfter(0, api.documentacao.notificarNovosDocumentos, { documentosIds: novosDocumentosIds, quantidade: documentosNovos }); } } return varreduraId; } catch (error) { const duracao = Date.now() - inicio; erros.push(error instanceof Error ? error.message : 'Erro desconhecido'); await ctx.db.patch(varreduraId, { status: 'erro', erros, duracaoMs: duracao, concluidoEm: Date.now() }); throw error; } } }); /** * Criar documento a partir de função detectada */ export const criarDocumentoFuncao = internalMutation({ args: { titulo: v.string(), conteudo: v.string(), tipo: v.union( v.literal('query'), v.literal('mutation'), v.literal('action'), v.literal('component'), v.literal('route'), v.literal('modulo'), v.literal('manual'), v.literal('outro') ), arquivoOrigem: v.string(), funcaoOrigem: v.string(), hashOrigem: v.string(), metadados: v.optional( v.object({ parametros: v.optional(v.array(v.string())), retorno: v.optional(v.string()), dependencias: v.optional(v.array(v.string())), exemplos: v.optional(v.array(v.string())), algoritmo: v.optional(v.string()) }) ), criadoPor: v.id('usuarios') }, handler: async (ctx, args) => { // Verificar se já existe documento com este hash const documentoExistente = await ctx.db .query('documentacao') .withIndex('by_hash_origem', (q) => q.eq('hashOrigem', args.hashOrigem)) .first(); if (documentoExistente) { // Atualizar documento existente const conteudoBusca = args.titulo.toLowerCase() + ' ' + args.conteudo.toLowerCase(); await ctx.db.patch(documentoExistente._id, { titulo: args.titulo, conteudo: args.conteudo, conteudoBusca, metadados: args.metadados, atualizadoEm: Date.now() }); return { documentoId: documentoExistente._id, novo: false }; } // Criar novo documento const agora = Date.now(); const conteudoBusca = args.titulo.toLowerCase() + ' ' + args.conteudo.toLowerCase(); // Determinar categoria baseada no tipo let categoriaId: Id<'documentacaoCategorias'> | undefined; const categoriaBackend = await ctx.db .query('documentacaoCategorias') .filter((q) => q.eq(q.field('nome'), 'Backend')) .first(); if (categoriaBackend) { categoriaId = categoriaBackend._id; } const documentoId = await ctx.db.insert('documentacao', { titulo: args.titulo, conteudo: args.conteudo, conteudoBusca, categoriaId, tags: [args.tipo, 'automatico'], tipo: args.tipo, versao: '1.0.0', arquivoOrigem: args.arquivoOrigem, funcaoOrigem: args.funcaoOrigem, hashOrigem: args.hashOrigem, metadados: args.metadados, ativo: true, criadoPor: args.criadoPor, criadoEm: agora, atualizadoEm: agora, visualizacoes: 0, geradoAutomaticamente: true }); return { documentoId, novo: true }; } }); // ========== PUBLIC MUTATIONS ========== /** * Executar varredura manualmente (chamado pelo frontend) */ export const executarVarreduraManual = mutation({ args: {}, handler: async (ctx) => { const usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { throw new Error('Não autenticado'); } // Verificar se é TI const role = await ctx.db.get(usuarioAtual.roleId); if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) { throw new Error('Acesso negado. Apenas usuários TI podem executar varredura.'); } // Executar varredura const varreduraId = await ctx.scheduler.runAfter( 0, internal.documentacaoVarredura.executarVarredura, { executadoPor: usuarioAtual._id, tipo: 'manual' } ); return varreduraId; } }); /** * Obter histórico de varreduras */ export const obterHistoricoVarreduras = mutation({ args: { limite: v.optional(v.number()) }, handler: async (ctx, args) => { const usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { throw new Error('Não autenticado'); } // Verificar se é TI const role = await ctx.db.get(usuarioAtual.roleId); if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) { throw new Error('Acesso negado.'); } const limite = args.limite || 20; const varreduras = await ctx.db .query('documentacaoVarredura') .withIndex('by_iniciado_em', (q) => q.gte('iniciadoEm', 0)) .order('desc') .take(limite); // Enriquecer com informações do usuário const varredurasEnriquecidas = await Promise.all( varreduras.map(async (varredura) => { const usuario = await ctx.db.get(varredura.executadoPor); return { ...varredura, executadoPorUsuario: usuario ? { _id: usuario._id, nome: usuario.nome, email: usuario.email } : null }; }) ); return varredurasEnriquecidas; } }); /** * Verificar e executar varredura agendada (chamado pelo cron) */ export const verificarEExecutarVarreduraAgendada = internalMutation({ args: {}, handler: async (ctx) => { // Buscar configuração ativa const config = await ctx.db .query('documentacaoConfig') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); if (!config) { return; // Nenhuma configuração ativa } const agora = new Date(); const diaSemanaAtual = agora.getDay(); // 0 = domingo, 1 = segunda, etc. const diasSemanaMap: Record< 'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado', number > = { domingo: 0, segunda: 1, terca: 2, quarta: 3, quinta: 4, sexta: 5, sabado: 6 }; // Verificar se hoje é um dos dias configurados const diaAtualNome = Object.keys(diasSemanaMap).find( (key) => diasSemanaMap[key as keyof typeof diasSemanaMap] === diaSemanaAtual ) as 'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado' | undefined; if (!diaAtualNome || !config.diasSemana.includes(diaAtualNome)) { return; // Hoje não é um dia configurado } // Verificar se é o horário configurado (com tolerância de 5 minutos) const [horaConfig, minutoConfig] = config.horario.split(':').map(Number); const horaAtual = agora.getHours(); const minutoAtual = agora.getMinutes(); const diferencaMinutos = horaAtual * 60 + minutoAtual - (horaConfig * 60 + minutoConfig); if (Math.abs(diferencaMinutos) > 5) { return; // Não é o horário configurado } // Verificar se já foi executado hoje neste horário const hojeInicio = new Date(agora); hojeInicio.setHours(0, 0, 0, 0); const hojeFim = new Date(agora); hojeFim.setHours(23, 59, 59, 999); const varredurasHoje = await ctx.db .query('documentacaoVarredura') .filter((q) => q.and( q.gte(q.field('iniciadoEm'), hojeInicio.getTime()), q.lte(q.field('iniciadoEm'), hojeFim.getTime()), q.eq(q.field('tipo'), 'automatica') ) ) .collect(); // Se já foi executado hoje neste horário, não executar novamente if (varredurasHoje.length > 0) { const ultimaVarredura = varredurasHoje.sort((a, b) => b.iniciadoEm - a.iniciadoEm)[0]; const ultimaVarreduraData = new Date(ultimaVarredura.iniciadoEm); const ultimaVarreduraHora = ultimaVarreduraData.getHours(); const ultimaVarreduraMinuto = ultimaVarreduraData.getMinutes(); if ( ultimaVarreduraHora === horaConfig && Math.abs(ultimaVarreduraMinuto - minutoConfig) <= 5 ) { return; // Já foi executado hoje neste horário } } // Buscar um usuário TI_master para executar a varredura const roleTIMaster = await ctx.db .query('roles') .filter((q) => q.eq(q.field('nivel'), 0)) .first(); if (!roleTIMaster) { return; // Não há TI_master configurado } const usuarioTIMaster = await ctx.db .query('usuarios') .filter((q) => q.eq(q.field('roleId'), roleTIMaster._id)) .first(); if (!usuarioTIMaster) { return; // Não há usuário TI_master } // Executar varredura await ctx.scheduler.runAfter( 0, internal.documentacaoVarredura.executarVarredura, { executadoPor: usuarioTIMaster._id, tipo: 'automatica' } ); // Atualizar próxima execução na configuração await ctx.db.patch(config._id, { ultimaExecucao: Date.now() }); } });