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": "5",
"dialect": "mysql",
"id": "bc9c1809-8c1b-40b5-abc1-fd3ff12d8f47",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"attachments": {
"name": "attachments",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"filename": {
"name": "filename",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"originalName": {
"name": "originalName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mimeType": {
"name": "mimeType",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"size": {
"name": "size",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {
"attachment_occurrence_idx": {
"name": "attachment_occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"attachments_id": {
"name": "attachments_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"comments": {
"name": "comments",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"isInternal": {
"name": "isInternal",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"authorId": {
"name": "authorId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"occurrence_idx": {
"name": "occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
},
"author_idx": {
"name": "author_idx",
"columns": [
"authorId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"comments_id": {
"name": "comments_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"conclusions": {
"name": "conclusions",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fieldMaterial": {
"name": "fieldMaterial",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"materialUsed": {
"name": "materialUsed",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"occurrenceId": {
"name": "occurrenceId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"conclusion_occurrence_idx": {
"name": "conclusion_occurrence_idx",
"columns": [
"occurrenceId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"conclusions_id": {
"name": "conclusions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"conclusions_occurrenceId_unique": {
"name": "conclusions_occurrenceId_unique",
"columns": [
"occurrenceId"
]
}
},
"checkConstraint": {}
},
"occurrences": {
"name": "occurrences",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('OPEN','IN_PROGRESS','RESOLVED','CLOSED','PARCIAL_RESOLVED','CANCELLED')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'OPEN'"
},
"priority": {
"name": "priority",
"type": "enum('LOW','MEDIUM','HIGH','URGENT')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'MEDIUM'"
},
"category": {
"name": "category",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"location": {
"name": "location",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reporterId": {
"name": "reporterId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"assigneeId": {
"name": "assigneeId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"managerId": {
"name": "managerId",
"type": "varchar(25)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"closedAt": {
"name": "closedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"reporter_idx": {
"name": "reporter_idx",
"columns": [
"reporterId"
],
"isUnique": false
},
"assignee_idx": {
"name": "assignee_idx",
"columns": [
"assigneeId"
],
"isUnique": false
},
"manager_idx": {
"name": "manager_idx",
"columns": [
"managerId"
],
"isUnique": false
},
"status_idx": {
"name": "status_idx",
"columns": [
"status"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"occurrences_id": {
"name": "occurrences_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "varchar(25)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"firstName": {
"name": "firstName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastName": {
"name": "lastName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_role": {
"name": "user_role",
"type": "enum('USER','MODERATOR','ADMIN')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'USER'"
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"email_idx": {
"name": "email_idx",
"columns": [
"email"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}
\ No newline at end of file
{
"version": "7",
"dialect": "mysql",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1757350101348,
"tag": "0000_add_manager_to_occurrences",
"breakpoints": true
}
]
}
......@@ -10,21 +10,22 @@
"license": "UNLICENSED",
"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"
......@@ -44,6 +45,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",
......@@ -779,6 +781,891 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@drizzle-team/brocli": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
"source-map-support": "^0.5.21"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/@esbuild-kit/esm-loader": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"@esbuild-kit/core-utils": "^3.3.2",
"get-tsconfig": "^4.7.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
"integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
"integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
"integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
"integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
"integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
"integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
"integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
"integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
"integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
"integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
"integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
"integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
"integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
"integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
"integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
"integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
"integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
"integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
"integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
"integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
"integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
"integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
"integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
"integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
"integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
"integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
......@@ -2563,6 +3450,33 @@
}
}
},
"node_modules/@nestjs/config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz",
"integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==",
"license": "MIT",
"dependencies": {
"dotenv": "16.4.7",
"dotenv-expand": "12.0.1",
"lodash": "4.17.21"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@nestjs/core": {
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz",
......@@ -2842,6 +3756,8 @@
"integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18.18"
},
......@@ -2863,6 +3779,8 @@
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
......@@ -2874,7 +3792,9 @@
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
"license": "Apache-2.0"
"license": "Apache-2.0",
"optional": true,
"peer": true
},
"node_modules/@prisma/engines": {
"version": "6.15.0",
......@@ -2882,6 +3802,8 @@
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
......@@ -2893,13 +3815,17 @@
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
"license": "Apache-2.0"
"license": "Apache-2.0",
"optional": true,
"peer": true
},
"node_modules/@prisma/fetch-engine": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
......@@ -2911,6 +3837,8 @@
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/debug": "6.15.0"
}
......@@ -2966,7 +3894,9 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@swc/cli": {
"version": "0.6.0",
......@@ -4700,6 +5630,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
......@@ -5102,6 +6041,8 @@
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
......@@ -5262,6 +6203,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
......@@ -5304,6 +6246,8 @@
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"consola": "^3.2.3"
}
......@@ -5564,7 +6508,9 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/consola": {
"version": "3.4.2",
......@@ -5803,6 +6749,8 @@
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=16.0.0"
}
......@@ -5834,7 +6782,9 @@
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
......@@ -5846,6 +6796,15 @@
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
......@@ -5859,7 +6818,9 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/detect-newline": {
"version": "3.1.0",
......@@ -5914,6 +6875,162 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/drizzle-kit": {
"version": "0.31.4",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz",
"integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"esbuild": "^0.25.4",
"esbuild-register": "^3.5.0"
},
"bin": {
"drizzle-kit": "bin.cjs"
}
},
"node_modules/drizzle-orm": {
"version": "0.44.5",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz",
"integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@cloudflare/workers-types": ">=4",
"@electric-sql/pglite": ">=0.2.0",
"@libsql/client": ">=0.10.0",
"@libsql/client-wasm": ">=0.10.0",
"@neondatabase/serverless": ">=0.10.0",
"@op-engineering/op-sqlite": ">=2",
"@opentelemetry/api": "^1.4.1",
"@planetscale/database": ">=1.13",
"@prisma/client": "*",
"@tidbcloud/serverless": "*",
"@types/better-sqlite3": "*",
"@types/pg": "*",
"@types/sql.js": "*",
"@upstash/redis": ">=1.34.7",
"@vercel/postgres": ">=0.8.0",
"@xata.io/client": "*",
"better-sqlite3": ">=7",
"bun-types": "*",
"expo-sqlite": ">=14.0.0",
"gel": ">=2",
"knex": "*",
"kysely": "*",
"mysql2": ">=2",
"pg": ">=8",
"postgres": ">=3",
"sql.js": ">=1",
"sqlite3": ">=5"
},
"peerDependenciesMeta": {
"@aws-sdk/client-rds-data": {
"optional": true
},
"@cloudflare/workers-types": {
"optional": true
},
"@electric-sql/pglite": {
"optional": true
},
"@libsql/client": {
"optional": true
},
"@libsql/client-wasm": {
"optional": true
},
"@neondatabase/serverless": {
"optional": true
},
"@op-engineering/op-sqlite": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@planetscale/database": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@tidbcloud/serverless": {
"optional": true
},
"@types/better-sqlite3": {
"optional": true
},
"@types/pg": {
"optional": true
},
"@types/sql.js": {
"optional": true
},
"@upstash/redis": {
"optional": true
},
"@vercel/postgres": {
"optional": true
},
"@xata.io/client": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"bun-types": {
"optional": true
},
"expo-sqlite": {
"optional": true
},
"gel": {
"optional": true
},
"knex": {
"optional": true
},
"kysely": {
"optional": true
},
"mysql2": {
"optional": true
},
"pg": {
"optional": true
},
"postgres": {
"optional": true
},
"prisma": {
"optional": true
},
"sql.js": {
"optional": true
},
"sqlite3": {
"optional": true
}
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
......@@ -5955,6 +7072,8 @@
"resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
"integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
......@@ -5992,6 +7111,8 @@
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=14"
}
......@@ -6082,6 +7203,61 @@
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.9",
"@esbuild/android-arm": "0.25.9",
"@esbuild/android-arm64": "0.25.9",
"@esbuild/android-x64": "0.25.9",
"@esbuild/darwin-arm64": "0.25.9",
"@esbuild/darwin-x64": "0.25.9",
"@esbuild/freebsd-arm64": "0.25.9",
"@esbuild/freebsd-x64": "0.25.9",
"@esbuild/linux-arm": "0.25.9",
"@esbuild/linux-arm64": "0.25.9",
"@esbuild/linux-ia32": "0.25.9",
"@esbuild/linux-loong64": "0.25.9",
"@esbuild/linux-mips64el": "0.25.9",
"@esbuild/linux-ppc64": "0.25.9",
"@esbuild/linux-riscv64": "0.25.9",
"@esbuild/linux-s390x": "0.25.9",
"@esbuild/linux-x64": "0.25.9",
"@esbuild/netbsd-arm64": "0.25.9",
"@esbuild/netbsd-x64": "0.25.9",
"@esbuild/openbsd-arm64": "0.25.9",
"@esbuild/openbsd-x64": "0.25.9",
"@esbuild/openharmony-arm64": "0.25.9",
"@esbuild/sunos-x64": "0.25.9",
"@esbuild/win32-arm64": "0.25.9",
"@esbuild/win32-ia32": "0.25.9",
"@esbuild/win32-x64": "0.25.9"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
......@@ -6461,7 +7637,9 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/ext-list": {
"version": "2.2.2",
......@@ -6505,6 +7683,8 @@
}
],
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"pure-rand": "^6.1.0"
},
......@@ -6954,6 +8134,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
......@@ -7034,11 +8223,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
......@@ -7559,6 +8763,12 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
......@@ -8364,6 +9574,8 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
......@@ -8695,6 +9907,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
......@@ -8718,6 +9936,21 @@
"yallist": "^3.0.2"
}
},
"node_modules/lru.min": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
......@@ -9036,6 +10269,63 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/mysql2": {
"version": "3.14.5",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.5.tgz",
"integrity": "sha512-40hDf8LPUsuuJ2hFq+UgOuPwt2IFLIRDvMv6ez9hKbXeYuZPxDDwiJW7KdknvOsQqKznaKczOT1kELgFkhDvFg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
"license": "MIT",
"dependencies": {
"lru-cache": "^7.14.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/named-placeholders/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
......@@ -9089,7 +10379,9 @@
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
......@@ -9157,6 +10449,8 @@
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
......@@ -9196,7 +10490,9 @@
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/on-finished": {
"version": "2.4.1",
......@@ -9534,7 +10830,9 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/pause": {
"version": "0.0.1",
......@@ -9552,7 +10850,9 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/picocolors": {
"version": "1.1.1",
......@@ -9668,6 +10968,8 @@
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
......@@ -9757,6 +11059,8 @@
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@prisma/config": "6.15.0",
"@prisma/engines": "6.15.0"
......@@ -9817,6 +11121,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"devOptional": true,
"funding": [
{
"type": "individual",
......@@ -9917,6 +11222,8 @@
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
......@@ -9947,6 +11254,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
......@@ -10053,6 +11361,16 @@
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
......@@ -10292,6 +11610,11 @@
"node": ">= 18"
}
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
......@@ -10512,6 +11835,15 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
......@@ -11151,7 +12483,9 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/tmpl": {
"version": "1.0.5",
......
......@@ -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 {}
......@@ -4,58 +4,86 @@ import {
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.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 { comments, users, occurrences } from '../../drizzle/schema';
import { eq, and, desc, count } from 'drizzle-orm';
@Injectable()
export class CommentService {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly drizzle: DrizzleService) {}
private async getCommentWithRelations(
commentId: string,
): Promise<CommentResponseDto> {
const [comment] = await this.drizzle.db
.select({
id: comments.id,
content: comments.content,
isInternal: comments.isInternal,
occurrenceId: comments.occurrenceId,
authorId: comments.authorId,
createdAt: comments.createdAt,
updatedAt: comments.updatedAt,
author: {
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
},
})
.from(comments)
.leftJoin(users, eq(comments.authorId, users.id))
.where(eq(comments.id, commentId))
.limit(1);
if (!comment || !comment.author) {
throw new NotFoundException(`Comment with ID ${commentId} not found`);
}
return comment;
}
async create(
createCommentDto: CreateCommentDto,
authorId: string,
): Promise<CommentResponseDto> {
// Validate that occurrence exists
const occurrence = await this.prisma.occurrence.findUnique({
where: { id: createCommentDto.occurrenceId },
});
const [occurrence] = await this.drizzle.db
.select()
.from(occurrences)
.where(eq(occurrences.id, createCommentDto.occurrenceId))
.limit(1);
if (!occurrence) {
throw new BadRequestException('Occurrence not found');
}
// Validate that author exists
const author = await this.prisma.user.findUnique({
where: { id: authorId },
});
const [author] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, authorId))
.limit(1);
if (!author) {
throw new BadRequestException('Author user not found');
}
const comment = await this.prisma.comment.create({
data: {
content: createCommentDto.content,
isInternal: createCommentDto.isInternal || false,
occurrenceId: createCommentDto.occurrenceId,
authorId: authorId,
},
include: {
author: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
},
// Create comment
const commentId = this.drizzle.generateId();
await this.drizzle.db.insert(comments).values({
id: commentId,
content: createCommentDto.content,
isInternal: createCommentDto.isInternal || false,
occurrenceId: createCommentDto.occurrenceId,
authorId: authorId,
});
return comment;
return this.getCommentWithRelations(commentId);
}
async findByOccurrence(
......@@ -65,73 +93,63 @@ export class CommentService {
limit: number = 20,
): Promise<CommentResponseDto[]> {
// 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');
}
const skip = (page - 1) * limit;
const offset = (page - 1) * limit;
// Build where clause based on user role
const where: {
occurrenceId: string;
isInternal?: boolean;
} = { occurrenceId };
const whereConditions: any[] = [eq(comments.occurrenceId, occurrenceId)];
// If user is not admin or moderator, exclude internal comments
if (userRole !== UserRole.ADMIN && userRole !== UserRole.MODERATOR) {
where.isInternal = false;
if (
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
whereConditions.push(eq(comments.isInternal, false));
}
const comments = await this.prisma.comment.findMany({
where,
include: {
const commentList = await this.drizzle.db
.select({
id: comments.id,
content: comments.content,
isInternal: comments.isInternal,
occurrenceId: comments.occurrenceId,
authorId: comments.authorId,
createdAt: comments.createdAt,
updatedAt: comments.updatedAt,
author: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
},
},
orderBy: {
createdAt: 'asc',
},
skip,
take: limit,
});
return comments;
})
.from(comments)
.leftJoin(users, eq(comments.authorId, users.id))
.where(and(...whereConditions))
.orderBy(comments.createdAt)
.limit(limit)
.offset(offset);
return commentList.filter((comment) => comment.author !== null);
}
async findOne(id: string, userRole: UserRole): Promise<CommentResponseDto> {
const comment = await this.prisma.comment.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
},
});
if (!comment) {
throw new NotFoundException(`Comment with ID ${id} not found`);
}
const comment = await this.getCommentWithRelations(id);
// Check if user can view internal comments
if (
comment.isInternal &&
userRole !== UserRole.ADMIN &&
userRole !== UserRole.MODERATOR
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
throw new ForbiddenException('Access denied to internal comment');
}
......@@ -145,29 +163,13 @@ export class CommentService {
userId: string,
userRole: UserRole,
): Promise<CommentResponseDto> {
const comment = await this.prisma.comment.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
},
});
if (!comment) {
throw new NotFoundException(`Comment with ID ${id} not found`);
}
const comment = await this.getCommentWithRelations(id);
// Check if user can view the comment first
if (
comment.isInternal &&
userRole !== UserRole.ADMIN &&
userRole !== UserRole.MODERATOR
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
throw new ForbiddenException('Access denied to internal comment');
}
......@@ -176,8 +178,8 @@ export class CommentService {
// Users can only edit their own comments, admins and moderators can edit any comment
if (
comment.authorId !== userId &&
userRole !== UserRole.ADMIN &&
userRole !== UserRole.MODERATOR
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
throw new ForbiddenException('You can only edit your own comments');
}
......@@ -188,7 +190,10 @@ export class CommentService {
isInternal?: boolean;
} = { content: updateCommentDto.content };
if (updateCommentDto.isInternal !== undefined) {
if (userRole !== UserRole.ADMIN && userRole !== UserRole.MODERATOR) {
if (
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
throw new ForbiddenException(
'Only admins and moderators can change internal status',
);
......@@ -196,38 +201,22 @@ export class CommentService {
updateData.isInternal = updateCommentDto.isInternal;
}
const updatedComment = await this.prisma.comment.update({
where: { id },
data: updateData,
include: {
author: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
},
});
await this.drizzle.db
.update(comments)
.set(updateData)
.where(eq(comments.id, id));
return updatedComment;
return this.getCommentWithRelations(id);
}
async remove(id: string, userId: string, userRole: UserRole): Promise<void> {
const comment = await this.prisma.comment.findUnique({
where: { id },
});
if (!comment) {
throw new NotFoundException(`Comment with ID ${id} not found`);
}
const comment = await this.getCommentWithRelations(id);
// Check if user can view the comment first
if (
comment.isInternal &&
userRole !== UserRole.ADMIN &&
userRole !== UserRole.MODERATOR
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
throw new ForbiddenException('Access denied to internal comment');
}
......@@ -236,38 +225,43 @@ export class CommentService {
// Users can only delete their own comments, admins and moderators can delete any comment
if (
comment.authorId !== userId &&
userRole !== UserRole.ADMIN &&
userRole !== UserRole.MODERATOR
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
throw new ForbiddenException('You can only delete your own comments');
}
await this.prisma.comment.delete({
where: { id },
});
await this.drizzle.db.delete(comments).where(eq(comments.id, id));
}
async count(occurrenceId: string, userRole: UserRole): Promise<number> {
// 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');
}
// Build where clause based on user role
const where: {
occurrenceId: string;
isInternal?: boolean;
} = { occurrenceId };
const whereConditions: any[] = [eq(comments.occurrenceId, occurrenceId)];
// If user is not admin or moderator, exclude internal comments
if (userRole !== UserRole.ADMIN && userRole !== UserRole.MODERATOR) {
where.isInternal = false;
if (
userRole !== UserRoleEnum.ADMIN &&
userRole !== UserRoleEnum.MODERATOR
) {
whereConditions.push(eq(comments.isInternal, false));
}
return this.prisma.comment.count({ where });
const [result] = await this.drizzle.db
.select({ count: count() })
.from(comments)
.where(and(...whereConditions));
return result.count;
}
}
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 {}
......@@ -3,24 +3,140 @@ import {
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { DrizzleService } from '../../common/drizzle.service';
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 { OccurrenceStatus, Priority } from '../../../generated/prisma';
import { DualAssignOccurrenceDto } from './dto/dual-assign-occurrence.dto';
import {
OccurrenceStatus,
Priority,
OccurrenceStatusEnum,
PriorityEnum,
} from '../../types';
import {
occurrences,
users,
comments,
attachments,
conclusions,
} from '../../drizzle/schema';
import { eq, and, or, like, desc, count, sql } from 'drizzle-orm';
@Injectable()
export class OccurrenceService {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly drizzle: DrizzleService) {}
private async getOccurrenceWithRelations(
occurrenceId: string,
): Promise<OccurrenceResponseDto> {
// Get occurrence with reporter
const [occurrence] = await this.drizzle.db
.select({
id: occurrences.id,
title: occurrences.title,
description: occurrences.description,
status: occurrences.status,
priority: occurrences.priority,
category: occurrences.category,
location: occurrences.location,
reporterId: occurrences.reporterId,
assigneeId: occurrences.assigneeId,
managerId: occurrences.managerId,
createdAt: occurrences.createdAt,
updatedAt: occurrences.updatedAt,
closedAt: occurrences.closedAt,
reportedBy: {
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
},
})
.from(occurrences)
.leftJoin(users, eq(occurrences.reporterId, users.id))
.where(eq(occurrences.id, occurrenceId))
.limit(1);
if (!occurrence) {
throw new NotFoundException(
`Occurrence with ID ${occurrenceId} not found`,
);
}
// Get assignee if exists
let assignedTo: any = undefined;
if (occurrence.assigneeId) {
const [assignee] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
})
.from(users)
.where(eq(users.id, occurrence.assigneeId))
.limit(1);
assignedTo = assignee;
}
// Get manager if exists
let managedBy: any = undefined;
if (occurrence.managerId) {
const [manager] = await this.drizzle.db
.select({
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
})
.from(users)
.where(eq(users.id, occurrence.managerId))
.limit(1);
managedBy = manager;
}
// Get conclusion if exists
const [conclusion] = await this.drizzle.db
.select()
.from(conclusions)
.where(eq(conclusions.occurrenceId, occurrenceId))
.limit(1);
// Get counts
const [commentCount] = await this.drizzle.db
.select({ count: count() })
.from(comments)
.where(eq(comments.occurrenceId, occurrenceId));
const [attachmentCount] = await this.drizzle.db
.select({ count: count() })
.from(attachments)
.where(eq(attachments.occurrenceId, occurrenceId));
return {
...occurrence,
reportedBy: occurrence.reportedBy || undefined,
assignedTo,
managedBy,
conclusion: conclusion || undefined,
_count: {
comments: commentCount.count,
attachments: attachmentCount.count,
},
};
}
async create(
createOccurrenceDto: CreateOccurrenceDto,
): Promise<OccurrenceResponseDto> {
// Validate that reporter exists
const reporter = await this.prisma.user.findUnique({
where: { id: createOccurrenceDto.reporterId },
});
const [reporter] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, createOccurrenceDto.reporterId))
.limit(1);
if (!reporter) {
throw new BadRequestException('Reporter user not found');
......@@ -28,44 +144,27 @@ export class OccurrenceService {
// Validate assignee if provided
if (createOccurrenceDto.assigneeId) {
const assignee = await this.prisma.user.findUnique({
where: { id: createOccurrenceDto.assigneeId },
});
const [assignee] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, createOccurrenceDto.assigneeId))
.limit(1);
if (!assignee) {
throw new BadRequestException('Assignee user not found');
}
}
const occurrence = await this.prisma.occurrence.create({
data: createOccurrenceDto,
include: {
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
// Create occurrence
const occurrenceId = this.drizzle.generateId();
await this.drizzle.db.insert(occurrences).values({
id: occurrenceId,
...createOccurrenceDto,
});
// Get the full occurrence with relations
const occurrence = await this.getOccurrenceWithRelations(occurrenceId);
return occurrence;
}
......@@ -88,88 +187,62 @@ export class OccurrenceService {
limit = 10,
} = options || {};
const skip = (page - 1) * limit;
const where: any = {};
if (status) where.status = status;
if (priority) where.priority = priority;
if (category) where.category = { contains: category, mode: 'insensitive' };
if (reporterId) where.reporterId = reporterId;
if (assigneeId) where.assigneeId = assigneeId;
const occurrences = await this.prisma.occurrence.findMany({
where,
include: {
const offset = (page - 1) * limit;
// Build where conditions
const whereConditions: any[] = [];
if (status) whereConditions.push(eq(occurrences.status, status));
if (priority) whereConditions.push(eq(occurrences.priority, priority));
if (category)
whereConditions.push(like(occurrences.category, `%${category}%`));
if (reporterId)
whereConditions.push(eq(occurrences.reporterId, reporterId));
if (assigneeId)
whereConditions.push(eq(occurrences.assigneeId, assigneeId));
const whereClause =
whereConditions.length > 0 ? and(...whereConditions) : undefined;
// Get occurrences with basic info
const occurrenceList = await this.drizzle.db
.select({
id: occurrences.id,
title: occurrences.title,
description: occurrences.description,
status: occurrences.status,
priority: occurrences.priority,
category: occurrences.category,
location: occurrences.location,
reporterId: occurrences.reporterId,
assigneeId: occurrences.assigneeId,
managerId: occurrences.managerId,
createdAt: occurrences.createdAt,
updatedAt: occurrences.updatedAt,
closedAt: occurrences.closedAt,
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
skip,
take: limit,
});
})
.from(occurrences)
.leftJoin(users, eq(occurrences.reporterId, users.id))
.where(whereClause)
.orderBy(desc(occurrences.createdAt))
.limit(limit)
.offset(offset);
// Get full occurrences with relations
const result = await Promise.all(
occurrenceList.map((occ) => this.getOccurrenceWithRelations(occ.id)),
);
return occurrences;
return result;
}
async findOne(id: string): Promise<OccurrenceResponseDto> {
const occurrence = await this.prisma.occurrence.findUnique({
where: { id },
include: {
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
});
if (!occurrence) {
throw new NotFoundException(`Occurrence with ID ${id} not found`);
}
return occurrence;
return this.getOccurrenceWithRelations(id);
}
async update(
......@@ -181,9 +254,11 @@ export class OccurrenceService {
// Validate assignee if provided
if (updateOccurrenceDto.assigneeId) {
const assignee = await this.prisma.user.findUnique({
where: { id: updateOccurrenceDto.assigneeId },
});
const [assignee] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, updateOccurrenceDto.assigneeId))
.limit(1);
if (!assignee) {
throw new BadRequestException('Assignee user not found');
......@@ -194,8 +269,8 @@ export class OccurrenceService {
const updateData: any = { ...updateOccurrenceDto };
if (
updateOccurrenceDto.status &&
(updateOccurrenceDto.status === OccurrenceStatus.CLOSED ||
updateOccurrenceDto.status === OccurrenceStatus.RESOLVED)
(updateOccurrenceDto.status === OccurrenceStatusEnum.CLOSED ||
updateOccurrenceDto.status === OccurrenceStatusEnum.RESOLVED)
) {
updateData.closedAt = new Date();
}
......@@ -203,52 +278,25 @@ export class OccurrenceService {
// If status is being changed back to OPEN or IN_PROGRESS, clear closedAt
if (
updateOccurrenceDto.status &&
(updateOccurrenceDto.status === OccurrenceStatus.OPEN ||
updateOccurrenceDto.status === OccurrenceStatus.IN_PROGRESS)
(updateOccurrenceDto.status === OccurrenceStatusEnum.OPEN ||
updateOccurrenceDto.status === OccurrenceStatusEnum.IN_PROGRESS)
) {
updateData.closedAt = null;
}
const occurrence = await this.prisma.occurrence.update({
where: { id },
data: updateData,
include: {
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
});
await this.drizzle.db
.update(occurrences)
.set(updateData)
.where(eq(occurrences.id, id));
return occurrence;
return this.getOccurrenceWithRelations(id);
}
async remove(id: string): Promise<void> {
// Check if occurrence exists
await this.findOne(id);
await this.prisma.occurrence.delete({
where: { id },
});
await this.drizzle.db.delete(occurrences).where(eq(occurrences.id, id));
}
async count(options?: {
......@@ -261,15 +309,26 @@ export class OccurrenceService {
const { status, priority, category, reporterId, assigneeId } =
options || {};
const where: any = {};
if (status) where.status = status;
if (priority) where.priority = priority;
if (category) where.category = { contains: category, mode: 'insensitive' };
if (reporterId) where.reporterId = reporterId;
if (assigneeId) where.assigneeId = assigneeId;
return this.prisma.occurrence.count({ where });
// Build where conditions
const whereConditions: any[] = [];
if (status) whereConditions.push(eq(occurrences.status, status));
if (priority) whereConditions.push(eq(occurrences.priority, priority));
if (category)
whereConditions.push(like(occurrences.category, `%${category}%`));
if (reporterId)
whereConditions.push(eq(occurrences.reporterId, reporterId));
if (assigneeId)
whereConditions.push(eq(occurrences.assigneeId, assigneeId));
const whereClause =
whereConditions.length > 0 ? and(...whereConditions) : undefined;
const [result] = await this.drizzle.db
.select({ count: count() })
.from(occurrences)
.where(whereClause);
return result.count;
}
async getStats(): Promise<{
......@@ -277,28 +336,44 @@ export class OccurrenceService {
byStatus: Record<OccurrenceStatus, number>;
byPriority: Record<Priority, number>;
}> {
const [total, statusCounts, priorityCounts] = await Promise.all([
this.prisma.occurrence.count(),
this.prisma.occurrence.groupBy({
by: ['status'],
_count: true,
}),
this.prisma.occurrence.groupBy({
by: ['priority'],
_count: true,
}),
]);
const byStatus = Object.values(OccurrenceStatus).reduce(
(acc, status) => {
// Get total count
const [totalResult] = await this.drizzle.db
.select({ count: count() })
.from(occurrences);
const total = totalResult.count;
// Get status counts
const statusCounts = await this.drizzle.db
.select({
status: occurrences.status,
count: count(),
})
.from(occurrences)
.groupBy(occurrences.status);
// Get priority counts
const priorityCounts = await this.drizzle.db
.select({
priority: occurrences.priority,
count: count(),
})
.from(occurrences)
.groupBy(occurrences.priority);
const byStatus: Record<OccurrenceStatus, number> = Object.values(
OccurrenceStatusEnum,
).reduce(
(acc: Record<OccurrenceStatus, number>, status: OccurrenceStatus) => {
acc[status] = 0;
return acc;
},
{} as Record<OccurrenceStatus, number>,
);
const byPriority = Object.values(Priority).reduce(
(acc, priority) => {
const byPriority: Record<Priority, number> = Object.values(
PriorityEnum,
).reduce(
(acc: Record<Priority, number>, priority: Priority) => {
acc[priority] = 0;
return acc;
},
......@@ -306,11 +381,11 @@ export class OccurrenceService {
);
statusCounts.forEach((item) => {
byStatus[item.status] = item._count;
byStatus[item.status] = item.count;
});
priorityCounts.forEach((item) => {
byPriority[item.priority] = item._count;
byPriority[item.priority] = item.count;
});
return {
......@@ -322,44 +397,49 @@ export class OccurrenceService {
async findByUser(
userId: string,
type: 'reported' | 'assigned',
type: 'reported' | 'assigned' | 'managed',
): Promise<OccurrenceResponseDto[]> {
const where =
type === 'reported' ? { reporterId: userId } : { assigneeId: userId };
const occurrences = await this.prisma.occurrence.findMany({
where,
include: {
const whereCondition =
type === 'reported'
? eq(occurrences.reporterId, userId)
: type === 'assigned'
? eq(occurrences.assigneeId, userId)
: eq(occurrences.managerId, userId);
// Get occurrences with basic info
const occurrenceList = await this.drizzle.db
.select({
id: occurrences.id,
title: occurrences.title,
description: occurrences.description,
status: occurrences.status,
priority: occurrences.priority,
category: occurrences.category,
location: occurrences.location,
reporterId: occurrences.reporterId,
assigneeId: occurrences.assigneeId,
managerId: occurrences.managerId,
createdAt: occurrences.createdAt,
updatedAt: occurrences.updatedAt,
closedAt: occurrences.closedAt,
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
id: users.id,
firstName: users.firstName,
lastName: users.lastName,
email: users.email,
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
})
.from(occurrences)
.leftJoin(users, eq(occurrences.reporterId, users.id))
.where(whereCondition)
.orderBy(desc(occurrences.createdAt));
// Get full occurrences with relations
const result = await Promise.all(
occurrenceList.map((occ) => this.getOccurrenceWithRelations(occ.id)),
);
return occurrences;
return result;
}
async assignOccurrence(
......@@ -370,62 +450,38 @@ export class OccurrenceService {
await this.findOne(occurrenceId);
// Validate that assignee exists
const assignee = await this.prisma.user.findUnique({
where: { id: assignOccurrenceDto.assigneeId },
});
const [assignee] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, assignOccurrenceDto.assigneeId))
.limit(1);
if (!assignee) {
throw new BadRequestException('Assignee user not found');
}
// Update occurrence with new assignee
const occurrence = await this.prisma.occurrence.update({
where: { id: occurrenceId },
data: {
await this.drizzle.db
.update(occurrences)
.set({
assigneeId: assignOccurrenceDto.assigneeId,
// Optionally change status to IN_PROGRESS when assigned
status: OccurrenceStatus.IN_PROGRESS,
},
include: {
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
});
status: OccurrenceStatusEnum.IN_PROGRESS,
})
.where(eq(occurrences.id, occurrenceId));
// If there's an assignment note, create a comment
if (assignOccurrenceDto.assignmentNote) {
await this.prisma.comment.create({
data: {
content: `Assignment note: ${assignOccurrenceDto.assignmentNote}`,
isInternal: true,
occurrenceId: occurrenceId,
authorId: assignOccurrenceDto.assigneeId, // Could be the assigner instead
},
const commentId = this.drizzle.generateId();
await this.drizzle.db.insert(comments).values({
id: commentId,
content: `Assignment note: ${assignOccurrenceDto.assignmentNote}`,
isInternal: true,
occurrenceId: occurrenceId,
authorId: assignOccurrenceDto.assigneeId,
});
}
return occurrence;
return this.getOccurrenceWithRelations(occurrenceId);
}
async unassignOccurrence(
......@@ -435,40 +491,68 @@ export class OccurrenceService {
await this.findOne(occurrenceId);
// Remove assignee
const occurrence = await this.prisma.occurrence.update({
where: { id: occurrenceId },
data: {
await this.drizzle.db
.update(occurrences)
.set({
assigneeId: null,
// Optionally change status back to OPEN when unassigned
status: OccurrenceStatus.OPEN,
},
include: {
reportedBy: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
assignedTo: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
},
conclusion: true,
_count: {
select: {
comments: true,
attachments: true,
},
},
},
});
status: OccurrenceStatusEnum.OPEN,
})
.where(eq(occurrences.id, occurrenceId));
return occurrence;
return this.getOccurrenceWithRelations(occurrenceId);
}
async dualAssignOccurrence(
occurrenceId: string,
dualAssignOccurrenceDto: DualAssignOccurrenceDto,
): Promise<OccurrenceResponseDto> {
// Check if occurrence exists
await this.findOne(occurrenceId);
// Validate that assignee exists
const [assignee] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, dualAssignOccurrenceDto.assigneeId))
.limit(1);
if (!assignee) {
throw new BadRequestException('Assignee user not found');
}
// Validate that manager exists
const [manager] = await this.drizzle.db
.select()
.from(users)
.where(eq(users.id, dualAssignOccurrenceDto.managerId))
.limit(1);
if (!manager) {
throw new BadRequestException('Manager user not found');
}
// Update occurrence with both assignee and manager
await this.drizzle.db
.update(occurrences)
.set({
assigneeId: dualAssignOccurrenceDto.assigneeId,
managerId: dualAssignOccurrenceDto.managerId,
status: OccurrenceStatusEnum.IN_PROGRESS,
})
.where(eq(occurrences.id, occurrenceId));
// If there's an assignment note, create a comment
if (dualAssignOccurrenceDto.assignmentNote) {
const commentId = this.drizzle.generateId();
await this.drizzle.db.insert(comments).values({
id: commentId,
content: `Dual assignment note: ${dualAssignOccurrenceDto.assignmentNote}`,
isInternal: true,
occurrenceId: occurrenceId,
authorId: dualAssignOccurrenceDto.managerId, // Manager creates the note
});
}
return this.getOccurrenceWithRelations(occurrenceId);
}
}
......@@ -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