backend project
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
PORT=5000
|
||||||
|
DATABASE_URL="postgresql://postgres:SonetovSqlRoot123.@localhost:5432/project-athena?schema=Athena"
|
||||||
|
JWT_SECRET="please_generate_a_strong_random_secret"
|
||||||
|
|
||||||
|
APP_BASE_URL="http://localhost:8081"
|
||||||
|
DEEP_LINK_SCHEME="projectathena"
|
||||||
|
RESET_TOKEN_TTL=3600
|
||||||
|
|
||||||
|
# SMTP configuration for Nodemailer
|
||||||
|
SMTP_HOST="sandbox.smtp.mailtrap.io"
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER="0a508c201b8611"
|
||||||
|
SMTP_PASS="783cf1747d831a"
|
||||||
|
SMTP_FROM="Project Athena <no-reply@yourdomain.com>"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.vscode
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
Generated
+2303
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx --env-file=.env --watch src/index.ts",
|
||||||
|
"seed": "tsx --env-file=.env scripts/seed.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.17.1",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"nodemailer": "^7.0.9",
|
||||||
|
"pg": "^8.16.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.3",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
|
"prisma": "^6.17.1",
|
||||||
|
"tsx": "^4.20.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Athena"."TodoStatus" AS ENUM ('NEW', 'IN_PROGRESS', 'COMPLETED', 'ARCHIVED');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Athena"."Role" AS ENUM ('MANAGER', 'ADMIN', 'USER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Athena"."RiskStatus" AS ENUM ('LOW_RISK', 'MEDIUM_RISK', 'HIGH_RISK', 'NO_RISK');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Athena"."PriorityStatus" AS ENUM ('LOW_PRIORITY', 'MEDIUM_PRIORITY', 'HIGH_PRIORITY');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Athena"."User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"firstName" TEXT NOT NULL,
|
||||||
|
"lastName" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"riskStatus" "Athena"."RiskStatus" NOT NULL,
|
||||||
|
"imageUrl" TEXT,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"isLocked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isActivated" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"role" "Athena"."Role" NOT NULL,
|
||||||
|
"managerId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Athena"."Todo" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"task" TEXT NOT NULL,
|
||||||
|
"status" "Athena"."TodoStatus" NOT NULL,
|
||||||
|
"priority" "Athena"."PriorityStatus" NOT NULL,
|
||||||
|
"dueDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdById" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Todo_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Athena"."Comment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"todoId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Athena"."PasswordResetToken" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"usedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Athena"."_AssignedTodos" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_AssignedTodos_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "Athena"."User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PasswordResetToken_tokenHash_key" ON "Athena"."PasswordResetToken"("tokenHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "PasswordResetToken_userId_idx" ON "Athena"."PasswordResetToken"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "PasswordResetToken_expiresAt_idx" ON "Athena"."PasswordResetToken"("expiresAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_AssignedTodos_B_index" ON "Athena"."_AssignedTodos"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."User" ADD CONSTRAINT "User_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "Athena"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."Todo" ADD CONSTRAINT "Todo_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "Athena"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."Comment" ADD CONSTRAINT "Comment_todoId_fkey" FOREIGN KEY ("todoId") REFERENCES "Athena"."Todo"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Athena"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Athena"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."_AssignedTodos" ADD CONSTRAINT "_AssignedTodos_A_fkey" FOREIGN KEY ("A") REFERENCES "Athena"."Todo"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Athena"."_AssignedTodos" ADD CONSTRAINT "_AssignedTodos_B_fkey" FOREIGN KEY ("B") REFERENCES "Athena"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Athena"."User" ADD COLUMN "department" TEXT,
|
||||||
|
ADD COLUMN "middleName" TEXT,
|
||||||
|
ADD COLUMN "phoneNumber" TEXT;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The values [MEDIUM_RISK] on the enum `RiskStatus` will be removed. If these variants are still used in the database, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterEnum
|
||||||
|
BEGIN;
|
||||||
|
CREATE TYPE "Athena"."RiskStatus_new" AS ENUM ('LOW_RISK', 'MODERATE_RISK', 'HIGH_RISK', 'NO_RISK');
|
||||||
|
ALTER TABLE "Athena"."User" ALTER COLUMN "riskStatus" TYPE "Athena"."RiskStatus_new" USING ("riskStatus"::text::"Athena"."RiskStatus_new");
|
||||||
|
ALTER TYPE "Athena"."RiskStatus" RENAME TO "RiskStatus_old";
|
||||||
|
ALTER TYPE "Athena"."RiskStatus_new" RENAME TO "RiskStatus";
|
||||||
|
DROP TYPE "Athena"."RiskStatus_old";
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The values [MEDIUM_PRIORITY] on the enum `PriorityStatus` will be removed. If these variants are still used in the database, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterEnum
|
||||||
|
BEGIN;
|
||||||
|
CREATE TYPE "Athena"."PriorityStatus_new" AS ENUM ('LOW_PRIORITY', 'MODERATE_PRIORITY', 'HIGH_PRIORITY');
|
||||||
|
ALTER TABLE "Athena"."Todo" ALTER COLUMN "priority" TYPE "Athena"."PriorityStatus_new" USING ("priority"::text::"Athena"."PriorityStatus_new");
|
||||||
|
ALTER TYPE "Athena"."PriorityStatus" RENAME TO "PriorityStatus_old";
|
||||||
|
ALTER TYPE "Athena"."PriorityStatus_new" RENAME TO "PriorityStatus";
|
||||||
|
DROP TYPE "Athena"."PriorityStatus_old";
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Athena"."SecurityType" AS ENUM ('BASIC', 'ASSISTANCE', 'MAXIMUM');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Athena"."User" ADD COLUMN "securityType" "Athena"."SecurityType" NOT NULL DEFAULT 'BASIC';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
firstName String
|
||||||
|
middleName String?
|
||||||
|
lastName String
|
||||||
|
email String @unique
|
||||||
|
phoneNumber String?
|
||||||
|
department String?
|
||||||
|
password String
|
||||||
|
riskStatus RiskStatus
|
||||||
|
imageUrl String?
|
||||||
|
title String
|
||||||
|
isLocked Boolean @default(false)
|
||||||
|
isActivated Boolean @default(false)
|
||||||
|
role Role
|
||||||
|
managerId String?
|
||||||
|
comments Comment[]
|
||||||
|
PasswordResetToken PasswordResetToken[]
|
||||||
|
createdTodos Todo[] @relation("CreatedTodos")
|
||||||
|
manager User? @relation("ManagerMembers", fields: [managerId], references: [id])
|
||||||
|
teamMembers User[] @relation("ManagerMembers")
|
||||||
|
assignedTodos Todo[] @relation("AssignedTodos")
|
||||||
|
securityType SecurityType @default(BASIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Todo {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String
|
||||||
|
task String
|
||||||
|
status TodoStatus
|
||||||
|
priority PriorityStatus
|
||||||
|
dueDate DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
createdById String
|
||||||
|
comments Comment[]
|
||||||
|
createdBy User @relation("CreatedTodos", fields: [createdById], references: [id])
|
||||||
|
assignees User[] @relation("AssignedTodos")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Comment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
content String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
todoId String
|
||||||
|
userId String
|
||||||
|
todo Todo @relation(fields: [todoId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model PasswordResetToken {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
tokenHash String @unique
|
||||||
|
expiresAt DateTime
|
||||||
|
usedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([expiresAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TodoStatus {
|
||||||
|
NEW
|
||||||
|
IN_PROGRESS
|
||||||
|
COMPLETED
|
||||||
|
ARCHIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
MANAGER
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RiskStatus {
|
||||||
|
LOW_RISK
|
||||||
|
MODERATE_RISK
|
||||||
|
HIGH_RISK
|
||||||
|
NO_RISK
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PriorityStatus {
|
||||||
|
LOW_PRIORITY
|
||||||
|
MODERATE_PRIORITY
|
||||||
|
HIGH_PRIORITY
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SecurityType {
|
||||||
|
BASIC
|
||||||
|
ASSISTANCE
|
||||||
|
MAXIMUM
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
export const firstNames = [
|
||||||
|
"John",
|
||||||
|
"Jane",
|
||||||
|
"Michael",
|
||||||
|
"Sarah",
|
||||||
|
"David",
|
||||||
|
"Emma",
|
||||||
|
"Robert",
|
||||||
|
"Olivia",
|
||||||
|
"William",
|
||||||
|
"Sophia",
|
||||||
|
"James",
|
||||||
|
"Emily",
|
||||||
|
"Daniel",
|
||||||
|
"Ava",
|
||||||
|
"Matthew",
|
||||||
|
"Mia",
|
||||||
|
"Andrew",
|
||||||
|
"Isabella",
|
||||||
|
"Joseph",
|
||||||
|
"Charlotte",
|
||||||
|
"Thomas",
|
||||||
|
"Amelia",
|
||||||
|
"Anthony",
|
||||||
|
"Harper",
|
||||||
|
"Alexander",
|
||||||
|
"Abigail",
|
||||||
|
"Ryan",
|
||||||
|
"Madison",
|
||||||
|
"Tyler",
|
||||||
|
"Elizabeth",
|
||||||
|
"Nicholas",
|
||||||
|
"Sofia",
|
||||||
|
"Ethan",
|
||||||
|
"Ella",
|
||||||
|
"Brandon",
|
||||||
|
"Grace",
|
||||||
|
"Samuel",
|
||||||
|
"Chloe",
|
||||||
|
"Christian",
|
||||||
|
"Victoria",
|
||||||
|
"Noah",
|
||||||
|
"Lily",
|
||||||
|
"Christopher",
|
||||||
|
"Hannah",
|
||||||
|
"Justin",
|
||||||
|
"Zoe",
|
||||||
|
"Jonathan",
|
||||||
|
"Natalie",
|
||||||
|
"Benjamin",
|
||||||
|
"Addison",
|
||||||
|
"Kevin",
|
||||||
|
"Leah",
|
||||||
|
"Aaron",
|
||||||
|
"Aubrey",
|
||||||
|
"Timothy",
|
||||||
|
"Audrey",
|
||||||
|
"Richard",
|
||||||
|
"Brooklyn",
|
||||||
|
"Eric",
|
||||||
|
"Bella",
|
||||||
|
"Steven",
|
||||||
|
"Claire",
|
||||||
|
"Edward",
|
||||||
|
"Skylar",
|
||||||
|
"Charles",
|
||||||
|
"Lucy",
|
||||||
|
"Stephen",
|
||||||
|
"Paisley",
|
||||||
|
"Brian",
|
||||||
|
"Evelyn",
|
||||||
|
"Adam",
|
||||||
|
"Anna",
|
||||||
|
"Kyle",
|
||||||
|
"Caroline",
|
||||||
|
"Patrick",
|
||||||
|
"Genesis",
|
||||||
|
"Sean",
|
||||||
|
"Autumn",
|
||||||
|
"Jason",
|
||||||
|
"Hailey",
|
||||||
|
"Jeffrey",
|
||||||
|
"Maya",
|
||||||
|
"Marcus",
|
||||||
|
"Kaylee",
|
||||||
|
"Scott",
|
||||||
|
"Riley",
|
||||||
|
"Zachary",
|
||||||
|
"Nora",
|
||||||
|
"Nathan",
|
||||||
|
"Ellie",
|
||||||
|
"Paul",
|
||||||
|
"Hazel",
|
||||||
|
"Dustin",
|
||||||
|
"Violet",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Additional generators for todos (shared with seed script)
|
||||||
|
export const actionVerbs = [
|
||||||
|
"Prepare",
|
||||||
|
"Update",
|
||||||
|
"Review",
|
||||||
|
"Investigate",
|
||||||
|
"Schedule",
|
||||||
|
"Draft",
|
||||||
|
"Analyze",
|
||||||
|
"Refactor",
|
||||||
|
"Migrate",
|
||||||
|
"Optimize",
|
||||||
|
"Validate",
|
||||||
|
"Fix",
|
||||||
|
"Design",
|
||||||
|
"Document",
|
||||||
|
"Monitor",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const subjects = [
|
||||||
|
"monthly report",
|
||||||
|
"system documentation",
|
||||||
|
"user permissions",
|
||||||
|
"training materials",
|
||||||
|
"deployment pipeline",
|
||||||
|
"error logs",
|
||||||
|
"feature roadmap",
|
||||||
|
"API responses",
|
||||||
|
"database indices",
|
||||||
|
"access controls",
|
||||||
|
"notification service",
|
||||||
|
"onboarding flow",
|
||||||
|
"billing module",
|
||||||
|
"cache strategy",
|
||||||
|
"CI configuration",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const loremWords =
|
||||||
|
"lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".split(
|
||||||
|
/\s+/
|
||||||
|
);
|
||||||
|
|
||||||
|
export const lastNames = [
|
||||||
|
"Smith",
|
||||||
|
"Johnson",
|
||||||
|
"Williams",
|
||||||
|
"Brown",
|
||||||
|
"Jones",
|
||||||
|
"Miller",
|
||||||
|
"Davis",
|
||||||
|
"Garcia",
|
||||||
|
"Rodriguez",
|
||||||
|
"Wilson",
|
||||||
|
"Martinez",
|
||||||
|
"Anderson",
|
||||||
|
"Taylor",
|
||||||
|
"Thomas",
|
||||||
|
"Hernandez",
|
||||||
|
"Moore",
|
||||||
|
"Martin",
|
||||||
|
"Jackson",
|
||||||
|
"Thompson",
|
||||||
|
"White",
|
||||||
|
"Lopez",
|
||||||
|
"Lee",
|
||||||
|
"Gonzalez",
|
||||||
|
"Harris",
|
||||||
|
"Clark",
|
||||||
|
"Lewis",
|
||||||
|
"Robinson",
|
||||||
|
"Walker",
|
||||||
|
"Perez",
|
||||||
|
"Hall",
|
||||||
|
"Young",
|
||||||
|
"Allen",
|
||||||
|
"Sanchez",
|
||||||
|
"Wright",
|
||||||
|
"King",
|
||||||
|
"Scott",
|
||||||
|
"Green",
|
||||||
|
"Baker",
|
||||||
|
"Adams",
|
||||||
|
"Nelson",
|
||||||
|
"Hill",
|
||||||
|
"Ramirez",
|
||||||
|
"Campbell",
|
||||||
|
"Mitchell",
|
||||||
|
"Roberts",
|
||||||
|
"Carter",
|
||||||
|
"Phillips",
|
||||||
|
"Evans",
|
||||||
|
"Turner",
|
||||||
|
"Torres",
|
||||||
|
"Parker",
|
||||||
|
"Collins",
|
||||||
|
"Edwards",
|
||||||
|
"Stewart",
|
||||||
|
"Flores",
|
||||||
|
"Morris",
|
||||||
|
"Nguyen",
|
||||||
|
"Murphy",
|
||||||
|
"Rivera",
|
||||||
|
"Cook",
|
||||||
|
"Rogers",
|
||||||
|
"Morgan",
|
||||||
|
"Peterson",
|
||||||
|
"Cooper",
|
||||||
|
"Reed",
|
||||||
|
"Bailey",
|
||||||
|
"Bell",
|
||||||
|
"Gomez",
|
||||||
|
"Kelly",
|
||||||
|
"Howard",
|
||||||
|
"Ward",
|
||||||
|
"Cox",
|
||||||
|
"Diaz",
|
||||||
|
"Richardson",
|
||||||
|
"Wood",
|
||||||
|
"Watson",
|
||||||
|
"Brooks",
|
||||||
|
"Bennett",
|
||||||
|
"Gray",
|
||||||
|
"James",
|
||||||
|
"Reyes",
|
||||||
|
"Cruz",
|
||||||
|
"Hughes",
|
||||||
|
"Price",
|
||||||
|
"Myers",
|
||||||
|
"Long",
|
||||||
|
"Foster",
|
||||||
|
"Sanders",
|
||||||
|
"Ross",
|
||||||
|
"Morales",
|
||||||
|
"Powell",
|
||||||
|
"Sullivan",
|
||||||
|
"Russell",
|
||||||
|
"Ortiz",
|
||||||
|
"Jenkins",
|
||||||
|
"Perry",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const middleNames = [
|
||||||
|
"John",
|
||||||
|
"Jane",
|
||||||
|
"Michael",
|
||||||
|
"Sarah",
|
||||||
|
"David",
|
||||||
|
"Emma",
|
||||||
|
"Robert",
|
||||||
|
"Olivia",
|
||||||
|
"William",
|
||||||
|
"Sophia",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const phoneNumbers = [
|
||||||
|
"123-456-7890",
|
||||||
|
"987-654-3210",
|
||||||
|
"555-555-5555",
|
||||||
|
"111-222-3333",
|
||||||
|
"444-555-6666",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const departments = [
|
||||||
|
"IT",
|
||||||
|
"Marketing",
|
||||||
|
"Sales",
|
||||||
|
"Finance",
|
||||||
|
"Human Resources",
|
||||||
|
"Operations",
|
||||||
|
"Research and Development",
|
||||||
|
"Customer Support",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const jobTitles = [
|
||||||
|
"Software Engineer",
|
||||||
|
"Product Manager",
|
||||||
|
"Data Analyst",
|
||||||
|
"UX Designer",
|
||||||
|
"Marketing Specialist",
|
||||||
|
"HR Manager",
|
||||||
|
"Financial Analyst",
|
||||||
|
"Sales Representative",
|
||||||
|
"Customer Support",
|
||||||
|
"Operations Manager",
|
||||||
|
"Project Manager",
|
||||||
|
"Business Analyst",
|
||||||
|
"DevOps Engineer",
|
||||||
|
"Quality Assurance",
|
||||||
|
"Content Writer",
|
||||||
|
"Graphic Designer",
|
||||||
|
"Account Manager",
|
||||||
|
"System Administrator",
|
||||||
|
"Network Engineer",
|
||||||
|
"Database Administrator",
|
||||||
|
"Frontend Developer",
|
||||||
|
"Backend Developer",
|
||||||
|
"Full Stack Developer",
|
||||||
|
"Mobile Developer",
|
||||||
|
"UI Designer",
|
||||||
|
"SEO Specialist",
|
||||||
|
"Social Media Manager",
|
||||||
|
"Technical Writer",
|
||||||
|
"IT Support",
|
||||||
|
"Security Analyst",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const adminTodoTasks = [
|
||||||
|
{
|
||||||
|
title: "Review new user applications",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Update system documentation",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Prepare monthly report",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Investigate user complaint",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Schedule team meeting",
|
||||||
|
task: " lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Approve content submissions",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Update user permissions",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Review security logs",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Create training materials",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Follow up on escalated issues",
|
||||||
|
task: "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||||
|
},
|
||||||
|
];
|
||||||
+251
@@ -0,0 +1,251 @@
|
|||||||
|
import {
|
||||||
|
PrismaClient,
|
||||||
|
RiskStatus,
|
||||||
|
TodoStatus,
|
||||||
|
Role,
|
||||||
|
PriorityStatus,
|
||||||
|
SecurityType,
|
||||||
|
} from "@prisma/client";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
firstNames,
|
||||||
|
lastNames,
|
||||||
|
jobTitles,
|
||||||
|
actionVerbs,
|
||||||
|
subjects,
|
||||||
|
loremWords,
|
||||||
|
phoneNumbers,
|
||||||
|
middleNames,
|
||||||
|
departments,
|
||||||
|
} from "./mockData";
|
||||||
|
|
||||||
|
const sentenceFrom = (seed: number, min = 10, max = 22) => {
|
||||||
|
const len = min + (seed % (max - min + 1));
|
||||||
|
const words: string[] = [];
|
||||||
|
let i = seed;
|
||||||
|
for (let c = 0; c < len; c++) {
|
||||||
|
i = (i * 9301 + 49297) % 233280;
|
||||||
|
words.push(loremWords[i % loremWords.length]);
|
||||||
|
}
|
||||||
|
const s = words.join(" ");
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1) + ".";
|
||||||
|
};
|
||||||
|
|
||||||
|
const uniqueTitle = (idx: number) => {
|
||||||
|
const verb = actionVerbs[idx % actionVerbs.length];
|
||||||
|
const subj = subjects[(idx * 7) % subjects.length];
|
||||||
|
return `${verb} ${subj} #${idx + 1}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uniqueTask = (idx: number) => {
|
||||||
|
// Blend a seeded lorem sentence with a hint to ensure uniqueness
|
||||||
|
const hint = `${firstNames[idx % firstNames.length]} ${
|
||||||
|
lastNames[(idx * 3) % lastNames.length]
|
||||||
|
} ${jobTitles[(idx * 5) % jobTitles.length]}`;
|
||||||
|
return `${sentenceFrom(idx)}\n\nContext: ${hint} [T${idx + 1}]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const getRandom = <T>(arr: T[]): T =>
|
||||||
|
arr[Math.floor(Math.random() * arr.length)];
|
||||||
|
|
||||||
|
// Determine security type by role for consistent seed data
|
||||||
|
const securityFor = (role: Role): SecurityType => {
|
||||||
|
switch (role) {
|
||||||
|
case Role.MANAGER:
|
||||||
|
return SecurityType.MAXIMUM;
|
||||||
|
case Role.ADMIN:
|
||||||
|
return SecurityType.ASSISTANCE;
|
||||||
|
case Role.USER:
|
||||||
|
default:
|
||||||
|
return SecurityType.BASIC;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("🌱 Starting seed...");
|
||||||
|
await prisma.todo.deleteMany();
|
||||||
|
await prisma.user.deleteMany();
|
||||||
|
|
||||||
|
const users = [] as Array<Awaited<ReturnType<typeof prisma.user.create>>>;
|
||||||
|
|
||||||
|
const defaultPassword = await bcrypt.hash("password", 10);
|
||||||
|
|
||||||
|
const superAdmin = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: "Super",
|
||||||
|
middleName: "John Travolta",
|
||||||
|
lastName: "Admin",
|
||||||
|
email: "superadmin@example.com",
|
||||||
|
password: defaultPassword,
|
||||||
|
riskStatus: RiskStatus.NO_RISK,
|
||||||
|
phoneNumber: "1234567890",
|
||||||
|
department: "Admin",
|
||||||
|
imageUrl: null,
|
||||||
|
title: "Administrator",
|
||||||
|
isLocked: false,
|
||||||
|
isActivated: true,
|
||||||
|
role: Role.MANAGER,
|
||||||
|
securityType: securityFor(Role.MANAGER),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
users.push(superAdmin);
|
||||||
|
|
||||||
|
// Second MANAGER (Manager)
|
||||||
|
const managerSuperAdmin = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: "Manager",
|
||||||
|
middleName: "John Travolta",
|
||||||
|
lastName: "SuperAdmin",
|
||||||
|
email: "manager@example.com",
|
||||||
|
password: defaultPassword,
|
||||||
|
riskStatus: RiskStatus.NO_RISK,
|
||||||
|
phoneNumber: "1234567890",
|
||||||
|
department: "Admin",
|
||||||
|
imageUrl: null,
|
||||||
|
title: "Administrator",
|
||||||
|
isLocked: false,
|
||||||
|
isActivated: true,
|
||||||
|
role: Role.MANAGER,
|
||||||
|
securityType: securityFor(Role.MANAGER),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
users.push(managerSuperAdmin);
|
||||||
|
|
||||||
|
// Create 10 ADMIN users and split them between the two MANAGERs' teams
|
||||||
|
const teamAAdmins: Array<Awaited<ReturnType<typeof prisma.user.create>>> = [];
|
||||||
|
const teamBAdmins: Array<Awaited<ReturnType<typeof prisma.user.create>>> = [];
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
const assignedManagerId = i <= 5 ? superAdmin.id : managerSuperAdmin.id;
|
||||||
|
const adminUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName: `Admin${i}`,
|
||||||
|
middleName: "Admin",
|
||||||
|
lastName: "User",
|
||||||
|
email: `admin${i}@example.com`,
|
||||||
|
phoneNumber: `123456789${i}`,
|
||||||
|
department: "Admin",
|
||||||
|
password: defaultPassword,
|
||||||
|
riskStatus: RiskStatus.NO_RISK,
|
||||||
|
imageUrl: null,
|
||||||
|
title: "Administrator",
|
||||||
|
isLocked: false,
|
||||||
|
isActivated: true,
|
||||||
|
role: Role.ADMIN,
|
||||||
|
securityType: securityFor(Role.ADMIN),
|
||||||
|
managerId: assignedManagerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
users.push(adminUser);
|
||||||
|
if (assignedManagerId === superAdmin.id) {
|
||||||
|
teamAAdmins.push(adminUser);
|
||||||
|
} else {
|
||||||
|
teamBAdmins.push(adminUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const firstName = getRandom(firstNames);
|
||||||
|
const lastName = getRandom(lastNames);
|
||||||
|
const middleName = getRandom(middleNames);
|
||||||
|
const phoneNumber = getRandom(phoneNumbers);
|
||||||
|
const department = getRandom(departments);
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
firstName,
|
||||||
|
middleName,
|
||||||
|
lastName,
|
||||||
|
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${i}@example.com`,
|
||||||
|
phoneNumber,
|
||||||
|
department,
|
||||||
|
password: defaultPassword,
|
||||||
|
riskStatus: getRandom(Object.values(RiskStatus)),
|
||||||
|
imageUrl:
|
||||||
|
Math.random() > 0.5
|
||||||
|
? `https://i.pravatar.cc/150?u=${firstName}${lastName}${i}`
|
||||||
|
: null,
|
||||||
|
title: getRandom(jobTitles),
|
||||||
|
isLocked: false,
|
||||||
|
isActivated: true,
|
||||||
|
role: Role.USER,
|
||||||
|
securityType: securityFor(Role.USER),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build team pools (Manager + their Admins)
|
||||||
|
const teamA = [superAdmin, ...teamAAdmins];
|
||||||
|
const teamB = [managerSuperAdmin, ...teamBAdmins];
|
||||||
|
|
||||||
|
// Helper: get a random subset from an array (unique, size k)
|
||||||
|
const pickSubset = <T extends { id: string }>(arr: T[], k: number): T[] => {
|
||||||
|
const n = Math.max(1, Math.min(k, arr.length));
|
||||||
|
const pool = [...arr];
|
||||||
|
for (let i = pool.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[pool[i], pool[j]] = [pool[j], pool[i]];
|
||||||
|
}
|
||||||
|
return pool.slice(0, n);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create 50 todos split evenly between the two teams.
|
||||||
|
const totalTodos = 50;
|
||||||
|
const half = Math.floor(totalTodos / 2);
|
||||||
|
let todoIndex = 0;
|
||||||
|
|
||||||
|
const createTeamTodo = async (
|
||||||
|
team: Array<Awaited<ReturnType<typeof prisma.user.create>>>
|
||||||
|
) => {
|
||||||
|
const createdByUser = getRandom(team);
|
||||||
|
// 1-3 assignees from the same team (ensure createdBy is included)
|
||||||
|
const baseAssignees = pickSubset(
|
||||||
|
team,
|
||||||
|
1 + Math.floor(Math.random() * Math.min(3, team.length))
|
||||||
|
);
|
||||||
|
const ensureCreatedBy = baseAssignees.some((u) => u.id === createdByUser.id)
|
||||||
|
? baseAssignees
|
||||||
|
: [...baseAssignees, createdByUser];
|
||||||
|
const uniqueAssignees = Array.from(
|
||||||
|
new Map(ensureCreatedBy.map((u) => [u.id, u])).values()
|
||||||
|
);
|
||||||
|
|
||||||
|
const dueDate = new Date();
|
||||||
|
dueDate.setDate(dueDate.getDate() + Math.floor(Math.random() * 30));
|
||||||
|
|
||||||
|
await prisma.todo.create({
|
||||||
|
data: {
|
||||||
|
title: uniqueTitle(todoIndex),
|
||||||
|
task: uniqueTask(todoIndex),
|
||||||
|
status: getRandom(Object.values(TodoStatus)),
|
||||||
|
priority: getRandom(Object.values(PriorityStatus)),
|
||||||
|
dueDate,
|
||||||
|
createdBy: { connect: { id: createdByUser.id } },
|
||||||
|
assignees: { connect: uniqueAssignees.map((u) => ({ id: u.id })) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
todoIndex += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < half; i++) {
|
||||||
|
await createTeamTodo(teamA);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < totalTodos - half; i++) {
|
||||||
|
await createTeamTodo(teamB);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Seeded ${users.length} users and 50 todos`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("❌ Seed error:", e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { Response } from "express";
|
||||||
|
import { AuthenticatedRequest } from "../middleware/auth";
|
||||||
|
import { created, handleError, ok } from "../utils/http";
|
||||||
|
import * as todoService from "../services/todoService";
|
||||||
|
import { canAccessTodo, validateCommentAuthor } from "../services/permissions";
|
||||||
|
|
||||||
|
function getRequester(req: AuthenticatedRequest) {
|
||||||
|
const requesterId = req.user?.userId as string | undefined;
|
||||||
|
const requesterRole = req.user?.role as Role | undefined;
|
||||||
|
if (!requesterId || !requesterRole) return null;
|
||||||
|
return { id: requesterId, role: requesterRole } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTodo(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const { title, task, status, priority, dueDate, assigneeIds, createdById } = req.body as any;
|
||||||
|
const data = await todoService.createTodo({
|
||||||
|
title,
|
||||||
|
task,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
dueDate: new Date(dueDate),
|
||||||
|
createdById,
|
||||||
|
assigneeIds: (assigneeIds || []) as string[],
|
||||||
|
});
|
||||||
|
return created(res, data);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to create todo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTodos(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const requester = getRequester(req);
|
||||||
|
if (!requester) return handleError(res, "Unauthorized", "Missing user", 401);
|
||||||
|
|
||||||
|
const { items, total, page, limit } = await todoService.listTodos(
|
||||||
|
{
|
||||||
|
page: req.query.page as any,
|
||||||
|
limit: req.query.limit as any,
|
||||||
|
search: (req.query.search as string) || "",
|
||||||
|
statuses: (req.query.statuses as string) || "",
|
||||||
|
priorities: (req.query.priorities as string) || "",
|
||||||
|
sortBy: (req.query.sortBy as string) || "",
|
||||||
|
sortDir: (req.query.sortDir as string) || "",
|
||||||
|
assigneeUserId: (req.query.assigneeUserId as string) || "",
|
||||||
|
teamOfManagerId: (req.query.teamOfManagerId as string) || "",
|
||||||
|
},
|
||||||
|
requester
|
||||||
|
);
|
||||||
|
|
||||||
|
return ok(res, { items, total, page, limit });
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to fetch todos", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTodo(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const todoId = (req.params as any).todoId || (req.params as any).id;
|
||||||
|
const { title, task, status, priority, dueDate, assigneeIds } = req.body as any;
|
||||||
|
|
||||||
|
const data = await todoService.updateTodo(todoId, {
|
||||||
|
title,
|
||||||
|
task,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
dueDate: dueDate ? new Date(dueDate) : null,
|
||||||
|
assigneeIds: assigneeIds || undefined,
|
||||||
|
});
|
||||||
|
return ok(res, data);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to update todo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTodo(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const todoId = (req.params as any).todoId || (req.params as any).id;
|
||||||
|
|
||||||
|
// Check existence first
|
||||||
|
const existing = await todoService.getTodoAccessShape(todoId);
|
||||||
|
if (!existing) return handleError(res, "Todo not found", "Not found", 404);
|
||||||
|
|
||||||
|
await todoService.deleteTodo(todoId);
|
||||||
|
return ok(res, { message: "Todo deleted successfully" });
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to delete todo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listComments(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const requester = getRequester(req);
|
||||||
|
if (!requester) return handleError(res, "Unauthorized", "Missing user", 401);
|
||||||
|
|
||||||
|
const todoId = (req.params as any).todoId || (req.params as any).id;
|
||||||
|
const todo = await todoService.getTodoAccessShape(todoId);
|
||||||
|
if (!todo) return handleError(res, "Todo not found", "Not found", 404);
|
||||||
|
|
||||||
|
if (!canAccessTodo(requester, todo)) {
|
||||||
|
return handleError(res, "Forbidden", "Access denied", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await todoService.listComments(todoId);
|
||||||
|
return ok(res, comments);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to fetch comments", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addComment(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const requester = getRequester(req);
|
||||||
|
if (!requester) return handleError(res, "Unauthorized", "Missing user", 401);
|
||||||
|
|
||||||
|
const todoId = (req.params as any).todoId || (req.params as any).id;
|
||||||
|
const { content } = req.body as { content: string };
|
||||||
|
if (!content || !content.trim()) {
|
||||||
|
return handleError(res, "Content is required", "Validation error", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const todo = await todoService.getTodoAccessShape(todoId);
|
||||||
|
if (!todo) return handleError(res, "Todo not found", "Not found", 404);
|
||||||
|
|
||||||
|
if (!canAccessTodo(requester, todo)) {
|
||||||
|
return handleError(res, "Forbidden", "Access denied", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newComment = await todoService.addComment(todoId, requester.id, content.trim());
|
||||||
|
return created(res, newComment);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to add comment", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateComment(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const requester = getRequester(req);
|
||||||
|
if (!requester) return handleError(res, "Unauthorized", "Missing user", 401);
|
||||||
|
|
||||||
|
const todoId = (req.params as any).todoId || (req.params as any).id;
|
||||||
|
const { commentId } = req.params as any;
|
||||||
|
const { content } = req.body as { content: string };
|
||||||
|
if (!content || !content.trim()) {
|
||||||
|
return handleError(res, "Content is required", "Validation error", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const todo = await todoService.getTodoAccessShape(todoId);
|
||||||
|
if (!todo) return handleError(res, "Todo not found", "Not found", 404);
|
||||||
|
|
||||||
|
if (!canAccessTodo(requester, todo)) {
|
||||||
|
return handleError(res, "Forbidden", "Access denied", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await todoService.getCommentSlim(commentId);
|
||||||
|
if (!existing || existing.todoId !== todoId) {
|
||||||
|
return handleError(res, "Comment not found", "Not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCommentAuthor(requester.id, { userId: existing.userId });
|
||||||
|
|
||||||
|
const updated = await todoService.updateComment(commentId, content.trim());
|
||||||
|
return ok(res, updated);
|
||||||
|
} catch (error: any) {
|
||||||
|
const status = (error && typeof error === "object" && "status" in error ? (error as any).status : undefined) || 500;
|
||||||
|
return handleError(res, "Failed to update comment", error, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteComment(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const requester = getRequester(req);
|
||||||
|
if (!requester) return handleError(res, "Unauthorized", "Missing user", 401);
|
||||||
|
|
||||||
|
const todoId = (req.params as any).todoId || (req.params as any).id;
|
||||||
|
const { commentId } = req.params as any;
|
||||||
|
|
||||||
|
const todo = await todoService.getTodoAccessShape(todoId);
|
||||||
|
if (!todo) return handleError(res, "Todo not found", "Not found", 404);
|
||||||
|
|
||||||
|
if (!canAccessTodo(requester, todo)) {
|
||||||
|
return handleError(res, "Forbidden", "Access denied", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await todoService.getCommentSlim(commentId);
|
||||||
|
if (!existing || existing.todoId !== todoId) {
|
||||||
|
return handleError(res, "Comment not found", "Not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCommentAuthor(requester.id, { userId: existing.userId });
|
||||||
|
|
||||||
|
await todoService.deleteComment(commentId);
|
||||||
|
return ok(res, { message: "Comment deleted" });
|
||||||
|
} catch (error: any) {
|
||||||
|
const status = (error && typeof error === "object" && "status" in error ? (error as any).status : undefined) || 500;
|
||||||
|
return handleError(res, "Failed to delete comment", error, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTodosForUser(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const requester = getRequester(req);
|
||||||
|
if (!requester) return handleError(res, "Unauthorized", "Missing user", 401);
|
||||||
|
|
||||||
|
const { userId } = req.params as any;
|
||||||
|
const data = await todoService.getTodosForUser(userId, requester);
|
||||||
|
return ok(res, data);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(res, "Failed to fetch user todos", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
module.exports = { ...require('.') }
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
module.exports = { ...require('.') }
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./default"
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
PrismaClientKnownRequestError,
|
||||||
|
PrismaClientUnknownRequestError,
|
||||||
|
PrismaClientRustPanicError,
|
||||||
|
PrismaClientInitializationError,
|
||||||
|
PrismaClientValidationError,
|
||||||
|
getPrismaClient,
|
||||||
|
sqltag,
|
||||||
|
empty,
|
||||||
|
join,
|
||||||
|
raw,
|
||||||
|
skip,
|
||||||
|
Decimal,
|
||||||
|
Debug,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Extensions,
|
||||||
|
warnOnce,
|
||||||
|
defineDmmfProperty,
|
||||||
|
Public,
|
||||||
|
getRuntime,
|
||||||
|
createParam,
|
||||||
|
} = require('./runtime/edge.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.12.0
|
||||||
|
* Query Engine version: 8047c96bbd92db98a2abc7c9323ce77c02c89dbc
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "6.12.0",
|
||||||
|
engine: "8047c96bbd92db98a2abc7c9323ce77c02c89dbc"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||||
|
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
|
||||||
|
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
|
||||||
|
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
|
||||||
|
Prisma.PrismaClientValidationError = PrismaClientValidationError
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = sqltag
|
||||||
|
Prisma.empty = empty
|
||||||
|
Prisma.join = join
|
||||||
|
Prisma.raw = raw
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = Extensions.getExtensionContext
|
||||||
|
Prisma.defineExtension = Extensions.defineExtension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Create the Client
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
"generator": {
|
||||||
|
"name": "client",
|
||||||
|
"provider": {
|
||||||
|
"fromEnvVar": null,
|
||||||
|
"value": "prisma-client-js"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"value": "C:\\Users\\Sone\\Desktop\\projekti\\project-athena-workspace\\new-project\\AthenaBE\\src\\generated\\prisma",
|
||||||
|
"fromEnvVar": null
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"engineType": "library"
|
||||||
|
},
|
||||||
|
"binaryTargets": [
|
||||||
|
{
|
||||||
|
"fromEnvVar": null,
|
||||||
|
"value": "windows",
|
||||||
|
"native": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"previewFeatures": [],
|
||||||
|
"sourceFilePath": "C:\\Users\\Sone\\Desktop\\projekti\\project-athena-workspace\\new-project\\AthenaBE\\prisma\\schema.prisma",
|
||||||
|
"isCustomOutput": true
|
||||||
|
},
|
||||||
|
"relativeEnvPaths": {
|
||||||
|
"rootEnvPath": "../../../.env",
|
||||||
|
"schemaEnvPath": "../../../.env"
|
||||||
|
},
|
||||||
|
"relativePath": "../../../prisma",
|
||||||
|
"clientVersion": "6.12.0",
|
||||||
|
"engineVersion": "8047c96bbd92db98a2abc7c9323ce77c02c89dbc",
|
||||||
|
"datasourceNames": [
|
||||||
|
"db"
|
||||||
|
],
|
||||||
|
"activeProvider": "postgresql",
|
||||||
|
"postinstall": false,
|
||||||
|
"inlineDatasources": {
|
||||||
|
"db": {
|
||||||
|
"url": {
|
||||||
|
"fromEnvVar": "DATABASE_URL",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n",
|
||||||
|
"inlineSchemaHash": "f4defb510352aeb258e32fd0e4e744b44176032e921a554a415dd34c135bd53c",
|
||||||
|
"copyEngine": true
|
||||||
|
}
|
||||||
|
config.dirname = '/'
|
||||||
|
|
||||||
|
config.runtimeDataModel = JSON.parse("{\"models\":{},\"enums\":{},\"types\":{}}")
|
||||||
|
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
|
||||||
|
config.engineWasm = undefined
|
||||||
|
config.compilerWasm = undefined
|
||||||
|
|
||||||
|
config.injectableEdgeEnv = () => ({
|
||||||
|
parsed: {
|
||||||
|
DATABASE_URL: typeof globalThis !== 'undefined' && globalThis['DATABASE_URL'] || typeof process !== 'undefined' && process.env && process.env.DATABASE_URL || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined) {
|
||||||
|
Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrismaClient = getPrismaClient(config)
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
|
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
Decimal,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Public,
|
||||||
|
getRuntime,
|
||||||
|
skip
|
||||||
|
} = require('./runtime/index-browser.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.12.0
|
||||||
|
* Query Engine version: 8047c96bbd92db98a2abc7c9323ce77c02c89dbc
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "6.12.0",
|
||||||
|
engine: "8047c96bbd92db98a2abc7c9323ce77c02c89dbc"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)};
|
||||||
|
Prisma.PrismaClientUnknownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientRustPanicError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientInitializationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientValidationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.empty = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.join = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.raw = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.defineExtension = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub Prisma Client that will error at runtime if called.
|
||||||
|
*/
|
||||||
|
class PrismaClient {
|
||||||
|
constructor() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
let message
|
||||||
|
const runtime = getRuntime()
|
||||||
|
if (runtime.isEdge) {
|
||||||
|
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||||
|
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||||
|
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `
|
||||||
|
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
Vendored
+818
@@ -0,0 +1,818 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Client
|
||||||
|
**/
|
||||||
|
|
||||||
|
import * as runtime from './runtime/library.js';
|
||||||
|
import $Types = runtime.Types // general types
|
||||||
|
import $Public = runtime.Types.Public
|
||||||
|
import $Utils = runtime.Types.Utils
|
||||||
|
import $Extensions = runtime.Types.Extensions
|
||||||
|
import $Result = runtime.Types.Result
|
||||||
|
|
||||||
|
export type PrismaPromise<T> = $Public.PrismaPromise<T>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ## Prisma Client ʲˢ
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript & Node.js
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more Users
|
||||||
|
* const users = await prisma.user.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||||
|
*/
|
||||||
|
export class PrismaClient<
|
||||||
|
ClientOptions extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||||
|
U = 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never,
|
||||||
|
ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs
|
||||||
|
> {
|
||||||
|
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ## Prisma Client ʲˢ
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript & Node.js
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more Users
|
||||||
|
* const users = await prisma.user.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||||
|
*/
|
||||||
|
|
||||||
|
constructor(optionsArg ?: Prisma.Subset<ClientOptions, Prisma.PrismaClientOptions>);
|
||||||
|
$on<V extends U>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect with the database
|
||||||
|
*/
|
||||||
|
$connect(): $Utils.JsPromise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the database
|
||||||
|
*/
|
||||||
|
$disconnect(): $Utils.JsPromise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a middleware
|
||||||
|
* @deprecated since 4.16.0. For new code, prefer client extensions instead.
|
||||||
|
* @see https://pris.ly/d/extensions
|
||||||
|
*/
|
||||||
|
$use(cb: Prisma.Middleware): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a prepared raw query and returns the number of affected rows.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
|
*/
|
||||||
|
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a raw query and returns the number of affected rows.
|
||||||
|
* Susceptible to SQL injections, see documentation.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
|
*/
|
||||||
|
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a prepared raw query and returns the `SELECT` data.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
|
*/
|
||||||
|
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a raw query and returns the `SELECT` data.
|
||||||
|
* Susceptible to SQL injections, see documentation.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
|
*/
|
||||||
|
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const [george, bob, alice] = await prisma.$transaction([
|
||||||
|
* prisma.user.create({ data: { name: 'George' } }),
|
||||||
|
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||||
|
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||||
|
* ])
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||||
|
*/
|
||||||
|
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): $Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||||
|
|
||||||
|
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => $Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): $Utils.JsPromise<R>
|
||||||
|
|
||||||
|
|
||||||
|
$extends: $Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<ClientOptions>, ExtArgs, $Utils.Call<Prisma.TypeMapCb<ClientOptions>, {
|
||||||
|
extArgs: ExtArgs
|
||||||
|
}>>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Prisma {
|
||||||
|
export import DMMF = runtime.DMMF
|
||||||
|
|
||||||
|
export type PrismaPromise<T> = $Public.PrismaPromise<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator
|
||||||
|
*/
|
||||||
|
export import validator = runtime.Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Errors
|
||||||
|
*/
|
||||||
|
export import PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
|
||||||
|
export import PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
|
||||||
|
export import PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
|
||||||
|
export import PrismaClientInitializationError = runtime.PrismaClientInitializationError
|
||||||
|
export import PrismaClientValidationError = runtime.PrismaClientValidationError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
export import sql = runtime.sqltag
|
||||||
|
export import empty = runtime.empty
|
||||||
|
export import join = runtime.join
|
||||||
|
export import raw = runtime.raw
|
||||||
|
export import Sql = runtime.Sql
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decimal.js
|
||||||
|
*/
|
||||||
|
export import Decimal = runtime.Decimal
|
||||||
|
|
||||||
|
export type DecimalJsLike = runtime.DecimalJsLike
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics
|
||||||
|
*/
|
||||||
|
export type Metrics = runtime.Metrics
|
||||||
|
export type Metric<T> = runtime.Metric<T>
|
||||||
|
export type MetricHistogram = runtime.MetricHistogram
|
||||||
|
export type MetricHistogramBucket = runtime.MetricHistogramBucket
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
export import Extension = $Extensions.UserArgs
|
||||||
|
export import getExtensionContext = runtime.Extensions.getExtensionContext
|
||||||
|
export import Args = $Public.Args
|
||||||
|
export import Payload = $Public.Payload
|
||||||
|
export import Result = $Public.Result
|
||||||
|
export import Exact = $Public.Exact
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.12.0
|
||||||
|
* Query Engine version: 8047c96bbd92db98a2abc7c9323ce77c02c89dbc
|
||||||
|
*/
|
||||||
|
export type PrismaVersion = {
|
||||||
|
client: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prismaVersion: PrismaVersion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export import JsonObject = runtime.JsonObject
|
||||||
|
export import JsonArray = runtime.JsonArray
|
||||||
|
export import JsonValue = runtime.JsonValue
|
||||||
|
export import InputJsonObject = runtime.InputJsonObject
|
||||||
|
export import InputJsonArray = runtime.InputJsonArray
|
||||||
|
export import InputJsonValue = runtime.InputJsonValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of the values used to represent different kinds of `null` values when working with JSON fields.
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
namespace NullTypes {
|
||||||
|
/**
|
||||||
|
* Type of `Prisma.DbNull`.
|
||||||
|
*
|
||||||
|
* You cannot use other instances of this class. Please use the `Prisma.DbNull` value.
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
class DbNull {
|
||||||
|
private DbNull: never
|
||||||
|
private constructor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of `Prisma.JsonNull`.
|
||||||
|
*
|
||||||
|
* You cannot use other instances of this class. Please use the `Prisma.JsonNull` value.
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
class JsonNull {
|
||||||
|
private JsonNull: never
|
||||||
|
private constructor()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of `Prisma.AnyNull`.
|
||||||
|
*
|
||||||
|
* You cannot use other instances of this class. Please use the `Prisma.AnyNull` value.
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
class AnyNull {
|
||||||
|
private AnyNull: never
|
||||||
|
private constructor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const DbNull: NullTypes.DbNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const JsonNull: NullTypes.JsonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const AnyNull: NullTypes.AnyNull
|
||||||
|
|
||||||
|
type SelectAndInclude = {
|
||||||
|
select: any
|
||||||
|
include: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectAndOmit = {
|
||||||
|
select: any
|
||||||
|
omit: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of the value, that the Promise holds.
|
||||||
|
*/
|
||||||
|
export type PromiseType<T extends PromiseLike<any>> = T extends PromiseLike<infer U> ? U : T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the return type of a function which returns a Promise.
|
||||||
|
*/
|
||||||
|
export type PromiseReturnType<T extends (...args: any) => $Utils.JsPromise<any>> = PromiseType<ReturnType<T>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From T, pick a set of properties whose keys are in the union K
|
||||||
|
*/
|
||||||
|
type Prisma__Pick<T, K extends keyof T> = {
|
||||||
|
[P in K]: T[P];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type Enumerable<T> = T | Array<T>;
|
||||||
|
|
||||||
|
export type RequiredKeys<T> = {
|
||||||
|
[K in keyof T]-?: {} extends Prisma__Pick<T, K> ? never : K
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
export type TruthyKeys<T> = keyof {
|
||||||
|
[K in keyof T as T[K] extends false | undefined | null ? never : K]: K
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TrueKeys<T> = TruthyKeys<Prisma__Pick<T, RequiredKeys<T>>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset
|
||||||
|
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
|
||||||
|
*/
|
||||||
|
export type Subset<T, U> = {
|
||||||
|
[key in keyof T]: key extends keyof U ? T[key] : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectSubset
|
||||||
|
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
|
||||||
|
* Additionally, it validates, if both select and include are present. If the case, it errors.
|
||||||
|
*/
|
||||||
|
export type SelectSubset<T, U> = {
|
||||||
|
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||||
|
} &
|
||||||
|
(T extends SelectAndInclude
|
||||||
|
? 'Please either choose `select` or `include`.'
|
||||||
|
: T extends SelectAndOmit
|
||||||
|
? 'Please either choose `select` or `omit`.'
|
||||||
|
: {})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset + Intersection
|
||||||
|
* @desc From `T` pick properties that exist in `U` and intersect `K`
|
||||||
|
*/
|
||||||
|
export type SubsetIntersection<T, U, K> = {
|
||||||
|
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||||
|
} &
|
||||||
|
K
|
||||||
|
|
||||||
|
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XOR is needed to have a real mutually exclusive union type
|
||||||
|
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
|
||||||
|
*/
|
||||||
|
type XOR<T, U> =
|
||||||
|
T extends object ?
|
||||||
|
U extends object ?
|
||||||
|
(Without<T, U> & U) | (Without<U, T> & T)
|
||||||
|
: U : T
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is T a Record?
|
||||||
|
*/
|
||||||
|
type IsObject<T extends any> = T extends Array<any>
|
||||||
|
? False
|
||||||
|
: T extends Date
|
||||||
|
? False
|
||||||
|
: T extends Uint8Array
|
||||||
|
? False
|
||||||
|
: T extends BigInt
|
||||||
|
? False
|
||||||
|
: T extends object
|
||||||
|
? True
|
||||||
|
: False
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If it's T[], return T
|
||||||
|
*/
|
||||||
|
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From ts-toolbelt
|
||||||
|
*/
|
||||||
|
|
||||||
|
type __Either<O extends object, K extends Key> = Omit<O, K> &
|
||||||
|
{
|
||||||
|
// Merge all but K
|
||||||
|
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
|
||||||
|
}[K]
|
||||||
|
|
||||||
|
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
|
||||||
|
|
||||||
|
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
|
||||||
|
|
||||||
|
type _Either<
|
||||||
|
O extends object,
|
||||||
|
K extends Key,
|
||||||
|
strict extends Boolean
|
||||||
|
> = {
|
||||||
|
1: EitherStrict<O, K>
|
||||||
|
0: EitherLoose<O, K>
|
||||||
|
}[strict]
|
||||||
|
|
||||||
|
type Either<
|
||||||
|
O extends object,
|
||||||
|
K extends Key,
|
||||||
|
strict extends Boolean = 1
|
||||||
|
> = O extends unknown ? _Either<O, K, strict> : never
|
||||||
|
|
||||||
|
export type Union = any
|
||||||
|
|
||||||
|
type PatchUndefined<O extends object, O1 extends object> = {
|
||||||
|
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
|
||||||
|
} & {}
|
||||||
|
|
||||||
|
/** Helper Types for "Merge" **/
|
||||||
|
export type IntersectOf<U extends Union> = (
|
||||||
|
U extends unknown ? (k: U) => void : never
|
||||||
|
) extends (k: infer I) => void
|
||||||
|
? I
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type Overwrite<O extends object, O1 extends object> = {
|
||||||
|
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
|
||||||
|
} & {};
|
||||||
|
|
||||||
|
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
|
||||||
|
[K in keyof U]-?: At<U, K>;
|
||||||
|
}>>;
|
||||||
|
|
||||||
|
type Key = string | number | symbol;
|
||||||
|
type AtBasic<O extends object, K extends Key> = K extends keyof O ? O[K] : never;
|
||||||
|
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
|
||||||
|
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
|
||||||
|
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
|
||||||
|
1: AtStrict<O, K>;
|
||||||
|
0: AtLoose<O, K>;
|
||||||
|
}[strict];
|
||||||
|
|
||||||
|
export type ComputeRaw<A extends any> = A extends Function ? A : {
|
||||||
|
[K in keyof A]: A[K];
|
||||||
|
} & {};
|
||||||
|
|
||||||
|
export type OptionalFlat<O> = {
|
||||||
|
[K in keyof O]?: O[K];
|
||||||
|
} & {};
|
||||||
|
|
||||||
|
type _Record<K extends keyof any, T> = {
|
||||||
|
[P in K]: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
// cause typescript not to expand types and preserve names
|
||||||
|
type NoExpand<T> = T extends unknown ? T : never;
|
||||||
|
|
||||||
|
// this type assumes the passed object is entirely optional
|
||||||
|
type AtLeast<O extends object, K extends string> = NoExpand<
|
||||||
|
O extends unknown
|
||||||
|
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
|
||||||
|
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
|
||||||
|
: never>;
|
||||||
|
|
||||||
|
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
|
||||||
|
|
||||||
|
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
|
||||||
|
/** End Helper Types for "Merge" **/
|
||||||
|
|
||||||
|
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
A [[Boolean]]
|
||||||
|
*/
|
||||||
|
export type Boolean = True | False
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// 1
|
||||||
|
// */
|
||||||
|
export type True = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
0
|
||||||
|
*/
|
||||||
|
export type False = 0
|
||||||
|
|
||||||
|
export type Not<B extends Boolean> = {
|
||||||
|
0: 1
|
||||||
|
1: 0
|
||||||
|
}[B]
|
||||||
|
|
||||||
|
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
|
||||||
|
? 0 // anything `never` is false
|
||||||
|
: A1 extends A2
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
|
|
||||||
|
export type Has<U extends Union, U1 extends Union> = Not<
|
||||||
|
Extends<Exclude<U1, U>, U1>
|
||||||
|
>
|
||||||
|
|
||||||
|
export type Or<B1 extends Boolean, B2 extends Boolean> = {
|
||||||
|
0: {
|
||||||
|
0: 0
|
||||||
|
1: 1
|
||||||
|
}
|
||||||
|
1: {
|
||||||
|
0: 1
|
||||||
|
1: 1
|
||||||
|
}
|
||||||
|
}[B1][B2]
|
||||||
|
|
||||||
|
export type Keys<U extends Union> = U extends unknown ? keyof U : never
|
||||||
|
|
||||||
|
type Cast<A, B> = A extends B ? A : B;
|
||||||
|
|
||||||
|
export const type: unique symbol;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by group by
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type GetScalarType<T, O> = O extends object ? {
|
||||||
|
[P in keyof T]: P extends keyof O
|
||||||
|
? O[P]
|
||||||
|
: never
|
||||||
|
} : never
|
||||||
|
|
||||||
|
type FieldPaths<
|
||||||
|
T,
|
||||||
|
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
|
||||||
|
> = IsObject<T> extends True ? U : T
|
||||||
|
|
||||||
|
type GetHavingFields<T> = {
|
||||||
|
[K in keyof T]: Or<
|
||||||
|
Or<Extends<'OR', K>, Extends<'AND', K>>,
|
||||||
|
Extends<'NOT', K>
|
||||||
|
> extends True
|
||||||
|
? // infer is only needed to not hit TS limit
|
||||||
|
// based on the brilliant idea of Pierre-Antoine Mills
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
|
||||||
|
T[K] extends infer TK
|
||||||
|
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
|
||||||
|
: never
|
||||||
|
: {} extends FieldPaths<T[K]>
|
||||||
|
? never
|
||||||
|
: K
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert tuple to union
|
||||||
|
*/
|
||||||
|
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
|
||||||
|
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
|
||||||
|
type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `Pick`, but additionally can also accept an array of keys
|
||||||
|
*/
|
||||||
|
type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude all keys with underscores
|
||||||
|
*/
|
||||||
|
type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
|
||||||
|
|
||||||
|
|
||||||
|
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
|
||||||
|
|
||||||
|
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
|
||||||
|
|
||||||
|
|
||||||
|
export const ModelName: {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
|
|
||||||
|
|
||||||
|
export type Datasources = {
|
||||||
|
db?: Datasource
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypeMapCb<ClientOptions = {}> extends $Utils.Fn<{extArgs: $Extensions.InternalArgs }, $Utils.Record<string, any>> {
|
||||||
|
returns: Prisma.TypeMap<this['params']['extArgs'], ClientOptions extends { omit: infer OmitOptions } ? OmitOptions : {}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypeMap<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
|
||||||
|
globalOmitOptions: {
|
||||||
|
omit: GlobalOmitOptions
|
||||||
|
}
|
||||||
|
meta: {
|
||||||
|
modelProps: never
|
||||||
|
txIsolationLevel: Prisma.TransactionIsolationLevel
|
||||||
|
}
|
||||||
|
model: {}
|
||||||
|
} & {
|
||||||
|
other: {
|
||||||
|
payload: any
|
||||||
|
operations: {
|
||||||
|
$executeRaw: {
|
||||||
|
args: [query: TemplateStringsArray | Prisma.Sql, ...values: any[]],
|
||||||
|
result: any
|
||||||
|
}
|
||||||
|
$executeRawUnsafe: {
|
||||||
|
args: [query: string, ...values: any[]],
|
||||||
|
result: any
|
||||||
|
}
|
||||||
|
$queryRaw: {
|
||||||
|
args: [query: TemplateStringsArray | Prisma.Sql, ...values: any[]],
|
||||||
|
result: any
|
||||||
|
}
|
||||||
|
$queryRawUnsafe: {
|
||||||
|
args: [query: string, ...values: any[]],
|
||||||
|
result: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const defineExtension: $Extensions.ExtendsHook<"define", Prisma.TypeMapCb, $Extensions.DefaultArgs>
|
||||||
|
export type DefaultPrismaClient = PrismaClient
|
||||||
|
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
|
||||||
|
export interface PrismaClientOptions {
|
||||||
|
/**
|
||||||
|
* Overwrites the datasource url from your schema.prisma file
|
||||||
|
*/
|
||||||
|
datasources?: Datasources
|
||||||
|
/**
|
||||||
|
* Overwrites the datasource url from your schema.prisma file
|
||||||
|
*/
|
||||||
|
datasourceUrl?: string
|
||||||
|
/**
|
||||||
|
* @default "colorless"
|
||||||
|
*/
|
||||||
|
errorFormat?: ErrorFormat
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* // Defaults to stdout
|
||||||
|
* log: ['query', 'info', 'warn', 'error']
|
||||||
|
*
|
||||||
|
* // Emit as events
|
||||||
|
* log: [
|
||||||
|
* { emit: 'stdout', level: 'query' },
|
||||||
|
* { emit: 'stdout', level: 'info' },
|
||||||
|
* { emit: 'stdout', level: 'warn' }
|
||||||
|
* { emit: 'stdout', level: 'error' }
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/logging#the-log-option).
|
||||||
|
*/
|
||||||
|
log?: (LogLevel | LogDefinition)[]
|
||||||
|
/**
|
||||||
|
* The default values for transactionOptions
|
||||||
|
* maxWait ?= 2000
|
||||||
|
* timeout ?= 5000
|
||||||
|
*/
|
||||||
|
transactionOptions?: {
|
||||||
|
maxWait?: number
|
||||||
|
timeout?: number
|
||||||
|
isolationLevel?: Prisma.TransactionIsolationLevel
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Global configuration for omitting model fields by default.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient({
|
||||||
|
* omit: {
|
||||||
|
* user: {
|
||||||
|
* password: true
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
omit?: Prisma.GlobalOmitConfig
|
||||||
|
}
|
||||||
|
export type GlobalOmitConfig = {}
|
||||||
|
|
||||||
|
/* Types for Logging */
|
||||||
|
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
|
||||||
|
export type LogDefinition = {
|
||||||
|
level: LogLevel
|
||||||
|
emit: 'stdout' | 'event'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetLogType<T extends LogLevel | LogDefinition> = T extends LogDefinition ? T['emit'] extends 'event' ? T['level'] : never : never
|
||||||
|
export type GetEvents<T extends any> = T extends Array<LogLevel | LogDefinition> ?
|
||||||
|
GetLogType<T[0]> | GetLogType<T[1]> | GetLogType<T[2]> | GetLogType<T[3]>
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type QueryEvent = {
|
||||||
|
timestamp: Date
|
||||||
|
query: string
|
||||||
|
params: string
|
||||||
|
duration: number
|
||||||
|
target: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogEvent = {
|
||||||
|
timestamp: Date
|
||||||
|
message: string
|
||||||
|
target: string
|
||||||
|
}
|
||||||
|
/* End Types for Logging */
|
||||||
|
|
||||||
|
|
||||||
|
export type PrismaAction =
|
||||||
|
| 'findUnique'
|
||||||
|
| 'findUniqueOrThrow'
|
||||||
|
| 'findMany'
|
||||||
|
| 'findFirst'
|
||||||
|
| 'findFirstOrThrow'
|
||||||
|
| 'create'
|
||||||
|
| 'createMany'
|
||||||
|
| 'createManyAndReturn'
|
||||||
|
| 'update'
|
||||||
|
| 'updateMany'
|
||||||
|
| 'updateManyAndReturn'
|
||||||
|
| 'upsert'
|
||||||
|
| 'delete'
|
||||||
|
| 'deleteMany'
|
||||||
|
| 'executeRaw'
|
||||||
|
| 'queryRaw'
|
||||||
|
| 'aggregate'
|
||||||
|
| 'count'
|
||||||
|
| 'runCommandRaw'
|
||||||
|
| 'findRaw'
|
||||||
|
| 'groupBy'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These options are being passed into the middleware as "params"
|
||||||
|
*/
|
||||||
|
export type MiddlewareParams = {
|
||||||
|
model?: ModelName
|
||||||
|
action: PrismaAction
|
||||||
|
args: any
|
||||||
|
dataPath: string[]
|
||||||
|
runInTransaction: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `T` type makes sure, that the `return proceed` is not forgotten in the middleware implementation
|
||||||
|
*/
|
||||||
|
export type Middleware<T = any> = (
|
||||||
|
params: MiddlewareParams,
|
||||||
|
next: (params: MiddlewareParams) => $Utils.JsPromise<T>,
|
||||||
|
) => $Utils.JsPromise<T>
|
||||||
|
|
||||||
|
// tested in getLogLevel.test.ts
|
||||||
|
export function getLogLevel(log: Array<LogLevel | LogDefinition>): LogLevel | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `PrismaClient` proxy available in interactive transactions.
|
||||||
|
*/
|
||||||
|
export type TransactionClient = Omit<Prisma.DefaultPrismaClient, runtime.ITXClientDenyList>
|
||||||
|
|
||||||
|
export type Datasource = {
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TransactionIsolationLevel: {
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep Input Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
undefined
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch Payload for updateMany & deleteMany & createMany
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type BatchPayload = {
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMMF
|
||||||
|
*/
|
||||||
|
export const dmmf: runtime.BaseDMMF
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
PrismaClientKnownRequestError,
|
||||||
|
PrismaClientUnknownRequestError,
|
||||||
|
PrismaClientRustPanicError,
|
||||||
|
PrismaClientInitializationError,
|
||||||
|
PrismaClientValidationError,
|
||||||
|
getPrismaClient,
|
||||||
|
sqltag,
|
||||||
|
empty,
|
||||||
|
join,
|
||||||
|
raw,
|
||||||
|
skip,
|
||||||
|
Decimal,
|
||||||
|
Debug,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Extensions,
|
||||||
|
warnOnce,
|
||||||
|
defineDmmfProperty,
|
||||||
|
Public,
|
||||||
|
getRuntime,
|
||||||
|
createParam,
|
||||||
|
} = require('./runtime/library.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.12.0
|
||||||
|
* Query Engine version: 8047c96bbd92db98a2abc7c9323ce77c02c89dbc
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "6.12.0",
|
||||||
|
engine: "8047c96bbd92db98a2abc7c9323ce77c02c89dbc"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
|
||||||
|
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
|
||||||
|
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
|
||||||
|
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
|
||||||
|
Prisma.PrismaClientValidationError = PrismaClientValidationError
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = sqltag
|
||||||
|
Prisma.empty = empty
|
||||||
|
Prisma.join = join
|
||||||
|
Prisma.raw = raw
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = Extensions.getExtensionContext
|
||||||
|
Prisma.defineExtension = Extensions.defineExtension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Create the Client
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
"generator": {
|
||||||
|
"name": "client",
|
||||||
|
"provider": {
|
||||||
|
"fromEnvVar": null,
|
||||||
|
"value": "prisma-client-js"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"value": "C:\\Users\\Sone\\Desktop\\projekti\\project-athena-workspace\\new-project\\AthenaBE\\src\\generated\\prisma",
|
||||||
|
"fromEnvVar": null
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"engineType": "library"
|
||||||
|
},
|
||||||
|
"binaryTargets": [
|
||||||
|
{
|
||||||
|
"fromEnvVar": null,
|
||||||
|
"value": "windows",
|
||||||
|
"native": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"previewFeatures": [],
|
||||||
|
"sourceFilePath": "C:\\Users\\Sone\\Desktop\\projekti\\project-athena-workspace\\new-project\\AthenaBE\\prisma\\schema.prisma",
|
||||||
|
"isCustomOutput": true
|
||||||
|
},
|
||||||
|
"relativeEnvPaths": {
|
||||||
|
"rootEnvPath": "../../../.env",
|
||||||
|
"schemaEnvPath": "../../../.env"
|
||||||
|
},
|
||||||
|
"relativePath": "../../../prisma",
|
||||||
|
"clientVersion": "6.12.0",
|
||||||
|
"engineVersion": "8047c96bbd92db98a2abc7c9323ce77c02c89dbc",
|
||||||
|
"datasourceNames": [
|
||||||
|
"db"
|
||||||
|
],
|
||||||
|
"activeProvider": "postgresql",
|
||||||
|
"postinstall": false,
|
||||||
|
"inlineDatasources": {
|
||||||
|
"db": {
|
||||||
|
"url": {
|
||||||
|
"fromEnvVar": "DATABASE_URL",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n",
|
||||||
|
"inlineSchemaHash": "f4defb510352aeb258e32fd0e4e744b44176032e921a554a415dd34c135bd53c",
|
||||||
|
"copyEngine": true
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
config.dirname = __dirname
|
||||||
|
if (!fs.existsSync(path.join(__dirname, 'schema.prisma'))) {
|
||||||
|
const alternativePaths = [
|
||||||
|
"src/generated/prisma",
|
||||||
|
"generated/prisma",
|
||||||
|
]
|
||||||
|
|
||||||
|
const alternativePath = alternativePaths.find((altPath) => {
|
||||||
|
return fs.existsSync(path.join(process.cwd(), altPath, 'schema.prisma'))
|
||||||
|
}) ?? alternativePaths[0]
|
||||||
|
|
||||||
|
config.dirname = path.join(process.cwd(), alternativePath)
|
||||||
|
config.isBundled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
config.runtimeDataModel = JSON.parse("{\"models\":{},\"enums\":{},\"types\":{}}")
|
||||||
|
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
|
||||||
|
config.engineWasm = undefined
|
||||||
|
config.compilerWasm = undefined
|
||||||
|
|
||||||
|
|
||||||
|
const { warnEnvConflicts } = require('./runtime/library.js')
|
||||||
|
|
||||||
|
warnEnvConflicts({
|
||||||
|
rootEnvPath: config.relativeEnvPaths.rootEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.rootEnvPath),
|
||||||
|
schemaEnvPath: config.relativeEnvPaths.schemaEnvPath && path.resolve(config.dirname, config.relativeEnvPaths.schemaEnvPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const PrismaClient = getPrismaClient(config)
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
|
|
||||||
|
// file annotations for bundling tools to include these files
|
||||||
|
path.join(__dirname, "query_engine-windows.dll.node");
|
||||||
|
path.join(process.cwd(), "src/generated/prisma/query_engine-windows.dll.node")
|
||||||
|
// file annotations for bundling tools to include these files
|
||||||
|
path.join(__dirname, "schema.prisma");
|
||||||
|
path.join(process.cwd(), "src/generated/prisma/schema.prisma")
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"name": "prisma-client-2543993b427ae3445e0a04b169d588528a29c1dbe468991ee6aa323f569fdf45",
|
||||||
|
"main": "index.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"browser": "index-browser.js",
|
||||||
|
"exports": {
|
||||||
|
"./client": {
|
||||||
|
"require": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"require": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./edge": {
|
||||||
|
"types": "./edge.d.ts",
|
||||||
|
"require": "./edge.js",
|
||||||
|
"import": "./edge.js",
|
||||||
|
"default": "./edge.js"
|
||||||
|
},
|
||||||
|
"./react-native": {
|
||||||
|
"types": "./react-native.d.ts",
|
||||||
|
"require": "./react-native.js",
|
||||||
|
"import": "./react-native.js",
|
||||||
|
"default": "./react-native.js"
|
||||||
|
},
|
||||||
|
"./extension": {
|
||||||
|
"types": "./extension.d.ts",
|
||||||
|
"require": "./extension.js",
|
||||||
|
"import": "./extension.js",
|
||||||
|
"default": "./extension.js"
|
||||||
|
},
|
||||||
|
"./index-browser": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"require": "./index-browser.js",
|
||||||
|
"import": "./index-browser.js",
|
||||||
|
"default": "./index-browser.js"
|
||||||
|
},
|
||||||
|
"./index": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"require": "./index.js",
|
||||||
|
"import": "./index.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./wasm": {
|
||||||
|
"types": "./wasm.d.ts",
|
||||||
|
"require": "./wasm.js",
|
||||||
|
"import": "./wasm.mjs",
|
||||||
|
"default": "./wasm.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/client": {
|
||||||
|
"types": "./runtime/client.d.ts",
|
||||||
|
"node": {
|
||||||
|
"require": "./runtime/client.js",
|
||||||
|
"default": "./runtime/client.js"
|
||||||
|
},
|
||||||
|
"require": "./runtime/client.js",
|
||||||
|
"import": "./runtime/client.mjs",
|
||||||
|
"default": "./runtime/client.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/library": {
|
||||||
|
"types": "./runtime/library.d.ts",
|
||||||
|
"require": "./runtime/library.js",
|
||||||
|
"import": "./runtime/library.mjs",
|
||||||
|
"default": "./runtime/library.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/binary": {
|
||||||
|
"types": "./runtime/binary.d.ts",
|
||||||
|
"require": "./runtime/binary.js",
|
||||||
|
"import": "./runtime/binary.mjs",
|
||||||
|
"default": "./runtime/binary.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/wasm-engine-edge": {
|
||||||
|
"types": "./runtime/wasm-engine-edge.d.ts",
|
||||||
|
"require": "./runtime/wasm-engine-edge.js",
|
||||||
|
"import": "./runtime/wasm-engine-edge.mjs",
|
||||||
|
"default": "./runtime/wasm-engine-edge.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/wasm-compiler-edge": {
|
||||||
|
"types": "./runtime/wasm-compiler-edge.d.ts",
|
||||||
|
"require": "./runtime/wasm-compiler-edge.js",
|
||||||
|
"import": "./runtime/wasm-compiler-edge.mjs",
|
||||||
|
"default": "./runtime/wasm-compiler-edge.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/edge": {
|
||||||
|
"types": "./runtime/edge.d.ts",
|
||||||
|
"require": "./runtime/edge.js",
|
||||||
|
"import": "./runtime/edge-esm.js",
|
||||||
|
"default": "./runtime/edge-esm.js"
|
||||||
|
},
|
||||||
|
"./runtime/react-native": {
|
||||||
|
"types": "./runtime/react-native.d.ts",
|
||||||
|
"require": "./runtime/react-native.js",
|
||||||
|
"import": "./runtime/react-native.js",
|
||||||
|
"default": "./runtime/react-native.js"
|
||||||
|
},
|
||||||
|
"./generator-build": {
|
||||||
|
"require": "./generator-build/index.js",
|
||||||
|
"import": "./generator-build/index.js",
|
||||||
|
"default": "./generator-build/index.js"
|
||||||
|
},
|
||||||
|
"./sql": {
|
||||||
|
"require": {
|
||||||
|
"types": "./sql.d.ts",
|
||||||
|
"node": "./sql.js",
|
||||||
|
"default": "./sql.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./sql.d.ts",
|
||||||
|
"node": "./sql.mjs",
|
||||||
|
"default": "./sql.mjs"
|
||||||
|
},
|
||||||
|
"default": "./sql.js"
|
||||||
|
},
|
||||||
|
"./*": "./*"
|
||||||
|
},
|
||||||
|
"version": "6.12.0",
|
||||||
|
"sideEffects": false
|
||||||
|
}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+370
@@ -0,0 +1,370 @@
|
|||||||
|
declare class AnyNull extends NullTypesEnumValue {
|
||||||
|
#private;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Args<T, F extends Operation> = T extends {
|
||||||
|
[K: symbol]: {
|
||||||
|
types: {
|
||||||
|
operations: {
|
||||||
|
[K in F]: {
|
||||||
|
args: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} ? T[symbol]['types']['operations'][F]['args'] : any;
|
||||||
|
|
||||||
|
declare class DbNull extends NullTypesEnumValue {
|
||||||
|
#private;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function Decimal(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
export declare namespace Decimal {
|
||||||
|
export type Constructor = typeof Decimal;
|
||||||
|
export type Instance = Decimal;
|
||||||
|
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||||
|
export type Modulo = Rounding | 9;
|
||||||
|
export type Value = string | number | Decimal;
|
||||||
|
|
||||||
|
// http://mikemcl.github.io/decimal.js/#constructor-properties
|
||||||
|
export interface Config {
|
||||||
|
precision?: number;
|
||||||
|
rounding?: Rounding;
|
||||||
|
toExpNeg?: number;
|
||||||
|
toExpPos?: number;
|
||||||
|
minE?: number;
|
||||||
|
maxE?: number;
|
||||||
|
crypto?: boolean;
|
||||||
|
modulo?: Modulo;
|
||||||
|
defaults?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class Decimal {
|
||||||
|
readonly d: number[];
|
||||||
|
readonly e: number;
|
||||||
|
readonly s: number;
|
||||||
|
|
||||||
|
constructor(n: Decimal.Value);
|
||||||
|
|
||||||
|
absoluteValue(): Decimal;
|
||||||
|
abs(): Decimal;
|
||||||
|
|
||||||
|
ceil(): Decimal;
|
||||||
|
|
||||||
|
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
comparedTo(n: Decimal.Value): number;
|
||||||
|
cmp(n: Decimal.Value): number;
|
||||||
|
|
||||||
|
cosine(): Decimal;
|
||||||
|
cos(): Decimal;
|
||||||
|
|
||||||
|
cubeRoot(): Decimal;
|
||||||
|
cbrt(): Decimal;
|
||||||
|
|
||||||
|
decimalPlaces(): number;
|
||||||
|
dp(): number;
|
||||||
|
|
||||||
|
dividedBy(n: Decimal.Value): Decimal;
|
||||||
|
div(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
dividedToIntegerBy(n: Decimal.Value): Decimal;
|
||||||
|
divToInt(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
equals(n: Decimal.Value): boolean;
|
||||||
|
eq(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
floor(): Decimal;
|
||||||
|
|
||||||
|
greaterThan(n: Decimal.Value): boolean;
|
||||||
|
gt(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
greaterThanOrEqualTo(n: Decimal.Value): boolean;
|
||||||
|
gte(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
hyperbolicCosine(): Decimal;
|
||||||
|
cosh(): Decimal;
|
||||||
|
|
||||||
|
hyperbolicSine(): Decimal;
|
||||||
|
sinh(): Decimal;
|
||||||
|
|
||||||
|
hyperbolicTangent(): Decimal;
|
||||||
|
tanh(): Decimal;
|
||||||
|
|
||||||
|
inverseCosine(): Decimal;
|
||||||
|
acos(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicCosine(): Decimal;
|
||||||
|
acosh(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicSine(): Decimal;
|
||||||
|
asinh(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicTangent(): Decimal;
|
||||||
|
atanh(): Decimal;
|
||||||
|
|
||||||
|
inverseSine(): Decimal;
|
||||||
|
asin(): Decimal;
|
||||||
|
|
||||||
|
inverseTangent(): Decimal;
|
||||||
|
atan(): Decimal;
|
||||||
|
|
||||||
|
isFinite(): boolean;
|
||||||
|
|
||||||
|
isInteger(): boolean;
|
||||||
|
isInt(): boolean;
|
||||||
|
|
||||||
|
isNaN(): boolean;
|
||||||
|
|
||||||
|
isNegative(): boolean;
|
||||||
|
isNeg(): boolean;
|
||||||
|
|
||||||
|
isPositive(): boolean;
|
||||||
|
isPos(): boolean;
|
||||||
|
|
||||||
|
isZero(): boolean;
|
||||||
|
|
||||||
|
lessThan(n: Decimal.Value): boolean;
|
||||||
|
lt(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
lessThanOrEqualTo(n: Decimal.Value): boolean;
|
||||||
|
lte(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
logarithm(n?: Decimal.Value): Decimal;
|
||||||
|
log(n?: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
minus(n: Decimal.Value): Decimal;
|
||||||
|
sub(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
modulo(n: Decimal.Value): Decimal;
|
||||||
|
mod(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
naturalExponential(): Decimal;
|
||||||
|
exp(): Decimal;
|
||||||
|
|
||||||
|
naturalLogarithm(): Decimal;
|
||||||
|
ln(): Decimal;
|
||||||
|
|
||||||
|
negated(): Decimal;
|
||||||
|
neg(): Decimal;
|
||||||
|
|
||||||
|
plus(n: Decimal.Value): Decimal;
|
||||||
|
add(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
precision(includeZeros?: boolean): number;
|
||||||
|
sd(includeZeros?: boolean): number;
|
||||||
|
|
||||||
|
round(): Decimal;
|
||||||
|
|
||||||
|
sine() : Decimal;
|
||||||
|
sin() : Decimal;
|
||||||
|
|
||||||
|
squareRoot(): Decimal;
|
||||||
|
sqrt(): Decimal;
|
||||||
|
|
||||||
|
tangent() : Decimal;
|
||||||
|
tan() : Decimal;
|
||||||
|
|
||||||
|
times(n: Decimal.Value): Decimal;
|
||||||
|
mul(n: Decimal.Value) : Decimal;
|
||||||
|
|
||||||
|
toBinary(significantDigits?: number): string;
|
||||||
|
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toDecimalPlaces(decimalPlaces?: number): Decimal;
|
||||||
|
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
toDP(decimalPlaces?: number): Decimal;
|
||||||
|
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toExponential(decimalPlaces?: number): string;
|
||||||
|
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toFixed(decimalPlaces?: number): string;
|
||||||
|
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toFraction(max_denominator?: Decimal.Value): Decimal[];
|
||||||
|
|
||||||
|
toHexadecimal(significantDigits?: number): string;
|
||||||
|
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
toHex(significantDigits?: number): string;
|
||||||
|
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toJSON(): string;
|
||||||
|
|
||||||
|
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toNumber(): number;
|
||||||
|
|
||||||
|
toOctal(significantDigits?: number): string;
|
||||||
|
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toPower(n: Decimal.Value): Decimal;
|
||||||
|
pow(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
toPrecision(significantDigits?: number): string;
|
||||||
|
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toSignificantDigits(significantDigits?: number): Decimal;
|
||||||
|
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
toSD(significantDigits?: number): Decimal;
|
||||||
|
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toString(): string;
|
||||||
|
|
||||||
|
truncated(): Decimal;
|
||||||
|
trunc(): Decimal;
|
||||||
|
|
||||||
|
valueOf(): string;
|
||||||
|
|
||||||
|
static abs(n: Decimal.Value): Decimal;
|
||||||
|
static acos(n: Decimal.Value): Decimal;
|
||||||
|
static acosh(n: Decimal.Value): Decimal;
|
||||||
|
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static asin(n: Decimal.Value): Decimal;
|
||||||
|
static asinh(n: Decimal.Value): Decimal;
|
||||||
|
static atan(n: Decimal.Value): Decimal;
|
||||||
|
static atanh(n: Decimal.Value): Decimal;
|
||||||
|
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
|
||||||
|
static cbrt(n: Decimal.Value): Decimal;
|
||||||
|
static ceil(n: Decimal.Value): Decimal;
|
||||||
|
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
static clone(object?: Decimal.Config): Decimal.Constructor;
|
||||||
|
static config(object: Decimal.Config): Decimal.Constructor;
|
||||||
|
static cos(n: Decimal.Value): Decimal;
|
||||||
|
static cosh(n: Decimal.Value): Decimal;
|
||||||
|
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static exp(n: Decimal.Value): Decimal;
|
||||||
|
static floor(n: Decimal.Value): Decimal;
|
||||||
|
static hypot(...n: Decimal.Value[]): Decimal;
|
||||||
|
static isDecimal(object: any): object is Decimal;
|
||||||
|
static ln(n: Decimal.Value): Decimal;
|
||||||
|
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
|
||||||
|
static log2(n: Decimal.Value): Decimal;
|
||||||
|
static log10(n: Decimal.Value): Decimal;
|
||||||
|
static max(...n: Decimal.Value[]): Decimal;
|
||||||
|
static min(...n: Decimal.Value[]): Decimal;
|
||||||
|
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static noConflict(): Decimal.Constructor; // Browser only
|
||||||
|
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
|
||||||
|
static random(significantDigits?: number): Decimal;
|
||||||
|
static round(n: Decimal.Value): Decimal;
|
||||||
|
static set(object: Decimal.Config): Decimal.Constructor;
|
||||||
|
static sign(n: Decimal.Value): number;
|
||||||
|
static sin(n: Decimal.Value): Decimal;
|
||||||
|
static sinh(n: Decimal.Value): Decimal;
|
||||||
|
static sqrt(n: Decimal.Value): Decimal;
|
||||||
|
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static sum(...n: Decimal.Value[]): Decimal;
|
||||||
|
static tan(n: Decimal.Value): Decimal;
|
||||||
|
static tanh(n: Decimal.Value): Decimal;
|
||||||
|
static trunc(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
static readonly default?: Decimal.Constructor;
|
||||||
|
static readonly Decimal?: Decimal.Constructor;
|
||||||
|
|
||||||
|
static readonly precision: number;
|
||||||
|
static readonly rounding: Decimal.Rounding;
|
||||||
|
static readonly toExpNeg: number;
|
||||||
|
static readonly toExpPos: number;
|
||||||
|
static readonly minE: number;
|
||||||
|
static readonly maxE: number;
|
||||||
|
static readonly crypto: boolean;
|
||||||
|
static readonly modulo: Decimal.Modulo;
|
||||||
|
|
||||||
|
static readonly ROUND_UP: 0;
|
||||||
|
static readonly ROUND_DOWN: 1;
|
||||||
|
static readonly ROUND_CEIL: 2;
|
||||||
|
static readonly ROUND_FLOOR: 3;
|
||||||
|
static readonly ROUND_HALF_UP: 4;
|
||||||
|
static readonly ROUND_HALF_DOWN: 5;
|
||||||
|
static readonly ROUND_HALF_EVEN: 6;
|
||||||
|
static readonly ROUND_HALF_CEIL: 7;
|
||||||
|
static readonly ROUND_HALF_FLOOR: 8;
|
||||||
|
static readonly EUCLID: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
|
||||||
|
[K in keyof A]: Exact<A[K], W[K]>;
|
||||||
|
} : W) : never) | (A extends Narrowable ? A : never);
|
||||||
|
|
||||||
|
export declare function getRuntime(): GetRuntimeOutput;
|
||||||
|
|
||||||
|
declare type GetRuntimeOutput = {
|
||||||
|
id: RuntimeName;
|
||||||
|
prettyName: string;
|
||||||
|
isEdge: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class JsonNull extends NullTypesEnumValue {
|
||||||
|
#private;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates more strict variant of an enum which, unlike regular enum,
|
||||||
|
* throws on non-existing property access. This can be useful in following situations:
|
||||||
|
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
|
||||||
|
* - enum values are generated dynamically from DMMF.
|
||||||
|
*
|
||||||
|
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
|
||||||
|
* will result in `undefined` value being used, which will be accepted. Using strict enum
|
||||||
|
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
|
||||||
|
*
|
||||||
|
* Note: if you need to check for existence of a value in the enum you can still use either
|
||||||
|
* `in` operator or `hasOwnProperty` function.
|
||||||
|
*
|
||||||
|
* @param definition
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
|
||||||
|
|
||||||
|
declare type Narrowable = string | number | bigint | boolean | [];
|
||||||
|
|
||||||
|
declare class NullTypesEnumValue extends ObjectEnumValue {
|
||||||
|
_getNamespace(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for unique values of object-valued enums.
|
||||||
|
*/
|
||||||
|
declare abstract class ObjectEnumValue {
|
||||||
|
constructor(arg?: symbol);
|
||||||
|
abstract _getNamespace(): string;
|
||||||
|
_getName(): string;
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare const objectEnumValues: {
|
||||||
|
classes: {
|
||||||
|
DbNull: typeof DbNull;
|
||||||
|
JsonNull: typeof JsonNull;
|
||||||
|
AnyNull: typeof AnyNull;
|
||||||
|
};
|
||||||
|
instances: {
|
||||||
|
DbNull: DbNull;
|
||||||
|
JsonNull: JsonNull;
|
||||||
|
AnyNull: AnyNull;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
|
||||||
|
|
||||||
|
declare namespace Public {
|
||||||
|
export {
|
||||||
|
validator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { Public }
|
||||||
|
|
||||||
|
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
|
||||||
|
|
||||||
|
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
||||||
|
|
||||||
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
||||||
|
|
||||||
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
|
||||||
|
|
||||||
|
export { }
|
||||||
File diff suppressed because one or more lines are too long
+3687
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+83
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
output = "../src/generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
Decimal,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Public,
|
||||||
|
getRuntime,
|
||||||
|
skip
|
||||||
|
} = require('./runtime/index-browser.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.12.0
|
||||||
|
* Query Engine version: 8047c96bbd92db98a2abc7c9323ce77c02c89dbc
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "6.12.0",
|
||||||
|
engine: "8047c96bbd92db98a2abc7c9323ce77c02c89dbc"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)};
|
||||||
|
Prisma.PrismaClientUnknownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientRustPanicError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientInitializationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientValidationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.empty = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.join = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.raw = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.defineExtension = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub Prisma Client that will error at runtime if called.
|
||||||
|
*/
|
||||||
|
class PrismaClient {
|
||||||
|
constructor() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
let message
|
||||||
|
const runtime = getRuntime()
|
||||||
|
if (runtime.isEdge) {
|
||||||
|
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||||
|
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||||
|
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `
|
||||||
|
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import cors from "cors";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
import todosRoutes from "./routes/todos/todos.routes";
|
||||||
|
import authRoutes from "./routes/auth/auth.routes";
|
||||||
|
import usersRoutes from "./routes/users/users.routes";
|
||||||
|
import searchRoutes from "./routes/search/search.routes";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: true,
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.use("/api/auth", authRoutes);
|
||||||
|
app.use("/api/todos", todosRoutes);
|
||||||
|
app.use("/api/users", usersRoutes);
|
||||||
|
app.use("/api/search", searchRoutes);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server started on port ${port}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
|
const smtpHost = process.env.SMTP_HOST || "";
|
||||||
|
const smtpPort = Number(process.env.SMTP_PORT || 587);
|
||||||
|
const smtpUser = process.env.SMTP_USER || "";
|
||||||
|
const smtpPass = process.env.SMTP_PASS || "";
|
||||||
|
const smtpFrom = process.env.SMTP_FROM || "no-reply@example.com";
|
||||||
|
|
||||||
|
let transporter: nodemailer.Transporter | null = null;
|
||||||
|
|
||||||
|
export const getTransporter = () => {
|
||||||
|
if (!transporter) {
|
||||||
|
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
host: smtpHost,
|
||||||
|
port: smtpPort,
|
||||||
|
secure: smtpPort === 465,
|
||||||
|
auth: { user: smtpUser, pass: smtpPass },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return transporter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendResetPasswordEmail = async (to: string, link: string) => {
|
||||||
|
const t = getTransporter();
|
||||||
|
const subject = "Reset your Project Athena password";
|
||||||
|
const html = `
|
||||||
|
<p>You requested to reset your Project Athena password.</p>
|
||||||
|
<p>Click the link below to set a new password. If you didn't request this, you can ignore this email.</p>
|
||||||
|
<p><a href="${link}">Reset Password</a></p>
|
||||||
|
<p>This link will expire shortly for security reasons.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await t?.sendMail({ from: smtpFrom, to, subject, html });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendInvitationEmail = async (to: string, link: string) => {
|
||||||
|
const t = getTransporter();
|
||||||
|
const subject = "You're invited to Project Athena";
|
||||||
|
const html = `
|
||||||
|
<p>You have been invited to join Project Athena.</p>
|
||||||
|
<p>Click the link below to set your password and activate your account.</p>
|
||||||
|
<p><a href="${link}">Accept Invitation</a></p>
|
||||||
|
<p>This link will expire shortly for security reasons.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await t?.sendMail({ from: smtpFrom, to, subject, html });
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import jwt, { type Secret, type SignOptions } from "jsonwebtoken";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
|
||||||
|
const JWT_SECRET: Secret = (process.env.JWT_SECRET || "dev-secret-change-me") as Secret;
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
role: Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const signJwt = (
|
||||||
|
payload: JwtPayload,
|
||||||
|
options: SignOptions = { expiresIn: "7d" }
|
||||||
|
) => {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyJwt = (token: string): JwtPayload => {
|
||||||
|
return jwt.verify(token, JWT_SECRET) as JwtPayload;
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import prisma from "../prismaClient";
|
||||||
|
import { randomBytes, createHash } from "crypto";
|
||||||
|
|
||||||
|
const APP_BASE_URL = process.env.APP_BASE_URL || "http://localhost:8081";
|
||||||
|
const RESET_TOKEN_TTL = Number(process.env.RESET_TOKEN_TTL || 3600);
|
||||||
|
|
||||||
|
export const generateRawToken = (bytes = 32) => {
|
||||||
|
return randomBytes(bytes).toString("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hashToken = (raw: string) => {
|
||||||
|
return createHash("sha256").update(raw).digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPasswordResetToken = async (userId: string) => {
|
||||||
|
const rawToken = generateRawToken();
|
||||||
|
const tokenHash = hashToken(rawToken);
|
||||||
|
const expiresAt = new Date(Date.now() + RESET_TOKEN_TTL * 1000);
|
||||||
|
|
||||||
|
// Ensure single active token per user (optional)
|
||||||
|
await prisma.passwordResetToken.deleteMany({ where: { userId } });
|
||||||
|
|
||||||
|
await prisma.passwordResetToken.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
tokenHash,
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = `${APP_BASE_URL.replace(
|
||||||
|
/\/$/,
|
||||||
|
""
|
||||||
|
)}/reset-password?token=${rawToken}`;
|
||||||
|
return { rawToken, link, expiresAt };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const consumePasswordResetToken = async (rawToken: string) => {
|
||||||
|
const tokenHash = hashToken(rawToken);
|
||||||
|
const rec = await prisma.passwordResetToken.findUnique({
|
||||||
|
where: { tokenHash },
|
||||||
|
});
|
||||||
|
if (!rec) return null;
|
||||||
|
if (rec.usedAt) return null;
|
||||||
|
if (rec.expiresAt.getTime() < Date.now()) return null;
|
||||||
|
return rec; // Caller should mark used
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markTokenUsed = async (tokenHash: string) => {
|
||||||
|
await prisma.passwordResetToken.update({
|
||||||
|
where: { tokenHash },
|
||||||
|
data: { usedAt: new Date() },
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { NextFunction, Response } from "express";
|
||||||
|
import { Request } from "express-serve-static-core";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
|
||||||
|
import { verifyJwt, JwtPayload } from "../lib/jwt";
|
||||||
|
|
||||||
|
export interface AuthenticatedRequest extends Request {
|
||||||
|
user?: JwtPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requireAuth = (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
const auth = req.headers.authorization || "";
|
||||||
|
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: "Missing Authorization token" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const payload = verifyJwt(token);
|
||||||
|
req.user = payload;
|
||||||
|
return next();
|
||||||
|
} catch (e: any) {
|
||||||
|
return res.status(401).json({ error: "Invalid token" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireRole = (allowed: Role[]) => {
|
||||||
|
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
const userRole = req.user.role as Role | undefined;
|
||||||
|
if (!userRole || !allowed.includes(userRole)) {
|
||||||
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { AuthenticatedRequest } from "../../middleware/auth";
|
||||||
|
import { authService } from "./auth.service";
|
||||||
|
import { handleError } from "../../utils/http";
|
||||||
|
|
||||||
|
export class AuthController {
|
||||||
|
async register(_req: Request, res: Response) {
|
||||||
|
// Maintain existing behavior: registration disabled
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Registration is disabled",
|
||||||
|
message:
|
||||||
|
"Only admins or managers can invite new users. Please use your invitation link to set your password.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body || {};
|
||||||
|
const result = await authService.login(email, password);
|
||||||
|
return res.json(result);
|
||||||
|
} catch (e: any) {
|
||||||
|
const code = typeof e.code === "number" ? e.code : 500;
|
||||||
|
return handleError(res, e.message || "Login failed", e, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async me(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const user = await authService.getCurrentUser(userId);
|
||||||
|
return res.json(user);
|
||||||
|
} catch (e: any) {
|
||||||
|
const code = typeof e.code === "number" ? e.code : 500;
|
||||||
|
return handleError(res, e.message || "Failed to fetch current user", e, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async forgotPassword(req: Request, res: Response) {
|
||||||
|
const { email } = req.body || {};
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ error: "Email is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always respond with success, action is best-effort inside
|
||||||
|
await authService.forgotPassword(email).catch(() => {});
|
||||||
|
return res.json({
|
||||||
|
message: "If an account exists for this email, a reset link was sent.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPassword(req: Request, res: Response) {
|
||||||
|
const { token, password } = req.body || {};
|
||||||
|
if (!token || !password) {
|
||||||
|
return res.status(400).json({ error: "Missing token or password" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authService.resetPassword(token, password);
|
||||||
|
return res.json({ message: "Password has been reset" });
|
||||||
|
} catch (e: any) {
|
||||||
|
const code = typeof e.code === "number" ? e.code : 400;
|
||||||
|
return handleError(res, e.message || "Invalid or expired token", e, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authController = new AuthController();
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { requireAuth } from "../../middleware/auth";
|
||||||
|
import { authController } from "./auth.controller";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Registration is disabled (kept for parity with previous route)
|
||||||
|
router.post("/register", (req, res) => authController.register(req, res));
|
||||||
|
|
||||||
|
// Login
|
||||||
|
router.post("/login", (req, res) => authController.login(req, res));
|
||||||
|
|
||||||
|
// Current user
|
||||||
|
router.get("/me", requireAuth, (req, res) => authController.me(req as any, res));
|
||||||
|
|
||||||
|
// Password reset: request
|
||||||
|
router.post("/password/forgot", (req, res) => authController.forgotPassword(req, res));
|
||||||
|
|
||||||
|
// Password reset: consume
|
||||||
|
router.post("/password/reset", (req, res) => authController.resetPassword(req, res));
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
|
||||||
|
import prisma from "../../prismaClient";
|
||||||
|
import { signJwt } from "../../lib/jwt";
|
||||||
|
import {
|
||||||
|
createPasswordResetToken,
|
||||||
|
consumePasswordResetToken,
|
||||||
|
} from "../../lib/passwordReset";
|
||||||
|
import { sendResetPasswordEmail } from "../../lib/email";
|
||||||
|
|
||||||
|
export class AuthService {
|
||||||
|
async registerDisabled() {
|
||||||
|
const error: any = new Error("Registration is disabled");
|
||||||
|
error.code = 403;
|
||||||
|
error.messageExtended =
|
||||||
|
"Only managers can invite new admins. Please use your invitation link to set your password.";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(email?: string, password?: string) {
|
||||||
|
if (!email || !password) {
|
||||||
|
const error: any = new Error("Missing email or password");
|
||||||
|
error.code = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({ where: { email } });
|
||||||
|
|
||||||
|
if (!user || user.role === Role.USER) {
|
||||||
|
const error: any = new Error("Invalid credentials");
|
||||||
|
error.code = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isActivated) {
|
||||||
|
const error: any = new Error(
|
||||||
|
"Account is not activated. Please use your invitation link to set a password."
|
||||||
|
);
|
||||||
|
error.code = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isLocked) {
|
||||||
|
const error: any = new Error(
|
||||||
|
"Account is locked due to suspicious activity. Please contact an administrator."
|
||||||
|
);
|
||||||
|
error.code = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await bcrypt.compare(password, user.password);
|
||||||
|
if (!ok) {
|
||||||
|
const error: any = new Error("Invalid credentials");
|
||||||
|
error.code = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = signJwt({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
});
|
||||||
|
return { token, user };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentUser(userId: string) {
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
|
if (!user) {
|
||||||
|
const error: any = new Error("User not found");
|
||||||
|
error.code = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async forgotPassword(email: string) {
|
||||||
|
const user = await prisma.user.findUnique({ where: { email } });
|
||||||
|
if (user && user.role !== Role.USER) {
|
||||||
|
try {
|
||||||
|
const { link } = await createPasswordResetToken(user.id);
|
||||||
|
await sendResetPasswordEmail(email, link);
|
||||||
|
} catch (_e) {
|
||||||
|
// swallow to avoid enumeration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// always resolve without throwing
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPassword(token: string, password: string) {
|
||||||
|
const rec = await consumePasswordResetToken(token);
|
||||||
|
if (!rec) {
|
||||||
|
const error: any = new Error("Invalid or expired token");
|
||||||
|
error.code = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: rec.userId } });
|
||||||
|
if (!user) {
|
||||||
|
const error: any = new Error("Invalid or expired token");
|
||||||
|
error.code = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await bcrypt.hash(password, 10);
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { password: hash, isActivated: true },
|
||||||
|
}),
|
||||||
|
prisma.passwordResetToken.update({
|
||||||
|
where: { tokenHash: rec.tokenHash },
|
||||||
|
data: { usedAt: new Date() },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authService = new AuthService();
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { AuthenticatedRequest } from "../../middleware/auth";
|
||||||
|
import { searchService } from "./search.service";
|
||||||
|
import { handleError } from "../../utils/http";
|
||||||
|
|
||||||
|
export class SearchController {
|
||||||
|
async quick(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const q = String(req.query.q || "").trim();
|
||||||
|
const limit = parseInt(String(req.query.limit ?? "8"), 10) || 8;
|
||||||
|
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as Role | undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as string | undefined;
|
||||||
|
|
||||||
|
const result = await searchService.quickSearch(q, limit, requesterRole, requesterId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleError(res, "Search failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async full(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const q = String(req.query.q || "").trim();
|
||||||
|
const page = Math.max(parseInt(String(req.query.page ?? "1"), 10) || 1, 1);
|
||||||
|
const limit = parseInt(String(req.query.limit ?? "25"), 10) || 25;
|
||||||
|
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as Role | undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as string | undefined;
|
||||||
|
|
||||||
|
const result = await searchService.fullSearch(q, page, limit, requesterRole, requesterId);
|
||||||
|
res.json(result);
|
||||||
|
} catch (err: any) {
|
||||||
|
return handleError(res, "Search failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchController = new SearchController();
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { requireAuth, requireRole } from "../../middleware/auth";
|
||||||
|
import { searchController } from "./search.controller";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// All search endpoints require authentication
|
||||||
|
router.use(requireAuth);
|
||||||
|
|
||||||
|
// GET /api/search/quick?q=...&limit=8
|
||||||
|
router.get(
|
||||||
|
"/quick",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
(req, res) => searchController.quick(req, res)
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/search?q=...&page=1&limit=25
|
||||||
|
router.get(
|
||||||
|
"/",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
(req, res) => searchController.full(req, res)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import prisma from "../../prismaClient";
|
||||||
|
|
||||||
|
export type SearchType = "user" | "admin" | "todo";
|
||||||
|
|
||||||
|
export interface SearchResultItem {
|
||||||
|
id: string;
|
||||||
|
type: SearchType;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
description?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
score: number;
|
||||||
|
meta?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleLabel = (role: string): string => {
|
||||||
|
switch (role) {
|
||||||
|
case "MANAGER":
|
||||||
|
return "Manager";
|
||||||
|
case "ADMIN":
|
||||||
|
return "Admin";
|
||||||
|
case "USER":
|
||||||
|
return "User";
|
||||||
|
default:
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const riskLabel = (risk: string): string => {
|
||||||
|
switch (risk) {
|
||||||
|
case "HIGH_RISK":
|
||||||
|
return "High Risk";
|
||||||
|
case "MODERATE_RISK":
|
||||||
|
return "Moderate Risk";
|
||||||
|
case "LOW_RISK":
|
||||||
|
return "Low Risk";
|
||||||
|
case "NO_RISK":
|
||||||
|
return "No Risk";
|
||||||
|
default:
|
||||||
|
return risk;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const todoStatusLabel = (status: string): string => {
|
||||||
|
switch (status) {
|
||||||
|
case "NEW":
|
||||||
|
return "New";
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "In Progress";
|
||||||
|
case "COMPLETED":
|
||||||
|
return "Completed";
|
||||||
|
case "ARCHIVED":
|
||||||
|
return "Archived";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityLabel = (priority: string): string => {
|
||||||
|
switch (priority) {
|
||||||
|
case "HIGH_PRIORITY":
|
||||||
|
return "High Priority";
|
||||||
|
case "MODERATE_PRIORITY":
|
||||||
|
return "Moderate Priority";
|
||||||
|
case "LOW_PRIORITY":
|
||||||
|
return "Low Priority";
|
||||||
|
default:
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenize = (query: string) => {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
const tokens = q.split(/\s+/).filter(Boolean);
|
||||||
|
return { q, tokens, exact: q };
|
||||||
|
};
|
||||||
|
|
||||||
|
const scoreText = (text: string, q: string, tokens: string[]): number => {
|
||||||
|
const hay = (text || "").toLowerCase();
|
||||||
|
if (!hay) return 0;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (hay.includes(q)) {
|
||||||
|
score += 80;
|
||||||
|
if (hay.startsWith(q)) score += 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distinct = Array.from(new Set(tokens));
|
||||||
|
const found = distinct.filter((t) => hay.includes(t)).length;
|
||||||
|
if (distinct.length) {
|
||||||
|
score += Math.round((found / distinct.length) * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
for (let i = 0; i < tokens.length - 1; i++) {
|
||||||
|
const pair = tokens[i] + " " + tokens[i + 1];
|
||||||
|
if (hay.includes(pair)) score += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clamp = (n: number, min: number, max: number) =>
|
||||||
|
Math.max(min, Math.min(max, n));
|
||||||
|
|
||||||
|
export class SearchService {
|
||||||
|
async quickSearch(
|
||||||
|
query: string,
|
||||||
|
limitRaw: number,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
const q = String(query || "").trim();
|
||||||
|
const limit = clamp(parseInt(String(limitRaw || 8), 10) || 8, 1, 20);
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return { results: [] as SearchResultItem[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tokens } = tokenize(q);
|
||||||
|
|
||||||
|
const usersCandidates = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ firstName: { contains: q, mode: "insensitive" } },
|
||||||
|
{ lastName: { contains: q, mode: "insensitive" } },
|
||||||
|
{ email: { contains: q, mode: "insensitive" } },
|
||||||
|
{ title: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
title: true,
|
||||||
|
role: true,
|
||||||
|
riskStatus: true,
|
||||||
|
},
|
||||||
|
take: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
const todoWhere: any = {
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: q, mode: "insensitive" } },
|
||||||
|
{ task: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (requesterRole === Role.MANAGER && requesterId) {
|
||||||
|
todoWhere.AND = [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { id: requesterId } } },
|
||||||
|
{ assignees: { some: { managerId: requesterId } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (requesterRole === Role.ADMIN && requesterId) {
|
||||||
|
todoWhere.AND = [{ assignees: { some: { id: requesterId } } }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const todosCandidates = await prisma.todo.findMany({
|
||||||
|
where: todoWhere,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
task: true,
|
||||||
|
status: true,
|
||||||
|
priority: true,
|
||||||
|
createdAt: true,
|
||||||
|
dueDate: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userResults: SearchResultItem[] = usersCandidates.map((u) => {
|
||||||
|
const fullName = `${u.firstName} ${u.lastName}`.trim();
|
||||||
|
const titleScore =
|
||||||
|
scoreText(fullName, q, tokens) + scoreText(u.email, q, tokens);
|
||||||
|
const descScore = scoreText(u.title || "", q, tokens);
|
||||||
|
const base = titleScore + descScore;
|
||||||
|
const type: SearchType =
|
||||||
|
u.role === "ADMIN" || u.role === "MANAGER" ? "admin" : "user";
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
type,
|
||||||
|
title: fullName || u.email,
|
||||||
|
subtitle: `${u.email} • ${u.title ?? ""}`.trim(),
|
||||||
|
description: `Role: ${roleLabel(u.role)} • Risk: ${riskLabel(
|
||||||
|
u.riskStatus
|
||||||
|
)}`,
|
||||||
|
score: base,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const todoResults: SearchResultItem[] = todosCandidates.map((t) => {
|
||||||
|
const base = scoreText(t.title, q, tokens) + scoreText(t.task, q, tokens);
|
||||||
|
const createdAt = t.createdAt?.toISOString?.() ?? undefined;
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
type: "todo",
|
||||||
|
title: t.title,
|
||||||
|
subtitle: `Status: ${todoStatusLabel(
|
||||||
|
t.status
|
||||||
|
)} • Priority: ${priorityLabel(t.priority)}`,
|
||||||
|
description: t.task?.slice(0, 160) ?? undefined,
|
||||||
|
createdAt,
|
||||||
|
score: base,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const combined = [...userResults, ...todoResults]
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
return { results: combined };
|
||||||
|
}
|
||||||
|
|
||||||
|
async fullSearch(
|
||||||
|
query: string,
|
||||||
|
pageRaw: number,
|
||||||
|
limitRaw: number,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
const q = String(query || "").trim();
|
||||||
|
const page = Math.max(parseInt(String(pageRaw || 1), 10) || 1, 1);
|
||||||
|
const limit = clamp(parseInt(String(limitRaw || 25), 10) || 25, 1, 100);
|
||||||
|
|
||||||
|
if (!q || q.length < 2) {
|
||||||
|
return { results: [] as SearchResultItem[], total: 0, page, limit };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tokens } = tokenize(q);
|
||||||
|
|
||||||
|
const userCandidates = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ firstName: { contains: q, mode: "insensitive" } },
|
||||||
|
{ lastName: { contains: q, mode: "insensitive" } },
|
||||||
|
{ email: { contains: q, mode: "insensitive" } },
|
||||||
|
{ title: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
title: true,
|
||||||
|
role: true,
|
||||||
|
riskStatus: true,
|
||||||
|
isLocked: true,
|
||||||
|
imageUrl: true,
|
||||||
|
},
|
||||||
|
take: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const todoWhere: any = {
|
||||||
|
OR: [
|
||||||
|
{ title: { contains: q, mode: "insensitive" } },
|
||||||
|
{ task: { contains: q, mode: "insensitive" } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
if (requesterRole === Role.MANAGER && requesterId) {
|
||||||
|
todoWhere.AND = [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { id: requesterId } } },
|
||||||
|
{ assignees: { some: { managerId: requesterId } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (requesterRole === Role.ADMIN && requesterId) {
|
||||||
|
todoWhere.AND = [{ assignees: { some: { id: requesterId } } }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const todoCandidates = await prisma.todo.findMany({
|
||||||
|
where: todoWhere,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
task: true,
|
||||||
|
status: true,
|
||||||
|
priority: true,
|
||||||
|
createdAt: true,
|
||||||
|
dueDate: true,
|
||||||
|
createdBy: { select: { id: true, firstName: true, lastName: true } },
|
||||||
|
assignees: { select: { id: true, firstName: true, lastName: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userResults: SearchResultItem[] = userCandidates.map((u) => {
|
||||||
|
const fullName = `${u.firstName} ${u.lastName}`.trim();
|
||||||
|
const titleScore =
|
||||||
|
scoreText(fullName, q, tokens) + scoreText(u.email, q, tokens);
|
||||||
|
const descScore = scoreText(u.title || "", q, tokens);
|
||||||
|
const base = titleScore + descScore;
|
||||||
|
const type: SearchType =
|
||||||
|
u.role === "ADMIN" || u.role === "MANAGER" ? "admin" : "user";
|
||||||
|
const meta = { imageUrl: u.imageUrl, isLocked: u.isLocked };
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
type,
|
||||||
|
title: fullName || u.email,
|
||||||
|
subtitle: `${u.email} • ${u.title ?? ""}`.trim(),
|
||||||
|
description: `Role: ${roleLabel(u.role)} • Risk: ${riskLabel(
|
||||||
|
u.riskStatus
|
||||||
|
)}`,
|
||||||
|
score: base,
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const todoResults: SearchResultItem[] = todoCandidates.map((t) => {
|
||||||
|
const base = scoreText(t.title, q, tokens) + scoreText(t.task, q, tokens);
|
||||||
|
const createdAt = t.createdAt?.toISOString?.() ?? undefined;
|
||||||
|
const assignees = t.assignees
|
||||||
|
.map((a) => `${a.firstName} ${a.lastName}`.trim())
|
||||||
|
.join(", ");
|
||||||
|
const createdBy = t.createdBy
|
||||||
|
? `${t.createdBy.firstName} ${t.createdBy.lastName}`.trim()
|
||||||
|
: "";
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
type: "todo",
|
||||||
|
title: t.title,
|
||||||
|
subtitle: `Status: ${todoStatusLabel(
|
||||||
|
t.status
|
||||||
|
)} • Priority: ${priorityLabel(t.priority)} • Due: ${
|
||||||
|
t.dueDate?.toISOString?.().slice(0, 10) ?? ""
|
||||||
|
}`,
|
||||||
|
description: t.task?.slice(0, 240) ?? undefined,
|
||||||
|
createdAt,
|
||||||
|
score: base,
|
||||||
|
meta: { assignees, createdBy },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const combined = [...userResults, ...todoResults].sort((a, b) => {
|
||||||
|
if (b.score !== a.score) return b.score - a.score;
|
||||||
|
const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
|
||||||
|
const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
|
||||||
|
return bTime - aTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = combined.length;
|
||||||
|
const start = (page - 1) * limit;
|
||||||
|
const end = start + limit;
|
||||||
|
const paged = combined.slice(start, end);
|
||||||
|
|
||||||
|
return { results: paged, total, page, limit };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchService = new SearchService();
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { AuthenticatedRequest } from "../../middleware/auth";
|
||||||
|
import { todoService, CreateTodoData, UpdateTodoData } from "./todos.service";
|
||||||
|
import { handleError } from "../../utils/http";
|
||||||
|
|
||||||
|
export class TodoController {
|
||||||
|
async createTodo(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const data: CreateTodoData = {
|
||||||
|
title: req.body.title,
|
||||||
|
task: req.body.task,
|
||||||
|
status: req.body.status,
|
||||||
|
priority: req.body.priority,
|
||||||
|
dueDate: req.body.dueDate,
|
||||||
|
createdById: req.body.createdById,
|
||||||
|
assigneeIds: req.body.assigneeIds || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const todo = await todoService.createTodo(data);
|
||||||
|
res.status(201).json(todo);
|
||||||
|
} catch (error: any) {
|
||||||
|
return handleError(res, "Failed to create todo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTodos(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: Math.max(parseInt((req.query.page as string) || "1", 10), 1),
|
||||||
|
limit: Math.max(parseInt((req.query.limit as string) || "25", 10), 1),
|
||||||
|
search: ((req.query.search as string) || "").trim(),
|
||||||
|
statuses: ((req.query.statuses as string) || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => !!s),
|
||||||
|
priorities: ((req.query.priorities as string) || "")
|
||||||
|
.split(",")
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter((p) => !!p),
|
||||||
|
sortBy: req.query.sortBy || "",
|
||||||
|
sortDir: (((req.query.sortDir as string) || "asc").toLowerCase() ===
|
||||||
|
"desc"
|
||||||
|
? "desc"
|
||||||
|
: "asc") as "asc" | "desc",
|
||||||
|
assigneeUserId: (req.query.assigneeUserId as string) || undefined,
|
||||||
|
teamOfManagerId: (req.query.teamOfManagerId as string) || undefined,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as
|
||||||
|
| Role
|
||||||
|
| undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const result = await todoService.listTodos(
|
||||||
|
params,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
res.json({
|
||||||
|
items: result.items,
|
||||||
|
total: result.total,
|
||||||
|
page: params.page,
|
||||||
|
limit: params.limit,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return handleError(res, "Failed to fetch todos", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserTodos(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params as { userId: string };
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as
|
||||||
|
| Role
|
||||||
|
| undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const todos = await todoService.getUserTodos(
|
||||||
|
userId,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
res.json(todos);
|
||||||
|
} catch (error: any) {
|
||||||
|
return handleError(res, "Failed to fetch user todos", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTodo(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const data: UpdateTodoData = {
|
||||||
|
title: req.body.title,
|
||||||
|
task: req.body.task,
|
||||||
|
status: req.body.status,
|
||||||
|
priority: req.body.priority,
|
||||||
|
dueDate: req.body.dueDate,
|
||||||
|
assigneeIds: req.body.assigneeIds,
|
||||||
|
};
|
||||||
|
|
||||||
|
const todo = await todoService.updateTodo(id, data);
|
||||||
|
res.json(todo);
|
||||||
|
} catch (error: any) {
|
||||||
|
return handleError(res, "Failed to update todo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTodo(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
await todoService.deleteTodo(id);
|
||||||
|
res.status(200).json({ message: "Todo deleted successfully" });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "NOT_FOUND") {
|
||||||
|
return handleError(res, "Todo not found", error, 404);
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to delete todo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listComments(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params as { id: string }; // todoId
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as
|
||||||
|
| Role
|
||||||
|
| undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const comments = await todoService.listComments(
|
||||||
|
id,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
res.json(comments);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "FORBIDDEN") {
|
||||||
|
return handleError(res, "Forbidden", error, 403);
|
||||||
|
}
|
||||||
|
if (error.code === "NOT_FOUND") {
|
||||||
|
return handleError(res, "Todo not found", error, 404);
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to fetch comments", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addComment(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params as { id: string }; // todoId
|
||||||
|
const { content } = req.body as { content: string };
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as
|
||||||
|
| Role
|
||||||
|
| undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const newComment = await todoService.addComment(
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
res.status(201).json(newComment);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "BAD_REQUEST") {
|
||||||
|
return handleError(res, "Content is required", error, 400);
|
||||||
|
}
|
||||||
|
if (error.code === "FORBIDDEN") {
|
||||||
|
return handleError(res, "Forbidden", error, 403);
|
||||||
|
}
|
||||||
|
if (error.code === "NOT_FOUND") {
|
||||||
|
return handleError(res, "Todo not found", error, 404);
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to add comment", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateComment(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { todoId, commentId } = req.params as {
|
||||||
|
todoId: string;
|
||||||
|
commentId: string;
|
||||||
|
};
|
||||||
|
const { content } = req.body as { content: string };
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as
|
||||||
|
| Role
|
||||||
|
| undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const updated = await todoService.updateComment(
|
||||||
|
todoId,
|
||||||
|
commentId,
|
||||||
|
content,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
res.json(updated);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "BAD_REQUEST") {
|
||||||
|
return handleError(res, "Content is required", error, 400);
|
||||||
|
}
|
||||||
|
if (error.code === "FORBIDDEN") {
|
||||||
|
return handleError(res, "Forbidden", error, 403);
|
||||||
|
}
|
||||||
|
if (error.code === "NOT_FOUND") {
|
||||||
|
return handleError(res, "Comment not found", error, 404);
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to update comment", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteComment(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { todoId, commentId } = req.params as {
|
||||||
|
todoId: string;
|
||||||
|
commentId: string;
|
||||||
|
};
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as
|
||||||
|
| Role
|
||||||
|
| undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
await todoService.deleteComment(
|
||||||
|
todoId,
|
||||||
|
commentId,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
res.status(200).json({ message: "Comment deleted" });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "FORBIDDEN") {
|
||||||
|
return handleError(res, "Forbidden", error, 403);
|
||||||
|
}
|
||||||
|
if (error.code === "NOT_FOUND") {
|
||||||
|
return handleError(res, "Comment not found", error, 404);
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to delete comment", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const todoController = new TodoController();
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { requireAuth, requireRole } from "../../middleware/auth";
|
||||||
|
import { todoController } from "./todos.controller";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// All todo endpoints require authentication
|
||||||
|
router.use(requireAuth);
|
||||||
|
|
||||||
|
// Create todo
|
||||||
|
router.post("/", requireRole([Role.ADMIN, Role.MANAGER]), (req, res) => todoController.createTodo(req, res));
|
||||||
|
|
||||||
|
// List todos with filters/sort/pagination + role-based visibility
|
||||||
|
router.get("/", requireRole([Role.ADMIN, Role.MANAGER]), (req, res) => todoController.listTodos(req, res));
|
||||||
|
|
||||||
|
// Get todos for a specific user (role-based visibility)
|
||||||
|
router.get("/user/:userId", requireRole([Role.ADMIN, Role.MANAGER]), (req, res) => todoController.getUserTodos(req, res));
|
||||||
|
|
||||||
|
// Update todo
|
||||||
|
router.put("/:id", requireRole([Role.ADMIN, Role.MANAGER]), (req, res) => todoController.updateTodo(req, res));
|
||||||
|
|
||||||
|
// Delete todo
|
||||||
|
router.delete("/:id", requireRole([Role.ADMIN, Role.MANAGER]), (req, res) => todoController.deleteTodo(req, res));
|
||||||
|
|
||||||
|
// Comments: list
|
||||||
|
router.get(
|
||||||
|
"/:id/comments",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
(req, res) => todoController.listComments(req, res)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Comments: add
|
||||||
|
router.post(
|
||||||
|
"/:id/comments",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
(req, res) => todoController.addComment(req, res)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Comments: update (author only)
|
||||||
|
router.put(
|
||||||
|
"/:todoId/comments/:commentId",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
(req, res) => todoController.updateComment(req, res)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Comments: delete (author only)
|
||||||
|
router.delete(
|
||||||
|
"/:todoId/comments/:commentId",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
(req, res) => todoController.deleteComment(req, res)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
import { TodoStatus, PriorityStatus, Role } from "@prisma/client";
|
||||||
|
import prisma from "../../prismaClient";
|
||||||
|
|
||||||
|
export interface TodoListParams {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
search: string;
|
||||||
|
statuses: TodoStatus[];
|
||||||
|
priorities: PriorityStatus[];
|
||||||
|
sortBy: "" | "title" | "dueDate" | "status" | "priority" | "createdAt";
|
||||||
|
sortDir: "asc" | "desc";
|
||||||
|
assigneeUserId?: string;
|
||||||
|
teamOfManagerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTodoData {
|
||||||
|
title: string;
|
||||||
|
task?: string;
|
||||||
|
status: TodoStatus;
|
||||||
|
priority: PriorityStatus;
|
||||||
|
dueDate: Date | string;
|
||||||
|
createdById: string;
|
||||||
|
assigneeIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTodoData {
|
||||||
|
title?: string;
|
||||||
|
task?: string;
|
||||||
|
status?: TodoStatus;
|
||||||
|
priority?: PriorityStatus;
|
||||||
|
dueDate?: Date | string | null;
|
||||||
|
assigneeIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TodoService {
|
||||||
|
async createTodo(data: CreateTodoData) {
|
||||||
|
const { title, task, status, priority, dueDate, createdById, assigneeIds } =
|
||||||
|
data;
|
||||||
|
|
||||||
|
// Validate: at least one admin or manager must be assigned
|
||||||
|
if (!assigneeIds || assigneeIds.length === 0) {
|
||||||
|
const err: any = new Error("At least one admin or manager is required");
|
||||||
|
err.code = "BAD_REQUEST";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const supervisorsCount = await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
id: { in: assigneeIds },
|
||||||
|
role: { in: [Role.ADMIN, Role.MANAGER] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (supervisorsCount === 0) {
|
||||||
|
const err: any = new Error("At least one admin or manager is required");
|
||||||
|
err.code = "BAD_REQUEST";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const todo = await prisma.todo.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
// Prisma schema requires task: String (non-nullable)
|
||||||
|
task: task ?? "",
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
// Prisma schema requires dueDate (non-nullable), so ensure it's always provided
|
||||||
|
dueDate: new Date(dueDate),
|
||||||
|
createdBy: { connect: { id: createdById } },
|
||||||
|
assignees: { connect: assigneeIds.map((id) => ({ id })) },
|
||||||
|
},
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return todo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildVisibilityAndWhere(
|
||||||
|
params: TodoListParams,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
if (params.search) {
|
||||||
|
where.OR = [
|
||||||
|
{ title: { contains: params.search, mode: "insensitive" } },
|
||||||
|
{ task: { contains: params.search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.statuses.length > 0) {
|
||||||
|
where.status = { in: params.statuses };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.priorities.length > 0) {
|
||||||
|
where.priority = { in: params.priorities };
|
||||||
|
}
|
||||||
|
|
||||||
|
const andClauses: any[] = [];
|
||||||
|
if (params.assigneeUserId) {
|
||||||
|
andClauses.push({ assignees: { some: { id: params.assigneeUserId } } });
|
||||||
|
}
|
||||||
|
if (params.teamOfManagerId) {
|
||||||
|
andClauses.push({
|
||||||
|
assignees: { some: { managerId: params.teamOfManagerId } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requesterRole === Role.ADMIN && requesterId) {
|
||||||
|
andClauses.push({ assignees: { some: { id: requesterId } } });
|
||||||
|
} else if (requesterRole === Role.MANAGER && requesterId) {
|
||||||
|
andClauses.push({
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { id: requesterId } } },
|
||||||
|
{ assignees: { some: { managerId: requesterId } } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (andClauses.length) {
|
||||||
|
where.AND = [...(where.AND || []), ...andClauses];
|
||||||
|
}
|
||||||
|
|
||||||
|
let orderBy: any = undefined;
|
||||||
|
if (params.sortBy) {
|
||||||
|
if (params.sortBy === "title") orderBy = { title: params.sortDir };
|
||||||
|
else if (params.sortBy === "dueDate")
|
||||||
|
orderBy = { dueDate: params.sortDir };
|
||||||
|
else if (params.sortBy === "status") orderBy = { status: params.sortDir };
|
||||||
|
else if (params.sortBy === "priority")
|
||||||
|
orderBy = { priority: params.sortDir };
|
||||||
|
else if (params.sortBy === "createdAt")
|
||||||
|
orderBy = { createdAt: params.sortDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { where, orderBy };
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTodos(
|
||||||
|
params: TodoListParams,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
): Promise<{ items: any[]; total: number }> {
|
||||||
|
const { where, orderBy } = this.buildVisibilityAndWhere(
|
||||||
|
params,
|
||||||
|
requesterRole,
|
||||||
|
requesterId
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = await prisma.todo.count({ where });
|
||||||
|
const items = await prisma.todo.findMany({
|
||||||
|
where,
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
orderBy,
|
||||||
|
skip: (params.page - 1) * params.limit,
|
||||||
|
take: params.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTodo(id: string, data: UpdateTodoData) {
|
||||||
|
// If assigneeIds were provided in update, enforce at least one admin among them
|
||||||
|
if (typeof data.assigneeIds !== "undefined") {
|
||||||
|
const ids = data.assigneeIds || [];
|
||||||
|
if (ids.length === 0) {
|
||||||
|
const err: any = new Error("At least one admin or manager is required");
|
||||||
|
err.code = "BAD_REQUEST";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const supervisorsCount = await prisma.user.count({
|
||||||
|
where: { id: { in: ids }, role: { in: [Role.ADMIN, Role.MANAGER] } },
|
||||||
|
});
|
||||||
|
if (supervisorsCount === 0) {
|
||||||
|
const err: any = new Error("At least one admin or manager is required");
|
||||||
|
err.code = "BAD_REQUEST";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const todo = await prisma.todo.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
task: data.task,
|
||||||
|
status: data.status,
|
||||||
|
priority: data.priority,
|
||||||
|
// Only include fields when provided; avoid explicitly setting undefined
|
||||||
|
...(typeof data.dueDate === "undefined"
|
||||||
|
? {}
|
||||||
|
: data.dueDate === null
|
||||||
|
? {}
|
||||||
|
: { dueDate: new Date(data.dueDate) }),
|
||||||
|
...(typeof data.assigneeIds !== "undefined"
|
||||||
|
? { assignees: { set: data.assigneeIds.map((i) => ({ id: i })) } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
});
|
||||||
|
return todo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTodo(id: string) {
|
||||||
|
// Ensure exists
|
||||||
|
const existing = await prisma.todo.findUnique({ where: { id } });
|
||||||
|
if (!existing) {
|
||||||
|
const err: any = new Error("Todo not found");
|
||||||
|
err.code = "NOT_FOUND";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await prisma.todo.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserTodos(
|
||||||
|
userId: string,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
// Determine target user for manager override
|
||||||
|
const targetUser = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibilityClause = (() => {
|
||||||
|
if (requesterRole === Role.MANAGER && requesterId) {
|
||||||
|
if (targetUser?.role === Role.ADMIN) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { id: requesterId } } },
|
||||||
|
{ assignees: { some: { managerId: requesterId } } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (requesterRole === Role.ADMIN && requesterId) {
|
||||||
|
return { assignees: { some: { id: requesterId } } };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const todos = await prisma.todo.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [{ assignees: { some: { id: userId } } }, visibilityClause],
|
||||||
|
},
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return todos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkTodoVisibility(
|
||||||
|
todoId: string,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
const todo = await prisma.todo.findUnique({
|
||||||
|
where: { id: todoId },
|
||||||
|
include: {
|
||||||
|
assignees: { select: { id: true, managerId: true } },
|
||||||
|
createdBy: { select: { id: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!todo) {
|
||||||
|
const err: any = new Error("Todo not found");
|
||||||
|
err.code = "NOT_FOUND";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAssignee = !!todo.assignees.find((a) => a.id === requesterId);
|
||||||
|
const isCreator = todo.createdBy.id === requesterId;
|
||||||
|
const isManagerOfAssignee = !!todo.assignees.find(
|
||||||
|
(a) => a.managerId === requesterId
|
||||||
|
);
|
||||||
|
|
||||||
|
const allowed =
|
||||||
|
!!requesterRole &&
|
||||||
|
((requesterRole === Role.ADMIN && isAssignee) ||
|
||||||
|
(requesterRole === Role.MANAGER &&
|
||||||
|
(isAssignee || isManagerOfAssignee || isCreator)));
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
const err: any = new Error("Forbidden");
|
||||||
|
err.code = "FORBIDDEN";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { todo, isAssignee, isCreator };
|
||||||
|
}
|
||||||
|
|
||||||
|
async listComments(
|
||||||
|
todoId: string,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
await this.checkTodoVisibility(todoId, requesterRole, requesterId);
|
||||||
|
|
||||||
|
const comments = await prisma.comment.findMany({
|
||||||
|
where: { todoId },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
imageUrl: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
});
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addComment(
|
||||||
|
todoId: string,
|
||||||
|
content: string,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
if (!content || !content.trim()) {
|
||||||
|
const err: any = new Error("Content is required");
|
||||||
|
err.code = "BAD_REQUEST";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.checkTodoVisibility(todoId, requesterRole, requesterId);
|
||||||
|
|
||||||
|
const newComment = await prisma.comment.create({
|
||||||
|
data: {
|
||||||
|
content: content.trim(),
|
||||||
|
todo: { connect: { id: todoId } },
|
||||||
|
user: { connect: { id: requesterId! } },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
imageUrl: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return newComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateComment(
|
||||||
|
todoId: string,
|
||||||
|
commentId: string,
|
||||||
|
content: string,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
if (!content || !content.trim()) {
|
||||||
|
const err: any = new Error("Content is required");
|
||||||
|
err.code = "BAD_REQUEST";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.checkTodoVisibility(todoId, requesterRole, requesterId);
|
||||||
|
|
||||||
|
const existing = await prisma.comment.findUnique({
|
||||||
|
where: { id: commentId },
|
||||||
|
select: { id: true, userId: true, todoId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing || existing.todoId !== todoId) {
|
||||||
|
const err: any = new Error("Comment not found");
|
||||||
|
err.code = "NOT_FOUND";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (existing.userId !== requesterId) {
|
||||||
|
const err: any = new Error("Only the author can edit this comment");
|
||||||
|
err.code = "FORBIDDEN";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.comment.update({
|
||||||
|
where: { id: commentId },
|
||||||
|
data: { content: content.trim() },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
imageUrl: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteComment(
|
||||||
|
todoId: string,
|
||||||
|
commentId: string,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
) {
|
||||||
|
await this.checkTodoVisibility(todoId, requesterRole, requesterId);
|
||||||
|
|
||||||
|
const existing = await prisma.comment.findUnique({
|
||||||
|
where: { id: commentId },
|
||||||
|
select: { id: true, userId: true, todoId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing || existing.todoId !== todoId) {
|
||||||
|
const err: any = new Error("Comment not found");
|
||||||
|
err.code = "NOT_FOUND";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (existing.userId !== requesterId) {
|
||||||
|
const err: any = new Error("Only the author can delete this comment");
|
||||||
|
err.code = "FORBIDDEN";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.comment.delete({ where: { id: commentId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const todoService = new TodoService();
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Role, RiskStatus } from "@prisma/client";
|
||||||
|
import { AuthenticatedRequest } from "../../middleware/auth";
|
||||||
|
import { userService, CreateUserData, UpdateUserData } from "./users.service";
|
||||||
|
import { UserListParams } from "./users.repository";
|
||||||
|
import { handleError } from "../../utils/http";
|
||||||
|
|
||||||
|
export class UserController {
|
||||||
|
/**
|
||||||
|
* List users with pagination and filtering
|
||||||
|
*/
|
||||||
|
async listUsers(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const params: UserListParams = {
|
||||||
|
page: Math.max(parseInt((req.query.page as string) || "1", 10), 1),
|
||||||
|
limit: Math.max(parseInt((req.query.limit as string) || "25", 10), 1),
|
||||||
|
search: ((req.query.search as string) || "").trim(),
|
||||||
|
risks: ((req.query.risks as string) || "")
|
||||||
|
.split(",")
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.filter((r) => !!r),
|
||||||
|
sortBy: (req.query.sortBy as string) || "",
|
||||||
|
sortDir: ((req.query.sortDir as string) || "asc").toLowerCase() === "desc"
|
||||||
|
? "desc"
|
||||||
|
: "asc",
|
||||||
|
roleType: ((req.query.roleType as string) || "user").toLowerCase() as "user" | "admin",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await userService.getUsers(params);
|
||||||
|
res.json({
|
||||||
|
items: result.items,
|
||||||
|
total: result.total,
|
||||||
|
page: params.page,
|
||||||
|
limit: params.limit,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return handleError(res, "Failed to fetch users", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user login activity logs
|
||||||
|
*/
|
||||||
|
async getUserLogs(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const page = Math.max(parseInt((req.query.page as string) || "1", 10), 1);
|
||||||
|
const limit = Math.max(parseInt((req.query.limit as string) || "25", 10), 1);
|
||||||
|
const search = ((req.query.search as string) || "").trim();
|
||||||
|
|
||||||
|
const result = await userService.getUserLogs(page, limit, search);
|
||||||
|
res.json({
|
||||||
|
items: result.items,
|
||||||
|
total: result.total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return handleError(res, "Failed to fetch login logs", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend invitation email to a user
|
||||||
|
*/
|
||||||
|
async resendInvitation(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
await userService.resendInvitation(id);
|
||||||
|
res.json({ message: "Invitation email sent" });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "User not found") {
|
||||||
|
return res.status(404).json({ error: error.message });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error.message === "Invitations are not applicable for end-user accounts" ||
|
||||||
|
error.message === "User is already activated"
|
||||||
|
) {
|
||||||
|
return res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Failed to send invitation", details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single user by ID
|
||||||
|
*/
|
||||||
|
async getUser(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const user = await userService.getUserById(id);
|
||||||
|
res.json(user);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "User not found") {
|
||||||
|
return res.status(404).json({ error: error.message });
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to fetch user", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
*/
|
||||||
|
async createUser(req: AuthenticatedRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const data: CreateUserData = {
|
||||||
|
firstName: req.body.firstName,
|
||||||
|
lastName: req.body.lastName,
|
||||||
|
email: req.body.email,
|
||||||
|
password: req.body.password,
|
||||||
|
title: req.body.title,
|
||||||
|
imageUrl: req.body.imageUrl,
|
||||||
|
riskStatus: req.body.riskStatus || RiskStatus.LOW_RISK,
|
||||||
|
role: req.body.role || Role.USER,
|
||||||
|
managerId: req.body.managerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requesterRole = req.user?.role as Role | undefined;
|
||||||
|
const requesterId = req.user?.userId as string | undefined;
|
||||||
|
|
||||||
|
const user = await userService.createUser(data, requesterRole, requesterId);
|
||||||
|
res.status(201).json(user);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "Missing required fields") {
|
||||||
|
return res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
if (error.message === "Email already exists") {
|
||||||
|
return res.status(409).json({ error: error.message });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error.message === "Admins may only create end-user accounts" ||
|
||||||
|
error.message === "Only Managers can create Admin accounts"
|
||||||
|
) {
|
||||||
|
return res.status(403).json({ error: "Forbidden", details: error.message });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error.message === "Could not determine manager for the new Admin" ||
|
||||||
|
error.message === "managerId must reference an existing Manager"
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: error.message.includes("manager") ? "Invalid manager assignment" : "Invalid manager",
|
||||||
|
details: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to create user", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user
|
||||||
|
*/
|
||||||
|
async updateUser(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data: UpdateUserData = {
|
||||||
|
firstName: req.body.firstName,
|
||||||
|
lastName: req.body.lastName,
|
||||||
|
email: req.body.email,
|
||||||
|
title: req.body.title,
|
||||||
|
riskStatus: req.body.riskStatus,
|
||||||
|
imageUrl: req.body.imageUrl,
|
||||||
|
isLocked: req.body.isLocked,
|
||||||
|
role: req.body.role,
|
||||||
|
managerId: req.body.managerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role as Role | undefined;
|
||||||
|
const requesterId = (req as AuthenticatedRequest).user?.userId as string | undefined;
|
||||||
|
|
||||||
|
const user = await userService.updateUser(id, data, requesterRole, requesterId);
|
||||||
|
res.json(user);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "User not found") {
|
||||||
|
return res.status(404).json({ error: error.message });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error.message === "Admins may not change role to Admin/Manager" ||
|
||||||
|
error.message === "Admins may not assign or reassign Admins" ||
|
||||||
|
error.message === "Only Managers can assign/reassign Admins"
|
||||||
|
) {
|
||||||
|
return res.status(403).json({ error: "Forbidden", details: error.message });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error.message === "managerId may only be set for Admin accounts" ||
|
||||||
|
error.message === "Could not determine manager for the Admin"
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: error.message.includes("manager") ? "Invalid manager assignment" : "Invalid assignment",
|
||||||
|
details: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.message === "managerId must reference an existing Manager") {
|
||||||
|
return res.status(400).json({ error: "Invalid manager", details: error.message });
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to update user", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle user lock status
|
||||||
|
*/
|
||||||
|
async toggleLock(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const user = await userService.toggleUserLock(id);
|
||||||
|
res.json(user);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "User not found") {
|
||||||
|
return res.status(404).json({ error: error.message });
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to lock/unlock user", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single user
|
||||||
|
*/
|
||||||
|
async deleteUser(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role;
|
||||||
|
|
||||||
|
await userService.deleteUser(id, requesterRole);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "User not found") {
|
||||||
|
return res.status(404).json({ error: error.message });
|
||||||
|
}
|
||||||
|
if (error.message === "Admins may only delete regular users") {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Forbidden",
|
||||||
|
details: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to delete user", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk delete users
|
||||||
|
*/
|
||||||
|
async deleteUsers(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { ids } = req.body;
|
||||||
|
const requesterRole = (req as AuthenticatedRequest).user?.role;
|
||||||
|
|
||||||
|
await userService.deleteUsers(ids, requesterRole);
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === "No user IDs provided") {
|
||||||
|
return res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
if (error.message === "Admins may only delete regular users") {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Forbidden",
|
||||||
|
details: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return handleError(res, "Failed to delete users", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userController = new UserController();
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import { Role, RiskStatus, User, Prisma } from "@prisma/client";
|
||||||
|
import prisma from "../../prismaClient";
|
||||||
|
|
||||||
|
export interface UserWithRelations extends User {
|
||||||
|
assignedTodos?: any[];
|
||||||
|
createdTodos?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserListParams {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
search: string;
|
||||||
|
risks: string[];
|
||||||
|
sortBy: string;
|
||||||
|
sortDir: "asc" | "desc";
|
||||||
|
roleType: "user" | "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserListResult {
|
||||||
|
items: UserWithRelations[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserRepository {
|
||||||
|
/**
|
||||||
|
* Count users matching the given criteria
|
||||||
|
*/
|
||||||
|
async countUsers(where: Prisma.UserWhereInput): Promise<number> {
|
||||||
|
return await prisma.user.count({ where });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find many users with standard Prisma ordering
|
||||||
|
*/
|
||||||
|
async findManyUsers(
|
||||||
|
where: Prisma.UserWhereInput,
|
||||||
|
orderBy?: Prisma.UserOrderByWithRelationInput,
|
||||||
|
skip?: number,
|
||||||
|
take?: number
|
||||||
|
): Promise<UserWithRelations[]> {
|
||||||
|
return await prisma.user.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
assignedTodos: true,
|
||||||
|
createdTodos: true,
|
||||||
|
},
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find users with custom risk-based sorting using raw SQL
|
||||||
|
*/
|
||||||
|
async findUsersWithRiskSort(
|
||||||
|
where: any,
|
||||||
|
search: string,
|
||||||
|
sortDir: "asc" | "desc",
|
||||||
|
page: number,
|
||||||
|
limit: number
|
||||||
|
): Promise<UserWithRelations[]> {
|
||||||
|
const whereSqlParts: string[] = [];
|
||||||
|
const params: any[] = [];
|
||||||
|
|
||||||
|
// role filter
|
||||||
|
if (where.role && where.role.in) {
|
||||||
|
const p1 = params.length + 1;
|
||||||
|
const p2 = params.length + 2;
|
||||||
|
whereSqlParts.push(`"role" IN ($${p1}::"Role", $${p2}::"Role")`);
|
||||||
|
params.push("ADMIN", "MANAGER");
|
||||||
|
} else if (where.role) {
|
||||||
|
const p = params.length + 1;
|
||||||
|
whereSqlParts.push(`"role" = $${p}::"Role"`);
|
||||||
|
params.push("USER");
|
||||||
|
}
|
||||||
|
|
||||||
|
// search filter
|
||||||
|
if (where.OR && Array.isArray(where.OR) && where.OR.length) {
|
||||||
|
const s = `%${search}%`;
|
||||||
|
const p1 = params.length + 1;
|
||||||
|
const p2 = params.length + 2;
|
||||||
|
const p3 = params.length + 3;
|
||||||
|
const p4 = params.length + 4;
|
||||||
|
whereSqlParts.push(
|
||||||
|
`("firstName" ILIKE $${p1} OR "lastName" ILIKE $${p2} OR "email" ILIKE $${p3} OR "title" ILIKE $${p4})`
|
||||||
|
);
|
||||||
|
params.push(s, s, s, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// risks filter
|
||||||
|
if (where.riskStatus && where.riskStatus.in && where.riskStatus.in.length) {
|
||||||
|
const placeholders = where.riskStatus.in
|
||||||
|
.map(
|
||||||
|
(_r: string, i: number) => `$${params.length + 1 + i}::"RiskStatus"`
|
||||||
|
)
|
||||||
|
.join(", ");
|
||||||
|
whereSqlParts.push(`"riskStatus" IN (${placeholders})`);
|
||||||
|
params.push(...where.riskStatus.in);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereSql = whereSqlParts.length
|
||||||
|
? `WHERE ${whereSqlParts.join(" AND ")}`
|
||||||
|
: "";
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const dir = sortDir === "desc" ? "DESC" : "ASC";
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT "id"
|
||||||
|
FROM "User"
|
||||||
|
${whereSql}
|
||||||
|
ORDER BY CASE "riskStatus"
|
||||||
|
WHEN 'NO_RISK'::"RiskStatus" THEN 0
|
||||||
|
WHEN 'LOW_RISK'::"RiskStatus" THEN 1
|
||||||
|
WHEN 'MODERATE_RISK'::"RiskStatus" THEN 2
|
||||||
|
WHEN 'HIGH_RISK'::"RiskStatus" THEN 3
|
||||||
|
END ${dir}, "firstName" ASC
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const idRows: { id: string }[] = await prisma.$queryRawUnsafe(
|
||||||
|
query,
|
||||||
|
...params
|
||||||
|
);
|
||||||
|
const ids = idRows.map((r) => r.id);
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetched = await prisma.user.findMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
include: { assignedTodos: true, createdTodos: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// preserve order from ids
|
||||||
|
const orderMap = new Map(ids.map((id, idx) => [id, idx] as const));
|
||||||
|
return fetched.sort(
|
||||||
|
(a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all users with minimal fields for logs
|
||||||
|
*/
|
||||||
|
async findUsersForLogs(): Promise<
|
||||||
|
Pick<User, "id" | "firstName" | "lastName" | "email">[]
|
||||||
|
> {
|
||||||
|
return await prisma.user.findMany({
|
||||||
|
select: { id: true, firstName: true, lastName: true, email: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a single user by ID
|
||||||
|
*/
|
||||||
|
async findUserById(id: string): Promise<UserWithRelations | null> {
|
||||||
|
return await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
assignedTodos: true,
|
||||||
|
createdTodos: true,
|
||||||
|
manager: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a user by email
|
||||||
|
*/
|
||||||
|
async findUserByEmail(email: string): Promise<User | null> {
|
||||||
|
return await prisma.user.findUnique({ where: { email } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
*/
|
||||||
|
async createUser(data: Prisma.UserCreateInput): Promise<User> {
|
||||||
|
return await prisma.user.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user
|
||||||
|
*/
|
||||||
|
async updateUser(id: string, data: Prisma.UserUpdateInput): Promise<User> {
|
||||||
|
return await prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user
|
||||||
|
*/
|
||||||
|
async deleteUser(id: string): Promise<void> {
|
||||||
|
await prisma.user.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete multiple users
|
||||||
|
*/
|
||||||
|
async deleteUsers(ids: string[]): Promise<void> {
|
||||||
|
await prisma.user.deleteMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete todos created by a user
|
||||||
|
*/
|
||||||
|
async deleteTodosByCreator(createdById: string): Promise<void> {
|
||||||
|
await prisma.todo.deleteMany({ where: { createdById } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete todos created by multiple users
|
||||||
|
*/
|
||||||
|
async deleteTodosByCreators(createdByIds: string[]): Promise<void> {
|
||||||
|
await prisma.todo.deleteMany({
|
||||||
|
where: { createdById: { in: createdByIds } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find users by IDs with specific fields
|
||||||
|
*/
|
||||||
|
async findUsersByIds(ids: string[]): Promise<Pick<User, "id" | "role">[]> {
|
||||||
|
return await prisma.user.findMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
select: { id: true, role: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a user with specific fields for validation
|
||||||
|
*/
|
||||||
|
async findUserForValidation(
|
||||||
|
id: string
|
||||||
|
): Promise<Pick<User, "id" | "role"> | null> {
|
||||||
|
return await prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { id: true, role: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userRepository = new UserRepository();
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import { requireAuth, requireRole } from "../../middleware/auth";
|
||||||
|
import { userController } from "./users.controller";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// All user management endpoints require authentication
|
||||||
|
router.use(requireAuth);
|
||||||
|
|
||||||
|
// List users (admins and managers)
|
||||||
|
router.get(
|
||||||
|
"/",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.listUsers
|
||||||
|
);
|
||||||
|
|
||||||
|
// User login activity logs (admins and managers)
|
||||||
|
router.get(
|
||||||
|
"/logs",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.getUserLogs
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resend invitation email (admins and managers)
|
||||||
|
router.post(
|
||||||
|
"/:id/invite",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.resendInvitation
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get a single user (admins and managers)
|
||||||
|
router.get(
|
||||||
|
"/:id",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.getUser
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create user (admins and managers)
|
||||||
|
router.post(
|
||||||
|
"/",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.createUser
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update user (admins and managers)
|
||||||
|
router.put(
|
||||||
|
"/:id",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.updateUser
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle lock (admins and managers)
|
||||||
|
router.put(
|
||||||
|
"/:id/lock",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.toggleLock
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete single user (admins and managers)
|
||||||
|
router.delete(
|
||||||
|
"/:id",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.deleteUser
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bulk delete (admins and managers)
|
||||||
|
router.delete(
|
||||||
|
"/",
|
||||||
|
requireRole([Role.ADMIN, Role.MANAGER]),
|
||||||
|
userController.deleteUsers
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
import { Role, RiskStatus, User, Prisma } from "@prisma/client";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import {
|
||||||
|
userRepository,
|
||||||
|
UserListParams,
|
||||||
|
UserListResult,
|
||||||
|
} from "./users.repository";
|
||||||
|
import {
|
||||||
|
createPasswordResetToken,
|
||||||
|
generateRawToken,
|
||||||
|
} from "../../lib/passwordReset";
|
||||||
|
import { sendInvitationEmail } from "../../lib/email";
|
||||||
|
|
||||||
|
export interface CreateUserData {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
password?: string;
|
||||||
|
title?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
riskStatus?: RiskStatus;
|
||||||
|
role?: Role;
|
||||||
|
managerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserData {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
title?: string;
|
||||||
|
riskStatus?: RiskStatus;
|
||||||
|
imageUrl?: string;
|
||||||
|
isLocked?: boolean;
|
||||||
|
role?: Role;
|
||||||
|
managerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginLogItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
lastLoginAt: Date;
|
||||||
|
activity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
/**
|
||||||
|
* Build where clause for user queries
|
||||||
|
*/
|
||||||
|
private buildWhereClause(params: UserListParams): any {
|
||||||
|
const where: any = {};
|
||||||
|
|
||||||
|
where.role =
|
||||||
|
params.roleType === "admin"
|
||||||
|
? { in: [Role.ADMIN, Role.MANAGER] }
|
||||||
|
: Role.USER;
|
||||||
|
|
||||||
|
if (params.search?.trim()) {
|
||||||
|
const tokens = params.search
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const searchableFields = ["firstName", "lastName", "email", "title"];
|
||||||
|
|
||||||
|
const andClauses = tokens.map((tok) => ({
|
||||||
|
OR: searchableFields.map((field) => ({
|
||||||
|
[field]: { contains: tok, mode: "insensitive" },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If only one token, AND with one item behaves like OR
|
||||||
|
where.AND = [...(where.AND || []), ...andClauses];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.risks?.length) {
|
||||||
|
where.riskStatus = { in: params.risks };
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get paginated list of users
|
||||||
|
*/
|
||||||
|
async getUsers(params: UserListParams): Promise<UserListResult> {
|
||||||
|
const where = this.buildWhereClause(params);
|
||||||
|
const total = await userRepository.countUsers(where);
|
||||||
|
|
||||||
|
let items;
|
||||||
|
if (params.sortBy === "risk") {
|
||||||
|
// Special handling for risk sort
|
||||||
|
items = await userRepository.findUsersWithRiskSort(
|
||||||
|
where,
|
||||||
|
params.search,
|
||||||
|
params.sortDir,
|
||||||
|
params.page,
|
||||||
|
params.limit
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Default Prisma ordering
|
||||||
|
let orderBy: any = undefined;
|
||||||
|
if (params.sortBy) {
|
||||||
|
if (params.sortBy === "name") {
|
||||||
|
orderBy = { firstName: params.sortDir };
|
||||||
|
} else if (params.sortBy === "email") {
|
||||||
|
orderBy = { email: params.sortDir };
|
||||||
|
} else if (params.sortBy === "title") {
|
||||||
|
orderBy = { title: params.sortDir };
|
||||||
|
} else if (params.sortBy === "role") {
|
||||||
|
orderBy = { role: params.sortDir };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items = await userRepository.findManyUsers(
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
(params.page - 1) * params.limit,
|
||||||
|
params.limit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user login activity logs
|
||||||
|
*/
|
||||||
|
async getUserLogs(
|
||||||
|
page: number,
|
||||||
|
limit: number,
|
||||||
|
search: string
|
||||||
|
): Promise<{ items: LoginLogItem[]; total: number }> {
|
||||||
|
const users = await userRepository.findUsersForLogs();
|
||||||
|
|
||||||
|
// Filter by search
|
||||||
|
const filtered = users.filter((u) => {
|
||||||
|
if (!search) return true;
|
||||||
|
const name = `${u.firstName} ${u.lastName}`.toLowerCase();
|
||||||
|
return (
|
||||||
|
name.includes(search.toLowerCase()) ||
|
||||||
|
u.email.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build dummy logs
|
||||||
|
const activities = [
|
||||||
|
"Login success",
|
||||||
|
"Login failed",
|
||||||
|
"2FA challenge passed",
|
||||||
|
"Password reset",
|
||||||
|
"Logout",
|
||||||
|
];
|
||||||
|
const now = Date.now();
|
||||||
|
const itemsAll = filtered.map((u, idx) => {
|
||||||
|
const offsetMinutes =
|
||||||
|
((idx * 37) % 7_200) + Math.floor(Math.random() * 60);
|
||||||
|
const lastLoginAt = new Date(now - offsetMinutes * 60 * 1000);
|
||||||
|
const activity = activities[(idx * 13) % activities.length];
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
name: `${u.firstName} ${u.lastName}`,
|
||||||
|
email: u.email,
|
||||||
|
lastLoginAt,
|
||||||
|
activity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = itemsAll.length;
|
||||||
|
const start = (page - 1) * limit;
|
||||||
|
const end = start + limit;
|
||||||
|
const items = itemsAll.slice(start, end);
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend invitation email to a user
|
||||||
|
*/
|
||||||
|
async resendInvitation(userId: string): Promise<void> {
|
||||||
|
const user = await userRepository.findUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
if (user.role === Role.USER) {
|
||||||
|
throw new Error("Invitations are not applicable for end-user accounts");
|
||||||
|
}
|
||||||
|
if (user.isActivated) {
|
||||||
|
throw new Error("User is already activated");
|
||||||
|
}
|
||||||
|
const { link } = await createPasswordResetToken(user.id);
|
||||||
|
await sendInvitationEmail(user.email, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single user by ID
|
||||||
|
*/
|
||||||
|
async getUserById(userId: string) {
|
||||||
|
const user = await userRepository.findUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user with role-based validation
|
||||||
|
*/
|
||||||
|
async createUser(
|
||||||
|
data: CreateUserData,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
): Promise<User> {
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
title,
|
||||||
|
imageUrl,
|
||||||
|
riskStatus = RiskStatus.LOW_RISK,
|
||||||
|
role = Role.USER,
|
||||||
|
managerId,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!firstName || !lastName || !email) {
|
||||||
|
throw new Error("Missing required fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await userRepository.findUserByEmail(email);
|
||||||
|
if (existing) {
|
||||||
|
throw new Error("Email already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-based permissions
|
||||||
|
if (requesterRole === Role.ADMIN && role !== Role.USER) {
|
||||||
|
throw new Error("Admins may only create end-user accounts");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password and invitation logic
|
||||||
|
let passwordToSet = password;
|
||||||
|
let shouldInvite = false;
|
||||||
|
|
||||||
|
if (role === Role.USER) {
|
||||||
|
if (!passwordToSet) {
|
||||||
|
passwordToSet = generateRawToken(16);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!passwordToSet) {
|
||||||
|
passwordToSet = generateRawToken(16);
|
||||||
|
shouldInvite = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager assignment for ADMINs
|
||||||
|
let managerIdToSet: string | undefined = undefined;
|
||||||
|
if (role === Role.ADMIN) {
|
||||||
|
if (requesterRole !== Role.MANAGER) {
|
||||||
|
throw new Error("Only Managers can create Admin accounts");
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetManagerId = managerId || requesterId;
|
||||||
|
if (!targetManagerId) {
|
||||||
|
throw new Error("Could not determine manager for the new Admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetManager = await userRepository.findUserForValidation(
|
||||||
|
targetManagerId
|
||||||
|
);
|
||||||
|
if (!targetManager || targetManager.role !== Role.MANAGER) {
|
||||||
|
throw new Error("managerId must reference an existing Manager");
|
||||||
|
}
|
||||||
|
managerIdToSet = targetManagerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await bcrypt.hash(passwordToSet, 10);
|
||||||
|
const userData: Prisma.UserCreateInput = {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
password: hash,
|
||||||
|
title: title || "",
|
||||||
|
imageUrl,
|
||||||
|
riskStatus,
|
||||||
|
role,
|
||||||
|
isActivated: role === Role.USER ? true : shouldInvite ? false : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only set managerId if it's defined
|
||||||
|
if (managerIdToSet) {
|
||||||
|
userData.manager = { connect: { id: managerIdToSet } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userRepository.createUser(userData);
|
||||||
|
|
||||||
|
// Send invitation email if needed
|
||||||
|
if (shouldInvite) {
|
||||||
|
try {
|
||||||
|
const { link } = await createPasswordResetToken(user.id);
|
||||||
|
await sendInvitationEmail(email, link);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to send invitation email:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user with role-based validation
|
||||||
|
*/
|
||||||
|
async updateUser(
|
||||||
|
userId: string,
|
||||||
|
data: UpdateUserData,
|
||||||
|
requesterRole?: Role,
|
||||||
|
requesterId?: string
|
||||||
|
): Promise<User> {
|
||||||
|
const target = await userRepository.findUserById(userId);
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-based permission checks
|
||||||
|
if (requesterRole === Role.ADMIN) {
|
||||||
|
if (data.role && data.role !== Role.USER) {
|
||||||
|
throw new Error("Admins may not change role to Admin/Manager");
|
||||||
|
}
|
||||||
|
if (typeof data.managerId !== "undefined") {
|
||||||
|
throw new Error("Admins may not assign or reassign Admins");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager assignment validation
|
||||||
|
let updateData: Prisma.UserUpdateInput = {
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
email: data.email,
|
||||||
|
title: data.title,
|
||||||
|
riskStatus: data.riskStatus,
|
||||||
|
imageUrl: data.imageUrl,
|
||||||
|
isLocked: data.isLocked,
|
||||||
|
role: data.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof data.managerId !== "undefined") {
|
||||||
|
if (requesterRole !== Role.MANAGER) {
|
||||||
|
throw new Error("Only Managers can assign/reassign Admins");
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveRole = data.role ?? target.role;
|
||||||
|
if (effectiveRole !== Role.ADMIN) {
|
||||||
|
throw new Error("managerId may only be set for Admin accounts");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mgrId = data.managerId || requesterId;
|
||||||
|
if (!mgrId) {
|
||||||
|
throw new Error("Could not determine manager for the Admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mgr = await userRepository.findUserForValidation(mgrId);
|
||||||
|
if (!mgr || mgr.role !== Role.MANAGER) {
|
||||||
|
throw new Error("managerId must reference an existing Manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData.manager = { connect: { id: mgrId } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await userRepository.updateUser(userId, updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle user lock status
|
||||||
|
*/
|
||||||
|
async toggleUserLock(userId: string): Promise<User> {
|
||||||
|
const user = await userRepository.findUserById(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await userRepository.updateUser(userId, {
|
||||||
|
isLocked: !user.isLocked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single user with role-based validation
|
||||||
|
*/
|
||||||
|
async deleteUser(userId: string, requesterRole?: Role): Promise<void> {
|
||||||
|
const target = await userRepository.findUserById(userId);
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requesterRole === Role.ADMIN && target.role !== Role.USER) {
|
||||||
|
throw new Error("Admins may only delete regular users");
|
||||||
|
}
|
||||||
|
|
||||||
|
await userRepository.deleteTodosByCreator(userId);
|
||||||
|
await userRepository.deleteUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk delete users with role-based validation
|
||||||
|
*/
|
||||||
|
async deleteUsers(userIds: string[], requesterRole?: Role): Promise<void> {
|
||||||
|
if (!Array.isArray(userIds) || userIds.length === 0) {
|
||||||
|
throw new Error("No user IDs provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate targets for admin constraints
|
||||||
|
if (requesterRole === Role.ADMIN) {
|
||||||
|
const targets = await userRepository.findUsersByIds(userIds);
|
||||||
|
const forbidden = targets
|
||||||
|
.filter((t) => t.role !== Role.USER)
|
||||||
|
.map((t) => t.id);
|
||||||
|
if (forbidden.length > 0) {
|
||||||
|
throw new Error("Admins may only delete regular users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await userRepository.deleteTodosByCreators(userIds);
|
||||||
|
await userRepository.deleteUsers(userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = new UserService();
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Role } from "@prisma/client";
|
||||||
|
|
||||||
|
export type Requester = { id: string; role: Role };
|
||||||
|
|
||||||
|
export type TodoAccessShape = {
|
||||||
|
assignees: Array<{ id: string; managerId: string | null }>;
|
||||||
|
createdBy: { id: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function canAccessTodo(requester: Requester, todo: TodoAccessShape): boolean {
|
||||||
|
// Policy: end users never access app endpoints
|
||||||
|
if (requester.role === Role.USER) return false;
|
||||||
|
|
||||||
|
const isAssignee = !!todo.assignees.find((a) => a.id === requester.id);
|
||||||
|
const isCreator = todo.createdBy.id === requester.id;
|
||||||
|
const isManagerOfAssignee = !!todo.assignees.find((a) => a.managerId === requester.id);
|
||||||
|
|
||||||
|
if (requester.role === Role.ADMIN) {
|
||||||
|
return isAssignee; // Admins can only access Todos assigned to themselves
|
||||||
|
}
|
||||||
|
if (requester.role === Role.MANAGER) {
|
||||||
|
return isAssignee || isManagerOfAssignee || isCreator;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateCommentAuthor(requesterId: string, comment: { userId: string }) {
|
||||||
|
if (comment.userId !== requesterId) {
|
||||||
|
const err: any = new Error("Only the author can modify this comment");
|
||||||
|
err.status = 403;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { PriorityStatus, Role, TodoStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
export type ListParams = {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
statuses?: string | string[];
|
||||||
|
priorities?: string | string[];
|
||||||
|
sortBy?: string;
|
||||||
|
sortDir?: "asc" | "desc" | string;
|
||||||
|
assigneeUserId?: string;
|
||||||
|
teamOfManagerId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Requester = { id: string; role: Role };
|
||||||
|
|
||||||
|
export function parseCsv(value?: string | string[]): string[] {
|
||||||
|
if (!value) return [];
|
||||||
|
if (Array.isArray(value)) return value.flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
||||||
|
return value.split(",").map((v) => v.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTodoQuery(params: ListParams, requester: Requester) {
|
||||||
|
const page = Math.max(parseInt(String(params.page ?? 1), 10), 1);
|
||||||
|
const limit = Math.max(parseInt(String(params.limit ?? 25), 10), 1);
|
||||||
|
const search = (params.search ?? "").trim();
|
||||||
|
const statuses = parseCsv(params.statuses) as TodoStatus[];
|
||||||
|
const priorities = parseCsv(params.priorities) as PriorityStatus[];
|
||||||
|
const sortBy = (params.sortBy ?? "") as string;
|
||||||
|
const sortDir = String(params.sortDir ?? "asc").toLowerCase() === "desc" ? "desc" : "asc";
|
||||||
|
const assigneeUserId = params.assigneeUserId ?? "";
|
||||||
|
const teamOfManagerId = params.teamOfManagerId ?? "";
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ title: { contains: search, mode: "insensitive" } },
|
||||||
|
{ task: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (statuses.length) where.status = { in: statuses };
|
||||||
|
if (priorities.length) where.priority = { in: priorities };
|
||||||
|
|
||||||
|
const andClauses: any[] = [];
|
||||||
|
if (assigneeUserId) andClauses.push({ assignees: { some: { id: assigneeUserId } } });
|
||||||
|
if (teamOfManagerId) andClauses.push({ assignees: { some: { managerId: teamOfManagerId } } });
|
||||||
|
|
||||||
|
// Server-side scoping by role
|
||||||
|
if (requester.role === Role.ADMIN) {
|
||||||
|
andClauses.push({ assignees: { some: { id: requester.id } } });
|
||||||
|
} else if (requester.role === Role.MANAGER) {
|
||||||
|
andClauses.push({
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { id: requester.id } } },
|
||||||
|
{ assignees: { some: { managerId: requester.id } } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (andClauses.length) where.AND = [...(where.AND || []), ...andClauses];
|
||||||
|
|
||||||
|
let orderBy: any = undefined;
|
||||||
|
if (sortBy) {
|
||||||
|
if (sortBy === "title") orderBy = { title: sortDir };
|
||||||
|
else if (sortBy === "dueDate") orderBy = { dueDate: sortDir };
|
||||||
|
else if (sortBy === "status") orderBy = { status: sortDir };
|
||||||
|
else if (sortBy === "priority") orderBy = { priority: sortDir };
|
||||||
|
else if (sortBy === "createdAt") orderBy = { createdAt: sortDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
where,
|
||||||
|
orderBy,
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { PriorityStatus, Role, TodoStatus } from "@prisma/client";
|
||||||
|
import prisma from "../prismaClient";
|
||||||
|
import { buildTodoQuery, ListParams, Requester as QueryRequester } from "./todoQuery";
|
||||||
|
|
||||||
|
export type TodoCreateInput = {
|
||||||
|
title: string;
|
||||||
|
task: string;
|
||||||
|
status: TodoStatus;
|
||||||
|
priority: PriorityStatus;
|
||||||
|
dueDate: Date;
|
||||||
|
createdById: string;
|
||||||
|
assigneeIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TodoUpdateInput = Partial<{
|
||||||
|
title: string;
|
||||||
|
task: string;
|
||||||
|
status: TodoStatus;
|
||||||
|
priority: PriorityStatus;
|
||||||
|
dueDate: Date | null;
|
||||||
|
assigneeIds: string[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export async function createTodo(data: TodoCreateInput) {
|
||||||
|
const todo = await prisma.todo.create({
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
task: data.task,
|
||||||
|
status: data.status,
|
||||||
|
priority: data.priority,
|
||||||
|
dueDate: data.dueDate,
|
||||||
|
createdBy: { connect: { id: data.createdById } },
|
||||||
|
assignees: { connect: data.assigneeIds.map((id) => ({ id })) },
|
||||||
|
},
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
});
|
||||||
|
return todo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTodo(todoId: string, data: TodoUpdateInput) {
|
||||||
|
const todo = await prisma.todo.update({
|
||||||
|
where: { id: todoId },
|
||||||
|
data: {
|
||||||
|
...(data.title !== undefined ? { title: data.title } : {}),
|
||||||
|
...(data.task !== undefined ? { task: data.task } : {}),
|
||||||
|
...(data.status !== undefined ? { status: data.status } : {}),
|
||||||
|
...(data.priority !== undefined ? { priority: data.priority } : {}),
|
||||||
|
...(data.dueDate !== undefined ? { dueDate: data.dueDate ?? undefined } : {}),
|
||||||
|
...(data.assigneeIds !== undefined
|
||||||
|
? { assignees: { set: data.assigneeIds.map((id) => ({ id })) } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
});
|
||||||
|
return todo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTodo(todoId: string) {
|
||||||
|
await prisma.todo.delete({ where: { id: todoId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTodos(params: ListParams, requester: QueryRequester) {
|
||||||
|
const { page, limit, where, orderBy, skip, take } = buildTodoQuery(params, requester);
|
||||||
|
const [total, items] = await Promise.all([
|
||||||
|
prisma.todo.count({ where }),
|
||||||
|
prisma.todo.findMany({ where, include: { assignees: true, createdBy: true }, orderBy, skip, take }),
|
||||||
|
]);
|
||||||
|
return { items, total, page, limit } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTodoAccessShape(todoId: string) {
|
||||||
|
return prisma.todo.findUnique({
|
||||||
|
where: { id: todoId },
|
||||||
|
include: {
|
||||||
|
assignees: { select: { id: true, managerId: true } },
|
||||||
|
createdBy: { select: { id: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listComments(todoId: string) {
|
||||||
|
return prisma.comment.findMany({
|
||||||
|
where: { todoId },
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, firstName: true, lastName: true, imageUrl: true, role: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addComment(todoId: string, userId: string, content: string) {
|
||||||
|
return prisma.comment.create({
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
todo: { connect: { id: todoId } },
|
||||||
|
user: { connect: { id: userId } },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, firstName: true, lastName: true, imageUrl: true, role: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommentSlim(commentId: string) {
|
||||||
|
return prisma.comment.findUnique({
|
||||||
|
where: { id: commentId },
|
||||||
|
select: { id: true, userId: true, todoId: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateComment(commentId: string, content: string) {
|
||||||
|
return prisma.comment.update({
|
||||||
|
where: { id: commentId },
|
||||||
|
data: { content },
|
||||||
|
include: {
|
||||||
|
user: { select: { id: true, firstName: true, lastName: true, imageUrl: true, role: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteComment(commentId: string) {
|
||||||
|
await prisma.comment.delete({ where: { id: commentId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTodosForUser(targetUserId: string, requester: { id: string; role: Role }) {
|
||||||
|
// Determine target user's role for Manager override
|
||||||
|
const targetUser = await prisma.user.findUnique({ where: { id: targetUserId }, select: { role: true } });
|
||||||
|
|
||||||
|
let visibilityClause: any = {};
|
||||||
|
if (requester.role === Role.MANAGER) {
|
||||||
|
if (targetUser?.role === Role.ADMIN) {
|
||||||
|
// Managers can view ANY Admin's todos (admin detail view use-case)
|
||||||
|
visibilityClause = {};
|
||||||
|
} else {
|
||||||
|
visibilityClause = {
|
||||||
|
OR: [
|
||||||
|
{ assignees: { some: { id: requester.id } } },
|
||||||
|
{ assignees: { some: { managerId: requester.id } } },
|
||||||
|
],
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
} else if (requester.role === Role.ADMIN) {
|
||||||
|
visibilityClause = { assignees: { some: { id: requester.id } } } as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const todos = await prisma.todo.findMany({
|
||||||
|
where: { AND: [{ assignees: { some: { id: targetUserId } } }, visibilityClause] },
|
||||||
|
include: { assignees: true, createdBy: true },
|
||||||
|
});
|
||||||
|
return todos;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
export function ok<T>(res: Response, data: T, message?: string) {
|
||||||
|
return res.status(200).json({ data, ...(message ? { message } : {}) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function created<T>(res: Response, data: T, message?: string) {
|
||||||
|
return res.status(201).json({ data, ...(message ? { message } : {}) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function noContent(res: Response) {
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleError(
|
||||||
|
res: Response,
|
||||||
|
message: string,
|
||||||
|
error: unknown,
|
||||||
|
status = 500
|
||||||
|
) {
|
||||||
|
const details =
|
||||||
|
error && typeof error === "object" && "message" in error
|
||||||
|
? (error as any).message
|
||||||
|
: String(error ?? "Unknown error");
|
||||||
|
if (status >= 500) {
|
||||||
|
// basic logging hook for server errors
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("[Error]", message, details);
|
||||||
|
}
|
||||||
|
return res.status(status).json({ error: message, details });
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user