1153 lines
35 KiB
TypeScript
1153 lines
35 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { internalMutation, mutation, query } from './_generated/server';
|
|
import { Doc, Id } from './_generated/dataModel';
|
|
import { internal, api } from './_generated/api';
|
|
import { getCurrentUserFunction } from './auth';
|
|
|
|
// ========== HELPERS ==========
|
|
|
|
/**
|
|
* Normaliza texto para busca (remove acentos, converte para lowercase)
|
|
*/
|
|
function normalizarTextoParaBusca(texto: string): string {
|
|
return texto
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '') // Remove diacríticos
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
corpoFuncao: string;
|
|
}> {
|
|
const funcoes: Array<{
|
|
nome: string;
|
|
tipo: 'query' | 'mutation' | 'action';
|
|
descricao: string;
|
|
parametros: string[];
|
|
retorno: string;
|
|
hash: string;
|
|
corpoFuncao: 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 com melhor parsing
|
|
const padraoJSDoc = /\/\*\*([\s\S]*?)\*\//g;
|
|
const jsdocs: Map<string, { descricao: string; parametros: Map<string, string>; retorno?: string }> = new Map();
|
|
|
|
let match;
|
|
while ((match = padraoJSDoc.exec(conteudo)) !== null) {
|
|
const jsdocRaw = 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) {
|
|
// Parse JSDoc para extrair descrição, @param, @returns
|
|
const linhas = jsdocRaw.split('\n').map((l) => l.trim().replace(/^\*\s*/, ''));
|
|
let descricao = '';
|
|
const parametros = new Map<string, string>();
|
|
let retorno: string | undefined;
|
|
|
|
for (let i = 0; i < linhas.length; i++) {
|
|
const linha = linhas[i];
|
|
if (linha.startsWith('@param')) {
|
|
const paramMatch = linha.match(/@param\s+(\w+)\s+(.+)/);
|
|
if (paramMatch) {
|
|
parametros.set(paramMatch[1], paramMatch[2]);
|
|
}
|
|
} else if (linha.startsWith('@returns') || linha.startsWith('@return')) {
|
|
retorno = linha.replace(/@returns?\s+/, '');
|
|
} else if (!linha.startsWith('@') && linha.length > 0) {
|
|
if (descricao) descricao += ' ';
|
|
descricao += linha;
|
|
}
|
|
}
|
|
|
|
jsdocs.set(nomeFuncao[1], {
|
|
descricao: descricao || jsdocRaw,
|
|
parametros,
|
|
retorno
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 com mais detalhes
|
|
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
|
|
const parametros: string[] = [];
|
|
const jsdocInfo = jsdocs.get(nome);
|
|
if (argsMatch) {
|
|
const argsContent = argsMatch[1];
|
|
const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g);
|
|
for (const paramMatch of paramMatches) {
|
|
const paramNome = paramMatch[1];
|
|
const paramTipo = paramMatch[2];
|
|
const paramDesc = jsdocInfo?.parametros.get(paramNome);
|
|
if (paramDesc) {
|
|
parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`);
|
|
} else {
|
|
parametros.push(`${paramNome}: ${paramTipo}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extrair returns
|
|
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
|
|
let retorno = returnsMatch ? returnsMatch[1] : 'void';
|
|
if (jsdocInfo?.retorno) {
|
|
retorno = `${retorno} - ${jsdocInfo.retorno}`;
|
|
}
|
|
|
|
let descricao = jsdocInfo?.descricao || `Query ${nome} do arquivo ${nomeArquivo}. Esta função realiza uma consulta no banco de dados.`;
|
|
|
|
// Adicionar análise da lógica se não houver JSDoc detalhado
|
|
if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) {
|
|
const logica = analisarLogicaFuncao(corpoFuncao, 'query');
|
|
if (logica) {
|
|
descricao += ' ' + logica;
|
|
}
|
|
}
|
|
|
|
funcoes.push({
|
|
nome,
|
|
tipo: 'query',
|
|
descricao,
|
|
parametros,
|
|
retorno,
|
|
hash,
|
|
corpoFuncao
|
|
});
|
|
}
|
|
|
|
// 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[] = [];
|
|
const jsdocInfo = jsdocs.get(nome);
|
|
if (argsMatch) {
|
|
const argsContent = argsMatch[1];
|
|
const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g);
|
|
for (const paramMatch of paramMatches) {
|
|
const paramNome = paramMatch[1];
|
|
const paramTipo = paramMatch[2];
|
|
const paramDesc = jsdocInfo?.parametros.get(paramNome);
|
|
if (paramDesc) {
|
|
parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`);
|
|
} else {
|
|
parametros.push(`${paramNome}: ${paramTipo}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
|
|
let retorno = returnsMatch ? returnsMatch[1] : 'void';
|
|
if (jsdocInfo?.retorno) {
|
|
retorno = `${retorno} - ${jsdocInfo.retorno}`;
|
|
}
|
|
|
|
let descricao = jsdocInfo?.descricao || `Mutation ${nome} do arquivo ${nomeArquivo}. Esta função modifica dados no banco de dados.`;
|
|
|
|
// Adicionar análise da lógica se não houver JSDoc detalhado
|
|
if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) {
|
|
const logica = analisarLogicaFuncao(corpoFuncao, 'mutation');
|
|
if (logica) {
|
|
descricao += ' ' + logica;
|
|
}
|
|
}
|
|
|
|
funcoes.push({
|
|
nome,
|
|
tipo: 'mutation',
|
|
descricao,
|
|
parametros,
|
|
retorno,
|
|
hash,
|
|
corpoFuncao
|
|
});
|
|
}
|
|
|
|
// 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[] = [];
|
|
const jsdocInfo = jsdocs.get(nome);
|
|
if (argsMatch) {
|
|
const argsContent = argsMatch[1];
|
|
const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g);
|
|
for (const paramMatch of paramMatches) {
|
|
const paramNome = paramMatch[1];
|
|
const paramTipo = paramMatch[2];
|
|
const paramDesc = jsdocInfo?.parametros.get(paramNome);
|
|
if (paramDesc) {
|
|
parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`);
|
|
} else {
|
|
parametros.push(`${paramNome}: ${paramTipo}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
|
|
let retorno = returnsMatch ? returnsMatch[1] : 'void';
|
|
if (jsdocInfo?.retorno) {
|
|
retorno = `${retorno} - ${jsdocInfo.retorno}`;
|
|
}
|
|
|
|
let descricao = jsdocInfo?.descricao || `Action ${nome} do arquivo ${nomeArquivo}. Esta função executa ações no servidor, podendo acessar APIs externas e realizar operações assíncronas.`;
|
|
|
|
// Adicionar análise da lógica se não houver JSDoc detalhado
|
|
if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) {
|
|
const logica = analisarLogicaFuncao(corpoFuncao, 'action');
|
|
if (logica) {
|
|
descricao += ' ' + logica;
|
|
}
|
|
}
|
|
|
|
funcoes.push({
|
|
nome,
|
|
tipo: 'action',
|
|
descricao,
|
|
parametros,
|
|
retorno,
|
|
hash,
|
|
corpoFuncao
|
|
});
|
|
}
|
|
|
|
return funcoes;
|
|
}
|
|
|
|
/**
|
|
* Analisa a lógica de uma função e gera uma explicação detalhada
|
|
*/
|
|
function analisarLogicaFuncao(corpoFuncao: string, tipo: 'query' | 'mutation' | 'action'): string {
|
|
const explicacoes: string[] = [];
|
|
|
|
// Detectar operações de banco de dados
|
|
const operacoesDB = {
|
|
query: corpoFuncao.match(/ctx\.db\.query\(['"]([^'"]+)['"]\)/g) || [],
|
|
insert: corpoFuncao.match(/ctx\.db\.insert\(['"]([^'"]+)['"]/g) || [],
|
|
update: corpoFuncao.match(/ctx\.db\.patch\(/g) || [],
|
|
delete: corpoFuncao.match(/ctx\.db\.delete\(/g) || [],
|
|
replace: corpoFuncao.match(/ctx\.db\.replace\(/g) || []
|
|
};
|
|
|
|
if (operacoesDB.query.length > 0) {
|
|
const tabelas = operacoesDB.query.map((q) => {
|
|
const match = q.match(/['"]([^'"]+)['"]/);
|
|
return match ? match[1] : '';
|
|
});
|
|
explicacoes.push(
|
|
`Esta função realiza consultas na(s) tabela(s): **${tabelas.filter((t) => t).join(', ')}**.`
|
|
);
|
|
}
|
|
|
|
if (operacoesDB.insert.length > 0) {
|
|
const tabelas = operacoesDB.insert.map((i) => {
|
|
const match = i.match(/['"]([^'"]+)['"]/);
|
|
return match ? match[1] : '';
|
|
});
|
|
explicacoes.push(
|
|
`A função **insere novos registros** na(s) tabela(s): **${tabelas.filter((t) => t).join(', ')}**.`
|
|
);
|
|
}
|
|
|
|
if (operacoesDB.update.length > 0) {
|
|
explicacoes.push(`A função **atualiza registros existentes** no banco de dados.`);
|
|
}
|
|
|
|
if (operacoesDB.delete.length > 0) {
|
|
explicacoes.push(`A função **remove registros** do banco de dados.`);
|
|
}
|
|
|
|
if (operacoesDB.replace.length > 0) {
|
|
explicacoes.push(`A função **substitui registros completos** no banco de dados.`);
|
|
}
|
|
|
|
// Detectar uso de índices
|
|
if (corpoFuncao.includes('.withIndex(')) {
|
|
explicacoes.push(`A função utiliza **índices do banco de dados** para otimizar as consultas.`);
|
|
}
|
|
|
|
// Detectar filtros
|
|
if (corpoFuncao.includes('.filter(')) {
|
|
explicacoes.push(`A função aplica **filtros** para restringir os resultados da consulta.`);
|
|
}
|
|
|
|
// Detectar ordenação
|
|
if (corpoFuncao.includes('.order(')) {
|
|
explicacoes.push(`A função **ordena os resultados** de acordo com critérios específicos.`);
|
|
}
|
|
|
|
// Detectar paginação
|
|
if (corpoFuncao.includes('.take(') || corpoFuncao.includes('.limit')) {
|
|
explicacoes.push(`A função implementa **paginação** para limitar a quantidade de resultados retornados.`);
|
|
}
|
|
|
|
// Detectar validações
|
|
if (corpoFuncao.includes('if (') || corpoFuncao.includes('if(')) {
|
|
const condicoes = (corpoFuncao.match(/if\s*\([^)]+\)/g) || []).length;
|
|
if (condicoes > 0) {
|
|
explicacoes.push(
|
|
`A função contém **${condicoes} condição(ões) de validação** para garantir a integridade dos dados.`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Detectar loops
|
|
if (corpoFuncao.includes('for (') || corpoFuncao.includes('forEach(') || corpoFuncao.includes('.map(')) {
|
|
explicacoes.push(`A função **processa múltiplos itens** iterando sobre coleções de dados.`);
|
|
}
|
|
|
|
// Detectar chamadas a outras funções
|
|
const chamadasInternas = corpoFuncao.match(/ctx\.run(Query|Mutation|Action)\(/g) || [];
|
|
if (chamadasInternas.length > 0) {
|
|
explicacoes.push(
|
|
`A função **chama outras funções** do sistema (${chamadasInternas.length} chamada(s)) para realizar operações auxiliares.`
|
|
);
|
|
}
|
|
|
|
// Detectar tratamento de erros
|
|
if (corpoFuncao.includes('try {') || corpoFuncao.includes('catch')) {
|
|
explicacoes.push(`A função implementa **tratamento de erros** para garantir robustez.`);
|
|
}
|
|
|
|
// Detectar operações assíncronas (para actions)
|
|
if (tipo === 'action') {
|
|
if (corpoFuncao.includes('fetch(') || corpoFuncao.includes('await fetch')) {
|
|
explicacoes.push(`A função realiza **chamadas HTTP** para APIs externas.`);
|
|
}
|
|
if (corpoFuncao.includes('fs.') || corpoFuncao.includes('readFile') || corpoFuncao.includes('writeFile')) {
|
|
explicacoes.push(`A função realiza **operações de arquivo** no sistema.`);
|
|
}
|
|
}
|
|
|
|
// Detectar agregações
|
|
if (corpoFuncao.includes('.collect()')) {
|
|
explicacoes.push(`A função **coleta todos os resultados** da consulta em memória.`);
|
|
}
|
|
|
|
// Detectar contagem
|
|
if (corpoFuncao.includes('.count()') || corpoFuncao.includes('length')) {
|
|
explicacoes.push(`A função **conta elementos** ou calcula quantidades.`);
|
|
}
|
|
|
|
// Detectar transformações de dados
|
|
if (corpoFuncao.includes('.map(') || corpoFuncao.includes('.reduce(') || corpoFuncao.includes('.filter(')) {
|
|
explicacoes.push(`A função **transforma os dados** antes de retorná-los.`);
|
|
}
|
|
|
|
// Detectar autenticação/autorização
|
|
if (corpoFuncao.includes('getCurrentUser') || corpoFuncao.includes('auth') || corpoFuncao.includes('usuario')) {
|
|
explicacoes.push(`A função verifica **autenticação e autorização** do usuário.`);
|
|
}
|
|
|
|
if (explicacoes.length === 0) {
|
|
return 'A função executa operações básicas conforme sua implementação.';
|
|
}
|
|
|
|
return explicacoes.join(' ');
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
corpoFuncao: string;
|
|
},
|
|
arquivoOrigem: string
|
|
): string {
|
|
const tipoCapitalizado = funcao.tipo.charAt(0).toUpperCase() + funcao.tipo.slice(1);
|
|
|
|
// Descrição do tipo
|
|
let tipoDescricao = '';
|
|
switch (funcao.tipo) {
|
|
case 'query':
|
|
tipoDescricao = 'Uma **Query** é uma função de leitura que busca dados do banco de dados. Queries são otimizadas para leitura e podem ser executadas múltiplas vezes sem efeitos colaterais.';
|
|
break;
|
|
case 'mutation':
|
|
tipoDescricao = 'Uma **Mutation** é uma função de escrita que modifica dados no banco de dados. Mutations garantem consistência e podem ser executadas de forma transacional.';
|
|
break;
|
|
case 'action':
|
|
tipoDescricao = 'Uma **Action** é uma função que pode executar operações no servidor, incluindo chamadas a APIs externas, operações de arquivo e outras tarefas assíncronas.';
|
|
break;
|
|
}
|
|
|
|
let markdown = `# ${funcao.nome}\n\n`;
|
|
markdown += `## Visão Geral\n\n`;
|
|
markdown += `${funcao.descricao}\n\n`;
|
|
|
|
markdown += `## Tipo de Função\n\n`;
|
|
markdown += `**${tipoCapitalizado}**\n\n`;
|
|
markdown += `${tipoDescricao}\n\n`;
|
|
|
|
// Adicionar seção de lógica implementada
|
|
const logicaDetalhada = analisarLogicaFuncao(funcao.corpoFuncao, funcao.tipo);
|
|
if (logicaDetalhada && logicaDetalhada !== 'A função executa operações básicas conforme sua implementação.') {
|
|
markdown += `## Lógica Implementada\n\n`;
|
|
markdown += `${logicaDetalhada}\n\n`;
|
|
}
|
|
|
|
if (funcao.parametros.length > 0) {
|
|
markdown += `## Parâmetros de Entrada\n\n`;
|
|
markdown += `Esta função aceita os seguintes parâmetros:\n\n`;
|
|
for (const param of funcao.parametros) {
|
|
// Separar nome, tipo e descrição se houver
|
|
const paramMatch = param.match(/^(.+?):\s*(.+?)(?:\s*-\s*(.+))?$/);
|
|
if (paramMatch) {
|
|
const [, nome, tipo, desc] = paramMatch;
|
|
markdown += `### \`${nome}\`\n\n`;
|
|
markdown += `- **Tipo:** \`${tipo}\`\n`;
|
|
if (desc) {
|
|
markdown += `- **Descrição:** ${desc}\n`;
|
|
}
|
|
markdown += `\n`;
|
|
} else {
|
|
markdown += `- \`${param}\`\n`;
|
|
}
|
|
}
|
|
markdown += `\n`;
|
|
} else {
|
|
markdown += `## Parâmetros de Entrada\n\n`;
|
|
markdown += `Esta função não requer parâmetros.\n\n`;
|
|
}
|
|
|
|
markdown += `## Valor de Retorno\n\n`;
|
|
const retornoMatch = funcao.retorno.match(/^(.+?)(?:\s*-\s*(.+))?$/);
|
|
if (retornoMatch) {
|
|
const [, tipo, desc] = retornoMatch;
|
|
markdown += `- **Tipo:** \`${tipo}\`\n`;
|
|
if (desc) {
|
|
markdown += `- **Descrição:** ${desc}\n`;
|
|
}
|
|
} else {
|
|
markdown += `\`${funcao.retorno}\`\n`;
|
|
}
|
|
markdown += `\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<string, Id<'documentacao'>>();
|
|
for (const doc of documentosExistentes) {
|
|
if (doc.hashOrigem) {
|
|
hashDocumentosExistentes.set(doc.hashOrigem, doc._id);
|
|
}
|
|
}
|
|
|
|
// Chamar Action para ler e analisar arquivos do sistema de arquivos
|
|
// Usar scheduler para não bloquear, mas a Action atualizará a varredura quando terminar
|
|
console.log(`🚀 [executarVarredura] Iniciando análise de ${arquivosConvex.length} arquivos`);
|
|
|
|
ctx.scheduler.runAfter(
|
|
0,
|
|
api.actions.documentacaoVarredura.analisarArquivos,
|
|
{
|
|
varreduraId,
|
|
arquivos: arquivosConvex,
|
|
executadoPor: args.executadoPor
|
|
}
|
|
).catch((error) => {
|
|
console.error('❌ [executarVarredura] Erro ao agendar Action:', error);
|
|
erros.push(`Erro ao agendar análise: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
|
|
});
|
|
|
|
// Atualizar status inicial (a Action atualizará quando terminar)
|
|
arquivosAnalisados = arquivosConvex.length;
|
|
const duracao = Date.now() - inicio;
|
|
await ctx.db.patch(varreduraId, {
|
|
status: 'em_andamento', // Manter como em_andamento até a Action terminar
|
|
documentosEncontrados: documentosExistentes.length,
|
|
documentosNovos: 0,
|
|
documentosAtualizados: 0,
|
|
arquivosAnalisados,
|
|
erros: erros.length > 0 ? erros : undefined,
|
|
duracaoMs: duracao
|
|
});
|
|
|
|
console.log(`✅ [executarVarredura] Varredura agendada, ID: ${varreduraId}`);
|
|
|
|
// A notificação será feita pela Action quando terminar
|
|
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Atualizar varredura com resultados da Action
|
|
*/
|
|
export const atualizarVarreduraComResultados = internalMutation({
|
|
args: {
|
|
varreduraId: v.id('documentacaoVarredura'),
|
|
documentosNovos: v.number(),
|
|
documentosAtualizados: v.number(),
|
|
arquivosAnalisados: v.number(),
|
|
erros: v.array(v.string())
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const varredura = await ctx.db.get(args.varreduraId);
|
|
if (!varredura) return;
|
|
|
|
const duracao = Date.now() - varredura.iniciadoEm;
|
|
|
|
// Buscar documentos encontrados
|
|
const documentosEncontrados = await ctx.db
|
|
.query('documentacao')
|
|
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
|
|
.collect();
|
|
|
|
await ctx.db.patch(args.varreduraId, {
|
|
status: 'concluida',
|
|
documentosEncontrados: documentosEncontrados.length,
|
|
documentosNovos: args.documentosNovos,
|
|
documentosAtualizados: args.documentosAtualizados,
|
|
arquivosAnalisados: args.arquivosAnalisados,
|
|
erros: args.erros.length > 0 ? args.erros : undefined,
|
|
duracaoMs: duracao,
|
|
concluidoEm: Date.now()
|
|
});
|
|
|
|
// Notificar TI_master se houver novos documentos
|
|
if (args.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'), varredura.iniciadoEm))
|
|
.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: args.documentosNovos
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Processar um arquivo e criar/atualizar documentos (chamado pela Action)
|
|
*/
|
|
export const processarArquivo = internalMutation({
|
|
args: {
|
|
varreduraId: v.id('documentacaoVarredura'),
|
|
nomeArquivo: v.string(),
|
|
conteudo: v.string(),
|
|
executadoPor: v.id('usuarios')
|
|
},
|
|
returns: v.object({
|
|
documentosNovos: v.number(),
|
|
documentosAtualizados: v.number()
|
|
}),
|
|
handler: async (ctx, args) => {
|
|
let documentosNovos = 0;
|
|
let documentosAtualizados = 0;
|
|
|
|
console.log(`📝 [processarArquivo] Processando arquivo: ${args.nomeArquivo}`);
|
|
|
|
try {
|
|
// Analisar funções no arquivo
|
|
const funcoes = analisarFuncaoConvex(args.conteudo, args.nomeArquivo);
|
|
console.log(`🔍 [processarArquivo] Encontradas ${funcoes.length} funções em ${args.nomeArquivo}`);
|
|
|
|
if (funcoes.length === 0) {
|
|
console.log(`⚠️ [processarArquivo] Nenhuma função encontrada em ${args.nomeArquivo}`);
|
|
}
|
|
|
|
// Processar cada função encontrada
|
|
for (const funcao of funcoes) {
|
|
console.log(` 📌 Processando função: ${funcao.nome} (${funcao.tipo})`);
|
|
const markdown = gerarMarkdownFuncao(funcao, args.nomeArquivo);
|
|
const hashOrigem = `${args.nomeArquivo}:${funcao.hash}`;
|
|
|
|
// Verificar se já existe documento com este hash
|
|
const documentoExistente = await ctx.db
|
|
.query('documentacao')
|
|
.withIndex('by_hash_origem', (q) => q.eq('hashOrigem', hashOrigem))
|
|
.first();
|
|
|
|
const conteudoBusca = normalizarTextoParaBusca(
|
|
`${funcao.nome} ${funcao.descricao} ${markdown}`
|
|
);
|
|
|
|
if (documentoExistente) {
|
|
// Verificar se houve mudança no conteúdo (comparar hash)
|
|
const hashAtual = funcao.hash;
|
|
const hashAnterior = documentoExistente.hashOrigem?.split(':')[1] || '';
|
|
|
|
// Calcular nova versão se houver mudança
|
|
let novaVersao = documentoExistente.versao;
|
|
if (hashAtual !== hashAnterior) {
|
|
// Incrementar versão (ex: 1.0.0 -> 1.0.1, 1.0.1 -> 1.0.2, etc)
|
|
const partes = documentoExistente.versao.split('.');
|
|
const patch = parseInt(partes[2] || '0', 10) + 1;
|
|
novaVersao = `${partes[0]}.${partes[1]}.${patch}`;
|
|
console.log(
|
|
` 🔄 Função ${funcao.nome} alterada: versão ${documentoExistente.versao} -> ${novaVersao}`
|
|
);
|
|
}
|
|
|
|
// Atualizar documento existente
|
|
await ctx.db.patch(documentoExistente._id, {
|
|
titulo: `${funcao.nome} (${funcao.tipo})`,
|
|
conteudo: markdown,
|
|
conteudoBusca,
|
|
versao: novaVersao,
|
|
metadados: {
|
|
parametros: funcao.parametros,
|
|
retorno: funcao.retorno,
|
|
algoritmo: funcao.descricao
|
|
},
|
|
atualizadoEm: Date.now()
|
|
});
|
|
console.log(` ✅ Documento atualizado: ${funcao.nome} (versão ${novaVersao})`);
|
|
documentosAtualizados++;
|
|
} else {
|
|
// Criar novo documento
|
|
const agora = Date.now();
|
|
|
|
// 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: `${funcao.nome} (${funcao.tipo})`,
|
|
conteudo: markdown,
|
|
conteudoBusca,
|
|
categoriaId,
|
|
tags: [funcao.tipo, 'automatico'],
|
|
tipo: funcao.tipo,
|
|
versao: '1.0.0',
|
|
arquivoOrigem: args.nomeArquivo,
|
|
funcaoOrigem: funcao.nome,
|
|
hashOrigem: hashOrigem,
|
|
metadados: {
|
|
parametros: funcao.parametros,
|
|
retorno: funcao.retorno,
|
|
algoritmo: funcao.descricao
|
|
},
|
|
ativo: true,
|
|
criadoPor: args.executadoPor,
|
|
criadoEm: agora,
|
|
atualizadoEm: agora,
|
|
visualizacoes: 0,
|
|
geradoAutomaticamente: true
|
|
});
|
|
console.log(` ✨ Novo documento criado: ${funcao.nome} (ID: ${documentoId})`);
|
|
documentosNovos++;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const erroMsg = error instanceof Error ? error.message : 'Erro desconhecido';
|
|
console.error(`❌ [processarArquivo] Erro ao processar arquivo ${args.nomeArquivo}:`, erroMsg);
|
|
throw error;
|
|
}
|
|
|
|
console.log(
|
|
`✅ [processarArquivo] Arquivo ${args.nomeArquivo} processado: ${documentosNovos} novos, ${documentosAtualizados} atualizados`
|
|
);
|
|
|
|
return { documentosNovos, documentosAtualizados };
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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 as any).documentacaoVarredura.executarVarredura,
|
|
{
|
|
executadoPor: usuarioAtual._id,
|
|
tipo: 'manual'
|
|
}
|
|
);
|
|
|
|
return varreduraId;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Obter histórico de varreduras
|
|
*/
|
|
export const obterHistoricoVarreduras = query({
|
|
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 e arquivos modificados
|
|
const varredurasEnriquecidas = await Promise.all(
|
|
varreduras.map(async (varredura) => {
|
|
const usuario = await ctx.db.get(varredura.executadoPor);
|
|
|
|
// Buscar documentos atualizados durante esta varredura
|
|
const documentosAtualizados = await ctx.db
|
|
.query('documentacao')
|
|
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
|
|
.filter((q) => q.gte(q.field('atualizadoEm'), varredura.iniciadoEm))
|
|
.filter((q) => q.lte(q.field('atualizadoEm'), varredura.concluidoEm || Date.now()))
|
|
.collect();
|
|
|
|
// Agrupar por arquivo e obter versões
|
|
const arquivosModificados = new Map<string, { versao: string; funcoes: string[] }>();
|
|
|
|
for (const doc of documentosAtualizados) {
|
|
if (doc.arquivoOrigem) {
|
|
const arquivo = doc.arquivoOrigem;
|
|
if (!arquivosModificados.has(arquivo)) {
|
|
arquivosModificados.set(arquivo, { versao: doc.versao, funcoes: [] });
|
|
}
|
|
const info = arquivosModificados.get(arquivo)!;
|
|
if (doc.funcaoOrigem) {
|
|
info.funcoes.push(doc.funcaoOrigem);
|
|
}
|
|
// Manter a versão mais recente (comparar numericamente)
|
|
const partesAtual = doc.versao.split('.').map(Number);
|
|
const partesInfo = info.versao.split('.').map(Number);
|
|
let versaoMaisNova = info.versao;
|
|
for (let i = 0; i < Math.max(partesAtual.length, partesInfo.length); i++) {
|
|
const atual = partesAtual[i] || 0;
|
|
const infoParte = partesInfo[i] || 0;
|
|
if (atual > infoParte) {
|
|
versaoMaisNova = doc.versao;
|
|
break;
|
|
} else if (atual < infoParte) {
|
|
break;
|
|
}
|
|
}
|
|
info.versao = versaoMaisNova;
|
|
}
|
|
}
|
|
|
|
const arquivosModificadosArray = Array.from(arquivosModificados.entries()).map(([arquivo, info]) => ({
|
|
arquivo,
|
|
versao: info.versao,
|
|
funcoes: info.funcoes
|
|
}));
|
|
|
|
return {
|
|
...varredura,
|
|
executadoPorUsuario: usuario
|
|
? {
|
|
_id: usuario._id,
|
|
nome: usuario.nome,
|
|
email: usuario.email
|
|
}
|
|
: null,
|
|
arquivosModificados: arquivosModificadosArray,
|
|
totalArquivosModificados: arquivosModificados.size
|
|
};
|
|
})
|
|
);
|
|
|
|
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 as any).documentacaoVarredura.executarVarredura,
|
|
{
|
|
executadoPor: usuarioTIMaster._id,
|
|
tipo: 'automatica'
|
|
}
|
|
);
|
|
|
|
// Atualizar próxima execução na configuração
|
|
await ctx.db.patch(config._id, {
|
|
ultimaExecucao: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|