feat: add orphanage management functionality with image upload and validation

This commit is contained in:
2025-06-11 18:33:42 -03:00
parent bcd47dbdc5
commit 610108950d
12 changed files with 282 additions and 0 deletions

View File

@@ -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);
}
})
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,3 @@
import { createConnection } from 'typeorm';
createConnection();

Binary file not shown.

View File

@@ -0,0 +1,24 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class InitialMigration1749673967883 implements MigrationInterface {
name = 'InitialMigration1749673967883'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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[];
}

21
backend/src/routes.ts Normal file
View File

@@ -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;

22
backend/src/server.ts Normal file
View File

@@ -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');
});

View File

@@ -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));
}
}

View File

@@ -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));
}
}