From c074bc18721dfceaeefe3d4f5b4e07aa42ae6322 Mon Sep 17 00:00:00 2001 From: Janaina <jsk22@inf.ufpr.br> Date: Wed, 23 Oct 2024 11:24:57 -0300 Subject: [PATCH] Issue #26/CREATE intermediary table user-achievements --- src/db/migrations/0000_cold_old_lace.sql | 431 ++++++++++++++++++ src/db/migrations/meta/0000_snapshot.json | 145 ++---- src/db/migrations/meta/_journal.json | 10 +- .../relations/user-achievements.relation.ts | 40 ++ src/db/repo/user-achievements.repo.ts | 158 +++++++ src/db/schema/index.ts | 5 +- src/db/schema/user.schema.ts | 4 +- src/db/seed.ts | 1 + src/db/seeds/index.ts | 1 + src/db/seeds/user-achievements.seed.ts | 26 ++ src/index.ts | 3 + src/routes/user-achievements.route.ts | 140 ++++++ src/services/user-achievements.service.ts | 34 ++ 13 files changed, 900 insertions(+), 98 deletions(-) create mode 100644 src/db/migrations/0000_cold_old_lace.sql create mode 100644 src/db/relations/user-achievements.relation.ts create mode 100644 src/db/repo/user-achievements.repo.ts create mode 100644 src/db/seeds/user-achievements.seed.ts create mode 100644 src/routes/user-achievements.route.ts create mode 100644 src/services/user-achievements.service.ts diff --git a/src/db/migrations/0000_cold_old_lace.sql b/src/db/migrations/0000_cold_old_lace.sql new file mode 100644 index 0000000..05cab79 --- /dev/null +++ b/src/db/migrations/0000_cold_old_lace.sql @@ -0,0 +1,431 @@ +CREATE TABLE IF NOT EXISTS "achievement" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "reward_experience" numeric, + "reward_points" numeric, + "state" integer DEFAULT 0 NOT NULL, + "repeatable" integer NOT NULL, + "is_resettable" boolean NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "achievement_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "collection_likes" ( + "user_id" integer NOT NULL, + "collection" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "collection_resources" ( + "collection_id" integer NOT NULL, + "resource_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "collection_stats" ( + "id" serial PRIMARY KEY NOT NULL, + "collection_id" bigint NOT NULL, + "views" bigint DEFAULT 0, + "downloads" bigint DEFAULT 0, + "likes" bigint DEFAULT 0, + "shares" bigint DEFAULT 0, + "score" bigint DEFAULT 0, + "follows" bigint DEFAULT 0, + CONSTRAINT "collection_stats_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "collection" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255), + "description" text, + "is_private" boolean, + "is_active" boolean DEFAULT true, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp, + "deleted_at" timestamp, + "thumbnail" varchar(255), + CONSTRAINT "collection_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "complaint" ( + "id" serial PRIMARY KEY NOT NULL, + "state" integer DEFAULT 0 NOT NULL, + "description" text NOT NULL, + "evaluatedUser" integer NOT NULL, + "resource_id" integer, + "collection_id" integer, + "user_id" integer, + "evaluated_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "q1" boolean, + "q2" boolean, + "q3" boolean, + "q4" boolean +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "educational_stages" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + CONSTRAINT "educational_stages_id_unique" UNIQUE("id"), + CONSTRAINT "educational_stages_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "follows" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "follower_id" integer NOT NULL, + CONSTRAINT "follows_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "institution" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "uf" varchar(2), + "city" varchar(255), + "cep" varchar(10), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "institution_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "language" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "code" varchar NOT NULL, + CONSTRAINT "language_id_unique" UNIQUE("id"), + CONSTRAINT "language_name_unique" UNIQUE("name"), + CONSTRAINT "language_code_unique" UNIQUE("code") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "licenses" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "url" varchar NOT NULL, + CONSTRAINT "licenses_id_unique" UNIQUE("id"), + CONSTRAINT "licenses_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "object_types" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + CONSTRAINT "object_types_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "reset_ticket" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "token_hash" varchar(255) NOT NULL, + "expiration_date" timestamp NOT NULL, + "token_used" boolean DEFAULT false NOT NULL, + "valid_token" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "reset_ticket_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "resource_educational_stages" ( + "resource_id" integer NOT NULL, + "educational_stage_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "resource_languages" ( + "resource_id" integer NOT NULL, + "language_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "resource_likes" ( + "user_id" integer NOT NULL, + "resource_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "resource_subjects" ( + "resource_id" integer NOT NULL, + "subject_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "resource" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(256) NOT NULL, + "author" varchar(256) NOT NULL, + "description" varchar(256), + "bucket_key" varchar(256), + "link" varchar(256), + "thumbnail" varchar(256) NOT NULL, + "active" boolean DEFAULT false NOT NULL, + "published_at" timestamp, + "submited_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp, + "deleted_at" timestamp, + CONSTRAINT "resource_id_unique" UNIQUE("id"), + CONSTRAINT "resource_bucket_key_unique" UNIQUE("bucket_key") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "role" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "role_id_unique" UNIQUE("id"), + CONSTRAINT "role_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "stats_resources" ( + "id" serial PRIMARY KEY NOT NULL, + "resource_id" bigint NOT NULL, + "views" bigint DEFAULT 0 NOT NULL, + "downloads" bigint DEFAULT 0 NOT NULL, + "shares" bigint DEFAULT 0 NOT NULL, + "score" bigint DEFAULT 0 NOT NULL, + CONSTRAINT "stats_resources_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "subjects" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + CONSTRAINT "subjects_id_unique" UNIQUE("id"), + CONSTRAINT "subjects_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "submission" ( + "id" serial PRIMARY KEY NOT NULL, + "is_accepted" boolean DEFAULT false NOT NULL, + "justification" text, + "resource_id" integer NOT NULL, + "submitter_id" integer NOT NULL, + "curator_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp, + "answered_at" timestamp, + "q1" boolean, + "q2" boolean, + "q3" boolean, + "q4" boolean +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_achievements" ( + "user_id" integer NOT NULL, + "achievement_id" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_institution" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "institution_id" integer NOT NULL, + CONSTRAINT "user_institution_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_role" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "role_id" integer NOT NULL, + CONSTRAINT "user_role_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user_stats" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "score" integer DEFAULT 0 NOT NULL, + "likes" integer DEFAULT 0 NOT NULL, + "likes_received" integer DEFAULT 0 NOT NULL, + "follows" integer DEFAULT 0 NOT NULL, + "followers" integer DEFAULT 0 NOT NULL, + "collections" integer DEFAULT 0 NOT NULL, + "submitted_resources" integer DEFAULT 0 NOT NULL, + "approved_resources" integer DEFAULT 0 NOT NULL, + "reviewed_resources" integer DEFAULT 0 NOT NULL, + "comments" integer DEFAULT 0 NOT NULL, + CONSTRAINT "user_stats_id_unique" UNIQUE("id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "username" varchar(255) NOT NULL, + "password" varchar(255) NOT NULL, + "email" varchar(255) NOT NULL, + "description" text DEFAULT 'sem descrição', + "institution" text DEFAULT 'sem instituição', + "birthday" timestamp NOT NULL, + "cpf" varchar(255) NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "confirmed_at" timestamp, + "confirmation_sent_at" timestamp, + "deleted_at" timestamp, + "reactivated_at" timestamp, + "active" boolean DEFAULT true, + CONSTRAINT "user_id_unique" UNIQUE("id"), + CONSTRAINT "user_username_unique" UNIQUE("username"), + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "collection_likes" ADD CONSTRAINT "collection_likes_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "collection_likes" ADD CONSTRAINT "collection_likes_collection_collection_id_fk" FOREIGN KEY ("collection") REFERENCES "public"."collection"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "collection_resources" ADD CONSTRAINT "collection_resources_collection_id_collection_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collection"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "collection_resources" ADD CONSTRAINT "collection_resources_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "collection_stats" ADD CONSTRAINT "collection_stats_collection_id_collection_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collection"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "complaint" ADD CONSTRAINT "complaint_evaluatedUser_user_id_fk" FOREIGN KEY ("evaluatedUser") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "follows" ADD CONSTRAINT "follows_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "follows" ADD CONSTRAINT "follows_follower_id_user_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "reset_ticket" ADD CONSTRAINT "reset_ticket_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_educational_stages" ADD CONSTRAINT "resource_educational_stages_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_educational_stages" ADD CONSTRAINT "resource_educational_stages_educational_stage_id_educational_stages_id_fk" FOREIGN KEY ("educational_stage_id") REFERENCES "public"."educational_stages"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_languages" ADD CONSTRAINT "resource_languages_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_languages" ADD CONSTRAINT "resource_languages_language_id_language_id_fk" FOREIGN KEY ("language_id") REFERENCES "public"."language"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_likes" ADD CONSTRAINT "resource_likes_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_likes" ADD CONSTRAINT "resource_likes_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_subjects" ADD CONSTRAINT "resource_subjects_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "resource_subjects" ADD CONSTRAINT "resource_subjects_subject_id_subjects_id_fk" FOREIGN KEY ("subject_id") REFERENCES "public"."subjects"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "stats_resources" ADD CONSTRAINT "stats_resources_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "submission" ADD CONSTRAINT "submission_resource_id_resource_id_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."resource"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "submission" ADD CONSTRAINT "submission_submitter_id_user_id_fk" FOREIGN KEY ("submitter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "submission" ADD CONSTRAINT "submission_curator_id_user_id_fk" FOREIGN KEY ("curator_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_achievements" ADD CONSTRAINT "user_achievements_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_achievements" ADD CONSTRAINT "user_achievements_achievement_id_user_id_fk" FOREIGN KEY ("achievement_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_institution" ADD CONSTRAINT "user_institution_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_institution" ADD CONSTRAINT "user_institution_institution_id_institution_id_fk" FOREIGN KEY ("institution_id") REFERENCES "public"."institution"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_role" ADD CONSTRAINT "user_role_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_role" ADD CONSTRAINT "user_role_role_id_role_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."role"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_stats" ADD CONSTRAINT "user_stats_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/db/migrations/meta/0000_snapshot.json b/src/db/migrations/meta/0000_snapshot.json index 81c96f1..12f80d5 100644 --- a/src/db/migrations/meta/0000_snapshot.json +++ b/src/db/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "a57a8a9a-72a9-43a1-8379-ae187d1f5d05", + "id": "859d43fc-3e36-405b-8a21-e76b61e071e6", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -609,100 +609,6 @@ } } }, - "public.items": { - "name": "items", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true - }, - "price": { - "name": "price", - "type": "numeric(10, 2)", - "primaryKey": false, - "notNull": false - }, - "discount": { - "name": "discount", - "type": "numeric(10, 2)", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "item_type": { - "name": "item_type", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "achievement_id": { - "name": "achievement_id", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "items_achievement_id_achievement_id_fk": { - "name": "items_achievement_id_achievement_id_fk", - "tableFrom": "items", - "tableTo": "achievement", - "columnsFrom": [ - "achievement_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "items_id_unique": { - "name": "items_id_unique", - "nullsNotDistinct": false, - "columns": [ - "id" - ] - } - } - }, "public.language": { "name": "language", "schema": "", @@ -1513,6 +1419,55 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, + "public.user_achievements": { + "name": "user_achievements", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "achievement_id": { + "name": "achievement_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_achievements_user_id_user_id_fk": { + "name": "user_achievements_user_id_user_id_fk", + "tableFrom": "user_achievements", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_achievements_achievement_id_user_id_fk": { + "name": "user_achievements_achievement_id_user_id_fk", + "tableFrom": "user_achievements", + "tableTo": "user", + "columnsFrom": [ + "achievement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "public.user_institution": { "name": "user_institution", "schema": "", diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index eaa8fcf..a7ac230 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -1,5 +1,13 @@ { "version": "7", "dialect": "postgresql", - "entries": [] + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1729690423126, + "tag": "0000_cold_old_lace", + "breakpoints": true + } + ] } \ No newline at end of file diff --git a/src/db/relations/user-achievements.relation.ts b/src/db/relations/user-achievements.relation.ts new file mode 100644 index 0000000..fca23ef --- /dev/null +++ b/src/db/relations/user-achievements.relation.ts @@ -0,0 +1,40 @@ +import { integer, pgTable } from "drizzle-orm/pg-core"; +import { achievementTable, userTable } from "../schema"; +import { relations } from "drizzle-orm"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import type { z } from "zod"; + +const userAchievementsTable = pgTable("user_achievements", { + user_id: integer("user_id").notNull().references(() => userTable.id, {onDelete: 'cascade'}), + achievement_id: integer("achievement_id").notNull().references(() => userTable.id, {onDelete: 'cascade'}) +}) + +export const userAchievementRelations = relations(userAchievementsTable, ({one}) => ({ + user: one (userTable, { + fields: [userAchievementsTable.user_id], + references: [userTable.id] + }), + achievement: one(achievementTable, { + fields: [userAchievementsTable.achievement_id], + references: [achievementTable.id] + }) +})) + +const userAchievementsModelSchema = createSelectSchema(userAchievementsTable) +const userAchievementsRelationsDtoSchema = userAchievementsModelSchema +const userAchievementsInputSchema = createInsertSchema(userAchievementsTable) +const userAchievementsUpdateSchema = userAchievementsInputSchema.partial().required({ user_id: true, achievement_id: true }) + +export type UserAchievementsModel = z.infer<typeof userAchievementsModelSchema> +export type UserAchievementDto = z.infer<typeof userAchievementsRelationsDtoSchema> +export type UserAchievementsInput = z.infer<typeof userAchievementsInputSchema> +export type UserAchievementsUpdate = z.infer<typeof userAchievementsUpdateSchema> + +export const userAchievementsSchemas = { + userAchievementsModelSchema, + userAchievementsRelationsDtoSchema, + userAchievementsInputSchema, + userAchievementsUpdateSchema, +} + +export default userAchievementsTable \ No newline at end of file diff --git a/src/db/repo/user-achievements.repo.ts b/src/db/repo/user-achievements.repo.ts new file mode 100644 index 0000000..b9057c2 --- /dev/null +++ b/src/db/repo/user-achievements.repo.ts @@ -0,0 +1,158 @@ +import { Service } from "typedi"; +import type { UserModel } from "../schema/user.schema"; +import db from ".."; +import { achievementTable, userAchievementsTable, userTable } from "../schema"; +import { and, eq, inArray } from "drizzle-orm"; +import type { AchievementModel } from "../schema/achievements.schema"; + +@Service() +export class userAchievementsRepo { + async associateUserWithAchievements(userId: UserModel['id'], achievementIds: UserModel['id'][]): Promise<void> { + try { + const existingAssociations = await db + .select({ + user_id: userAchievementsTable.user_id, + }) + .from(userAchievementsTable) + .where(eq(userAchievementsTable.user_id, userId)) + .execute(); + + const existingAchievementIds = existingAssociations.map(row => row.user_id); + + const newAchievementIds = achievementIds.filter(achievementId => !existingAchievementIds.includes(achievementId)); + + if (newAchievementIds.length > 0) { + const valuesToInsert = newAchievementIds.map(achievementId => ({ + user_id: userId, + achievement_id: achievementId, + })); + + await db + .insert(userAchievementsTable) + .values(valuesToInsert) + .onConflictDoNothing() + .execute(); + + console.log(`User ${userId} associated with new achievements: ${newAchievementIds.join(', ')}`); + } else { + console.log(`No new achievements to associate with user ${userId}`); + } + } catch (error) { + console.error("Error associating user with achievements", error); + } + } + + async getAchievementsByUser(userId: UserModel['id']): Promise<Partial<AchievementModel>[]> { + try { + const achievements = await db + .select({ + id: achievementTable.id, + name: achievementTable.name, + }) + .from(achievementTable) + .innerJoin(userAchievementsTable, eq(userAchievementsTable.achievement_id, achievementTable.id)) + .where(eq(userAchievementsTable.user_id, userId)) + .execute(); + + return achievements; + } catch (error) { + console.error("Error getting achievements by user", error); + throw error; + } + } + + async removeAchievementsFromUser(userId: UserModel['id'], achievementIds: UserModel['id'][]): Promise<void> { + try { + await db + .delete(userAchievementsTable) + .where( + and( + eq(userAchievementsTable.user_id, userId), + inArray(userAchievementsTable.achievement_id, achievementIds) + ) + ) + .execute(); + + console.log(`Achievements ${achievementIds.join(', ')} removed from user ${userId}`); + } catch (error) { + console.error("Error removing achievements from user", error); + throw error; + } + } + + async isAssociationExists(userId: UserModel['id'], achievementId: AchievementModel['id']): Promise<boolean> { + try { + const result = await db + .select() + .from(userAchievementsTable) + .where( + and( + eq(userAchievementsTable.user_id, userId), + eq(userAchievementsTable.achievement_id, achievementId) + ) + ) + .execute(); + + return result.length > 0; + } catch (error) { + console.error("Error checking if association exists", error); + throw error; + } + } + + async getUsersByAchievement(achievementId: AchievementModel['id']): Promise<UserModel[]> { + try { + const users = await db + .select({ + id: userTable.id, + name: userTable.name, + }) + .from(userAchievementsTable) + .innerJoin(userTable, eq(userAchievementsTable.user_id, userTable.id)) + .where(eq(userAchievementsTable.achievement_id, achievementId)) + .execute(); + + return users; + } catch (error) { + console.error("Error getting users by achievement", error); + throw error; + } + } + + async updateUserAchievements(userId: number, newAchievementIds: number[]): Promise<void> { + const existingAssociation = await db + .select({ achievementTable_id: userAchievementsTable.achievement_id }) + .from(userAchievementsTable) + .where(eq(userAchievementsTable.user_id, userId)) + .execute(); + + const existingAchievementIds = existingAssociation.map(row => row.achievementTable_id); + + const achievementsIdsToAdd = newAchievementIds.filter(id => !existingAchievementIds.includes(id)); + const achievementsIdsToRemove = existingAchievementIds.filter(id => !newAchievementIds.includes(id)); + + if (achievementsIdsToAdd.length > 0) { + const valuesToInsert = achievementsIdsToAdd.map(achievementId => ({ + user_id: userId, + achievementId: achievementId, + })); + + await db + .insert(userAchievementsTable) + .values(valuesToInsert) + .execute(); + } + + if (achievementsIdsToRemove.length > 0) { + await db + .delete(userAchievementsTable) + .where( + and( + eq(userAchievementsTable.user_id, userId), + inArray(userAchievementsTable.achievement_id, achievementsIdsToRemove) + ) + ) + .execute(); + } + } +} \ No newline at end of file diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 1468616..da1a4fd 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -28,6 +28,7 @@ import userInstitutionRelationTable from '../relations/user-institution.relation import resourceLikesTable from '../relations/resource-likes.relation' import resourceEducationalStagesTable from './resource-educational-stages.schema' import itemsTable from './items.schema' +import userAchievementsTable from '../relations/user-achievements.relation' export { userTable, @@ -58,6 +59,7 @@ export { resourceEducationalStagesTable, achievementTable, itemsTable, + userAchievementsTable, } export const tables = [ @@ -84,5 +86,6 @@ export const tables = [ userInstitutionRelationTable, resourceLikesTable, achievementTable, - itemsTable + itemsTable, + userAchievementsTable, ] diff --git a/src/db/schema/user.schema.ts b/src/db/schema/user.schema.ts index d369961..caac90e 100644 --- a/src/db/schema/user.schema.ts +++ b/src/db/schema/user.schema.ts @@ -4,6 +4,7 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import { z } from 'zod' import userStatsTable from './user-stats.schema' import passwordRecoveryTable from './password-recovery.schema' +import achievementTable from './achievements.schema' const userTable = pgTable('user', { id: serial('id').primaryKey() @@ -49,7 +50,8 @@ const userTable = pgTable('user', { export const userTableRelation = relations( userTable, ({ one, many }) => ({ userStats: one(userStatsTable), - passwordRecovery: many(passwordRecoveryTable) + passwordRecovery: many(passwordRecoveryTable), + achievements: many(userAchievementsTable) }) ) diff --git a/src/db/seed.ts b/src/db/seed.ts index e6faaab..cd53713 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -46,5 +46,6 @@ await seeds.resourceLikesSeed(db) await seeds.resourceEducationalStagesSeed(db) await seeds.achievementSeed(db) await seeds.itemsSeed(db) +await seeds.userAchievementsSeed(db) await connection.end() diff --git a/src/db/seeds/index.ts b/src/db/seeds/index.ts index ba20da5..19c5bcf 100644 --- a/src/db/seeds/index.ts +++ b/src/db/seeds/index.ts @@ -23,3 +23,4 @@ export { default as resourceLikesSeed } from './resource-likes.seed' export { default as resourceEducationalStagesSeed } from './resource-educational-stages.seed' export { default as achievementSeed } from './achievement.seed' export { default as itemsSeed } from './items.seed' +export { default as userAchievementsSeed } from './user-achievements.seed' diff --git a/src/db/seeds/user-achievements.seed.ts b/src/db/seeds/user-achievements.seed.ts new file mode 100644 index 0000000..523eae0 --- /dev/null +++ b/src/db/seeds/user-achievements.seed.ts @@ -0,0 +1,26 @@ +import type db from ".."; +import type { UserAchievementsInput } from "../relations/user-achievements.relation"; +import { userAchievementsTable } from "../schema"; + +export default async function seed(db: db) { + await db.insert(userAchievementsTable).values(userAchievementsData) +} + +const userAchievementsData: UserAchievementsInput[] = [ + { + user_id: 1, + achievement_id: 1 + }, + { + user_id: 1, + achievement_id: 2 + }, + { + user_id: 2, + achievement_id: 1 + }, + { + user_id: 3, + achievement_id: 2 + } +] \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 76fac32..5612fc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { publicResourceLikesRoutes, resourceLikesRoutes } from './routes/resourc import { publicResourceEducationalStagesRouter, resourceEducationalStagesRouter } from './routes/resource-educational-stages.route' import { achievementRouter, publicAchievementRouter } from './routes/achievement.route' import { itemsRouter, publicItemsRouter } from './routes/items.route' +import { publicUserAchievementsRoute, userAchievementsRouter } from './routes/user-achievements.route' const app = new Hono() @@ -92,6 +93,7 @@ app .route('/items', publicItemsRouter) .route('/resourceLikes', publicResourceLikesRoutes) .route('/achievements', publicAchievementRouter) + .route('/userAchievements', publicUserAchievementsRoute) //rotas que precisam de token app .basePath('/api') @@ -121,6 +123,7 @@ app .route('/items', itemsRouter) .route('/achievements', achievementRouter) .route('/resourceLikes', resourceLikesRoutes) + .route('/userAchievements', userAchievementsRouter) export default app export type AppType = typeof app diff --git a/src/routes/user-achievements.route.ts b/src/routes/user-achievements.route.ts new file mode 100644 index 0000000..526f0a3 --- /dev/null +++ b/src/routes/user-achievements.route.ts @@ -0,0 +1,140 @@ +import { UserAchievementsService } from "@/services/user-achievements.service"; +import Container from "typedi"; +import { z } from "zod"; +import { honoWithJwt } from ".."; +import { zValidator } from "@hono/zod-validator"; +import { createApexError, HttpStatus } from "@/services/error.service"; +import { Hono } from "hono"; + +const associateSchema = z.object({ + userId: z.number(), + achievementIds: z.array(z.number()), +}); + +const service = Container.get(UserAchievementsService); + +export const userAchievementsRouter = honoWithJwt() + .post('/associate', zValidator('json', associateSchema), + async (c) => { + try { + const { userId, achievementIds } = await c.req.valid('json'); + await service.associateUserWithAchievements(userId, achievementIds); + return c.json({ message: 'Achievements associated successfully' }); + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'Failed to associate achievements', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'Check the input and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + .post('/:userId/delete/achievement/:achievementId', + async (c) => { + try { + const userId = +c.req.param('userId'); + const achievementId = +c.req.param('achievementId'); + await service.removeAchievementsFromUser(userId, [achievementId]); + return c.json({ message: 'Achievement removed successfully' }); + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'Failed to remove achievement', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'Check the input and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + .post('/update', zValidator('json', associateSchema), + async (c) => { + try { + const { userId, achievementIds } = await c.req.valid('json'); + await service.updateUserAchievements(userId, achievementIds); + return c.json({ message: 'Achievements updated successfully' }); + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'Failed to update achievements', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'Check the input and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + +export const publicUserAchievementsRoute = new Hono() + .get('/:userId/achievements', + async (c) => { + try { + const userId = +c.req.param('userId'); + const achievements = await service.getAchievementsByUser(userId); + return c.json({ achievements }); + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'Failed to get user achievements', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'Check the input and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + .get('/:userId/achievements/:achievementId/exists', + async (c) => { + try { + const userId = +c.req.param('userId'); + const achievementId = +c.req.param('achievementId'); + const exists = await service.isAssociationExists(userId, achievementId); + return c.json({ exists }); + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'Failed to check if association exists', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'Check the input and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + .get('/achievements/:achievementId/users', + async (c) => { + try { + const achievementId = +c.req.param('achievementId'); + const users = await service.getUsersByAchievement(achievementId); + return c.json({ users }); + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'Failed to get users by achievement', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'Check the input and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) \ No newline at end of file diff --git a/src/services/user-achievements.service.ts b/src/services/user-achievements.service.ts new file mode 100644 index 0000000..c94c632 --- /dev/null +++ b/src/services/user-achievements.service.ts @@ -0,0 +1,34 @@ +import { userAchievementsRepo } from "@/db/repo/user-achievements.repo"; +import type { AchievementModel } from "@/db/schema/achievements.schema"; +import type { UserModel } from "@/db/schema/user.schema"; +import { Inject, Service } from "typedi"; + +@Service() +export class UserAchievementsService { + @Inject() + private readonly repo: userAchievementsRepo + + async associateUserWithAchievements(userId: UserModel['id'], achievementIds: AchievementModel['id'][]): Promise<void> { + await this.repo.associateUserWithAchievements(userId, achievementIds); + } + + async getAchievementsByUser(userId: UserModel['id']): Promise<AchievementModel[]> { + return this.repo.getAchievementsByUser(userId); + } + + async removeAchievementsFromUser(userId: UserModel['id'], achievementIds: AchievementModel['id'][]): Promise<void> { + await this.repo.removeAchievementsFromUser(userId, achievementIds); + } + + async isAssociationExists(userId: UserModel['id'], achievementId: AchievementModel['id']): Promise<boolean> { + return this.repo.isAssociationExists(userId, achievementId); + } + + async getUsersByAchievement(achievementId: AchievementModel['id']): Promise<UserModel[]> { + return this.repo.getUsersByAchievement(achievementId); + } + + async updateUserAchievements(userId: UserModel['id'], achievementIds: AchievementModel['id'][]): Promise<void> { + await this.repo.updateUserAchievements(userId, achievementIds); + } +} \ No newline at end of file -- GitLab