Commit 4abd89bc by Augusto

Change from Prisma to Drizzle + bug fixes

parent e966ee3b
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/drizzle/schema.ts',
out: './drizzle',
dialect: 'mysql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
});
ALTER TABLE `occurrences` ADD COLUMN `managerId` varchar(25) NULL;
--> statement-breakpoint
CREATE INDEX `manager_idx` ON `occurrences` (`managerId`);
{
"version": "7",
"dialect": "mysql",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1757350101348,
"tag": "0000_add_manager_to_occurrences",
"breakpoints": true
}
]
}
......@@ -17,25 +17,30 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.6",
"@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.15.0",
"@types/bcrypt": "^6.0.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"drizzle-orm": "^0.44.5",
"multer": "^2.0.2",
"mysql2": "^3.14.5",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"prisma": "^6.15.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1"
......@@ -55,6 +60,7 @@
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.2",
"drizzle-kit": "^0.31.4",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
......
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
firstName String
lastName String
email String @unique
role UserRole @default(USER)
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reportedOccurrences Occurrence[] @relation("ReportedBy")
assignedOccurrences Occurrence[] @relation("AssignedTo")
comments Comment[]
@@map("users")
}
model Occurrence {
id String @id @default(cuid())
title String
description String @db.Text
status OccurrenceStatus @default(OPEN)
priority Priority @default(MEDIUM)
category String
location String?
// Relations
reportedBy User @relation("ReportedBy", fields: [reporterId], references: [id])
reporterId String
assignedTo User? @relation("AssignedTo", fields: [assigneeId], references: [id])
assigneeId String?
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
closedAt DateTime?
// Relations
comments Comment[]
attachments Attachment[]
conclusion Conclusion?
@@map("occurrences")
}
model Comment {
id String @id @default(cuid())
content String @db.Text
isInternal Boolean @default(false)
// Relations
occurrence Occurrence @relation(fields: [occurrenceId], references: [id], onDelete: Cascade)
occurrenceId String
author User @relation(fields: [authorId], references: [id])
authorId String
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("comments")
}
model Attachment {
id String @id @default(cuid())
filename String
originalName String
mimeType String
size Int
path String
// Relations
occurrence Occurrence @relation(fields: [occurrenceId], references: [id], onDelete: Cascade)
occurrenceId String
// Timestamps
createdAt DateTime @default(now())
@@map("attachments")
}
model Conclusion {
id String @id @default(cuid())
description String @db.Text
fieldMaterial Boolean @default(false)
materialUsed String? @db.Text
// Relations
occurrence Occurrence @relation(fields: [occurrenceId], references: [id], onDelete: Cascade)
occurrenceId String @unique
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("conclusions")
}
enum UserRole {
USER
MODERATOR
ADMIN
}
enum OccurrenceStatus {
OPEN
IN_PROGRESS
RESOLVED
CLOSED
PARCIAL_RESOLVED
CANCELLED
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './common/prisma.service';
import { DrizzleService } from './common/drizzle.service';
import { UserModule } from './modules/user/user.module';
import { AuthModule } from './modules/auth/auth.module';
import { OccurrenceModule } from './modules/occurrence/occurrence.module';
......@@ -13,6 +14,10 @@ import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Make ConfigModule available globally
envFilePath: '.env', // Specify the .env file path
}),
UserModule,
AuthModule,
OccurrenceModule,
......@@ -23,12 +28,12 @@ import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
controllers: [AppController],
providers: [
AppService,
PrismaService,
DrizzleService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard, // Apply JWT guard globally
},
],
exports: [PrismaService], // Export for use in other modules
exports: [DrizzleService], // Export for use in other modules
})
export class AppModule {}
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { drizzle } from 'drizzle-orm/mysql2';
import * as mysql from 'mysql2/promise';
import * as schema from '../drizzle/schema';
@Injectable()
export class DrizzleService implements OnModuleInit, OnModuleDestroy {
private pool: mysql.Pool;
public db: any;
async onModuleInit() {
// Create MySQL connection pool using environment variables
this.pool = mysql.createPool({
uri: process.env.DATABASE_URL,
charset: 'utf8mb4',
timezone: '+00:00',
});
// Create Drizzle instance
this.db = drizzle(this.pool, { schema, mode: 'default' });
}
async onModuleDestroy() {
if (this.pool) {
await this.pool.end();
}
}
// Helper method to generate CUID-like IDs (similar to Prisma's cuid())
generateId(): string {
const timestamp = Date.now().toString(36);
const randomPart = Math.random().toString(36).substring(2, 15);
return `${timestamp}${randomPart}`;
}
}
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '../../generated/prisma';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
export * from './schema';
import {
mysqlTable,
varchar,
text,
boolean,
int,
timestamp,
mysqlEnum,
primaryKey,
unique,
index,
} from 'drizzle-orm/mysql-core';
import { relations } from 'drizzle-orm';
// Enums
export const userRoleEnum = mysqlEnum('user_role', [
'USER',
'MODERATOR',
'ADMIN',
]);
export const occurrenceStatusEnum = mysqlEnum('status', [
'OPEN',
'IN_PROGRESS',
'RESOLVED',
'CLOSED',
'PARCIAL_RESOLVED',
'CANCELLED',
]);
export const priorityEnum = mysqlEnum('priority', [
'LOW',
'MEDIUM',
'HIGH',
'URGENT',
]);
// Users table
export const users = mysqlTable(
'users',
{
id: varchar('id', { length: 25 }).primaryKey(),
firstName: varchar('firstName', { length: 255 }).notNull(),
lastName: varchar('lastName', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
user_role: userRoleEnum.notNull().default('USER'),
password: varchar('password', { length: 255 }).notNull(),
createdAt: timestamp('createdAt').notNull().defaultNow(),
updatedAt: timestamp('updatedAt').notNull().defaultNow().onUpdateNow(),
},
(table) => ({
emailIdx: index('email_idx').on(table.email),
}),
);
// Occurrences table
export const occurrences = mysqlTable(
'occurrences',
{
id: varchar('id', { length: 25 }).primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
description: text('description').notNull(),
status: occurrenceStatusEnum.notNull().default('OPEN'),
priority: priorityEnum.notNull().default('MEDIUM'),
category: varchar('category', { length: 255 }).notNull(),
location: varchar('location', { length: 255 }),
reporterId: varchar('reporterId', { length: 25 }).notNull(),
assigneeId: varchar('assigneeId', { length: 25 }),
managerId: varchar('managerId', { length: 25 }),
createdAt: timestamp('createdAt').notNull().defaultNow(),
updatedAt: timestamp('updatedAt').notNull().defaultNow().onUpdateNow(),
closedAt: timestamp('closedAt'),
},
(table) => ({
reporterIdx: index('reporter_idx').on(table.reporterId),
assigneeIdx: index('assignee_idx').on(table.assigneeId),
managerIdx: index('manager_idx').on(table.managerId),
statusIdx: index('status_idx').on(table.status),
}),
);
// Comments table
export const comments = mysqlTable(
'comments',
{
id: varchar('id', { length: 25 }).primaryKey(),
content: text('content').notNull(),
isInternal: boolean('isInternal').notNull().default(false),
occurrenceId: varchar('occurrenceId', { length: 25 }).notNull(),
authorId: varchar('authorId', { length: 25 }).notNull(),
createdAt: timestamp('createdAt').notNull().defaultNow(),
updatedAt: timestamp('updatedAt').notNull().defaultNow().onUpdateNow(),
},
(table) => ({
occurrenceIdx: index('occurrence_idx').on(table.occurrenceId),
authorIdx: index('author_idx').on(table.authorId),
}),
);
// Attachments table
export const attachments = mysqlTable(
'attachments',
{
id: varchar('id', { length: 25 }).primaryKey(),
filename: varchar('filename', { length: 255 }).notNull(),
originalName: varchar('originalName', { length: 255 }).notNull(),
mimeType: varchar('mimeType', { length: 100 }).notNull(),
size: int('size').notNull(),
path: varchar('path', { length: 500 }).notNull(),
occurrenceId: varchar('occurrenceId', { length: 25 }).notNull(),
createdAt: timestamp('createdAt').notNull().defaultNow(),
},
(table) => ({
occurrenceIdx: index('attachment_occurrence_idx').on(table.occurrenceId),
}),
);
// Conclusions table
export const conclusions = mysqlTable(
'conclusions',
{
id: varchar('id', { length: 25 }).primaryKey(),
description: text('description').notNull(),
fieldMaterial: boolean('fieldMaterial').notNull().default(false),
materialUsed: text('materialUsed'),
occurrenceId: varchar('occurrenceId', { length: 25 }).notNull().unique(),
createdAt: timestamp('createdAt').notNull().defaultNow(),
updatedAt: timestamp('updatedAt').notNull().defaultNow().onUpdateNow(),
},
(table) => ({
occurrenceIdx: index('conclusion_occurrence_idx').on(table.occurrenceId),
}),
);
// Relations
export const usersRelations = relations(users, ({ many }) => ({
reportedOccurrences: many(occurrences, { relationName: 'reportedBy' }),
assignedOccurrences: many(occurrences, { relationName: 'assignedTo' }),
managedOccurrences: many(occurrences, { relationName: 'managedBy' }),
comments: many(comments),
}));
export const occurrencesRelations = relations(occurrences, ({ one, many }) => ({
reportedBy: one(users, {
fields: [occurrences.reporterId],
references: [users.id],
relationName: 'reportedBy',
}),
assignedTo: one(users, {
fields: [occurrences.assigneeId],
references: [users.id],
relationName: 'assignedTo',
}),
managedBy: one(users, {
fields: [occurrences.managerId],
references: [users.id],
relationName: 'managedBy',
}),
comments: many(comments),
attachments: many(attachments),
conclusion: one(conclusions),
}));
export const commentsRelations = relations(comments, ({ one }) => ({
occurrence: one(occurrences, {
fields: [comments.occurrenceId],
references: [occurrences.id],
}),
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
}));
export const attachmentsRelations = relations(attachments, ({ one }) => ({
occurrence: one(occurrences, {
fields: [attachments.occurrenceId],
references: [occurrences.id],
}),
}));
export const conclusionsRelations = relations(conclusions, ({ one }) => ({
occurrence: one(occurrences, {
fields: [conclusions.occurrenceId],
references: [occurrences.id],
}),
}));
// Type exports for TypeScript
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Occurrence = typeof occurrences.$inferSelect;
export type NewOccurrence = typeof occurrences.$inferInsert;
export type Comment = typeof comments.$inferSelect;
export type NewComment = typeof comments.$inferInsert;
export type Attachment = typeof attachments.$inferSelect;
export type NewAttachment = typeof attachments.$inferInsert;
export type Conclusion = typeof conclusions.$inferSelect;
export type NewConclusion = typeof conclusions.$inferInsert;
// Enum types
export type UserRole = 'USER' | 'MODERATOR' | 'ADMIN';
export type OccurrenceStatus =
| 'OPEN'
| 'IN_PROGRESS'
| 'RESOLVED'
| 'CLOSED'
| 'PARCIAL_RESOLVED'
| 'CANCELLED';
export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
// Enum constants for runtime usage
export const UserRoleEnum = {
USER: 'USER' as const,
MODERATOR: 'MODERATOR' as const,
ADMIN: 'ADMIN' as const,
} as const;
export const OccurrenceStatusEnum = {
OPEN: 'OPEN' as const,
IN_PROGRESS: 'IN_PROGRESS' as const,
RESOLVED: 'RESOLVED' as const,
CLOSED: 'CLOSED' as const,
PARCIAL_RESOLVED: 'PARCIAL_RESOLVED' as const,
CANCELLED: 'CANCELLED' as const,
} as const;
export const PriorityEnum = {
LOW: 'LOW' as const,
MEDIUM: 'MEDIUM' as const,
HIGH: 'HIGH' as const,
URGENT: 'URGENT' as const,
} as const;
......@@ -46,6 +46,6 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(process.env.PORT ?? 3000);
await app.listen(process.env.PORT ?? 3001);
}
bootstrap();
......@@ -32,7 +32,7 @@ import { UploadAttachmentDto } from './dto/upload-attachment.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { UserRole } from '../../../generated/prisma';
import { UserRole, UserRoleEnum } from '../../types';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { UserResponseDto } from '../user/dto/user-response.dto';
......@@ -343,7 +343,7 @@ export class AttachmentController {
@Delete('occurrence/:occurrenceId')
@UseGuards(RolesGuard)
@Roles(UserRole.ADMIN, UserRole.MODERATOR)
@Roles(UserRoleEnum.ADMIN, UserRoleEnum.MODERATOR)
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({
summary: 'Delete all attachments for an occurrence (Admin/Moderator only)',
......
......@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { AttachmentService } from './attachment.service';
import { AttachmentController } from './attachment.controller';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
import { memoryStorage } from 'multer';
@Module({
......@@ -49,7 +49,7 @@ import { memoryStorage } from 'multer';
}),
],
controllers: [AttachmentController],
providers: [AttachmentService, PrismaService],
providers: [AttachmentService, DrizzleService],
exports: [AttachmentService],
})
export class AttachmentModule {}
......@@ -4,7 +4,9 @@ import {
BadRequestException,
InternalServerErrorException,
} from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
import { attachments, occurrences } from '../../drizzle/schema';
import { eq, desc, count, sum } from 'drizzle-orm';
import { AttachmentResponseDto } from './dto/attachment-response.dto';
import { UploadAttachmentDto } from './dto/upload-attachment.dto';
import * as fs from 'fs';
......@@ -43,7 +45,7 @@ export class AttachmentService {
'application/x-7z-compressed',
];
constructor(private readonly prisma: PrismaService) {
constructor(private readonly drizzle: DrizzleService) {
this.ensureUploadDirectory();
}
......@@ -62,9 +64,11 @@ export class AttachmentService {
this.validateFile(file);
// Validate that occurrence exists
const occurrence = await this.prisma.occurrence.findUnique({
where: { id: uploadAttachmentDto.occurrenceId },
});
const [occurrence] = await this.drizzle.db
.select()
.from(occurrences)
.where(eq(occurrences.id, uploadAttachmentDto.occurrenceId))
.limit(1);
if (!occurrence) {
throw new BadRequestException('Occurrence not found');
......@@ -84,17 +88,24 @@ export class AttachmentService {
fs.writeFileSync(filePath, file.buffer);
// Save attachment record to database
const attachment = await this.prisma.attachment.create({
data: {
filename,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
path: filePath,
occurrenceId: uploadAttachmentDto.occurrenceId,
},
const attachmentId = this.drizzle.generateId();
await this.drizzle.db.insert(attachments).values({
id: attachmentId,
filename,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
path: filePath,
occurrenceId: uploadAttachmentDto.occurrenceId,
});
// Get the created attachment
const [attachment] = await this.drizzle.db
.select()
.from(attachments)
.where(eq(attachments.id, attachmentId))
.limit(1);
return attachment;
} catch (error) {
// Clean up file if database operation fails
......@@ -121,9 +132,11 @@ export class AttachmentService {
}
// Validate that occurrence exists
const occurrence = await this.prisma.occurrence.findUnique({
where: { id: uploadAttachmentDto.occurrenceId },
});
const [occurrence] = await this.drizzle.db
.select()
.from(occurrences)
.where(eq(occurrences.id, uploadAttachmentDto.occurrenceId))
.limit(1);
if (!occurrence) {
throw new BadRequestException('Occurrence not found');
......@@ -151,17 +164,24 @@ export class AttachmentService {
uploadedFiles.push(filePath);
// Save attachment record to database
const attachment = await this.prisma.attachment.create({
data: {
filename,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
path: filePath,
occurrenceId: uploadAttachmentDto.occurrenceId,
},
const attachmentId = this.drizzle.generateId();
await this.drizzle.db.insert(attachments).values({
id: attachmentId,
filename,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
path: filePath,
occurrenceId: uploadAttachmentDto.occurrenceId,
});
// Get the created attachment
const [attachment] = await this.drizzle.db
.select()
.from(attachments)
.where(eq(attachments.id, attachmentId))
.limit(1);
uploadedAttachments.push(attachment);
}
......@@ -188,24 +208,29 @@ export class AttachmentService {
occurrenceId: string,
): Promise<AttachmentResponseDto[]> {
// Validate that occurrence exists
const occurrence = await this.prisma.occurrence.findUnique({
where: { id: occurrenceId },
});
const [occurrence] = await this.drizzle.db
.select()
.from(occurrences)
.where(eq(occurrences.id, occurrenceId))
.limit(1);
if (!occurrence) {
throw new NotFoundException('Occurrence not found');
}
return this.prisma.attachment.findMany({
where: { occurrenceId },
orderBy: { createdAt: 'desc' },
});
return this.drizzle.db
.select()
.from(attachments)
.where(eq(attachments.occurrenceId, occurrenceId))
.orderBy(desc(attachments.createdAt));
}
async findOne(id: string): Promise<AttachmentResponseDto> {
const attachment = await this.prisma.attachment.findUnique({
where: { id },
});
const [attachment] = await this.drizzle.db
.select()
.from(attachments)
.where(eq(attachments.id, id))
.limit(1);
if (!attachment) {
throw new NotFoundException(`Attachment with ID ${id} not found`);
......@@ -248,18 +273,17 @@ export class AttachmentService {
}
// Delete record from database
await this.prisma.attachment.delete({
where: { id },
});
await this.drizzle.db.delete(attachments).where(eq(attachments.id, id));
}
async removeByOccurrence(occurrenceId: string): Promise<void> {
const attachments = await this.prisma.attachment.findMany({
where: { occurrenceId },
});
const attachmentList = await this.drizzle.db
.select()
.from(attachments)
.where(eq(attachments.occurrenceId, occurrenceId));
// Delete files from disk
for (const attachment of attachments) {
for (const attachment of attachmentList) {
try {
if (fs.existsSync(attachment.path)) {
await unlinkAsync(attachment.path);
......@@ -270,9 +294,9 @@ export class AttachmentService {
}
// Delete records from database
await this.prisma.attachment.deleteMany({
where: { occurrenceId },
});
await this.drizzle.db
.delete(attachments)
.where(eq(attachments.occurrenceId, occurrenceId));
}
private validateFile(file: Express.Multer.File): void {
......@@ -318,20 +342,22 @@ export class AttachmentService {
totalSize: number;
byMimeType: Record<string, number>;
}> {
const where = occurrenceId ? { occurrenceId } : {};
const attachments = await this.prisma.attachment.findMany({
where,
select: {
size: true,
mimeType: true,
},
});
const totalFiles = attachments.length;
const totalSize = attachments.reduce((sum, att) => sum + att.size, 0);
const byMimeType = attachments.reduce(
const whereClause = occurrenceId
? eq(attachments.occurrenceId, occurrenceId)
: undefined;
const attachmentList = await this.drizzle.db
.select({
size: attachments.size,
mimeType: attachments.mimeType,
})
.from(attachments)
.where(whereClause);
const totalFiles = attachmentList.length;
const totalSize = attachmentList.reduce((sum, att) => sum + att.size, 0);
const byMimeType = attachmentList.reduce(
(acc, att) => {
acc[att.mimeType] = (acc[att.mimeType] || 0) + 1;
return acc;
......
import { IsNotEmpty, IsUUID } from 'class-validator';
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UploadAttachmentDto {
......@@ -6,7 +6,7 @@ export class UploadAttachmentDto {
description: 'ID of the occurrence this attachment belongs to',
example: 'clxyz123abc456def',
})
@IsUUID()
@IsString()
@IsNotEmpty()
occurrenceId: string;
}
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';
import { DrizzleService } from '../../common/drizzle.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
......@@ -11,15 +13,19 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
imports: [
UserModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key', // Should be in .env
signOptions: {
expiresIn: '1h', // Default expiration
},
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: {
expiresIn: '1d', // 1 day expiration
},
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, JwtAuthGuard],
providers: [AuthService, JwtStrategy, JwtAuthGuard, DrizzleService],
exports: [AuthService, JwtAuthGuard], // Export for use in other modules
})
export class AuthModule {}
......@@ -6,6 +6,9 @@ import {
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UserService } from '../user/user.service';
import { DrizzleService } from '../../common/drizzle.service';
import { users } from '../../drizzle/schema';
import { eq } from 'drizzle-orm';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { AuthResponseDto } from './dto/auth-response.dto';
......@@ -17,6 +20,7 @@ export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly drizzle: DrizzleService,
) {}
async register(registerDto: RegisterDto): Promise<AuthResponseDto> {
......@@ -61,21 +65,20 @@ export class AuthService {
): Promise<UserResponseDto | null> {
try {
// Get user with password for validation
const userWithPassword = await this.userService['prisma'].user.findUnique(
{
where: { email },
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
password: true,
createdAt: true,
updatedAt: true,
},
},
);
const [userWithPassword] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
user_role: users.user_role,
password: users.password,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!userWithPassword) {
return null;
......@@ -107,10 +110,10 @@ export class AuthService {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
user_role: user.user_role,
};
const expiresIn = 3600; // 1 hour in seconds
const expiresIn = 86400; // 1 day in seconds
const accessToken = await this.jwtService.signAsync(payload, {
expiresIn: `${expiresIn}s`,
......
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../../../generated/prisma';
import { UserRole } from '../../../types';
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);
......@@ -7,7 +7,7 @@ import {
IsString,
MinLength,
} from 'class-validator';
import { UserRole } from '../../../../generated/prisma';
import { UserRole, UserRoleEnum } from '../../../types';
export class RegisterDto {
@ApiProperty({
......@@ -46,12 +46,12 @@ export class RegisterDto {
@ApiProperty({
description: 'User role',
enum: UserRole,
example: UserRole.USER,
default: UserRole.USER,
enum: UserRoleEnum,
example: UserRoleEnum.USER,
default: UserRoleEnum.USER,
required: false,
})
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
@IsEnum(UserRoleEnum)
user_role?: UserRole = UserRoleEnum.USER;
}
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRole } from '../../../../generated/prisma';
import { UserRole } from '../../../types';
@Injectable()
export class RolesGuard implements CanActivate {
......@@ -22,6 +22,6 @@ export class RolesGuard implements CanActivate {
return false;
}
return requiredRoles.some((role) => user.role === role);
return requiredRoles.some((role) => user.user_role === role);
}
}
import { Injectable, UnauthorizedException } from '@nestjs/common';
import {
Injectable,
UnauthorizedException,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '../../user/user.service';
......@@ -6,31 +11,32 @@ import { UserService } from '../../user/user.service';
export interface JwtPayload {
sub: string; // User ID
email: string;
role: string;
user_role: string;
iat?: number;
exp?: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly userService: UserService) {
constructor(
private readonly userService: UserService,
private readonly configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key', // Should be in .env
secretOrKey: configService.get<string>('JWT_SECRET') || 'your-secret-key',
});
}
async validate(payload: JwtPayload) {
try {
const user = await this.userService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
return user; // This will be available as req.user
} catch (error) {
if (error instanceof NotFoundException) {
throw new UnauthorizedException('User not found');
}
throw new UnauthorizedException('Invalid token');
}
}
......
......@@ -24,7 +24,7 @@ import { CommentService } from './comment.service';
import { CreateCommentDto } from './dto/create-comment.dto';
import { UpdateCommentDto } from './dto/update-comment.dto';
import { CommentResponseDto } from './dto/comment-response.dto';
import { UserRole } from '../../../generated/prisma';
import { UserRole, UserRoleEnum } from '../../types';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
......@@ -101,7 +101,7 @@ export class CommentController {
): Promise<CommentResponseDto[]> {
return this.commentService.findByOccurrence(
occurrenceId,
user.role,
user.user_role,
page ? Number(page) : undefined,
limit ? Number(limit) : undefined,
);
......@@ -139,7 +139,7 @@ export class CommentController {
@Param('occurrenceId') occurrenceId: string,
@CurrentUser() user: UserResponseDto,
): Promise<{ count: number }> {
const count = await this.commentService.count(occurrenceId, user.role);
const count = await this.commentService.count(occurrenceId, user.user_role);
return { count };
}
......@@ -171,7 +171,7 @@ export class CommentController {
@Param('id') id: string,
@CurrentUser() user: UserResponseDto,
): Promise<CommentResponseDto> {
return this.commentService.findOne(id, user.role);
return this.commentService.findOne(id, user.user_role);
}
@Patch(':id')
......@@ -205,7 +205,12 @@ export class CommentController {
@Body() updateCommentDto: UpdateCommentDto,
@CurrentUser() user: UserResponseDto,
): Promise<CommentResponseDto> {
return this.commentService.update(id, updateCommentDto, user.id, user.role);
return this.commentService.update(
id,
updateCommentDto,
user.id,
user.user_role,
);
}
@Delete(':id')
......@@ -236,14 +241,14 @@ export class CommentController {
@Param('id') id: string,
@CurrentUser() user: UserResponseDto,
): Promise<void> {
return this.commentService.remove(id, user.id, user.role);
return this.commentService.remove(id, user.id, user.user_role);
}
// Admin/Moderator only endpoints for managing internal comments
@Post('internal')
@UseGuards(RolesGuard)
@Roles(UserRole.ADMIN, UserRole.MODERATOR)
@Roles(UserRoleEnum.ADMIN, UserRoleEnum.MODERATOR)
@ApiOperation({
summary: 'Create an internal comment (Admin/Moderator only)',
description:
......@@ -278,7 +283,7 @@ export class CommentController {
@Get('occurrence/:occurrenceId/internal')
@UseGuards(RolesGuard)
@Roles(UserRole.ADMIN, UserRole.MODERATOR)
@Roles(UserRoleEnum.ADMIN, UserRoleEnum.MODERATOR)
@ApiOperation({
summary:
'Get only internal comments for an occurrence (Admin/Moderator only)',
......@@ -328,7 +333,7 @@ export class CommentController {
// which will show all comments including internal ones, then we'll filter
const allComments = await this.commentService.findByOccurrence(
occurrenceId,
UserRole.ADMIN,
UserRoleEnum.ADMIN,
page ? Number(page) : undefined,
limit ? Number(limit) : undefined,
);
......
import { Module } from '@nestjs/common';
import { CommentService } from './comment.service';
import { CommentController } from './comment.controller';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
@Module({
controllers: [CommentController],
providers: [CommentService, PrismaService],
providers: [CommentService, DrizzleService],
exports: [CommentService],
})
export class CommentModule {}
import {
IsString,
IsNotEmpty,
IsOptional,
IsBoolean,
IsUUID,
} from 'class-validator';
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateCommentDto {
......@@ -21,7 +15,7 @@ export class CreateCommentDto {
description: 'ID of the occurrence this comment belongs to',
example: 'clxyz123abc456def',
})
@IsUUID()
@IsString()
@IsNotEmpty()
occurrenceId: string;
......
import { Module } from '@nestjs/common';
import { ConclusionService } from './conclusion.service';
import { ConclusionController } from './conclusion.controller';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
@Module({
controllers: [ConclusionController],
providers: [ConclusionService, PrismaService],
providers: [ConclusionService, DrizzleService],
exports: [ConclusionService],
})
export class ConclusionModule {}
......@@ -4,23 +4,27 @@ import {
BadRequestException,
ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
import { conclusions, occurrences } from '../../drizzle/schema';
import { eq } from 'drizzle-orm';
import { CreateConclusionDto } from './dto/create-conclusion.dto';
import { UpdateConclusionDto } from './dto/update-conclusion.dto';
import { ConclusionResponseDto } from './dto/conclusion-response.dto';
@Injectable()
export class ConclusionService {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly drizzle: DrizzleService) {}
async create(
occurrenceId: string,
createConclusionDto: CreateConclusionDto,
): Promise<ConclusionResponseDto> {
// Check if occurrence exists
const occurrence = await this.prisma.occurrence.findUnique({
where: { id: occurrenceId },
});
const [occurrence] = await this.drizzle.db
.select()
.from(occurrences)
.where(eq(occurrences.id, occurrenceId))
.limit(1);
if (!occurrence) {
throw new NotFoundException(
......@@ -29,9 +33,11 @@ export class ConclusionService {
}
// Check if conclusion already exists for this occurrence
const existingConclusion = await this.prisma.conclusion.findUnique({
where: { occurrenceId },
});
const [existingConclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.occurrenceId, occurrenceId))
.limit(1);
if (existingConclusion) {
throw new ConflictException(
......@@ -58,30 +64,41 @@ export class ConclusionService {
);
}
const conclusion = await this.prisma.conclusion.create({
data: {
...createConclusionDto,
occurrenceId,
},
const conclusionId = this.drizzle.generateId();
await this.drizzle.db.insert(conclusions).values({
id: conclusionId,
...createConclusionDto,
occurrenceId,
});
// Get the created conclusion
const [conclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.id, conclusionId))
.limit(1);
return conclusion;
}
async findByOccurrence(
occurrenceId: string,
): Promise<ConclusionResponseDto | null> {
const conclusion = await this.prisma.conclusion.findUnique({
where: { occurrenceId },
});
const [conclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.occurrenceId, occurrenceId))
.limit(1);
return conclusion;
return conclusion || null;
}
async findOne(id: string): Promise<ConclusionResponseDto> {
const conclusion = await this.prisma.conclusion.findUnique({
where: { id },
});
const [conclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.id, id))
.limit(1);
if (!conclusion) {
throw new NotFoundException(`Conclusion with ID ${id} not found`);
......@@ -122,10 +139,17 @@ export class ConclusionService {
updateConclusionDto.materialUsed = null as any;
}
const conclusion = await this.prisma.conclusion.update({
where: { id },
data: updateConclusionDto,
});
await this.drizzle.db
.update(conclusions)
.set(updateConclusionDto)
.where(eq(conclusions.id, id));
// Get the updated conclusion
const [conclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.id, id))
.limit(1);
return conclusion;
}
......@@ -134,20 +158,20 @@ export class ConclusionService {
// Check if conclusion exists
await this.findOne(id);
await this.prisma.conclusion.delete({
where: { id },
});
await this.drizzle.db.delete(conclusions).where(eq(conclusions.id, id));
}
async removeByOccurrence(occurrenceId: string): Promise<void> {
const conclusion = await this.prisma.conclusion.findUnique({
where: { occurrenceId },
});
const [conclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.occurrenceId, occurrenceId))
.limit(1);
if (conclusion) {
await this.prisma.conclusion.delete({
where: { id: conclusion.id },
});
await this.drizzle.db
.delete(conclusions)
.where(eq(conclusions.id, conclusion.id));
}
}
}
......@@ -6,7 +6,12 @@ import {
IsString,
MaxLength,
} from 'class-validator';
import { OccurrenceStatus, Priority } from '../../../../generated/prisma';
import {
OccurrenceStatus,
Priority,
OccurrenceStatusEnum,
PriorityEnum,
} from '../../../types';
export class CreateOccurrenceDto {
@ApiProperty({
......@@ -46,23 +51,23 @@ export class CreateOccurrenceDto {
@ApiPropertyOptional({
description: 'Occurrence priority level',
enum: Priority,
example: Priority.HIGH,
default: Priority.MEDIUM,
enum: PriorityEnum,
example: PriorityEnum.HIGH,
default: PriorityEnum.MEDIUM,
})
@IsOptional()
@IsEnum(Priority)
priority?: Priority = Priority.MEDIUM;
@IsEnum(PriorityEnum)
priority?: Priority = PriorityEnum.MEDIUM;
@ApiPropertyOptional({
description: 'Occurrence status',
enum: OccurrenceStatus,
example: OccurrenceStatus.OPEN,
default: OccurrenceStatus.OPEN,
enum: OccurrenceStatusEnum,
example: OccurrenceStatusEnum.OPEN,
default: OccurrenceStatusEnum.OPEN,
})
@IsOptional()
@IsEnum(OccurrenceStatus)
status?: OccurrenceStatus = OccurrenceStatus.OPEN;
@IsEnum(OccurrenceStatusEnum)
status?: OccurrenceStatus = OccurrenceStatusEnum.OPEN;
@ApiProperty({
description: 'ID of the user reporting the occurrence',
......
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class DualAssignOccurrenceDto {
@ApiProperty({
description:
'ID of the user to assign the occurrence to (worker/technician)',
example: 'clxyz123abc456def',
})
@IsNotEmpty()
@IsString()
assigneeId: string;
@ApiProperty({
description: 'ID of the manager/admin to oversee the occurrence',
example: 'clxyz789def012ghi',
})
@IsNotEmpty()
@IsString()
managerId: string;
@ApiPropertyOptional({
description: 'Optional note about the dual assignment',
example:
'Assigned to technical team with manager oversight for quality control',
})
@IsOptional()
@IsString()
assignmentNote?: string;
}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { OccurrenceStatus, Priority } from '../../../../generated/prisma';
import {
OccurrenceStatus,
Priority,
OccurrenceStatusEnum,
PriorityEnum,
} from '../../../types';
import { ConclusionResponseDto } from '../../conclusion/dto/conclusion-response.dto';
export class OccurrenceResponseDto {
......@@ -23,15 +28,15 @@ export class OccurrenceResponseDto {
@ApiProperty({
description: 'Occurrence status',
enum: OccurrenceStatus,
example: OccurrenceStatus.OPEN,
enum: OccurrenceStatusEnum,
example: OccurrenceStatusEnum.OPEN,
})
status: OccurrenceStatus;
@ApiProperty({
description: 'Occurrence priority level',
enum: Priority,
example: Priority.MEDIUM,
enum: PriorityEnum,
example: PriorityEnum.MEDIUM,
})
priority: Priority;
......@@ -59,6 +64,12 @@ export class OccurrenceResponseDto {
})
assigneeId?: string | null;
@ApiPropertyOptional({
description: 'ID of the manager/admin overseeing the occurrence',
example: 'clxyz789def012ghi',
})
managerId?: string | null;
@ApiProperty({
description: 'Occurrence creation timestamp',
example: '2024-01-01T12:00:00.000Z',
......@@ -112,6 +123,23 @@ export class OccurrenceResponseDto {
} | null;
@ApiPropertyOptional({
description: 'Manager/admin overseeing the occurrence',
type: 'object',
properties: {
id: { type: 'string', example: 'clxyz789def012ghi' },
firstName: { type: 'string', example: 'Mike' },
lastName: { type: 'string', example: 'Johnson' },
email: { type: 'string', example: 'mike.johnson@example.com' },
},
})
managedBy?: {
id: string;
firstName: string;
lastName: string;
email: string;
} | null;
@ApiPropertyOptional({
description: 'Number of comments on this occurrence',
example: 5,
})
......
import { ApiPropertyOptional, PartialType } from '@nestjs/swagger';
import { IsEnum, IsOptional, IsDateString } from 'class-validator';
import { CreateOccurrenceDto } from './create-occurrence.dto';
import { OccurrenceStatus, Priority } from '../../../../generated/prisma';
import {
OccurrenceStatus,
Priority,
OccurrenceStatusEnum,
PriorityEnum,
} from '../../../types';
export class UpdateOccurrenceDto extends PartialType(CreateOccurrenceDto) {
@ApiPropertyOptional({
description: 'Occurrence status',
enum: OccurrenceStatus,
example: OccurrenceStatus.IN_PROGRESS,
enum: OccurrenceStatusEnum,
example: OccurrenceStatusEnum.IN_PROGRESS,
})
@IsOptional()
@IsEnum(OccurrenceStatus)
@IsEnum(OccurrenceStatusEnum)
status?: OccurrenceStatus;
@ApiPropertyOptional({
description: 'Occurrence priority level',
enum: Priority,
example: Priority.HIGH,
enum: PriorityEnum,
example: PriorityEnum.HIGH,
})
@IsOptional()
@IsEnum(Priority)
@IsEnum(PriorityEnum)
priority?: Priority;
@ApiPropertyOptional({
......
......@@ -25,11 +25,15 @@ import { CreateOccurrenceDto } from './dto/create-occurrence.dto';
import { UpdateOccurrenceDto } from './dto/update-occurrence.dto';
import { OccurrenceResponseDto } from './dto/occurrence-response.dto';
import { AssignOccurrenceDto } from './dto/assign-occurrence.dto';
import { DualAssignOccurrenceDto } from './dto/dual-assign-occurrence.dto';
import {
OccurrenceStatus,
Priority,
UserRole,
} from '../../../generated/prisma';
OccurrenceStatusEnum,
PriorityEnum,
UserRoleEnum,
} from '../../types';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
......@@ -62,13 +66,13 @@ export class OccurrenceController {
@ApiQuery({
name: 'status',
required: false,
enum: OccurrenceStatus,
enum: OccurrenceStatusEnum,
description: 'Filter by occurrence status',
})
@ApiQuery({
name: 'priority',
required: false,
enum: Priority,
enum: PriorityEnum,
description: 'Filter by priority level',
})
@ApiQuery({
......@@ -133,13 +137,13 @@ export class OccurrenceController {
@ApiQuery({
name: 'status',
required: false,
enum: OccurrenceStatus,
enum: OccurrenceStatusEnum,
description: 'Filter by occurrence status',
})
@ApiQuery({
name: 'priority',
required: false,
enum: Priority,
enum: PriorityEnum,
description: 'Filter by priority level',
})
@ApiQuery({
......@@ -266,6 +270,24 @@ export class OccurrenceController {
return this.occurrenceService.findByUser(userId, 'assigned');
}
@Get('user/:userId/managed')
@ApiOperation({ summary: 'Get occurrences managed by a specific user' })
@ApiParam({
name: 'userId',
description: 'User unique identifier',
example: 'clxyz123abc456def',
})
@ApiResponse({
status: 200,
description: 'List of occurrences managed by the user',
type: [OccurrenceResponseDto],
})
async findManagedByUser(
@Param('userId') userId: string,
): Promise<OccurrenceResponseDto[]> {
return this.occurrenceService.findByUser(userId, 'managed');
}
@Get(':id')
@ApiOperation({ summary: 'Get occurrence by ID' })
@ApiParam({
......@@ -316,7 +338,7 @@ export class OccurrenceController {
@Patch(':id/assign')
@UseGuards(RolesGuard)
@Roles(UserRole.ADMIN, UserRole.MODERATOR)
@Roles(UserRoleEnum.ADMIN, UserRoleEnum.MODERATOR)
@ApiOperation({
summary: 'Assign occurrence to a user (Admin/Moderator only)',
})
......@@ -350,9 +372,51 @@ export class OccurrenceController {
return this.occurrenceService.assignOccurrence(id, assignOccurrenceDto);
}
@Patch(':id/dual-assign')
@UseGuards(RolesGuard)
@Roles(UserRoleEnum.ADMIN, UserRoleEnum.MODERATOR)
@ApiOperation({
summary:
'Dual assign occurrence to both a user and a manager (Admin/Moderator only)',
description:
'Assigns an occurrence to both a worker/technician (assignee) and a manager/admin (manager) for dual oversight',
})
@ApiParam({
name: 'id',
description: 'Occurrence unique identifier',
example: 'clxyz123abc456def',
})
@ApiBody({ type: DualAssignOccurrenceDto })
@ApiResponse({
status: 200,
description: 'Occurrence dual assigned successfully',
type: OccurrenceResponseDto,
})
@ApiResponse({
status: 404,
description: 'Occurrence not found',
})
@ApiResponse({
status: 400,
description: 'Assignee or manager user not found',
})
@ApiResponse({
status: 403,
description: 'Access denied - Admin or Moderator role required',
})
async dualAssignOccurrence(
@Param('id') id: string,
@Body() dualAssignOccurrenceDto: DualAssignOccurrenceDto,
): Promise<OccurrenceResponseDto> {
return this.occurrenceService.dualAssignOccurrence(
id,
dualAssignOccurrenceDto,
);
}
@Patch(':id/unassign')
@UseGuards(RolesGuard)
@Roles(UserRole.ADMIN, UserRole.MODERATOR)
@Roles(UserRoleEnum.ADMIN, UserRoleEnum.MODERATOR)
@ApiOperation({ summary: 'Unassign occurrence (Admin/Moderator only)' })
@ApiParam({
name: 'id',
......
import { Module } from '@nestjs/common';
import { OccurrenceService } from './occurrence.service';
import { OccurrenceController } from './occurrence.controller';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
@Module({
controllers: [OccurrenceController],
providers: [OccurrenceService, PrismaService],
providers: [OccurrenceService, DrizzleService],
exports: [OccurrenceService], // Export service for use in other modules
})
export class OccurrenceModule {}
......@@ -6,7 +6,7 @@ import {
IsString,
MinLength,
} from 'class-validator';
import { UserRole } from '../../../../generated/prisma';
import { UserRole, UserRoleEnum } from '../../../types';
export class CreateUserDto {
@ApiProperty({
......@@ -45,10 +45,10 @@ export class CreateUserDto {
@ApiProperty({
description: 'User role',
enum: UserRole,
example: UserRole.USER,
default: UserRole.USER,
enum: UserRoleEnum,
example: UserRoleEnum.USER,
default: UserRoleEnum.USER,
})
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
@IsEnum(UserRoleEnum)
user_role?: UserRole = UserRoleEnum.USER;
}
import { ApiPropertyOptional, PartialType } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator';
import { CreateUserDto } from './create-user.dto';
import { UserRole } from '../../../../generated/prisma';
import { UserRole, UserRoleEnum } from '../../../types';
export class UpdateUserDto extends PartialType(CreateUserDto) {
@ApiPropertyOptional({
description: 'User role',
enum: UserRole,
example: UserRole.USER,
enum: UserRoleEnum,
example: UserRoleEnum.USER,
})
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
@IsEnum(UserRoleEnum)
user_role?: UserRole;
}
import { ApiProperty } from '@nestjs/swagger';
import { UserRole } from '../../../../generated/prisma';
import { UserRole, UserRoleEnum } from '../../../types';
export class UserResponseDto {
@ApiProperty({
......@@ -28,10 +28,10 @@ export class UserResponseDto {
@ApiProperty({
description: 'User role',
enum: UserRole,
example: UserRole.USER,
enum: UserRoleEnum,
example: UserRoleEnum.USER,
})
role: UserRole;
user_role: UserRole;
@ApiProperty({
description: 'User creation timestamp',
......
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
@Module({
controllers: [UserController],
providers: [UserService, PrismaService],
providers: [UserService, DrizzleService],
exports: [UserService], // Export service for use in other modules
})
export class UserModule {}
......@@ -5,7 +5,9 @@ import {
BadRequestException,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
import { users } from '../../drizzle/schema';
import { eq, and, ne } from 'drizzle-orm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserResponseDto } from './dto/user-response.dto';
......@@ -15,15 +17,17 @@ import * as bcrypt from 'bcrypt';
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly drizzle: DrizzleService) {}
async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
// Check if user with email already exists
const existingUser = await this.prisma.user.findUnique({
where: { email: createUserDto.email },
});
const existingUser = await this.drizzle.db
.select()
.from(users)
.where(eq(users.email, createUserDto.email))
.limit(1);
if (existingUser) {
if (existingUser.length > 0) {
throw new ConflictException('User with this email already exists');
}
......@@ -31,57 +35,65 @@ export class UserService {
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// Create user
const user = await this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
},
const userId = this.drizzle.generateId();
await this.drizzle.db.insert(users).values({
id: userId,
firstName: createUserDto.firstName,
lastName: createUserDto.lastName,
email: createUserDto.email,
user_role: createUserDto.user_role || 'USER',
password: hashedPassword,
});
// Get the created user
const [user] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
user_role: users.user_role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
return user;
}
async findAll(): Promise<UserResponseDto[]> {
const users = await this.prisma.user.findMany({
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc',
},
});
return users;
const usersList = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
user_role: users.user_role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.orderBy(users.createdAt);
return usersList;
}
async findOne(id: string): Promise<UserResponseDto> {
const user = await this.prisma.user.findUnique({
where: { id },
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
},
});
const [user] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
user_role: users.user_role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.id, id))
.limit(1);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
......@@ -91,20 +103,21 @@ export class UserService {
}
async findByEmail(email: string): Promise<UserResponseDto | null> {
const user = await this.prisma.user.findUnique({
where: { email },
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
},
});
return user;
const [user] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
user_role: users.user_role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.email, email))
.limit(1);
return user || null;
}
async update(
......@@ -116,11 +129,13 @@ export class UserService {
// If email is being updated, check for conflicts
if (updateUserDto.email) {
const existingUser = await this.prisma.user.findUnique({
where: { email: updateUserDto.email },
});
const existingUser = await this.drizzle.db
.select()
.from(users)
.where(and(eq(users.email, updateUserDto.email), ne(users.id, id)))
.limit(1);
if (existingUser && existingUser.id !== id) {
if (existingUser.length > 0) {
throw new ConflictException('User with this email already exists');
}
}
......@@ -131,19 +146,22 @@ export class UserService {
updateData.password = await bcrypt.hash(updateUserDto.password, 10);
}
const user = await this.prisma.user.update({
where: { id },
data: updateData,
select: {
id: true,
firstName: true,
lastName: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
},
});
await this.drizzle.db.update(users).set(updateData).where(eq(users.id, id));
// Get the updated user
const [user] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
user_role: users.user_role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.id, id))
.limit(1);
return user;
}
......@@ -152,13 +170,14 @@ export class UserService {
// Check if user exists
await this.findOne(id);
await this.prisma.user.delete({
where: { id },
});
await this.drizzle.db.delete(users).where(eq(users.id, id));
}
async count(): Promise<number> {
return this.prisma.user.count();
const result = await this.drizzle.db
.select({ count: users.id })
.from(users);
return result.length;
}
async changePassword(
......@@ -166,13 +185,14 @@ export class UserService {
changePasswordDto: ChangePasswordDto,
): Promise<{ message: string }> {
// Get user with password for verification
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
password: true,
},
});
const [user] = await this.drizzle.db
.select({
id: users.id,
password: users.password,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
......@@ -195,10 +215,10 @@ export class UserService {
);
// Update password
await this.prisma.user.update({
where: { id: userId },
data: { password: hashedNewPassword },
});
await this.drizzle.db
.update(users)
.set({ password: hashedNewPassword })
.where(eq(users.id, userId));
return { message: 'Password changed successfully' };
}
......@@ -217,10 +237,10 @@ export class UserService {
);
// Update password
await this.prisma.user.update({
where: { id: userId },
data: { password: hashedNewPassword },
});
await this.drizzle.db
.update(users)
.set({ password: hashedNewPassword })
.where(eq(users.id, userId));
return { message: 'Password changed successfully by admin' };
}
......
// Enum types
export type UserRole = 'USER' | 'MODERATOR' | 'ADMIN';
export type OccurrenceStatus =
| 'OPEN'
| 'IN_PROGRESS'
| 'RESOLVED'
| 'CLOSED'
| 'PARCIAL_RESOLVED'
| 'CANCELLED';
export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
// Enum constants for runtime usage
export const UserRoleEnum = {
USER: 'USER' as const,
MODERATOR: 'MODERATOR' as const,
ADMIN: 'ADMIN' as const,
} as const;
export const OccurrenceStatusEnum = {
OPEN: 'OPEN' as const,
IN_PROGRESS: 'IN_PROGRESS' as const,
RESOLVED: 'RESOLVED' as const,
CLOSED: 'CLOSED' as const,
PARCIAL_RESOLVED: 'PARCIAL_RESOLVED' as const,
CANCELLED: 'CANCELLED' as const,
} as const;
export const PriorityEnum = {
LOW: 'LOW' as const,
MEDIUM: 'MEDIUM' as const,
HIGH: 'HIGH' as const,
URGENT: 'URGENT' as const,
} as const;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment