feat: add orphanage management functionality with image upload and validation
This commit is contained in:
13
backend/src/config/upload.ts
Normal file
13
backend/src/config/upload.ts
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
74
backend/src/controllers/OrphanagesController.ts
Normal file
74
backend/src/controllers/OrphanagesController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
3
backend/src/database/connection.ts
Normal file
3
backend/src/database/connection.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createConnection } from 'typeorm';
|
||||
|
||||
createConnection();
|
||||
BIN
backend/src/database/database.sqlite
Normal file
BIN
backend/src/database/database.sqlite
Normal file
Binary file not shown.
@@ -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"`);
|
||||
}
|
||||
|
||||
}
|
||||
24
backend/src/errors/handler.ts
Normal file
24
backend/src/errors/handler.ts
Normal 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;
|
||||
15
backend/src/models/Image.ts
Normal file
15
backend/src/models/Image.ts
Normal 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;
|
||||
}
|
||||
48
backend/src/models/Orphanage.ts
Normal file
48
backend/src/models/Orphanage.ts
Normal 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
21
backend/src/routes.ts
Normal 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
22
backend/src/server.ts
Normal 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');
|
||||
});
|
||||
15
backend/src/views/images_view.ts
Normal file
15
backend/src/views/images_view.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
23
backend/src/views/orphanages_view.ts
Normal file
23
backend/src/views/orphanages_view.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user