diff --git a/backend/src/config/upload.ts b/backend/src/config/upload.ts new file mode 100644 index 0000000..5fd8523 --- /dev/null +++ b/backend/src/config/upload.ts @@ -0,0 +1,13 @@ +import multer from 'multer'; +import path from 'path'; + +export default { + storage: multer.diskStorage({ + destination: path.join(__dirname, '..','..','uploads'), + filename: (request, file, cb) => { + const fileName = `${Date.now()}_${file.originalname}`; + + cb(null, fileName); + } + }) +} \ No newline at end of file diff --git a/backend/src/controllers/OrphanagesController.ts b/backend/src/controllers/OrphanagesController.ts new file mode 100644 index 0000000..85acbac --- /dev/null +++ b/backend/src/controllers/OrphanagesController.ts @@ -0,0 +1,74 @@ + +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import Orphanage from '../models/Orphanage'; +import OrphanageView from '../views/orphanages_view'; +import * as Yup from 'yup'; + +export default { + + async getAll(request: Request, response: Response) { + return response.json(OrphanageView.renderMany(await getRepository(Orphanage).find({ + relations: ['images'] + }))); + }, + + async getIndex(request: Request, response: Response) { + return response.json(OrphanageView.render(await getRepository(Orphanage).findOneOrFail(request.params.id, { + relations: ['images'] + }))); + }, + + async create(request: Request, response: Response) { + const { + name, + latitude, + longitude, + about, + instructions, + opening_hours, + open_on_weekends, + phone + } = request.body; + + const requestImages = request.files as Express.Multer.File[]; + const images = requestImages.map(image => { + return { path: image.filename}; + }) + + const data = { + name, + latitude: Number(latitude), + longitude: Number(longitude), + about, + instructions, + opening_hours, + open_on_weekends: open_on_weekends === 'true', + phone, + images, + }; + + const schema = Yup.object().shape({ + name: Yup.string().required(), + latitude: Yup.number().required(), + longitude: Yup.number().required(), + about: Yup.string().required().max(300), + opening_hours: Yup.string().required(), + open_on_weekends: Yup.boolean().required(), + phone: Yup.string().required(), + images: Yup.array(Yup.object().shape({ + path: Yup.string().required() + })) + }); + + await schema.validate(data, { + abortEarly: false, + }); + + const orphanage = getRepository(Orphanage).create(data); + + await getRepository(Orphanage).save(orphanage); + + return response.status(201).json(orphanage); + } +} \ No newline at end of file diff --git a/backend/src/database/connection.ts b/backend/src/database/connection.ts new file mode 100644 index 0000000..3827f2b --- /dev/null +++ b/backend/src/database/connection.ts @@ -0,0 +1,3 @@ +import { createConnection } from 'typeorm'; + +createConnection(); \ No newline at end of file diff --git a/backend/src/database/database.sqlite b/backend/src/database/database.sqlite new file mode 100644 index 0000000..fe4cc49 Binary files /dev/null and b/backend/src/database/database.sqlite differ diff --git a/backend/src/database/migrations/1749673967883-InitialMigration.ts b/backend/src/database/migrations/1749673967883-InitialMigration.ts new file mode 100644 index 0000000..d6abdd5 --- /dev/null +++ b/backend/src/database/migrations/1749673967883-InitialMigration.ts @@ -0,0 +1,24 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class InitialMigration1749673967883 implements MigrationInterface { + name = 'InitialMigration1749673967883' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "orphanages" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(50) NOT NULL, "latitude" decimal(10,8) NOT NULL, "longitude" decimal(10,8) NOT NULL, "about" varchar(300) NOT NULL, "instructions" varchar(300), "opening_hours" varchar(20) NOT NULL, "open_on_weekends" boolean NOT NULL, "phone" varchar(15) NOT NULL)`); + await queryRunner.query(`CREATE TABLE "images" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "path" varchar NOT NULL, "orphanageId" integer)`); + await queryRunner.query(`CREATE TABLE "temporary_images" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "path" varchar NOT NULL, "orphanageId" integer, CONSTRAINT "FK_96b2848afc17474a8c87a0b8caf" FOREIGN KEY ("orphanageId") REFERENCES "orphanages" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_images"("id", "path", "orphanageId") SELECT "id", "path", "orphanageId" FROM "images"`); + await queryRunner.query(`DROP TABLE "images"`); + await queryRunner.query(`ALTER TABLE "temporary_images" RENAME TO "images"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "images" RENAME TO "temporary_images"`); + await queryRunner.query(`CREATE TABLE "images" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "path" varchar NOT NULL, "orphanageId" integer)`); + await queryRunner.query(`INSERT INTO "images"("id", "path", "orphanageId") SELECT "id", "path", "orphanageId" FROM "temporary_images"`); + await queryRunner.query(`DROP TABLE "temporary_images"`); + await queryRunner.query(`DROP TABLE "images"`); + await queryRunner.query(`DROP TABLE "orphanages"`); + } + +} diff --git a/backend/src/errors/handler.ts b/backend/src/errors/handler.ts new file mode 100644 index 0000000..c063b05 --- /dev/null +++ b/backend/src/errors/handler.ts @@ -0,0 +1,24 @@ +import { ErrorRequestHandler } from 'express'; +import { ValidationError } from 'yup'; + +interface ValidationErrors { + [key: string]: string[]; +} + +const errorHandler: ErrorRequestHandler = (error, request, response, next) => { + if (error instanceof ValidationError) { + let errors: ValidationErrors = {}; + + error.inner.forEach(err => { + errors[err.name] = err.errors; + }); + + return response.status(400).json({ message: 'Validation fails', errors}) + } + + console.log(error); + + return response.status(500).json({message: 'Internal Server Error'}) +}; + +export default errorHandler; \ No newline at end of file diff --git a/backend/src/models/Image.ts b/backend/src/models/Image.ts new file mode 100644 index 0000000..2b6a8cc --- /dev/null +++ b/backend/src/models/Image.ts @@ -0,0 +1,15 @@ +import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm'; +import Orphanage from './Orphanage'; + +@Entity('images') +export default class Image { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column() + path: string; + + @ManyToOne(() => Orphanage, orphanage => orphanage.images) + @JoinColumn({ name: 'orphanageId' }) + orphanage: Orphanage; +} \ No newline at end of file diff --git a/backend/src/models/Orphanage.ts b/backend/src/models/Orphanage.ts new file mode 100644 index 0000000..1a9ee0a --- /dev/null +++ b/backend/src/models/Orphanage.ts @@ -0,0 +1,48 @@ +import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; +import Image from './Image'; + +@Entity('orphanages') +export default class Orphanage { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column({ + length: 50 + }) + name: string; + + @Column('decimal', { precision: 10, scale: 8 }) + latitude: number; + + @Column('decimal', { precision: 10, scale: 8 }) + longitude: number; + + @Column({ + length: 300 + }) + about: string; + + @Column({ + length: 300, + nullable: true + }) + instructions: string; + + @Column({ + length: 20 + }) + opening_hours: string; + + @Column() + open_on_weekends: boolean; + + @Column({ + length: 15 + }) + phone: string; + + @OneToMany(() => Image, image => image.orphanage, { + cascade: ['insert', 'update'] + }) + images: Image[]; +} \ No newline at end of file diff --git a/backend/src/routes.ts b/backend/src/routes.ts new file mode 100644 index 0000000..15aa9d4 --- /dev/null +++ b/backend/src/routes.ts @@ -0,0 +1,21 @@ + +import { Router } from 'express'; +import multer from 'multer'; + +import OrphanagesController from './controllers/OrphanagesController'; +import UploadConfig from './config/upload'; + +const routes = Router(); +const upload = multer(UploadConfig); + +routes.get('/users', (request, response) => { + return response.json({message: ["João"]}); +}); + +routes.post('/orphanages', upload.array('images'), OrphanagesController.create); +routes.get('/orphanages', OrphanagesController.getAll); +routes.get('/orphanages/:id', OrphanagesController.getIndex); + + + +export default routes; \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts new file mode 100644 index 0000000..071a810 --- /dev/null +++ b/backend/src/server.ts @@ -0,0 +1,22 @@ +import 'dotenv/config'; +import express from 'express'; +import path from 'path'; +import 'express-async-errors'; +import cors from 'cors'; + +import './database/connection'; // This line now automatically runs the connection +import errorHandler from './errors/handler'; +import routes from './routes'; + +const app = express(); + +app.use(cors()); +app.use(express.json()); +app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'))); +app.use(routes); +app.use(errorHandler); + +app.listen(process.env.PORT || 3101, () => { + console.log('Server is running on http://localhost:' + (process.env.PORT || 3101)); + console.log('Environment:', process.env.NODE_ENV || 'development'); +}); \ No newline at end of file diff --git a/backend/src/views/images_view.ts b/backend/src/views/images_view.ts new file mode 100644 index 0000000..ee92219 --- /dev/null +++ b/backend/src/views/images_view.ts @@ -0,0 +1,15 @@ +import 'dotenv/config'; +import Image from '../models/Image'; + +export default { + render(image: Image) { + return { + id: image.id, + url: `${process.env.BACKEND_URL}/uploads/${image.path}` + }; + }, + + renderMany(images: Image[]) { + return images.map(image => this.render(image)); + } +} \ No newline at end of file diff --git a/backend/src/views/orphanages_view.ts b/backend/src/views/orphanages_view.ts new file mode 100644 index 0000000..728cab0 --- /dev/null +++ b/backend/src/views/orphanages_view.ts @@ -0,0 +1,23 @@ +import Orphanage from '../models/Orphanage'; +import ImagesView from './images_view'; + +export default { + render(orphanage: Orphanage) { + return { + id: orphanage.id, + name: orphanage.name, + latitude: Number(orphanage.latitude), + longitude: Number(orphanage.longitude), + about: orphanage.about, + instructions: orphanage.instructions, + opening_hours: orphanage.opening_hours, + open_on_weekends: orphanage.open_on_weekends, + phone: orphanage.phone, + images: ImagesView.renderMany(orphanage.images), + }; + }, + + renderMany(orphanages: Orphanage[]) { + return orphanages.map(orphanage => this.render(orphanage)); + } +} \ No newline at end of file