From 0b3636a96f2b67dfd6bed725d802afc60b947445 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sat, 8 Aug 2026 12:29:24 +0200 Subject: [PATCH] feat(db): implement task service logic and database schema with relational tables --- db/schema.ts | 78 ++++++++++++++++++++++++++++++++++++---- package-lock.json | 4 +-- services/task.service.ts | 55 ++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 services/task.service.ts diff --git a/db/schema.ts b/db/schema.ts index b958af0..15316be 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -1,9 +1,43 @@ /** * @file db/schema.ts - * @description Defines the PostgreSQL database schema for users using UUIDs, timestamps, and Drizzle ORM. + * @description Defines the PostgreSQL database schema for users, tasks, and task assignees using Drizzle ORM, including custom enums and relations. */ -import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + pgEnum, + pgTable, + primaryKey, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; + +// ========================================== +// Enums +// ========================================== + +/** + * Enumeration representing the current lifecycle status of a task. + */ +export const taskStatusEnum = pgEnum("task_status", [ + "todo", + "in_progress", + "await_feedback", + "done", +]); + +/** + * Enumeration representing the priority level of a task. + */ +export const taskPriorityEnum = pgEnum("task_priority", [ + "low", + "medium", + "high", +]); + +// ========================================== +// Tables +// ========================================== /** * Database table definition for application users. @@ -16,11 +50,43 @@ export const usersTable = pgTable("users", { }); /** - * Represents a user record selected from the database. + * Database table definition for application tasks. */ -export type User = typeof usersTable.$inferSelect; +export const tasksTable = pgTable("tasks", { + id: uuid("id").defaultRandom().primaryKey(), + userId: uuid("user_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + title: text("title").notNull(), + description: text("description").notNull(), + status: taskStatusEnum("status").default("todo").notNull(), + priority: taskPriorityEnum("priority").default("medium").notNull(), + dueDate: timestamp("due_date"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); /** - * Represents a new user object required for insertion into the database. + * Junction table for assigning multiple users to tasks (Many-to-Many). */ -export type NewUser = typeof usersTable.$inferInsert; +export const taskAssigneesTable = pgTable( + "task_assignees", + { + taskId: uuid("task_id") + .notNull() + .references(() => tasksTable.id, { onDelete: "cascade" }), + userId: uuid("user_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + assignedAt: timestamp("assigned_at").defaultNow().notNull(), + }, + (t) => ({ + pk: primaryKey({ columns: [t.taskId, t.userId] }), + }), +); + +// ========================================== +// Type Exports +// ========================================== + +export type Task = typeof tasksTable.$inferSelect; diff --git a/package-lock.json b/package-lock.json index 8ecb968..f8fa8f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2608,7 +2608,7 @@ "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -8342,7 +8342,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { diff --git a/services/task.service.ts b/services/task.service.ts new file mode 100644 index 0000000..e277f66 --- /dev/null +++ b/services/task.service.ts @@ -0,0 +1,55 @@ +/** + * @file services/task.service.ts + * @description Business logic service handling task permissions and database queries. + */ + +import { db } from "@/db"; +import { + tasksTable, + taskAssigneesTable, + type Task as DbTask, +} from "@/db/schema"; +import { and, eq, or, exists } from "drizzle-orm"; + +/** + * Service class for handling task-related operations and database interactions. + */ +export class TaskService { + /** + * Verifies whether a user has access to a specific task as either the owner or an assignee. + * + * @async + * @param {string} taskId - The unique identifier of the task. + * @param {string} userId - The unique identifier of the user. + * @returns {Promise} The task object if access is verified, otherwise undefined. + */ + static async verifyAccess( + taskId: string, + userId: string, + ): Promise { + const [task] = await db + .select() + .from(tasksTable) + .where( + and( + eq(tasksTable.id, taskId), + or( + eq(tasksTable.userId, userId), + exists( + db + .select({ taskId: taskAssigneesTable.taskId }) + .from(taskAssigneesTable) + .where( + and( + eq(taskAssigneesTable.taskId, taskId), + eq(taskAssigneesTable.userId, userId), + ), + ), + ), + ), + ), + ); + + return task as DbTask | undefined; + } +}