/** * schema.ts * * Fonte única de verdade do banco de dados. * Todos os tipos TypeScript são *inferidos* deste arquivo — nunca definidos * manualmente — garantindo que DB e aplicação nunca fiquem dessincronizados. */ import { sql } from 'drizzle-orm'; import { sqliteTable, text, integer, index, uniqueIndex, } from 'drizzle-orm/sqlite-core'; // ───────────────────────────────────────────────────────────────────────────── // HELPERS // ───────────────────────────────────────────────────────────────────────────── /** * Colunas de auditoria reutilizáveis. * SQLite armazena timestamps como texto ISO 8601. */ const timestamps = { createdAt: text('created_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), updatedAt: text('updated_at') .notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }; // ───────────────────────────────────────────────────────────────────────────── // ENUMS (simulados via union types — SQLite não tem tipo ENUM nativo) // ───────────────────────────────────────────────────────────────────────────── export const USER_ROLES = ['requester', 'agent', 'admin'] as const; export type UserRole = (typeof USER_ROLES)[number]; export const TICKET_STATUSES = [ 'open', // Chamado aberto, ainda não atribuído 'in_progress', // Atribuído a um agente, em andamento 'waiting', // Aguardando resposta do solicitante 'resolved', // Solução aplicada, aguardando confirmação 'closed', // Encerrado definitivamente ] as const; export type TicketStatus = (typeof TICKET_STATUSES)[number]; export const TICKET_PRIORITIES = ['low', 'medium', 'high', 'critical'] as const; export type TicketPriority = (typeof TICKET_PRIORITIES)[number]; // ───────────────────────────────────────────────────────────────────────────── // TABELA: users // ───────────────────────────────────────────────────────────────────────────── export const users = sqliteTable( 'users', { id: integer('id').primaryKey({ autoIncrement: true }), // Identificação name: text('name').notNull(), email: text('email').notNull(), password: text('password').notNull(), // hash bcrypt — nunca plain text // Controle de acesso role: text('role', { enum: USER_ROLES }).notNull().default('requester'), // Conta ativa? Soft-delete sem perder histórico de tickets/comments isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true), ...timestamps, }, (table) => ({ // Garante emails únicos na camada do banco emailUniqueIdx: uniqueIndex('users_email_unique_idx').on(table.email), // Consultas frequentes por role (ex: listar todos os agentes) roleIdx: index('users_role_idx').on(table.role), }), ); // ───────────────────────────────────────────────────────────────────────────── // TABELA: categories // ───────────────────────────────────────────────────────────────────────────── export const categories = sqliteTable( 'categories', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), // Slug para uso em URLs e filtros: "hardware", "acesso-sistema", etc. slug: text('slug').notNull(), description: text('description'), ...timestamps, }, (table) => ({ slugUniqueIdx: uniqueIndex('categories_slug_unique_idx').on(table.slug), }), ); // ───────────────────────────────────────────────────────────────────────────── // TABELA: tickets // ───────────────────────────────────────────────────────────────────────────── export const tickets = sqliteTable( 'tickets', { id: integer('id').primaryKey({ autoIncrement: true }), // Conteúdo title: text('title').notNull(), description: text('description').notNull(), // Classificação status: text('status', { enum: TICKET_STATUSES }) .notNull() .default('open'), priority: text('priority', { enum: TICKET_PRIORITIES }) .notNull() .default('medium'), // ── Relacionamentos ──────────────────────────────────────────────────── // Quem abriu o chamado (obrigatório) creatorId: integer('creator_id') .notNull() .references(() => users.id, { onDelete: 'restrict' }), // Agente responsável (opcional — pode ser atribuído depois) assigneeId: integer('assignee_id') .references(() => users.id, { onDelete: 'set null' }), // Categoria do chamado (opcional — pode ser classificado depois) categoryId: integer('category_id') .references(() => categories.id, { onDelete: 'set null' }), // Data limite para resolução (SLA) dueAt: text('due_at'), // Data em que o chamado foi efetivamente resolvido resolvedAt: text('resolved_at'), ...timestamps, }, (table) => ({ // Índices para os filtros mais comuns no painel do agente statusIdx: index('tickets_status_idx').on(table.status), priorityIdx: index('tickets_priority_idx').on(table.priority), assigneeIdx: index('tickets_assignee_id_idx').on(table.assigneeId), creatorIdx: index('tickets_creator_id_idx').on(table.creatorId), categoryIdx: index('tickets_category_id_idx').on(table.categoryId), // Índice composto: painel do agente filtra por status + assignee com frequência statusAssigneeIdx: index('tickets_status_assignee_idx').on( table.status, table.assigneeId, ), }), ); // ───────────────────────────────────────────────────────────────────────────── // TABELA: comments // ───────────────────────────────────────────────────────────────────────────── export const comments = sqliteTable( 'comments', { id: integer('id').primaryKey({ autoIncrement: true }), content: text('content').notNull(), /** * Diferencia mensagens públicas de notas internas da equipe. * true → nota interna (visível apenas para agentes/admins) * false → resposta pública (visível para o solicitante) */ isInternal: integer('is_internal', { mode: 'boolean' }) .notNull() .default(false), // ── Relacionamentos ──────────────────────────────────────────────────── ticketId: integer('ticket_id') .notNull() .references(() => tickets.id, { onDelete: 'cascade' }), authorId: integer('author_id') .notNull() .references(() => users.id, { onDelete: 'restrict' }), ...timestamps, }, (table) => ({ // Buscar todos os comentários de um ticket é a query mais frequente ticketIdx: index('comments_ticket_id_idx').on(table.ticketId), authorIdx: index('comments_author_id_idx').on(table.authorId), }), ); // ───────────────────────────────────────────────────────────────────────────── // TABELA: system_settings // ───────────────────────────────────────────────────────────────────────────── export const systemSettings = sqliteTable( 'system_settings', { id: integer('id').primaryKey({ autoIncrement: true }), // Configurações Globais allowRequesterChat: integer('allow_requester_chat', { mode: 'boolean' }).notNull().default(true), ...timestamps, } ); // ───────────────────────────────────────────────────────────────────────────── // TIPOS INFERIDOS // // Use estes tipos em toda a aplicação. // Ex: função que retorna um ticket → retorno tipado como `Ticket` // ───────────────────────────────────────────────────────────────────────────── export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert; export type Category = typeof categories.$inferSelect; export type NewCategory = typeof categories.$inferInsert; export type Ticket = typeof tickets.$inferSelect; export type NewTicket = typeof tickets.$inferInsert; export type Comment = typeof comments.$inferSelect; export type NewComment = typeof comments.$inferInsert; export type SystemSetting = typeof systemSettings.$inferSelect;