Compare commits
7 Commits
7b6da2767e
...
b64a6e9e2e
Author | SHA1 | Date | |
---|---|---|---|
b64a6e9e2e | |||
f739099524 | |||
76ef9a3380 | |||
d15bf3fe90 | |||
63458333ca | |||
0249d62951 | |||
9515c32016 |
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@ -8,6 +8,5 @@
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/backend" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -11,7 +11,7 @@ import * as process from "node:process";
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/database/schema/index.ts',
|
||||
out: './src/database/migrations',
|
||||
out: './src/database/migrations/sql',
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
host: String(process.env.POSTGRES_HOST || "localhost"),
|
||||
|
@ -18,11 +18,12 @@
|
||||
"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",
|
||||
"db:generate": "drizzle-kit generate:pg",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "ts-node src/database/migrations/migrate.ts",
|
||||
"db:generate:ts": "ts-node src/database/migrations/generate-migrations.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:push": "drizzle-kit push:pg"
|
||||
"db:push": "drizzle-kit push:pg",
|
||||
"db:update": "npm run db:generate:ts && npm run db:migrate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
|
@ -40,7 +40,7 @@ export class DatabaseService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
// Run migrations in all environments
|
||||
const result = await runMigrations({ migrationsFolder: './src/database/migrations' });
|
||||
const result = await runMigrations({ migrationsFolder: './src/database/migrations/sql' });
|
||||
|
||||
// In production, we want to fail if migrations fail
|
||||
if (!result.success && this.configService.get('NODE_ENV') === 'production') {
|
||||
|
54
backend/src/database/migrations/README.md
Normal file
54
backend/src/database/migrations/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Database Migrations
|
||||
|
||||
This directory contains the migration system for the database. The migrations are generated using DrizzleORM and are stored in the `sql` subdirectory.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `sql/` - Contains the generated SQL migration files
|
||||
- `generate-migrations.ts` - Script to generate migration files
|
||||
- `migrate.ts` - Script to run migrations
|
||||
- `README.md` - This file
|
||||
|
||||
## How to Use
|
||||
|
||||
### Generating Migrations
|
||||
|
||||
To generate migrations based on changes to the schema, run:
|
||||
|
||||
```bash
|
||||
npm run db:generate:ts
|
||||
```
|
||||
|
||||
This will generate SQL migration files in the `sql` directory.
|
||||
|
||||
### Running Migrations
|
||||
|
||||
To run all pending migrations, run:
|
||||
|
||||
```bash
|
||||
npm run db:migrate
|
||||
```
|
||||
|
||||
### Generating and Running Migrations in One Step
|
||||
|
||||
To generate and run migrations in one step, run:
|
||||
|
||||
```bash
|
||||
npm run db:update
|
||||
```
|
||||
|
||||
## Integration with NestJS
|
||||
|
||||
The migrations are automatically run when the application starts. This is configured in the `DatabaseService` class in `src/database/database.service.ts`.
|
||||
|
||||
## Migration Files
|
||||
|
||||
Migration files are SQL files that contain the SQL statements to create, alter, or drop database objects. They are named with a timestamp and a description, e.g. `0000_lively_tiger_shark.sql`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The migration system is configured in `drizzle.config.ts` at the root of the project. This file specifies:
|
||||
|
||||
- The schema file to use for generating migrations
|
||||
- The output directory for migration files
|
||||
- The database dialect and credentials
|
@ -14,13 +14,13 @@ const main = async () => {
|
||||
console.log('Generating migrations...');
|
||||
|
||||
// Ensure migrations directory exists
|
||||
const migrationsDir = path.join(__dirname);
|
||||
const migrationsDir = path.join(__dirname, 'sql');
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
fs.mkdirSync(migrationsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Run drizzle-kit generate command
|
||||
const command = 'npx drizzle-kit generate:pg';
|
||||
const command = 'npx drizzle-kit generate';
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
|
@ -57,7 +57,7 @@ export const runMigrations = async (options?: { migrationsFolder?: string }) =>
|
||||
}
|
||||
|
||||
// Determine migrations folder path
|
||||
const migrationsFolder = options?.migrationsFolder || path.join(__dirname);
|
||||
const migrationsFolder = options?.migrationsFolder || path.join(__dirname, 'sql');
|
||||
console.log(`Using migrations folder: ${migrationsFolder}`);
|
||||
|
||||
// Run migrations
|
||||
|
173
backend/src/database/migrations/sql/0000_lively_tiger_shark.sql
Normal file
173
backend/src/database/migrations/sql/0000_lively_tiger_shark.sql
Normal file
@ -0,0 +1,173 @@
|
||||
CREATE SCHEMA "groupmaker";
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."gender" AS ENUM('MALE', 'FEMALE', 'NON_BINARY');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."oralEaseLevel" AS ENUM('SHY', 'RESERVED', 'COMFORTABLE');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."tagType" AS ENUM('PROJECT', 'PERSON');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(100) NOT NULL,
|
||||
"avatar" text,
|
||||
"githubId" varchar(50) NOT NULL,
|
||||
"gdprTimestamp" timestamp with time zone,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||
CONSTRAINT "users_githubId_unique" UNIQUE("githubId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "projects" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(100) NOT NULL,
|
||||
"description" text,
|
||||
"ownerId" uuid NOT NULL,
|
||||
"settings" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "persons" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"firstName" varchar(50) NOT NULL,
|
||||
"lastName" varchar(50) NOT NULL,
|
||||
"gender" "gender" NOT NULL,
|
||||
"technicalLevel" smallint NOT NULL,
|
||||
"hasTechnicalTraining" boolean DEFAULT false NOT NULL,
|
||||
"frenchSpeakingLevel" smallint NOT NULL,
|
||||
"oralEaseLevel" "oralEaseLevel" NOT NULL,
|
||||
"age" smallint,
|
||||
"projectId" uuid NOT NULL,
|
||||
"attributes" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "groups" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(100) NOT NULL,
|
||||
"projectId" uuid NOT NULL,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "tags" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(50) NOT NULL,
|
||||
"color" varchar(7) NOT NULL,
|
||||
"type" "tagType" NOT NULL,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "person_to_group" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"personId" uuid NOT NULL,
|
||||
"groupId" uuid NOT NULL,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "person_to_tag" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"personId" uuid NOT NULL,
|
||||
"tagId" uuid NOT NULL,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "project_to_tag" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"projectId" uuid NOT NULL,
|
||||
"tagId" uuid NOT NULL,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_ownerId_users_id_fk" FOREIGN KEY ("ownerId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "persons" ADD CONSTRAINT "persons_projectId_projects_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "groups" ADD CONSTRAINT "groups_projectId_projects_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "person_to_group" ADD CONSTRAINT "person_to_group_personId_persons_id_fk" FOREIGN KEY ("personId") REFERENCES "public"."persons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "person_to_group" ADD CONSTRAINT "person_to_group_groupId_groups_id_fk" FOREIGN KEY ("groupId") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "person_to_tag" ADD CONSTRAINT "person_to_tag_personId_persons_id_fk" FOREIGN KEY ("personId") REFERENCES "public"."persons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "person_to_tag" ADD CONSTRAINT "person_to_tag_tagId_tags_id_fk" FOREIGN KEY ("tagId") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "project_to_tag" ADD CONSTRAINT "project_to_tag_projectId_projects_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "project_to_tag" ADD CONSTRAINT "project_to_tag_tagId_tags_id_fk" FOREIGN KEY ("tagId") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "githubId_idx" ON "users" ("githubId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "createdAt_idx" ON "users" ("createdAt");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_name_idx" ON "projects" ("name");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_ownerId_idx" ON "projects" ("ownerId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_createdAt_idx" ON "projects" ("createdAt");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "person_firstName_idx" ON "persons" ("firstName");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "person_lastName_idx" ON "persons" ("lastName");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "person_projectId_idx" ON "persons" ("projectId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "person_name_composite_idx" ON "persons" ("firstName","lastName");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "group_name_idx" ON "groups" ("name");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "group_projectId_idx" ON "groups" ("projectId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "tag_name_idx" ON "tags" ("name");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "tag_type_idx" ON "tags" ("type");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "ptg_personId_idx" ON "person_to_group" ("personId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "ptg_groupId_idx" ON "person_to_group" ("groupId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "ptg_person_group_unique_idx" ON "person_to_group" ("personId","groupId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "ptt_personId_idx" ON "person_to_tag" ("personId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "ptt_tagId_idx" ON "person_to_tag" ("tagId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "ptt_person_tag_unique_idx" ON "person_to_tag" ("personId","tagId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "pjt_projectId_idx" ON "project_to_tag" ("projectId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "pjt_tagId_idx" ON "project_to_tag" ("tagId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "pjt_project_tag_unique_idx" ON "project_to_tag" ("projectId","tagId");
|
762
backend/src/database/migrations/sql/meta/0000_snapshot.json
Normal file
762
backend/src/database/migrations/sql/meta/0000_snapshot.json
Normal file
@ -0,0 +1,762 @@
|
||||
{
|
||||
"id": "ebffb361-7a99-4ad4-a51f-e48d304b0260",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "6",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"avatar": {
|
||||
"name": "avatar",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"githubId": {
|
||||
"name": "githubId",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"gdprTimestamp": {
|
||||
"name": "gdprTimestamp",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'{}'::jsonb"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"githubId_idx": {
|
||||
"name": "githubId_idx",
|
||||
"columns": [
|
||||
"githubId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"createdAt_idx": {
|
||||
"name": "createdAt_idx",
|
||||
"columns": [
|
||||
"createdAt"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_githubId_unique": {
|
||||
"name": "users_githubId_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"githubId"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.projects": {
|
||||
"name": "projects",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ownerId": {
|
||||
"name": "ownerId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"project_name_idx": {
|
||||
"name": "project_name_idx",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"project_ownerId_idx": {
|
||||
"name": "project_ownerId_idx",
|
||||
"columns": [
|
||||
"ownerId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"project_createdAt_idx": {
|
||||
"name": "project_createdAt_idx",
|
||||
"columns": [
|
||||
"createdAt"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"projects_ownerId_users_id_fk": {
|
||||
"name": "projects_ownerId_users_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"ownerId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.persons": {
|
||||
"name": "persons",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"firstName": {
|
||||
"name": "firstName",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"lastName": {
|
||||
"name": "lastName",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"gender": {
|
||||
"name": "gender",
|
||||
"type": "gender",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"technicalLevel": {
|
||||
"name": "technicalLevel",
|
||||
"type": "smallint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"hasTechnicalTraining": {
|
||||
"name": "hasTechnicalTraining",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"frenchSpeakingLevel": {
|
||||
"name": "frenchSpeakingLevel",
|
||||
"type": "smallint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"oralEaseLevel": {
|
||||
"name": "oralEaseLevel",
|
||||
"type": "oralEaseLevel",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"age": {
|
||||
"name": "age",
|
||||
"type": "smallint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"projectId": {
|
||||
"name": "projectId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"attributes": {
|
||||
"name": "attributes",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"person_firstName_idx": {
|
||||
"name": "person_firstName_idx",
|
||||
"columns": [
|
||||
"firstName"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"person_lastName_idx": {
|
||||
"name": "person_lastName_idx",
|
||||
"columns": [
|
||||
"lastName"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"person_projectId_idx": {
|
||||
"name": "person_projectId_idx",
|
||||
"columns": [
|
||||
"projectId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"person_name_composite_idx": {
|
||||
"name": "person_name_composite_idx",
|
||||
"columns": [
|
||||
"firstName",
|
||||
"lastName"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"persons_projectId_projects_id_fk": {
|
||||
"name": "persons_projectId_projects_id_fk",
|
||||
"tableFrom": "persons",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"projectId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.groups": {
|
||||
"name": "groups",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"projectId": {
|
||||
"name": "projectId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"group_name_idx": {
|
||||
"name": "group_name_idx",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"group_projectId_idx": {
|
||||
"name": "group_projectId_idx",
|
||||
"columns": [
|
||||
"projectId"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"groups_projectId_projects_id_fk": {
|
||||
"name": "groups_projectId_projects_id_fk",
|
||||
"tableFrom": "groups",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"projectId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.tags": {
|
||||
"name": "tags",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "tagType",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tag_name_idx": {
|
||||
"name": "tag_name_idx",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"tag_type_idx": {
|
||||
"name": "tag_type_idx",
|
||||
"columns": [
|
||||
"type"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.person_to_group": {
|
||||
"name": "person_to_group",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"personId": {
|
||||
"name": "personId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"groupId": {
|
||||
"name": "groupId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ptg_personId_idx": {
|
||||
"name": "ptg_personId_idx",
|
||||
"columns": [
|
||||
"personId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"ptg_groupId_idx": {
|
||||
"name": "ptg_groupId_idx",
|
||||
"columns": [
|
||||
"groupId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"ptg_person_group_unique_idx": {
|
||||
"name": "ptg_person_group_unique_idx",
|
||||
"columns": [
|
||||
"personId",
|
||||
"groupId"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"person_to_group_personId_persons_id_fk": {
|
||||
"name": "person_to_group_personId_persons_id_fk",
|
||||
"tableFrom": "person_to_group",
|
||||
"tableTo": "persons",
|
||||
"columnsFrom": [
|
||||
"personId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"person_to_group_groupId_groups_id_fk": {
|
||||
"name": "person_to_group_groupId_groups_id_fk",
|
||||
"tableFrom": "person_to_group",
|
||||
"tableTo": "groups",
|
||||
"columnsFrom": [
|
||||
"groupId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.person_to_tag": {
|
||||
"name": "person_to_tag",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"personId": {
|
||||
"name": "personId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tagId": {
|
||||
"name": "tagId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ptt_personId_idx": {
|
||||
"name": "ptt_personId_idx",
|
||||
"columns": [
|
||||
"personId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"ptt_tagId_idx": {
|
||||
"name": "ptt_tagId_idx",
|
||||
"columns": [
|
||||
"tagId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"ptt_person_tag_unique_idx": {
|
||||
"name": "ptt_person_tag_unique_idx",
|
||||
"columns": [
|
||||
"personId",
|
||||
"tagId"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"person_to_tag_personId_persons_id_fk": {
|
||||
"name": "person_to_tag_personId_persons_id_fk",
|
||||
"tableFrom": "person_to_tag",
|
||||
"tableTo": "persons",
|
||||
"columnsFrom": [
|
||||
"personId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"person_to_tag_tagId_tags_id_fk": {
|
||||
"name": "person_to_tag_tagId_tags_id_fk",
|
||||
"tableFrom": "person_to_tag",
|
||||
"tableTo": "tags",
|
||||
"columnsFrom": [
|
||||
"tagId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.project_to_tag": {
|
||||
"name": "project_to_tag",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"projectId": {
|
||||
"name": "projectId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tagId": {
|
||||
"name": "tagId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"pjt_projectId_idx": {
|
||||
"name": "pjt_projectId_idx",
|
||||
"columns": [
|
||||
"projectId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"pjt_tagId_idx": {
|
||||
"name": "pjt_tagId_idx",
|
||||
"columns": [
|
||||
"tagId"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"pjt_project_tag_unique_idx": {
|
||||
"name": "pjt_project_tag_unique_idx",
|
||||
"columns": [
|
||||
"projectId",
|
||||
"tagId"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"project_to_tag_projectId_projects_id_fk": {
|
||||
"name": "project_to_tag_projectId_projects_id_fk",
|
||||
"tableFrom": "project_to_tag",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"projectId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_to_tag_tagId_tags_id_fk": {
|
||||
"name": "project_to_tag_tagId_tags_id_fk",
|
||||
"tableFrom": "project_to_tag",
|
||||
"tableTo": "tags",
|
||||
"columnsFrom": [
|
||||
"tagId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.gender": {
|
||||
"name": "gender",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"MALE",
|
||||
"FEMALE",
|
||||
"NON_BINARY"
|
||||
]
|
||||
},
|
||||
"public.oralEaseLevel": {
|
||||
"name": "oralEaseLevel",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"SHY",
|
||||
"RESERVED",
|
||||
"COMFORTABLE"
|
||||
]
|
||||
},
|
||||
"public.tagType": {
|
||||
"name": "tagType",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"PROJECT",
|
||||
"PERSON"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"groupmaker": "groupmaker"
|
||||
},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
13
backend/src/database/migrations/sql/meta/_journal.json
Normal file
13
backend/src/database/migrations/sql/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1747322785586,
|
||||
"tag": "0000_lively_tiger_shark",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
124
backend/src/modules/tags/controllers/tags.controller.ts
Normal file
124
backend/src/modules/tags/controllers/tags.controller.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Delete,
|
||||
Put,
|
||||
UseGuards,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { TagsService } from '../services/tags.service';
|
||||
import { CreateTagDto } from '../dto/create-tag.dto';
|
||||
import { UpdateTagDto } from '../dto/update-tag.dto';
|
||||
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('tags')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class TagsController {
|
||||
constructor(private readonly tagsService: TagsService) {}
|
||||
|
||||
/**
|
||||
* Create a new tag
|
||||
*/
|
||||
@Post()
|
||||
create(@Body() createTagDto: CreateTagDto) {
|
||||
return this.tagsService.create(createTagDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags or filter by type
|
||||
*/
|
||||
@Get()
|
||||
findAll(@Query('type') type?: 'PROJECT' | 'PERSON') {
|
||||
if (type) {
|
||||
return this.tagsService.findByType(type);
|
||||
}
|
||||
return this.tagsService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tag by ID
|
||||
*/
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.tagsService.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a tag
|
||||
*/
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() updateTagDto: UpdateTagDto) {
|
||||
return this.tagsService.update(id, updateTagDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tag
|
||||
*/
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.tagsService.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag to a person
|
||||
*/
|
||||
@Post('persons/:personId/tags/:tagId')
|
||||
addTagToPerson(
|
||||
@Param('personId') personId: string,
|
||||
@Param('tagId') tagId: string,
|
||||
) {
|
||||
return this.tagsService.addTagToPerson(tagId, personId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag from a person
|
||||
*/
|
||||
@Delete('persons/:personId/tags/:tagId')
|
||||
removeTagFromPerson(
|
||||
@Param('personId') personId: string,
|
||||
@Param('tagId') tagId: string,
|
||||
) {
|
||||
return this.tagsService.removeTagFromPerson(tagId, personId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a person
|
||||
*/
|
||||
@Get('persons/:personId/tags')
|
||||
getTagsForPerson(@Param('personId') personId: string) {
|
||||
return this.tagsService.getTagsForPerson(personId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag to a project
|
||||
*/
|
||||
@Post('projects/:projectId/tags/:tagId')
|
||||
addTagToProject(
|
||||
@Param('projectId') projectId: string,
|
||||
@Param('tagId') tagId: string,
|
||||
) {
|
||||
return this.tagsService.addTagToProject(tagId, projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag from a project
|
||||
*/
|
||||
@Delete('projects/:projectId/tags/:tagId')
|
||||
removeTagFromProject(
|
||||
@Param('projectId') projectId: string,
|
||||
@Param('tagId') tagId: string,
|
||||
) {
|
||||
return this.tagsService.removeTagFromProject(tagId, projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a project
|
||||
*/
|
||||
@Get('projects/:projectId/tags')
|
||||
getTagsForProject(@Param('projectId') projectId: string) {
|
||||
return this.tagsService.getTagsForProject(projectId);
|
||||
}
|
||||
}
|
32
backend/src/modules/tags/dto/create-tag.dto.ts
Normal file
32
backend/src/modules/tags/dto/create-tag.dto.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { IsNotEmpty, IsString, IsEnum, Matches } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for creating a new tag
|
||||
*/
|
||||
export class CreateTagDto {
|
||||
/**
|
||||
* The name of the tag
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The color of the tag (hex format)
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||
message: 'Color must be a valid hex color code (e.g., #FF5733)',
|
||||
})
|
||||
color: string;
|
||||
|
||||
/**
|
||||
* The type of the tag (PROJECT or PERSON)
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsEnum(['PROJECT', 'PERSON'], {
|
||||
message: 'Type must be either PROJECT or PERSON',
|
||||
})
|
||||
type: 'PROJECT' | 'PERSON';
|
||||
}
|
32
backend/src/modules/tags/dto/update-tag.dto.ts
Normal file
32
backend/src/modules/tags/dto/update-tag.dto.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { IsString, IsEnum, Matches, IsOptional } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for updating an existing tag
|
||||
*/
|
||||
export class UpdateTagDto {
|
||||
/**
|
||||
* The name of the tag
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The color of the tag (hex format)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||
message: 'Color must be a valid hex color code (e.g., #FF5733)',
|
||||
})
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* The type of the tag (PROJECT or PERSON)
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEnum(['PROJECT', 'PERSON'], {
|
||||
message: 'Type must be either PROJECT or PERSON',
|
||||
})
|
||||
type?: 'PROJECT' | 'PERSON';
|
||||
}
|
277
backend/src/modules/tags/services/tags.service.ts
Normal file
277
backend/src/modules/tags/services/tags.service.ts
Normal file
@ -0,0 +1,277 @@
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { DRIZZLE } from '../../../database/database.module';
|
||||
import * as schema from '../../../database/schema';
|
||||
import { CreateTagDto } from '../dto/create-tag.dto';
|
||||
import { UpdateTagDto } from '../dto/update-tag.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TagsService {
|
||||
constructor(@Inject(DRIZZLE) private readonly db: any) {}
|
||||
|
||||
/**
|
||||
* Create a new tag
|
||||
*/
|
||||
async create(createTagDto: CreateTagDto) {
|
||||
const [tag] = await this.db
|
||||
.insert(schema.tags)
|
||||
.values({
|
||||
...createTagDto,
|
||||
})
|
||||
.returning();
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all tags
|
||||
*/
|
||||
async findAll() {
|
||||
return this.db.select().from(schema.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find tags by type
|
||||
*/
|
||||
async findByType(type: 'PROJECT' | 'PERSON') {
|
||||
return this.db
|
||||
.select()
|
||||
.from(schema.tags)
|
||||
.where(eq(schema.tags.type, type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a tag by ID
|
||||
*/
|
||||
async findById(id: string) {
|
||||
const [tag] = await this.db
|
||||
.select()
|
||||
.from(schema.tags)
|
||||
.where(eq(schema.tags.id, id));
|
||||
|
||||
if (!tag) {
|
||||
throw new NotFoundException(`Tag with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a tag
|
||||
*/
|
||||
async update(id: string, updateTagDto: UpdateTagDto) {
|
||||
const [tag] = await this.db
|
||||
.update(schema.tags)
|
||||
.set({
|
||||
...updateTagDto,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.tags.id, id))
|
||||
.returning();
|
||||
|
||||
if (!tag) {
|
||||
throw new NotFoundException(`Tag with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tag
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const [tag] = await this.db
|
||||
.delete(schema.tags)
|
||||
.where(eq(schema.tags.id, id))
|
||||
.returning();
|
||||
|
||||
if (!tag) {
|
||||
throw new NotFoundException(`Tag with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag to a person
|
||||
*/
|
||||
async addTagToPerson(tagId: string, personId: string) {
|
||||
// Check if the tag exists and is of type PERSON
|
||||
const tag = await this.findById(tagId);
|
||||
if (tag.type !== 'PERSON') {
|
||||
throw new Error(`Tag with ID ${tagId} is not of type PERSON`);
|
||||
}
|
||||
|
||||
// Check if the person exists
|
||||
const [person] = await this.db
|
||||
.select()
|
||||
.from(schema.persons)
|
||||
.where(eq(schema.persons.id, personId));
|
||||
|
||||
if (!person) {
|
||||
throw new NotFoundException(`Person with ID ${personId} not found`);
|
||||
}
|
||||
|
||||
// Check if the tag is already associated with the person
|
||||
const [existingRelation] = await this.db
|
||||
.select()
|
||||
.from(schema.personToTag)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.personToTag.personId, personId),
|
||||
eq(schema.personToTag.tagId, tagId)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingRelation) {
|
||||
return existingRelation;
|
||||
}
|
||||
|
||||
// Add the tag to the person
|
||||
const [relation] = await this.db
|
||||
.insert(schema.personToTag)
|
||||
.values({
|
||||
personId,
|
||||
tagId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag from a person
|
||||
*/
|
||||
async removeTagFromPerson(tagId: string, personId: string) {
|
||||
const [relation] = await this.db
|
||||
.delete(schema.personToTag)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.personToTag.personId, personId),
|
||||
eq(schema.personToTag.tagId, tagId)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!relation) {
|
||||
throw new NotFoundException(`Tag with ID ${tagId} is not associated with person with ID ${personId}`);
|
||||
}
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag to a project
|
||||
*/
|
||||
async addTagToProject(tagId: string, projectId: string) {
|
||||
// Check if the tag exists and is of type PROJECT
|
||||
const tag = await this.findById(tagId);
|
||||
if (tag.type !== 'PROJECT') {
|
||||
throw new Error(`Tag with ID ${tagId} is not of type PROJECT`);
|
||||
}
|
||||
|
||||
// Check if the project exists
|
||||
const [project] = await this.db
|
||||
.select()
|
||||
.from(schema.projects)
|
||||
.where(eq(schema.projects.id, projectId));
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||
}
|
||||
|
||||
// Check if the tag is already associated with the project
|
||||
const [existingRelation] = await this.db
|
||||
.select()
|
||||
.from(schema.projectToTag)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.projectToTag.projectId, projectId),
|
||||
eq(schema.projectToTag.tagId, tagId)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingRelation) {
|
||||
return existingRelation;
|
||||
}
|
||||
|
||||
// Add the tag to the project
|
||||
const [relation] = await this.db
|
||||
.insert(schema.projectToTag)
|
||||
.values({
|
||||
projectId,
|
||||
tagId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag from a project
|
||||
*/
|
||||
async removeTagFromProject(tagId: string, projectId: string) {
|
||||
const [relation] = await this.db
|
||||
.delete(schema.projectToTag)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.projectToTag.projectId, projectId),
|
||||
eq(schema.projectToTag.tagId, tagId)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!relation) {
|
||||
throw new NotFoundException(`Tag with ID ${tagId} is not associated with project with ID ${projectId}`);
|
||||
}
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a person
|
||||
*/
|
||||
async getTagsForPerson(personId: string) {
|
||||
// Check if the person exists
|
||||
const [person] = await this.db
|
||||
.select()
|
||||
.from(schema.persons)
|
||||
.where(eq(schema.persons.id, personId));
|
||||
|
||||
if (!person) {
|
||||
throw new NotFoundException(`Person with ID ${personId} not found`);
|
||||
}
|
||||
|
||||
// Get all tags for the person
|
||||
return this.db
|
||||
.select({
|
||||
tag: schema.tags,
|
||||
})
|
||||
.from(schema.personToTag)
|
||||
.innerJoin(schema.tags, eq(schema.personToTag.tagId, schema.tags.id))
|
||||
.where(eq(schema.personToTag.personId, personId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags for a project
|
||||
*/
|
||||
async getTagsForProject(projectId: string) {
|
||||
// Check if the project exists
|
||||
const [project] = await this.db
|
||||
.select()
|
||||
.from(schema.projects)
|
||||
.where(eq(schema.projects.id, projectId));
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${projectId} not found`);
|
||||
}
|
||||
|
||||
// Get all tags for the project
|
||||
return this.db
|
||||
.select({
|
||||
tag: schema.tags,
|
||||
})
|
||||
.from(schema.projectToTag)
|
||||
.innerJoin(schema.tags, eq(schema.projectToTag.tagId, schema.tags.id))
|
||||
.where(eq(schema.projectToTag.projectId, projectId));
|
||||
}
|
||||
}
|
10
backend/src/modules/tags/tags.module.ts
Normal file
10
backend/src/modules/tags/tags.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TagsController } from './controllers/tags.controller';
|
||||
import { TagsService } from './services/tags.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TagsController],
|
||||
providers: [TagsService],
|
||||
exports: [TagsService],
|
||||
})
|
||||
export class TagsModule {}
|
@ -21,9 +21,11 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
|
||||
- ✅ Configuration Docker pour le déploiement
|
||||
|
||||
#### Composants En Cours
|
||||
- ⏳ Système de migrations de base de données
|
||||
- ⏳ Relations entre les modules existants
|
||||
|
||||
#### Composants Récemment Implémentés
|
||||
- ✅ Système de migrations de base de données avec DrizzleORM
|
||||
|
||||
#### Composants Non Implémentés
|
||||
- ❌ Module d'authentification avec GitHub OAuth
|
||||
- ❌ Stratégies JWT pour la gestion des sessions
|
||||
@ -58,9 +60,9 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
|
||||
#### Priorité Haute
|
||||
|
||||
##### Migrations de Base de Données
|
||||
- [ ] Configurer le système de migrations avec DrizzleORM
|
||||
- [ ] Générer les migrations initiales
|
||||
- [ ] Créer un script pour exécuter les migrations automatiquement au démarrage
|
||||
- [x] Configurer le système de migrations avec DrizzleORM
|
||||
- [x] Générer les migrations initiales
|
||||
- [x] Créer un script pour exécuter les migrations automatiquement au démarrage
|
||||
|
||||
##### Authentification
|
||||
- [ ] Implémenter le module d'authentification
|
||||
@ -162,17 +164,12 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
|
||||
## Prochaines Étapes Prioritaires
|
||||
|
||||
### Backend (Priorité Haute)
|
||||
1. **Migrations de Base de Données**
|
||||
- Configurer le système de migrations avec DrizzleORM
|
||||
- Générer les migrations initiales
|
||||
- Créer un script pour exécuter les migrations automatiquement
|
||||
|
||||
2. **Authentification**
|
||||
1. **Authentification**
|
||||
- Implémenter le module d'authentification avec GitHub OAuth
|
||||
- Configurer les stratégies JWT pour la gestion des sessions
|
||||
- Créer les guards et décorateurs pour la protection des routes
|
||||
|
||||
3. **Modules Manquants**
|
||||
2. **Modules Manquants**
|
||||
- Implémenter le module groupes
|
||||
- Implémenter le module tags
|
||||
- Compléter les relations entre les modules existants
|
||||
@ -193,7 +190,7 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
|
||||
| Composant | Progression |
|
||||
|-----------|-------------|
|
||||
| Backend - Structure de Base | 90% |
|
||||
| Backend - Base de Données | 80% |
|
||||
| Backend - Base de Données | 100% |
|
||||
| Backend - Modules Fonctionnels | 60% |
|
||||
| Backend - Authentification | 0% |
|
||||
| Backend - WebSockets | 0% |
|
||||
|
Loading…
x
Reference in New Issue
Block a user