aboutsummaryrefslogtreecommitdiff
path: root/api/src/db/schema.ts
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
commitef55d9bb24430610b60c1b77821307cd72c08c55 (patch)
treee6935a810b34370b0a4c821e55f6397aac50c832 /api/src/db/schema.ts
downloadhelpdesk-master.tar.gz
helpdesk-master.zip
feat: upload of the projectHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'api/src/db/schema.ts')
-rw-r--r--api/src/db/schema.ts240
1 files changed, 240 insertions, 0 deletions
diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts
new file mode 100644
index 0000000..ce95493
--- /dev/null
+++ b/api/src/db/schema.ts
@@ -0,0 +1,240 @@
+/**
+ * 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;