feat(db): implement task service logic and database schema with relational tables

This commit is contained in:
Chneemann 2026-08-08 12:29:24 +02:00
parent 6fc598be83
commit 0b3636a96f
No known key found for this signature in database
3 changed files with 129 additions and 8 deletions

View file

@ -1,9 +1,43 @@
/** /**
* @file db/schema.ts * @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. * 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;

4
package-lock.json generated
View file

@ -2608,7 +2608,7 @@
"version": "20.19.43", "version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -8342,7 +8342,7 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unrs-resolver": { "node_modules/unrs-resolver": {

55
services/task.service.ts Normal file
View file

@ -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<DbTask | undefined>} The task object if access is verified, otherwise undefined.
*/
static async verifyAccess(
taskId: string,
userId: string,
): Promise<DbTask | undefined> {
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;
}
}