feat: add marked library for markdown parsing and enhance documentation handling with new cron job for scheduled checks
This commit is contained in:
6
packages/backend/convex/_generated/api.d.ts
vendored
6
packages/backend/convex/_generated/api.d.ts
vendored
@@ -31,6 +31,8 @@ import type * as contratos from "../contratos.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as cursos from "../cursos.js";
|
||||
import type * as dashboard from "../dashboard.js";
|
||||
import type * as documentacao from "../documentacao.js";
|
||||
import type * as documentacaoVarredura from "../documentacaoVarredura.js";
|
||||
import type * as documentos from "../documentos.js";
|
||||
import type * as email from "../email.js";
|
||||
import type * as empresas from "../empresas.js";
|
||||
@@ -64,6 +66,7 @@ import type * as tables_auth from "../tables/auth.js";
|
||||
import type * as tables_chat from "../tables/chat.js";
|
||||
import type * as tables_contratos from "../tables/contratos.js";
|
||||
import type * as tables_cursos from "../tables/cursos.js";
|
||||
import type * as tables_documentacao from "../tables/documentacao.js";
|
||||
import type * as tables_empresas from "../tables/empresas.js";
|
||||
import type * as tables_enderecos from "../tables/enderecos.js";
|
||||
import type * as tables_ferias from "../tables/ferias.js";
|
||||
@@ -119,6 +122,8 @@ declare const fullApi: ApiFromModules<{
|
||||
crons: typeof crons;
|
||||
cursos: typeof cursos;
|
||||
dashboard: typeof dashboard;
|
||||
documentacao: typeof documentacao;
|
||||
documentacaoVarredura: typeof documentacaoVarredura;
|
||||
documentos: typeof documentos;
|
||||
email: typeof email;
|
||||
empresas: typeof empresas;
|
||||
@@ -152,6 +157,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"tables/chat": typeof tables_chat;
|
||||
"tables/contratos": typeof tables_contratos;
|
||||
"tables/cursos": typeof tables_cursos;
|
||||
"tables/documentacao": typeof tables_documentacao;
|
||||
"tables/empresas": typeof tables_empresas;
|
||||
"tables/enderecos": typeof tables_enderecos;
|
||||
"tables/ferias": typeof tables_ferias;
|
||||
|
||||
@@ -58,4 +58,12 @@ crons.interval(
|
||||
{}
|
||||
);
|
||||
|
||||
// Verificar e executar varredura de documentação a cada hora (verifica configuração internamente)
|
||||
crons.interval(
|
||||
'verificar-varredura-documentacao',
|
||||
{ hours: 1 },
|
||||
internal.documentacaoVarredura.verificarEExecutarVarreduraAgendada,
|
||||
{}
|
||||
);
|
||||
|
||||
export default crons;
|
||||
|
||||
842
packages/backend/convex/documentacao.ts
Normal file
842
packages/backend/convex/documentacao.ts
Normal file
@@ -0,0 +1,842 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { Doc, Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { api } from './_generated/api';
|
||||
|
||||
// ========== 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o usuário tem permissão de TI (nível 0 ou 1)
|
||||
*/
|
||||
async function verificarPermissaoTI(ctx: QueryCtx | MutationCtx): Promise<Doc<'usuarios'>> {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
if (!usuarioAtual) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
// Verificar se é TI (nível 0 ou 1)
|
||||
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 acessar a documentação.');
|
||||
}
|
||||
|
||||
return usuarioAtual;
|
||||
}
|
||||
|
||||
// ========== QUERIES ==========
|
||||
|
||||
/**
|
||||
* Listar todos os documentos (com filtros opcionais)
|
||||
*/
|
||||
export const listarDocumentos = query({
|
||||
args: {
|
||||
categoriaId: v.optional(v.id('documentacaoCategorias')),
|
||||
tipo: v.optional(
|
||||
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')
|
||||
)
|
||||
),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
ativo: v.optional(v.boolean()),
|
||||
busca: v.optional(v.string()),
|
||||
limite: v.optional(v.number()),
|
||||
offset: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
let documentos: Doc<'documentacao'>[] = [];
|
||||
|
||||
// Aplicar filtros usando índices quando possível
|
||||
if (args.categoriaId) {
|
||||
documentos = await ctx.db
|
||||
.query('documentacao')
|
||||
.withIndex('by_categoria', (q) => q.eq('categoriaId', args.categoriaId))
|
||||
.collect();
|
||||
} else if (args.tipo) {
|
||||
documentos = await ctx.db
|
||||
.query('documentacao')
|
||||
.withIndex('by_tipo', (q) => q.eq('tipo', args.tipo!))
|
||||
.collect();
|
||||
} else if (args.ativo !== undefined) {
|
||||
documentos = await ctx.db
|
||||
.query('documentacao')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', args.ativo!))
|
||||
.collect();
|
||||
} else {
|
||||
documentos = await ctx.db.query('documentacao').collect();
|
||||
}
|
||||
|
||||
// Filtrar por tags (se fornecido)
|
||||
if (args.tags && args.tags.length > 0) {
|
||||
documentos = documentos.filter((doc) =>
|
||||
args.tags!.some((tag) => doc.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
// Filtrar por busca (full-text)
|
||||
if (args.busca && args.busca.trim().length > 0) {
|
||||
const buscaNormalizada = normalizarTextoParaBusca(args.busca);
|
||||
documentos = documentos.filter((doc) => {
|
||||
const tituloBusca = normalizarTextoParaBusca(doc.titulo);
|
||||
return (
|
||||
doc.conteudoBusca.includes(buscaNormalizada) ||
|
||||
tituloBusca.includes(buscaNormalizada)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Ordenar por data de atualização (mais recentes primeiro)
|
||||
documentos.sort((a, b) => b.atualizadoEm - a.atualizadoEm);
|
||||
|
||||
// Aplicar paginação
|
||||
const offset = args.offset || 0;
|
||||
const limite = args.limite || 50;
|
||||
const documentosPaginados = documentos.slice(offset, offset + limite);
|
||||
|
||||
// Enriquecer com informações de categoria
|
||||
const documentosEnriquecidos = await Promise.all(
|
||||
documentosPaginados.map(async (doc) => {
|
||||
let categoria = null;
|
||||
if (doc.categoriaId) {
|
||||
categoria = await ctx.db.get(doc.categoriaId);
|
||||
}
|
||||
|
||||
return {
|
||||
...doc,
|
||||
categoria
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
documentos: documentosEnriquecidos,
|
||||
total: documentos.length,
|
||||
offset,
|
||||
limite
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter um documento por ID
|
||||
*/
|
||||
export const obterDocumento = query({
|
||||
args: {
|
||||
documentoId: v.id('documentacao')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
const documento = await ctx.db.get(args.documentoId);
|
||||
if (!documento) {
|
||||
throw new Error('Documento não encontrado');
|
||||
}
|
||||
|
||||
// Nota: Não podemos fazer patch em uma query, então o contador de visualizações
|
||||
// será atualizado apenas quando o documento for atualizado via mutation
|
||||
|
||||
// Enriquecer com informações
|
||||
let categoria = null;
|
||||
if (documento.categoriaId) {
|
||||
categoria = await ctx.db.get(documento.categoriaId);
|
||||
}
|
||||
|
||||
const criadoPor = await ctx.db.get(documento.criadoPor);
|
||||
const atualizadoPor = documento.atualizadoPor
|
||||
? await ctx.db.get(documento.atualizadoPor)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...documento,
|
||||
categoria,
|
||||
criadoPorUsuario: criadoPor
|
||||
? {
|
||||
_id: criadoPor._id,
|
||||
nome: criadoPor.nome,
|
||||
email: criadoPor.email
|
||||
}
|
||||
: null,
|
||||
atualizadoPorUsuario: atualizadoPor
|
||||
? {
|
||||
_id: atualizadoPor._id,
|
||||
nome: atualizadoPor.nome,
|
||||
email: atualizadoPor.email
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Buscar documentos (full-text search)
|
||||
*/
|
||||
export const buscarDocumentos = query({
|
||||
args: {
|
||||
query: v.string(),
|
||||
categoriaId: v.optional(v.id('documentacaoCategorias')),
|
||||
tipo: v.optional(
|
||||
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')
|
||||
)
|
||||
),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
limite: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
const queryNormalizada = normalizarTextoParaBusca(args.query);
|
||||
const limite = args.limite || 50;
|
||||
|
||||
// Buscar todos os documentos ativos
|
||||
let documentos = await ctx.db
|
||||
.query('documentacao')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
// Filtrar por busca
|
||||
documentos = documentos.filter((doc) => {
|
||||
const tituloBusca = normalizarTextoParaBusca(doc.titulo);
|
||||
return (
|
||||
doc.conteudoBusca.includes(queryNormalizada) ||
|
||||
tituloBusca.includes(queryNormalizada) ||
|
||||
doc.tags.some((tag) => normalizarTextoParaBusca(tag).includes(queryNormalizada))
|
||||
);
|
||||
});
|
||||
|
||||
// Aplicar filtros adicionais
|
||||
if (args.categoriaId) {
|
||||
documentos = documentos.filter((doc) => doc.categoriaId === args.categoriaId);
|
||||
}
|
||||
|
||||
if (args.tipo) {
|
||||
documentos = documentos.filter((doc) => doc.tipo === args.tipo);
|
||||
}
|
||||
|
||||
if (args.tags && args.tags.length > 0) {
|
||||
documentos = documentos.filter((doc) =>
|
||||
args.tags!.some((tag) => doc.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
// Ordenar por relevância (simples: documentos com mais matches primeiro)
|
||||
documentos.sort((a, b) => {
|
||||
const aMatches =
|
||||
(normalizarTextoParaBusca(a.titulo).includes(queryNormalizada) ? 2 : 0) +
|
||||
(a.conteudoBusca.includes(queryNormalizada) ? 1 : 0);
|
||||
const bMatches =
|
||||
(normalizarTextoParaBusca(b.titulo).includes(queryNormalizada) ? 2 : 0) +
|
||||
(b.conteudoBusca.includes(queryNormalizada) ? 1 : 0);
|
||||
return bMatches - aMatches;
|
||||
});
|
||||
|
||||
// Limitar resultados
|
||||
documentos = documentos.slice(0, limite);
|
||||
|
||||
// Enriquecer com categoria
|
||||
const documentosEnriquecidos = await Promise.all(
|
||||
documentos.map(async (doc) => {
|
||||
let categoria = null;
|
||||
if (doc.categoriaId) {
|
||||
categoria = await ctx.db.get(doc.categoriaId);
|
||||
}
|
||||
return {
|
||||
...doc,
|
||||
categoria
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return documentosEnriquecidos;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar categorias
|
||||
*/
|
||||
export const listarCategorias = query({
|
||||
args: {
|
||||
parentId: v.optional(v.id('documentacaoCategorias')),
|
||||
ativo: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
let categorias: Doc<'documentacaoCategorias'>[] = [];
|
||||
|
||||
if (args.parentId !== undefined) {
|
||||
categorias = await ctx.db
|
||||
.query('documentacaoCategorias')
|
||||
.withIndex('by_parent', (q) => q.eq('parentId', args.parentId))
|
||||
.collect();
|
||||
} else if (args.ativo !== undefined) {
|
||||
categorias = await ctx.db
|
||||
.query('documentacaoCategorias')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', args.ativo!))
|
||||
.collect();
|
||||
} else {
|
||||
categorias = await ctx.db.query('documentacaoCategorias').collect();
|
||||
}
|
||||
|
||||
// Ordenar por ordem
|
||||
categorias.sort((a, b) => a.ordem - b.ordem);
|
||||
|
||||
return categorias;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter múltiplos documentos por IDs
|
||||
*/
|
||||
export const obterDocumentosPorIds = query({
|
||||
args: {
|
||||
documentosIds: v.array(v.id('documentacao'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
const documentos = await Promise.all(
|
||||
args.documentosIds.map(async (id) => {
|
||||
const doc = await ctx.db.get(id);
|
||||
if (!doc) return null;
|
||||
|
||||
let categoria = null;
|
||||
if (doc.categoriaId) {
|
||||
categoria = await ctx.db.get(doc.categoriaId);
|
||||
}
|
||||
|
||||
return {
|
||||
...doc,
|
||||
categoria
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return documentos.filter((doc): doc is NonNullable<typeof doc> => doc !== null);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar tags
|
||||
*/
|
||||
export const listarTags = query({
|
||||
args: {
|
||||
ativo: v.optional(v.boolean()),
|
||||
limite: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
let tags: Doc<'documentacaoTags'>[] = [];
|
||||
|
||||
if (args.ativo !== undefined) {
|
||||
tags = await ctx.db
|
||||
.query('documentacaoTags')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', args.ativo!))
|
||||
.collect();
|
||||
} else {
|
||||
tags = await ctx.db.query('documentacaoTags').collect();
|
||||
}
|
||||
|
||||
// Ordenar por uso (mais usadas primeiro)
|
||||
tags.sort((a, b) => b.usadoEm - a.usadoEm);
|
||||
|
||||
// Limitar se necessário
|
||||
if (args.limite) {
|
||||
tags = tags.slice(0, args.limite);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== MUTATIONS ==========
|
||||
|
||||
/**
|
||||
* Criar novo documento
|
||||
*/
|
||||
export const criarDocumento = mutation({
|
||||
args: {
|
||||
titulo: v.string(),
|
||||
conteudo: v.string(),
|
||||
conteudoHtml: v.optional(v.string()),
|
||||
categoriaId: v.optional(v.id('documentacaoCategorias')),
|
||||
tags: v.array(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')
|
||||
),
|
||||
versao: v.string(),
|
||||
arquivoOrigem: v.optional(v.string()),
|
||||
funcaoOrigem: v.optional(v.string()),
|
||||
hashOrigem: v.optional(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())
|
||||
})
|
||||
),
|
||||
geradoAutomaticamente: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await verificarPermissaoTI(ctx);
|
||||
|
||||
const agora = Date.now();
|
||||
const conteudoBusca = normalizarTextoParaBusca(args.titulo + ' ' + args.conteudo);
|
||||
|
||||
const documentoId = await ctx.db.insert('documentacao', {
|
||||
titulo: args.titulo,
|
||||
conteudo: args.conteudo,
|
||||
conteudoHtml: args.conteudoHtml,
|
||||
conteudoBusca,
|
||||
categoriaId: args.categoriaId,
|
||||
tags: args.tags,
|
||||
tipo: args.tipo,
|
||||
versao: args.versao,
|
||||
arquivoOrigem: args.arquivoOrigem,
|
||||
funcaoOrigem: args.funcaoOrigem,
|
||||
hashOrigem: args.hashOrigem,
|
||||
metadados: args.metadados,
|
||||
ativo: true,
|
||||
criadoPor: usuarioAtual._id,
|
||||
criadoEm: agora,
|
||||
atualizadoEm: agora,
|
||||
visualizacoes: 0,
|
||||
geradoAutomaticamente: args.geradoAutomaticamente ?? false
|
||||
});
|
||||
|
||||
// Atualizar contadores de uso das tags
|
||||
for (const tagNome of args.tags) {
|
||||
const tag = await ctx.db
|
||||
.query('documentacaoTags')
|
||||
.withIndex('by_nome', (q) => q.eq('nome', tagNome))
|
||||
.first();
|
||||
|
||||
if (tag) {
|
||||
await ctx.db.patch(tag._id, {
|
||||
usadoEm: tag.usadoEm + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return documentoId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Atualizar documento
|
||||
*/
|
||||
export const atualizarDocumento = mutation({
|
||||
args: {
|
||||
documentoId: v.id('documentacao'),
|
||||
titulo: v.optional(v.string()),
|
||||
conteudo: v.optional(v.string()),
|
||||
conteudoHtml: v.optional(v.string()),
|
||||
categoriaId: v.optional(v.id('documentacaoCategorias')),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
tipo: v.optional(
|
||||
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')
|
||||
)
|
||||
),
|
||||
versao: v.optional(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())
|
||||
})
|
||||
),
|
||||
ativo: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await verificarPermissaoTI(ctx);
|
||||
|
||||
const documento = await ctx.db.get(args.documentoId);
|
||||
if (!documento) {
|
||||
throw new Error('Documento não encontrado');
|
||||
}
|
||||
|
||||
const atualizacoes: Partial<Doc<'documentacao'>> = {
|
||||
atualizadoPor: usuarioAtual._id,
|
||||
atualizadoEm: Date.now()
|
||||
};
|
||||
|
||||
if (args.titulo !== undefined) atualizacoes.titulo = args.titulo;
|
||||
if (args.conteudo !== undefined) atualizacoes.conteudo = args.conteudo;
|
||||
if (args.conteudoHtml !== undefined) atualizacoes.conteudoHtml = args.conteudoHtml;
|
||||
if (args.categoriaId !== undefined) atualizacoes.categoriaId = args.categoriaId;
|
||||
if (args.tags !== undefined) atualizacoes.tags = args.tags;
|
||||
if (args.tipo !== undefined) atualizacoes.tipo = args.tipo;
|
||||
if (args.versao !== undefined) atualizacoes.versao = args.versao;
|
||||
if (args.metadados !== undefined) atualizacoes.metadados = args.metadados;
|
||||
if (args.ativo !== undefined) atualizacoes.ativo = args.ativo;
|
||||
|
||||
// Atualizar conteúdo de busca se título ou conteúdo mudaram
|
||||
if (args.titulo !== undefined || args.conteudo !== undefined) {
|
||||
const novoTitulo = args.titulo ?? documento.titulo;
|
||||
const novoConteudo = args.conteudo ?? documento.conteudo;
|
||||
atualizacoes.conteudoBusca = normalizarTextoParaBusca(novoTitulo + ' ' + novoConteudo);
|
||||
}
|
||||
|
||||
// Atualizar contadores de tags se tags mudaram
|
||||
if (args.tags !== undefined) {
|
||||
// Decrementar tags antigas
|
||||
for (const tagNome of documento.tags) {
|
||||
if (!args.tags.includes(tagNome)) {
|
||||
const tag = await ctx.db
|
||||
.query('documentacaoTags')
|
||||
.withIndex('by_nome', (q) => q.eq('nome', tagNome))
|
||||
.first();
|
||||
|
||||
if (tag && tag.usadoEm > 0) {
|
||||
await ctx.db.patch(tag._id, {
|
||||
usadoEm: tag.usadoEm - 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Incrementar tags novas
|
||||
for (const tagNome of args.tags) {
|
||||
if (!documento.tags.includes(tagNome)) {
|
||||
const tag = await ctx.db
|
||||
.query('documentacaoTags')
|
||||
.withIndex('by_nome', (q) => q.eq('nome', tagNome))
|
||||
.first();
|
||||
|
||||
if (tag) {
|
||||
await ctx.db.patch(tag._id, {
|
||||
usadoEm: tag.usadoEm + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.documentoId, atualizacoes);
|
||||
|
||||
return { sucesso: true };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletar documento (soft delete)
|
||||
*/
|
||||
export const deletarDocumento = mutation({
|
||||
args: {
|
||||
documentoId: v.id('documentacao')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await verificarPermissaoTI(ctx);
|
||||
|
||||
const documento = await ctx.db.get(args.documentoId);
|
||||
if (!documento) {
|
||||
throw new Error('Documento não encontrado');
|
||||
}
|
||||
|
||||
// Soft delete: apenas marcar como inativo
|
||||
await ctx.db.patch(args.documentoId, {
|
||||
ativo: false,
|
||||
atualizadoPor: usuarioAtual._id,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Decrementar contadores de tags
|
||||
for (const tagNome of documento.tags) {
|
||||
const tag = await ctx.db
|
||||
.query('documentacaoTags')
|
||||
.withIndex('by_nome', (q) => q.eq('nome', tagNome))
|
||||
.first();
|
||||
|
||||
if (tag && tag.usadoEm > 0) {
|
||||
await ctx.db.patch(tag._id, {
|
||||
usadoEm: tag.usadoEm - 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { sucesso: true };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar categoria
|
||||
*/
|
||||
export const criarCategoria = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
icone: v.optional(v.string()),
|
||||
cor: v.optional(
|
||||
v.union(
|
||||
v.literal('primary'),
|
||||
v.literal('secondary'),
|
||||
v.literal('accent'),
|
||||
v.literal('success'),
|
||||
v.literal('warning'),
|
||||
v.literal('error'),
|
||||
v.literal('info')
|
||||
)
|
||||
),
|
||||
parentId: v.optional(v.id('documentacaoCategorias')),
|
||||
ordem: v.number()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
if (!usuarioAtual) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
const agora = Date.now();
|
||||
const categoriaId = await ctx.db.insert('documentacaoCategorias', {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
icone: args.icone,
|
||||
cor: args.cor,
|
||||
parentId: args.parentId,
|
||||
ordem: args.ordem,
|
||||
ativo: true,
|
||||
criadoPor: usuarioAtual._id,
|
||||
criadoEm: agora,
|
||||
atualizadoEm: agora
|
||||
});
|
||||
|
||||
return categoriaId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar tag
|
||||
*/
|
||||
export const criarTag = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
cor: v.optional(
|
||||
v.union(
|
||||
v.literal('primary'),
|
||||
v.literal('secondary'),
|
||||
v.literal('accent'),
|
||||
v.literal('success'),
|
||||
v.literal('warning'),
|
||||
v.literal('error'),
|
||||
v.literal('info')
|
||||
)
|
||||
)
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
// Verificar se tag já existe
|
||||
const tagExistente = await ctx.db
|
||||
.query('documentacaoTags')
|
||||
.withIndex('by_nome', (q) => q.eq('nome', args.nome))
|
||||
.first();
|
||||
|
||||
if (tagExistente) {
|
||||
throw new Error('Tag já existe');
|
||||
}
|
||||
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
if (!usuarioAtual) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
const agora = Date.now();
|
||||
const tagId = await ctx.db.insert('documentacaoTags', {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
cor: args.cor,
|
||||
usadoEm: 0,
|
||||
ativo: true,
|
||||
criadoPor: usuarioAtual._id,
|
||||
criadoEm: agora
|
||||
});
|
||||
|
||||
return tagId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter configuração de agendamento de varredura
|
||||
*/
|
||||
export const obterConfigVarredura = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
await verificarPermissaoTI(ctx);
|
||||
|
||||
const config = await ctx.db
|
||||
.query('documentacaoConfig')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
return config;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Salvar configuração de agendamento de varredura
|
||||
*/
|
||||
export const salvarConfigVarredura = mutation({
|
||||
args: {
|
||||
ativo: v.boolean(),
|
||||
diasSemana: v.array(
|
||||
v.union(
|
||||
v.literal('domingo'),
|
||||
v.literal('segunda'),
|
||||
v.literal('terca'),
|
||||
v.literal('quarta'),
|
||||
v.literal('quinta'),
|
||||
v.literal('sexta'),
|
||||
v.literal('sabado')
|
||||
)
|
||||
),
|
||||
horario: v.string(), // Formato "HH:MM"
|
||||
fusoHorario: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await verificarPermissaoTI(ctx);
|
||||
|
||||
// Buscar configuração existente
|
||||
const configExistente = await ctx.db
|
||||
.query('documentacaoConfig')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
const agora = Date.now();
|
||||
|
||||
if (configExistente) {
|
||||
// Atualizar configuração existente
|
||||
await ctx.db.patch(configExistente._id, {
|
||||
ativo: args.ativo,
|
||||
diasSemana: args.diasSemana,
|
||||
horario: args.horario,
|
||||
fusoHorario: args.fusoHorario,
|
||||
atualizadoPor: usuarioAtual._id,
|
||||
atualizadoEm: agora
|
||||
});
|
||||
|
||||
return configExistente._id;
|
||||
} else {
|
||||
// Criar nova configuração
|
||||
const configId = await ctx.db.insert('documentacaoConfig', {
|
||||
ativo: args.ativo,
|
||||
diasSemana: args.diasSemana,
|
||||
horario: args.horario,
|
||||
fusoHorario: args.fusoHorario || 'America/Recife',
|
||||
configuradoPor: usuarioAtual._id,
|
||||
configuradoEm: agora,
|
||||
atualizadoEm: agora
|
||||
});
|
||||
|
||||
return configId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notificar TI_master sobre novos documentos criados
|
||||
*/
|
||||
export const notificarNovosDocumentos = mutation({
|
||||
args: {
|
||||
documentosIds: v.array(v.id('documentacao')),
|
||||
quantidade: v.number()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar usuários TI_master (nível 0)
|
||||
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 usuariosTIMaster = await ctx.db
|
||||
.query('usuarios')
|
||||
.filter((q) => q.eq(q.field('roleId'), roleTIMaster._id))
|
||||
.collect();
|
||||
|
||||
// Criar notificações no chat
|
||||
for (const usuario of usuariosTIMaster) {
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: usuario._id,
|
||||
tipo: 'nova_mensagem',
|
||||
titulo: '📚 Novos Documentos Criados',
|
||||
descricao: `${args.quantidade} novo(s) documento(s) de documentação foram criados automaticamente pela varredura do sistema.`,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Enviar emails para TI_master (usando scheduler para não bloquear)
|
||||
for (const usuario of usuariosTIMaster) {
|
||||
if (usuario.email) {
|
||||
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: usuario.email,
|
||||
destinatarioId: usuario._id,
|
||||
templateCodigo: 'documentacao_novos_documentos',
|
||||
variaveis: {
|
||||
destinatarioNome: usuario.nome,
|
||||
quantidade: args.quantidade.toString(),
|
||||
urlSistema: process.env.FRONTEND_URL || 'http://localhost:5173'
|
||||
},
|
||||
enviadoPor: usuario._id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { sucesso: true };
|
||||
}
|
||||
});
|
||||
|
||||
634
packages/backend/convex/documentacaoVarredura.ts
Normal file
634
packages/backend/convex/documentacaoVarredura.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
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<string, string> = 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<string, Id<'documentacao'>>();
|
||||
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()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import { pontoTables } from './tables/ponto';
|
||||
import { pedidosTables } from './tables/pedidos';
|
||||
import { produtosTables } from './tables/produtos';
|
||||
import { lgpdTables } from './tables/lgpdTables';
|
||||
import { documentacaoTables } from './tables/documentacao';
|
||||
|
||||
export default defineSchema({
|
||||
...setoresTables,
|
||||
@@ -42,5 +43,6 @@ export default defineSchema({
|
||||
...pontoTables,
|
||||
...pedidosTables,
|
||||
...produtosTables,
|
||||
...lgpdTables
|
||||
...lgpdTables,
|
||||
...documentacaoTables
|
||||
});
|
||||
|
||||
@@ -1804,7 +1804,7 @@ export const enviarMensagemChatSistema = internalMutation({
|
||||
conteudo: args.mensagem,
|
||||
conteudoBusca: args.mensagem.toLowerCase(),
|
||||
tipo: 'texto',
|
||||
criadaEm: Date.now()
|
||||
enviadaEm: Date.now()
|
||||
});
|
||||
|
||||
// Atualizar última mensagem da conversa
|
||||
@@ -1831,6 +1831,99 @@ export const enviarMensagemChatSistema = internalMutation({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notificar quando rate limit é excedido
|
||||
*/
|
||||
export const notificarRateLimitExcedido = internalMutation({
|
||||
args: {
|
||||
configId: v.id('rateLimitConfig'),
|
||||
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
|
||||
identificador: v.string(),
|
||||
endpoint: v.string(),
|
||||
acaoExcedido: v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar')),
|
||||
limite: v.number(),
|
||||
janelaSegundos: v.number()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const config = await ctx.db.get(args.configId);
|
||||
if (!config) return null;
|
||||
|
||||
// Buscar usuários TI para notificar
|
||||
const rolesTi = await ctx.db
|
||||
.query('roles')
|
||||
.withIndex('by_nivel', (q) => q.lte('nivel', 1))
|
||||
.collect();
|
||||
|
||||
const usuariosNotificados: Id<'usuarios'>[] = [];
|
||||
|
||||
for (const role of rolesTi) {
|
||||
const membros = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_role', (q) => q.eq('roleId', role._id))
|
||||
.collect();
|
||||
for (const usuario of membros) {
|
||||
usuariosNotificados.push(usuario._id);
|
||||
}
|
||||
}
|
||||
|
||||
// Criar notificações para usuários TI
|
||||
const tipoAcao = args.acaoExcedido === 'bloquear' ? 'bloqueado' : args.acaoExcedido === 'alertar' ? 'alertado' : 'throttled';
|
||||
const emoji = args.acaoExcedido === 'bloquear' ? '🚫' : '⚠️';
|
||||
const titulo = `${emoji} Rate Limit ${tipoAcao === 'bloqueado' ? 'Bloqueado' : tipoAcao === 'alertado' ? 'Alertado' : 'Throttled'}`;
|
||||
const descricao = `${args.tipo.toUpperCase()}: ${args.identificador} excedeu o limite de ${args.limite} requisições em ${args.janelaSegundos}s no endpoint ${args.endpoint}`;
|
||||
|
||||
for (const usuarioId of usuariosNotificados) {
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId,
|
||||
tipo: 'alerta_seguranca',
|
||||
conversaId: undefined,
|
||||
mensagemId: undefined,
|
||||
remetenteId: undefined,
|
||||
titulo,
|
||||
descricao,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Criar evento de segurança se foi bloqueado
|
||||
if (args.acaoExcedido === 'bloquear') {
|
||||
// Determinar tipo de ataque baseado no contexto
|
||||
let tipoAtaque: AtaqueCiberneticoTipo = 'brute_force';
|
||||
if (args.tipo === 'ip') {
|
||||
tipoAtaque = 'ddos';
|
||||
} else if (args.tipo === 'usuario') {
|
||||
tipoAtaque = 'brute_force';
|
||||
}
|
||||
|
||||
// Criar evento de segurança
|
||||
const eventoId = await ctx.db.insert('securityEvents', {
|
||||
referencia: `rate_limit_${args.tipo}_${args.identificador}_${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
tipoAtaque,
|
||||
severidade: 'alto',
|
||||
status: 'detectado',
|
||||
descricao: `Rate limit bloqueado: ${args.identificador} excedeu ${args.limite} requisições em ${args.janelaSegundos}s`,
|
||||
origemIp: args.tipo === 'ip' ? args.identificador : undefined,
|
||||
tags: ['rate_limit', 'bloqueio_automatico'],
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Disparar alertas se configurado
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.security.dispararAlertasInternos, {
|
||||
eventoId
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erro ao agendar alertas de rate limit:', error);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const expirarBloqueiosIpAutomaticos = internalMutation({
|
||||
args: {},
|
||||
returns: v.null(),
|
||||
@@ -1961,6 +2054,24 @@ async function aplicarRateLimit(
|
||||
if (!result.ok) {
|
||||
const retryAfter = result.retryAfter ?? periodo;
|
||||
|
||||
// Criar notificações e eventos quando rate limit é excedido
|
||||
// Usar scheduler para não bloquear a requisição
|
||||
if ('runMutation' in ctx) {
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.security.notificarRateLimitExcedido, {
|
||||
configId: config._id,
|
||||
tipo,
|
||||
identificador,
|
||||
endpoint: endpoint ?? 'default',
|
||||
acaoExcedido: config.acaoExcedido,
|
||||
limite: config.limite,
|
||||
janelaSegundos: config.janelaSegundos
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erro ao agendar notificação de rate limit:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (config.acaoExcedido === 'bloquear') {
|
||||
return {
|
||||
permitido: false,
|
||||
|
||||
152
packages/backend/convex/tables/documentacao.ts
Normal file
152
packages/backend/convex/tables/documentacao.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { defineTable } from 'convex/server';
|
||||
import { v } from 'convex/values';
|
||||
|
||||
export const documentacaoTables = {
|
||||
// Documentos principais
|
||||
documentacao: defineTable({
|
||||
titulo: v.string(),
|
||||
conteudo: v.string(), // Conteúdo em Markdown
|
||||
conteudoHtml: v.optional(v.string()), // Conteúdo renderizado em HTML
|
||||
conteudoBusca: v.string(), // Versão normalizada para busca (lowercase, sem acentos)
|
||||
categoriaId: v.optional(v.id('documentacaoCategorias')),
|
||||
tags: v.array(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')
|
||||
),
|
||||
versao: v.string(), // Versão do documento (ex: "1.0.0")
|
||||
arquivoOrigem: v.optional(v.string()), // Caminho do arquivo que gerou este documento
|
||||
funcaoOrigem: v.optional(v.string()), // Nome da função/componente que gerou este documento
|
||||
hashOrigem: v.optional(v.string()), // Hash do arquivo/função para detectar mudanças
|
||||
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())
|
||||
})
|
||||
),
|
||||
ativo: v.boolean(),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoPor: v.optional(v.id('usuarios')),
|
||||
atualizadoEm: v.number(),
|
||||
visualizacoes: v.number(), // Contador de visualizações
|
||||
geradoAutomaticamente: v.boolean() // Se foi gerado pela varredura automática
|
||||
})
|
||||
.index('by_categoria', ['categoriaId'])
|
||||
.index('by_tipo', ['tipo'])
|
||||
.index('by_busca', ['conteudoBusca'])
|
||||
.index('by_ativo', ['ativo'])
|
||||
.index('by_criado_em', ['criadoEm'])
|
||||
.index('by_atualizado_em', ['atualizadoEm'])
|
||||
.index('by_arquivo_origem', ['arquivoOrigem'])
|
||||
.index('by_hash_origem', ['hashOrigem']),
|
||||
|
||||
// Categorias hierárquicas (Módulos, Seções, Funções)
|
||||
documentacaoCategorias: defineTable({
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
icone: v.optional(v.string()), // Nome do ícone (lucide-svelte)
|
||||
cor: v.optional(
|
||||
v.union(
|
||||
v.literal('primary'),
|
||||
v.literal('secondary'),
|
||||
v.literal('accent'),
|
||||
v.literal('success'),
|
||||
v.literal('warning'),
|
||||
v.literal('error'),
|
||||
v.literal('info')
|
||||
)
|
||||
),
|
||||
parentId: v.optional(v.id('documentacaoCategorias')), // Para hierarquia
|
||||
ordem: v.number(), // Ordem de exibição
|
||||
ativo: v.boolean(),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
.index('by_parent', ['parentId'])
|
||||
.index('by_ordem', ['ordem'])
|
||||
.index('by_ativo', ['ativo']),
|
||||
|
||||
// Tags para busca e organização
|
||||
documentacaoTags: defineTable({
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
cor: v.optional(
|
||||
v.union(
|
||||
v.literal('primary'),
|
||||
v.literal('secondary'),
|
||||
v.literal('accent'),
|
||||
v.literal('success'),
|
||||
v.literal('warning'),
|
||||
v.literal('error'),
|
||||
v.literal('info')
|
||||
)
|
||||
),
|
||||
usadoEm: v.number(), // Contador de quantos documentos usam esta tag
|
||||
ativo: v.boolean(),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number()
|
||||
})
|
||||
.index('by_nome', ['nome'])
|
||||
.index('by_ativo', ['ativo']),
|
||||
|
||||
// Histórico de varreduras realizadas
|
||||
documentacaoVarredura: defineTable({
|
||||
tipo: v.union(v.literal('automatica'), v.literal('manual')),
|
||||
status: v.union(
|
||||
v.literal('em_andamento'),
|
||||
v.literal('concluida'),
|
||||
v.literal('erro'),
|
||||
v.literal('cancelada')
|
||||
),
|
||||
documentosEncontrados: v.number(), // Quantidade de documentos encontrados
|
||||
documentosNovos: v.number(), // Quantidade de novos documentos criados
|
||||
documentosAtualizados: v.number(), // Quantidade de documentos atualizados
|
||||
arquivosAnalisados: v.number(), // Quantidade de arquivos analisados
|
||||
erros: v.optional(v.array(v.string())), // Lista de erros encontrados
|
||||
duracaoMs: v.optional(v.number()), // Duração em milissegundos
|
||||
executadoPor: v.id('usuarios'),
|
||||
iniciadoEm: v.number(),
|
||||
concluidoEm: v.optional(v.number())
|
||||
})
|
||||
.index('by_status', ['status'])
|
||||
.index('by_tipo', ['tipo'])
|
||||
.index('by_executado_por', ['executadoPor'])
|
||||
.index('by_iniciado_em', ['iniciadoEm']),
|
||||
|
||||
// Configurações de agendamento de varredura
|
||||
documentacaoConfig: defineTable({
|
||||
ativo: v.boolean(),
|
||||
diasSemana: v.array(
|
||||
v.union(
|
||||
v.literal('domingo'),
|
||||
v.literal('segunda'),
|
||||
v.literal('terca'),
|
||||
v.literal('quarta'),
|
||||
v.literal('quinta'),
|
||||
v.literal('sexta'),
|
||||
v.literal('sabado')
|
||||
)
|
||||
), // Dias da semana para executar varredura
|
||||
horario: v.string(), // Horário no formato "HH:MM" (ex: "08:00")
|
||||
fusoHorario: v.optional(v.string()), // Fuso horário (padrão: "America/Recife")
|
||||
ultimaExecucao: v.optional(v.number()), // Timestamp da última execução
|
||||
proximaExecucao: v.optional(v.number()), // Timestamp da próxima execução agendada
|
||||
configuradoPor: v.id('usuarios'),
|
||||
configuradoEm: v.number(),
|
||||
atualizadoPor: v.optional(v.id('usuarios')),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
.index('by_ativo', ['ativo'])
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user