diff --git a/docker-compose.yml b/docker-compose.yml index 98113da7176558f9aade7396bbefdcdb921bc9a7..bc28cb345ae39c7f62b389baca653be784d98afe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - - "5432:5432" + - "5433:5432" networks: - apex_network volumes: diff --git a/src/db/migrations/0004_confused_maestro.sql b/src/db/migrations/0004_confused_maestro.sql new file mode 100644 index 0000000000000000000000000000000000000000..83480b9d5beff2295d3c2b82417efc6e64d4e28f --- /dev/null +++ b/src/db/migrations/0004_confused_maestro.sql @@ -0,0 +1,44 @@ +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, + "likes_received" integer DEFAULT 0, + "follows" integer DEFAULT 0, + "followers" integer DEFAULT 0, + "collections" integer DEFAULT 0, + "submitted_resources" integer DEFAULT 0, + "approved_resources" integer DEFAULT 0, + "reviewed_resources" integer DEFAULT 0, + "comments" integer DEFAULT 0, + 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 +ALTER TABLE "resource" ADD COLUMN "active" boolean DEFAULT false NOT NULL;--> 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/0004_snapshot.json b/src/db/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000000000000000000000000000000000000..ec6a83a600e0cad2c1a9c0922159de9248299afc --- /dev/null +++ b/src/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,439 @@ +{ + "id": "1ab52913-6a40-4df5-b569-1fd63fb22a9a", + "prevId": "ed42f7cb-45e7-47c8-93d7-a35abfc74f5a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.resource": { + "name": "resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "bucket_key": { + "name": "bucket_key", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "submited_at": { + "name": "submited_at", + "type": "timestamp", + "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": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "resource_id_unique": { + "name": "resource_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + }, + "resource_bucket_key_unique": { + "name": "resource_bucket_key_unique", + "nullsNotDistinct": false, + "columns": [ + "bucket_key" + ] + } + } + }, + "public.stats_resources": { + "name": "stats_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "downloads": { + "name": "downloads", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "shares": { + "name": "shares", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "score": { + "name": "score", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "stats_resources_resource_id_resource_id_fk": { + "name": "stats_resources_resource_id_resource_id_fk", + "tableFrom": "stats_resources", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stats_resources_id_unique": { + "name": "stats_resources_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "likes": { + "name": "likes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "likes_received": { + "name": "likes_received", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "follows": { + "name": "follows", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "followers": { + "name": "followers", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "collections": { + "name": "collections", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "submitted_resources": { + "name": "submitted_resources", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "approved_resources": { + "name": "approved_resources", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "reviewed_resources": { + "name": "reviewed_resources", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "comments": { + "name": "comments", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_id_unique": { + "name": "user_stats_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sem descrição'" + }, + "institution": { + "name": "institution", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sem instituição'" + }, + "birthday": { + "name": "birthday", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "cpf": { + "name": "cpf", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "confirmation_sent_at": { + "name": "confirmation_sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reactivated_at": { + "name": "reactivated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_id_unique": { + "name": "user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index a73e2750bccfffdfba8fedf90ba511bb78b82d52..e06c040e2a31683c19d76df177495da3962db53d 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1726750555860, "tag": "0003_polite_wallflower", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1727099258603, + "tag": "0004_confused_maestro", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repo/resource.repo.ts b/src/db/repo/resource.repo.ts index 1a2c399b365e840b5e9726d2f55e74284352e0ea..3d9cae40e80b3c7ee9018bfce574f4a646751c36 100644 --- a/src/db/repo/resource.repo.ts +++ b/src/db/repo/resource.repo.ts @@ -2,52 +2,79 @@ import { Service } from "typedi"; import { resourceSchema, type ResourceInput, type ResourceModel, type ResourceUpdate } from "../schema/resource.schema"; import db from '..' import resourceTable from "../schema/resource.schema"; -import { eq } from 'drizzle-orm' +import { eq, sql, and } from 'drizzle-orm' @Service() export class ResourceRepo { - async create (resource: ResourceInput + async create(resource: ResourceInput ): Promise<ResourceModel> { const [ret] = await db - .insert(resourceTable) - .values(resource) - .returning() + .insert(resourceTable) + .values(resource) + .returning() return resourceSchema.model.parse(ret) } - async update (resource: ResourceUpdate): Promise<ResourceModel> { + async update(resource: ResourceUpdate): Promise<ResourceModel> { const [ret] = await db - .update(resourceTable) - .set(resource) - .where(eq(resourceTable.id, resource.id)) - .returning() - + .update(resourceTable) + .set(resource) + .where(eq(resourceTable.id, resource.id)) + .returning() + + return resourceSchema.model.parse(ret) + } + + async deleteData(id: ResourceModel['id']): Promise<ResourceModel> { + const [ret] = await db + .delete(resourceTable) + .where(eq(resourceTable.id, id)) + .returning() + return resourceSchema.model.parse(ret) } - async delete (id: ResourceModel['id']): Promise<ResourceModel> { + async delete(id: ResourceModel['id']): Promise<ResourceModel> { const [ret] = await db - .delete(resourceTable) - .where(eq(resourceTable.id, id)) - .returning() + .update(resourceTable) + .set({ + deleted_at: sql`NOW()`, + active: false + }) + .where(eq(resourceTable.id, id)) + .returning() return resourceSchema.model.parse(ret) } - async find (id: ResourceModel['id']): Promise<ResourceModel | undefined> { + async active(id: ResourceModel['id']): Promise<ResourceModel> { + const [ret] = await db + .update(resourceTable) + .set({ + deleted_at: null, + active: true + }) + .where(eq(resourceTable.id, id)) + .returning() + + return resourceSchema.model.parse(ret) + } + + + async find(id: ResourceModel['id']): Promise<ResourceModel | undefined> { const resource = await db.query.resourceTable.findFirst({ - where: eq(resourceTable.id, id), + where: and(eq(resourceTable.id, id), eq(resourceTable.active, true)), }) - return resourceSchema.model.parse(resource) + return resource ? resourceSchema.model.parse(resource) : undefined; } - async findMany (): Promise<ResourceModel[]> { + async findMany(): Promise<ResourceModel[]> { - return resourceSchema.model.array().parse(await db.query.resourceTable.findMany()) + return resourceSchema.model.array().parse(await db.query.resourceTable.findMany({ where: eq(resourceTable.active, true) })) } diff --git a/src/db/schema/resource.schema.ts b/src/db/schema/resource.schema.ts index 8f6ffd928a8716a64c261476701956f9f641e603..3bded7fbfb4819c318a2cd2322ccafa59f5fbe8c 100644 --- a/src/db/schema/resource.schema.ts +++ b/src/db/schema/resource.schema.ts @@ -3,13 +3,13 @@ import { serial, varchar, timestamp, + boolean, } from 'drizzle-orm/pg-core' import { sql } from 'drizzle-orm' import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import type { z } from 'zod' - - +//por padrao active é false, só é true quando o recurso é aprovado const resourceTable = pgTable('resource', { id: serial('id').notNull().primaryKey().unique(), name: varchar('name', { length: 256 }).notNull(), @@ -18,6 +18,7 @@ const resourceTable = pgTable('resource', { bucket_key: varchar('bucket_key', { length: 256 }).unique(), link: varchar('link', { length: 256 }), thumbnail: varchar('thumbnail', { length: 256 }), + active: boolean('active').notNull().default(false), published_at: timestamp('published_at', { mode: 'string' }), submited_at: timestamp('submited_at', { mode: 'string' }), created_at: timestamp('created_at', { mode: 'string' }) diff --git a/src/routes/resource.route.ts b/src/routes/resource.route.ts index fe23b62dbb1ea21e69a94946c9ef442bbd75cc53..80826cc9a4aa6e58ed4c91d0cf3defbf8348539c 100644 --- a/src/routes/resource.route.ts +++ b/src/routes/resource.route.ts @@ -65,11 +65,11 @@ export const resourceRouter = honoWithJwt() }) // rota para deletar um recurso - .post('/delete/:id', + .post('/deleteData/:id', async (c) => { try { const id = +c.req.param('id') - const resource = resourceSchema.dto.parse(await service.delete(id)) + const resource = resourceSchema.dto.parse(await service.deleteData(id)) //onDelete: "cascade" -> presente no stats-resources.schema.ts ja exclui o stats @@ -89,6 +89,53 @@ export const resourceRouter = honoWithJwt() } ) + //rota para dizer que o recurso foi deletado + .post('/delete/:id', + async (c) => { + try { + const id = +c.req.param('id') + const resource = resourceSchema.dto.parse(await service.delete(id)) + + return c.json(resource) + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'could not delete the resource', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'check the id and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + + //rota para tornar o recurso ativo + .post('/active/:id', + async (c) => { + try { + const id = +c.req.param('id') + const resource = resourceSchema.dto.parse(await service.active(id)) + + return c.json(resource) + } catch (e) { + return c.json( + createApexError({ + status: 'error', + message: 'could not active the resource', + code: HttpStatus.BAD_REQUEST, + path: c.req.routePath, + suggestion: 'check the id and try again', + }), + HttpStatus.BAD_REQUEST + ) + } + } + ) + + export const publicResourceRouter = new Hono() // rota para listar todos os recursos .get('/all', async (c) => { diff --git a/src/services/resource.service.ts b/src/services/resource.service.ts index a8e4066cd161cd20255fb86842274a3cda18747b..7d7f0e16da2ded254e7d8f362edda5b9b9ed4cc7 100644 --- a/src/services/resource.service.ts +++ b/src/services/resource.service.ts @@ -15,10 +15,18 @@ export class ResourceService { return this.repo.update(resource) } + async deleteData(id: ResourceModel['id']): Promise<ResourceModel> { + return this.repo.deleteData(id) + } + async delete(id: ResourceModel['id']): Promise<ResourceModel> { return this.repo.delete(id) } + async active(id: ResourceModel['id']): Promise<ResourceModel> { + return this.repo.active(id) + } + async findById(id: ResourceModel['id']): Promise<ResourceModel | undefined> { return this.repo.find(id) }