├── .npmrc ├── .gitignore ├── src ├── entities │ ├── models │ │ ├── user.interface.ts │ │ ├── category.interface.ts │ │ ├── person.interface.ts │ │ ├── address.interface.ts │ │ └── product.interface.ts │ ├── user.entity.ts │ ├── person.entity.ts │ ├── address.entity.ts │ ├── category.entity.ts │ └── product.entity.ts ├── use-cases │ ├── errors │ │ ├── resource-not-found-error.ts │ │ └── invalid-credentials-error.ts │ ├── delete-product.ts │ ├── create-category.ts │ ├── find-all-products.ts │ ├── factory │ │ ├── make-signin-use-case.ts │ │ ├── make-create-user-use-case.ts │ │ ├── make-create-person-use-case.ts │ │ ├── make-find-with-person.ts │ │ ├── make-find-product-use-case.ts │ │ ├── make-create-address-use-case.ts │ │ ├── make-create-product-use-case.ts │ │ ├── make-delete-product-use-case.ts │ │ ├── make-update-product-use-case.ts │ │ ├── make-create-category-use-case.ts │ │ ├── make-find-all-product-use-case.ts │ │ └── make-find-address-by-person-use-case.ts │ ├── create-person.ts │ ├── create-user.ts │ ├── update-product.ts │ ├── create-product.ts │ ├── create-address.ts │ ├── find-prodcut.ts │ ├── signin.ts │ ├── find-address-by-person.ts │ └── find-with-person.ts ├── repositories │ ├── person.repository.interface.ts │ ├── category.repository.interface.ts │ ├── user.repository.interface.ts │ ├── product.repository.interface.ts │ ├── address.repository.interface.ts │ ├── typeorm │ │ ├── category.repository.ts │ │ └── product.repository.ts │ └── pg │ │ ├── person.repository.ts │ │ ├── user.repository.ts │ │ └── address.repository.ts ├── http │ ├── controllers │ │ ├── person │ │ │ ├── routes.ts │ │ │ └── create.ts │ │ ├── category │ │ │ ├── routes.ts │ │ │ └── create.ts │ │ ├── address │ │ │ ├── routes.ts │ │ │ ├── create.ts │ │ │ └── find-address.ts │ │ ├── user │ │ │ ├── routes.ts │ │ │ ├── find-user.ts │ │ │ ├── create.ts │ │ │ └── signin.ts │ │ └── product │ │ │ ├── routes.ts │ │ │ ├── delete.ts │ │ │ ├── find-product.ts │ │ │ ├── find-all-products.ts │ │ │ ├── update.ts │ │ │ └── create.ts │ └── middlewares │ │ └── jwt-validate.ts ├── server.ts ├── utils │ ├── client-http.ts │ └── global-error-handler.ts ├── env │ └── index.ts ├── lib │ ├── typeorm │ │ ├── migrations │ │ │ └── 1714090143497-ProductAutoGenerateUUID.ts │ │ └── typeorm.ts │ └── pg │ │ └── db.ts └── app.ts ├── .env.example ├── .eslintrc.json ├── package.json ├── README.md └── tsconfig.json /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .env -------------------------------------------------------------------------------- /src/entities/models/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id?: number 3 | username: string 4 | password: string 5 | } 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT= 2 | ENV= 3 | DATABASE_USER= 4 | DATABASE_HOST= 5 | DATABASE_NAME= 6 | DATABASE_PASSWORD= 7 | DATABASE_PORT= 8 | JWT_SECRET= -------------------------------------------------------------------------------- /src/entities/models/category.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICategory { 2 | id?: number 3 | name: string 4 | createdAt?: Date 5 | } 6 | -------------------------------------------------------------------------------- /src/use-cases/errors/resource-not-found-error.ts: -------------------------------------------------------------------------------- 1 | export class ResourceNotFoundError extends Error { 2 | constructor() { 3 | super('Resource not found') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/entities/models/person.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IPerson { 2 | id?: number 3 | cpf: string 4 | name: string 5 | birth: Date 6 | email: string 7 | user_id?: number 8 | } 9 | -------------------------------------------------------------------------------- /src/use-cases/errors/invalid-credentials-error.ts: -------------------------------------------------------------------------------- 1 | export class InvalidCredentialsError extends Error { 2 | constructor() { 3 | super('Username or password is incorrect') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/entities/models/address.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAddress { 2 | id?: number 3 | street: string 4 | city: string 5 | state: string 6 | zip_code: string 7 | person_id?: number 8 | } 9 | -------------------------------------------------------------------------------- /src/repositories/person.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IPerson } from '@/entities/models/person.interface' 2 | 3 | export interface IPersonRepository { 4 | create(person: IPerson): Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/http/controllers/person/routes.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify' 2 | import { create } from './create' 3 | 4 | export async function personRoutes(app: FastifyInstance) { 5 | app.post('/person', create) 6 | } 7 | -------------------------------------------------------------------------------- /src/http/controllers/category/routes.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify' 2 | import { create } from './create' 3 | 4 | export async function categoryRoutes(app: FastifyInstance) { 5 | app.post('/category', create) 6 | } 7 | -------------------------------------------------------------------------------- /src/repositories/category.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/entities/models/product.interface' 2 | 3 | export interface ICategoryRepository { 4 | create(name: string, products?: IProduct[]): Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { env } from './env' 2 | import { app } from '@/app' 3 | 4 | app 5 | .listen({ 6 | host: '0.0.0.0', 7 | port: env.PORT, 8 | }) 9 | .then(() => { 10 | console.log(`Server is running on http://localhost:${env.PORT}`) 11 | }) 12 | -------------------------------------------------------------------------------- /src/entities/models/product.interface.ts: -------------------------------------------------------------------------------- 1 | import { ICategory } from './category.interface' 2 | 3 | export interface IProduct { 4 | id?: string 5 | name: string 6 | description: string 7 | image: string 8 | price: number 9 | categories?: ICategory[] 10 | } 11 | -------------------------------------------------------------------------------- /src/http/controllers/address/routes.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify' 2 | import { create } from './create' 3 | import { findAddress } from './find-address' 4 | 5 | export async function addressRoutes(app: FastifyInstance) { 6 | app.post('/address', create) 7 | app.get('/address/person/:personId', findAddress) 8 | } 9 | -------------------------------------------------------------------------------- /src/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './models/user.interface' 2 | 3 | export class User implements IUser { 4 | id?: number 5 | username: string 6 | password: string 7 | 8 | constructor(username: string, password: string) { 9 | this.username = username 10 | this.password = password 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/use-cases/delete-product.ts: -------------------------------------------------------------------------------- 1 | import { IProductRepository } from '@/repositories/product.repository.interface' 2 | 3 | export class DeleteProductUseCase { 4 | constructor(private productRepository: IProductRepository) {} 5 | 6 | async handler(id: string): Promise { 7 | return this.productRepository.delete(id) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/use-cases/create-category.ts: -------------------------------------------------------------------------------- 1 | import { ICategoryRepository } from '@/repositories/category.repository.interface' 2 | 3 | export class CreateCategoryUseCase { 4 | constructor(private categoryRepository: ICategoryRepository) {} 5 | 6 | async handler(name: string): Promise { 7 | await this.categoryRepository.create(name) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/use-cases/find-all-products.ts: -------------------------------------------------------------------------------- 1 | import { IProductRepository } from '@/repositories/product.repository.interface' 2 | 3 | export class FindAllPRoductUseCase { 4 | constructor(private productRepository: IProductRepository) {} 5 | 6 | async handler(page: number, limit: number) { 7 | return this.productRepository.findAll(page, limit) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-signin-use-case.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from '@/repositories/pg/user.repository' 2 | import { SigninUseCase } from '../signin' 3 | 4 | export function makeSigninUseCase() { 5 | const userRepository = new UserRepository() 6 | 7 | const signinUseCase = new SigninUseCase(userRepository) 8 | 9 | return signinUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/http/controllers/user/routes.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify' 2 | import { create } from './create' 3 | import { findUser } from './find-user' 4 | import { signin } from './signin' 5 | 6 | export async function userRoutes(app: FastifyInstance) { 7 | app.get('/user/:id', findUser) 8 | app.post('/user', create) 9 | app.post('/user/signin', signin) 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/create-person.ts: -------------------------------------------------------------------------------- 1 | import { Person } from '@/entities/person.entity' 2 | import { IPersonRepository } from '@/repositories/person.repository.interface' 3 | 4 | export class CreatePersonUseCase { 5 | constructor(private personRepository: IPersonRepository) {} 6 | 7 | handler(person: Person) { 8 | return this.personRepository.create(person) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/create-user.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@/entities/user.entity' 2 | import { IUserRepository } from '@/repositories/user.repository.interface' 3 | 4 | export class CreateUserUseCase { 5 | constructor(private userRepository: IUserRepository) {} 6 | 7 | async handler(user: User): Promise { 8 | return this.userRepository.create(user) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-create-user-use-case.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from '@/repositories/pg/user.repository' 2 | import { CreateUserUseCase } from '../create-user' 3 | 4 | export function makeCreateUserUseCase() { 5 | const userRepository = new UserRepository() 6 | const createUserUseCase = new CreateUserUseCase(userRepository) 7 | 8 | return createUserUseCase 9 | } 10 | -------------------------------------------------------------------------------- /src/repositories/user.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '@/entities/models/user.interface' 2 | import { Person } from '@/entities/person.entity' 3 | 4 | export interface IUserRepository { 5 | findWithPerson(userId: number): Promise<(IUser & Person) | undefined> 6 | findByUsername(username: string): Promise 7 | create(user: IUser): Promise 8 | } 9 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-create-person-use-case.ts: -------------------------------------------------------------------------------- 1 | import { PersonRepository } from '@/repositories/pg/person.repository' 2 | import { CreatePersonUseCase } from '../create-person' 3 | 4 | export function makeCreatePersonUseCase() { 5 | const personRepository = new PersonRepository() 6 | const createPersonUseCase = new CreatePersonUseCase(personRepository) 7 | return createPersonUseCase 8 | } 9 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-find-with-person.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from '@/repositories/pg/user.repository' 2 | import { FindWithPersonUseCase } from '../find-with-person' 3 | 4 | export function makeFindWithPersonUseCase() { 5 | const userRepository = new UserRepository() 6 | const findWithPersonUseCase = new FindWithPersonUseCase(userRepository) 7 | return findWithPersonUseCase 8 | } 9 | -------------------------------------------------------------------------------- /src/use-cases/update-product.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/entities/models/product.interface' 2 | import { IProductRepository } from '@/repositories/product.repository.interface' 3 | 4 | export class UpdateProductUseCase { 5 | constructor(private productRepository: IProductRepository) {} 6 | 7 | async handler(product: IProduct) { 8 | return this.productRepository.update(product) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-find-product-use-case.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from '@/repositories/typeorm/product.repository' 2 | import { FindProductUseCase } from '../find-prodcut' 3 | 4 | export function makeFindProductUseCase() { 5 | const productRepository = new ProductRepository() 6 | 7 | const findProductUseCase = new FindProductUseCase(productRepository) 8 | 9 | return findProductUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/repositories/product.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/entities/models/product.interface' 2 | 3 | export interface IProductRepository { 4 | findAll(page: number, limit: number): Promise 5 | findById(id: string): Promise 6 | create(product: IProduct): Promise 7 | update(product: IProduct): Promise 8 | delete(id: string): Promise 9 | } 10 | -------------------------------------------------------------------------------- /src/use-cases/create-product.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/entities/models/product.interface' 2 | import { IProductRepository } from '@/repositories/product.repository.interface' 3 | 4 | export class CrerateProductUseCase { 5 | constructor(private productRepository: IProductRepository) {} 6 | 7 | async handler(product: IProduct): Promise { 8 | return this.productRepository.create(product) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-create-address-use-case.ts: -------------------------------------------------------------------------------- 1 | import { AddressRepository } from '@/repositories/pg/address.repository' 2 | import { CreateAddressUseCase } from '../create-address' 3 | 4 | export function makeCreateAddressUeeCase() { 5 | const addressRepository = new AddressRepository() 6 | 7 | const createAddressUseCase = new CreateAddressUseCase(addressRepository) 8 | 9 | return createAddressUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-create-product-use-case.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from '@/repositories/typeorm/product.repository' 2 | import { CrerateProductUseCase } from '../create-product' 3 | 4 | export function makeCreateProductUseCase() { 5 | const productRepository = new ProductRepository() 6 | const createProductUseCase = new CrerateProductUseCase(productRepository) 7 | 8 | return createProductUseCase 9 | } 10 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-delete-product-use-case.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from '@/repositories/typeorm/product.repository' 2 | import { DeleteProductUseCase } from '../delete-product' 3 | 4 | export function makeDeleteProductUseCase() { 5 | const productRepository = new ProductRepository() 6 | 7 | const deleteProductUseCase = new DeleteProductUseCase(productRepository) 8 | 9 | return deleteProductUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-update-product-use-case.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from '@/repositories/typeorm/product.repository' 2 | import { UpdateProductUseCase } from '../update-product' 3 | 4 | export function makeUpdateProductUseCase() { 5 | const productRepository = new ProductRepository() 6 | 7 | const updateProductUseCase = new UpdateProductUseCase(productRepository) 8 | 9 | return updateProductUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/repositories/address.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { IAddress } from '@/entities/models/address.interface' 2 | import { IPerson } from '@/entities/models/person.interface' 3 | 4 | export interface IAddressRepository { 5 | findAddressByPersonId( 6 | personId: number, 7 | page: number, 8 | limit: number, 9 | ): Promise<(IAddress & IPerson)[]> 10 | create(address: IAddress): Promise 11 | } 12 | -------------------------------------------------------------------------------- /src/use-cases/create-address.ts: -------------------------------------------------------------------------------- 1 | import { IAddress } from '@/entities/models/address.interface' 2 | import { IAddressRepository } from '@/repositories/address.repository.interface' 3 | 4 | export class CreateAddressUseCase { 5 | constructor(private addressRepository: IAddressRepository) {} 6 | 7 | async handler(address: IAddress): Promise { 8 | return this.addressRepository.create(address) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-create-category-use-case.ts: -------------------------------------------------------------------------------- 1 | import { CategoryRepository } from '@/repositories/typeorm/category.repository' 2 | import { CreateCategoryUseCase } from '../create-category' 3 | 4 | export function makeCreateCategoryUseCase() { 5 | const categoryRepository = new CategoryRepository() 6 | 7 | const createCategoryUseCase = new CreateCategoryUseCase(categoryRepository) 8 | 9 | return createCategoryUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-find-all-product-use-case.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from '@/repositories/typeorm/product.repository' 2 | import { FindAllPRoductUseCase } from '../find-all-products' 3 | 4 | export function makeFindAllProductUseCase() { 5 | const productRepository = new ProductRepository() 6 | 7 | const findAllProductUseCase = new FindAllPRoductUseCase(productRepository) 8 | 9 | return findAllProductUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/entities/person.entity.ts: -------------------------------------------------------------------------------- 1 | import { IPerson } from './models/person.interface' 2 | 3 | export class Person implements IPerson { 4 | id?: number 5 | cpf: string 6 | name: string 7 | birth: Date 8 | email: string 9 | user_id?: number 10 | 11 | constructor(cpf: string, name: string, birth: Date, email: string) { 12 | this.cpf = cpf 13 | this.name = name 14 | this.birth = birth 15 | this.email = email 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/use-cases/factory/make-find-address-by-person-use-case.ts: -------------------------------------------------------------------------------- 1 | import { AddressRepository } from '@/repositories/pg/address.repository' 2 | import { FindAddressByPersonUseCase } from '../find-address-by-person' 3 | 4 | export function makeFindAddressByPersonUseCase() { 5 | const addressRepository = new AddressRepository() 6 | const findAddressByPersonUseCase = new FindAddressByPersonUseCase( 7 | addressRepository, 8 | ) 9 | return findAddressByPersonUseCase 10 | } 11 | -------------------------------------------------------------------------------- /src/entities/address.entity.ts: -------------------------------------------------------------------------------- 1 | import { IAddress } from './models/address.interface' 2 | 3 | export class Address implements IAddress { 4 | id?: number 5 | street: string 6 | city: string 7 | state: string 8 | zip_code: string 9 | person_id?: number 10 | 11 | constructor(street: string, city: string, state: string, zip_code: string) { 12 | this.street = street 13 | this.city = city 14 | this.state = state 15 | this.zip_code = zip_code 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/use-cases/find-prodcut.ts: -------------------------------------------------------------------------------- 1 | import { IProductRepository } from '@/repositories/product.repository.interface' 2 | import { ResourceNotFoundError } from './errors/resource-not-found-error' 3 | 4 | export class FindProductUseCase { 5 | constructor(private productRepository: IProductRepository) {} 6 | 7 | async handler(id: string) { 8 | const product = await this.productRepository.findById(id) 9 | 10 | if (!product) throw new ResourceNotFoundError() 11 | 12 | return product 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/use-cases/signin.ts: -------------------------------------------------------------------------------- 1 | import { IUserRepository } from '@/repositories/user.repository.interface' 2 | import { InvalidCredentialsError } from './errors/invalid-credentials-error' 3 | 4 | export class SigninUseCase { 5 | constructor(private readonly userRepository: IUserRepository) {} 6 | 7 | async handler(username: string) { 8 | const user = await this.userRepository.findByUsername(username) 9 | 10 | if (!user) { 11 | throw new InvalidCredentialsError() 12 | } 13 | 14 | return user 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/http/middlewares/jwt-validate.ts: -------------------------------------------------------------------------------- 1 | import { FastifyReply, FastifyRequest } from 'fastify' 2 | 3 | export async function validateJwt( 4 | request: FastifyRequest, 5 | reply: FastifyReply, 6 | ) { 7 | try { 8 | const routeFreeList = ['POST-/user', 'POST-/user/signin'] 9 | const validateRoute = `${request.method}-${request.routerPath}` 10 | 11 | if (routeFreeList.includes(validateRoute)) return 12 | 13 | await request.jwtVerify() 14 | } catch (error) { 15 | reply.status(401).send({ message: 'Unauthorized' }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/http/controllers/product/routes.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify' 2 | import { create } from './create' 3 | import { findAllProducts } from './find-all-products' 4 | import { findProduct } from './find-product' 5 | import { update } from './update' 6 | import { deleteProduct } from './delete' 7 | 8 | export async function productRoutes(app: FastifyInstance) { 9 | app.get('/product', findAllProducts) 10 | app.get('/product/:id', findProduct) 11 | app.post('/product', create) 12 | app.put('/product/:id', update) 13 | app.delete('/product/:id', deleteProduct) 14 | } 15 | -------------------------------------------------------------------------------- /src/use-cases/find-address-by-person.ts: -------------------------------------------------------------------------------- 1 | import { IAddress } from '@/entities/models/address.interface' 2 | import { IPerson } from '@/entities/models/person.interface' 3 | import { IAddressRepository } from '@/repositories/address.repository.interface' 4 | 5 | export class FindAddressByPersonUseCase { 6 | constructor(private addressRepository: IAddressRepository) {} 7 | 8 | async handler( 9 | personId: number, 10 | page: number, 11 | limit: number, 12 | ): Promise<(IAddress & IPerson)[]> { 13 | return this.addressRepository.findAddressByPersonId(personId, page, limit) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/repositories/typeorm/category.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from 'typeorm' 2 | import { ICategoryRepository } from '../category.repository.interface' 3 | import { Category } from '@/entities/category.entity' 4 | import { appDataSource } from '@/lib/typeorm/typeorm' 5 | 6 | export class CategoryRepository implements ICategoryRepository { 7 | private repository: Repository 8 | 9 | constructor() { 10 | this.repository = appDataSource.getRepository(Category) 11 | } 12 | 13 | async create(name: string): Promise { 14 | await this.repository.save({ name }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/http/controllers/category/create.ts: -------------------------------------------------------------------------------- 1 | import { makeCreateCategoryUseCase } from '@/use-cases/factory/make-create-category-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function create(request: FastifyRequest, reply: FastifyReply) { 6 | const registerBodySchema = z.object({ 7 | name: z.string(), 8 | }) 9 | 10 | const { name } = registerBodySchema.parse(request.body) 11 | 12 | const createCategoryUseCase = makeCreateCategoryUseCase() 13 | 14 | await createCategoryUseCase.handler(name) 15 | 16 | return reply.status(201).send() 17 | } 18 | -------------------------------------------------------------------------------- /src/use-cases/find-with-person.ts: -------------------------------------------------------------------------------- 1 | import { Person } from '@/entities/person.entity' 2 | import { User } from '@/entities/user.entity' 3 | import { ResourceNotFoundError } from './errors/resource-not-found-error' 4 | import { IUserRepository } from '@/repositories/user.repository.interface' 5 | 6 | export class FindWithPersonUseCase { 7 | constructor(private userRepository: IUserRepository) {} 8 | 9 | async handler(userId: number): Promise<(User & Person) | undefined> { 10 | const user = await this.userRepository.findWithPerson(userId) 11 | if (!user) throw new ResourceNotFoundError() 12 | return user 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/entities/category.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm' 2 | import { ICategory } from './models/category.interface' 3 | 4 | @Entity({ 5 | name: 'category', 6 | }) 7 | export class Category implements ICategory { 8 | @PrimaryGeneratedColumn('increment', { 9 | name: 'id', 10 | }) 11 | id?: number | undefined 12 | 13 | @Column({ 14 | name: 'name', 15 | type: 'varchar', 16 | }) 17 | name: string 18 | 19 | @Column({ 20 | name: 'creation_date', 21 | type: 'timestamp without time zone', 22 | default: () => 'CURRENT_TIMESTAMP', 23 | }) 24 | createdAt: Date 25 | } 26 | -------------------------------------------------------------------------------- /src/http/controllers/user/find-user.ts: -------------------------------------------------------------------------------- 1 | import { makeFindWithPersonUseCase } from '@/use-cases/factory/make-find-with-person' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function findUser(request: FastifyRequest, reply: FastifyReply) { 6 | const registerParamsSchema = z.object({ 7 | id: z.coerce.number(), 8 | }) 9 | 10 | const { id } = registerParamsSchema.parse(request.params) 11 | 12 | const findWithPersonUseCase = makeFindWithPersonUseCase() 13 | 14 | const user = await findWithPersonUseCase.handler(id) 15 | 16 | return reply.status(200).send(user) 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/client-http.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | interface IStock { 4 | name: string 5 | quantity: number 6 | relationId: string 7 | } 8 | 9 | export async function createProductInStock(product: IStock, token: string) { 10 | const response = await fetch('http://localhost:3010/stock', { 11 | method: 'POST', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | Authorization: token, 15 | }, 16 | body: JSON.stringify(product), 17 | }) 18 | 19 | if (!response.ok) { 20 | throw new Error(`Failed to create product in stock ${response.status}`) 21 | } 22 | 23 | return response 24 | } 25 | -------------------------------------------------------------------------------- /src/http/controllers/product/delete.ts: -------------------------------------------------------------------------------- 1 | import { makeDeleteProductUseCase } from '@/use-cases/factory/make-delete-product-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function deleteProduct( 6 | request: FastifyRequest, 7 | reply: FastifyReply, 8 | ) { 9 | const registerParamsSchema = z.object({ 10 | id: z.coerce.string(), 11 | }) 12 | 13 | const { id } = registerParamsSchema.parse(request.params) 14 | 15 | const deleteProductUseCase = makeDeleteProductUseCase() 16 | 17 | await deleteProductUseCase.handler(id) 18 | 19 | return reply.status(204).send() 20 | } 21 | -------------------------------------------------------------------------------- /src/http/controllers/product/find-product.ts: -------------------------------------------------------------------------------- 1 | import { makeFindProductUseCase } from '@/use-cases/factory/make-find-product-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function findProduct( 6 | request: FastifyRequest, 7 | reply: FastifyReply, 8 | ) { 9 | const registerParamsSchema = z.object({ 10 | id: z.coerce.string(), 11 | }) 12 | 13 | const { id } = registerParamsSchema.parse(request.params) 14 | 15 | const findProductUseCase = makeFindProductUseCase() 16 | 17 | const product = await findProductUseCase.handler(id) 18 | 19 | return reply.status(200).send(product) 20 | } 21 | -------------------------------------------------------------------------------- /src/repositories/pg/person.repository.ts: -------------------------------------------------------------------------------- 1 | import { database } from '@/lib/pg/db' 2 | import { IPersonRepository } from '../person.repository.interface' 3 | import { IPerson } from '@/entities/models/person.interface' 4 | 5 | export class PersonRepository implements IPersonRepository { 6 | async create({ 7 | cpf, 8 | name, 9 | birth, 10 | email, 11 | user_id, 12 | }: IPerson): Promise { 13 | const result = await database.clientInstance?.query( 14 | 'INSERT INTO person (cpf, name, birth, email, user_id) VALUES ($1, $2, $3, $4, $5) RETURNING *', 15 | [cpf, name, birth, email, user_id], 16 | ) 17 | 18 | return result?.rows[0] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/env/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | import { z } from 'zod' 4 | 5 | const envSchema = z.object({ 6 | NODE_ENV: z 7 | .enum(['development', 'production', 'test']) 8 | .default('development'), 9 | PORT: z.coerce.number().default(3000), 10 | DATABASE_USER: z.string(), 11 | DATABASE_HOST: z.string(), 12 | DATABASE_NAME: z.string(), 13 | DATABASE_PASSWORD: z.string(), 14 | DATABASE_PORT: z.coerce.number(), 15 | JWT_SECRET: z.string(), 16 | }) 17 | 18 | const _env = envSchema.safeParse(process.env) 19 | 20 | if (!_env.success) { 21 | console.error('Invalid environment variables', _env.error.format()) 22 | 23 | throw new Error('Invalid environment variables') 24 | } 25 | 26 | export const env = _env.data 27 | -------------------------------------------------------------------------------- /src/lib/typeorm/migrations/1714090143497-ProductAutoGenerateUUID.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class ProductAutoGenerateUUID1714090143497 4 | implements MigrationInterface 5 | { 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(` 8 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 9 | `) 10 | 11 | await queryRunner.query( 12 | `ALTER TABLE product 13 | ALTER COLUMN id SET DEFAULT uuid_generate_v4(); 14 | `, 15 | ) 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | await queryRunner.query( 20 | `ALTER TABLE product 21 | ALTER COLUMN id DROP DEFAULT; 22 | `, 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/http/controllers/product/find-all-products.ts: -------------------------------------------------------------------------------- 1 | import { makeFindAllProductUseCase } from '@/use-cases/factory/make-find-all-product-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function findAllProducts( 6 | request: FastifyRequest, 7 | reply: FastifyReply, 8 | ) { 9 | const registerQuerySchema = z.object({ 10 | page: z.coerce.number().default(1), 11 | limit: z.coerce.number().default(10), 12 | }) 13 | 14 | const { page, limit } = registerQuerySchema.parse(request.query) 15 | 16 | const findAllProductsUseCase = makeFindAllProductUseCase() 17 | 18 | const products = await findAllProductsUseCase.handler(page, limit) 19 | 20 | return reply.status(200).send(products) 21 | } 22 | -------------------------------------------------------------------------------- /src/http/controllers/address/create.ts: -------------------------------------------------------------------------------- 1 | import { makeCreateAddressUeeCase } from '@/use-cases/factory/make-create-address-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function create(request: FastifyRequest, reply: FastifyReply) { 6 | const registerBodySchema = z.object({ 7 | street: z.string(), 8 | city: z.string(), 9 | state: z.string(), 10 | zip_code: z.string(), 11 | person_id: z.number(), 12 | }) 13 | 14 | const { street, city, state, zip_code, person_id } = registerBodySchema.parse( 15 | request.body, 16 | ) 17 | 18 | const createAddressUseCase = makeCreateAddressUeeCase() 19 | const address = await createAddressUseCase.handler({ 20 | street, 21 | city, 22 | state, 23 | zip_code, 24 | person_id, 25 | }) 26 | 27 | reply.code(201).send(address) 28 | } 29 | -------------------------------------------------------------------------------- /src/http/controllers/person/create.ts: -------------------------------------------------------------------------------- 1 | import { makeCreatePersonUseCase } from '@/use-cases/factory/make-create-person-use-case' 2 | import { FastifyRequest, FastifyReply } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function create(request: FastifyRequest, reply: FastifyReply) { 6 | const registerBodySchema = z.object({ 7 | cpf: z.string(), 8 | name: z.string(), 9 | birth: z.coerce.date(), 10 | email: z.string().email(), 11 | user_id: z.coerce.number(), 12 | }) 13 | 14 | const { cpf, name, birth, email, user_id } = registerBodySchema.parse( 15 | request.body, 16 | ) 17 | 18 | const createPersonUseCase = makeCreatePersonUseCase() 19 | 20 | const person = await createPersonUseCase.handler({ 21 | cpf, 22 | name, 23 | birth, 24 | email, 25 | user_id, 26 | }) 27 | 28 | return reply.status(201).send(person) 29 | } 30 | -------------------------------------------------------------------------------- /src/http/controllers/user/create.ts: -------------------------------------------------------------------------------- 1 | import { makeCreateUserUseCase } from '@/use-cases/factory/make-create-user-use-case' 2 | import { hash } from 'bcryptjs' 3 | import { FastifyReply, FastifyRequest } from 'fastify' 4 | import { z } from 'zod' 5 | 6 | export async function create(request: FastifyRequest, reply: FastifyReply) { 7 | const registerBodySchema = z.object({ 8 | username: z.string(), 9 | password: z.string(), 10 | }) 11 | 12 | const { username, password } = registerBodySchema.parse(request.body) 13 | 14 | const hashedPassword = await hash(password, 8) 15 | 16 | const userWithHashedPassword = { username, password: hashedPassword } 17 | 18 | const createUserUseCase = makeCreateUserUseCase() 19 | 20 | const user = await createUserUseCase.handler(userWithHashedPassword) 21 | 22 | return reply.status(201).send({ id: user?.id, username: user?.username }) 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/pg/db.ts: -------------------------------------------------------------------------------- 1 | import { Pool, PoolClient } from 'pg' 2 | import { env } from '@/env' 3 | 4 | const CONFIG = { 5 | user: env.DATABASE_USER, 6 | host: env.DATABASE_HOST, 7 | database: env.DATABASE_NAME, 8 | password: env.DATABASE_PASSWORD, 9 | port: env.DATABASE_PORT, 10 | } 11 | 12 | class Database { 13 | private pool: Pool 14 | private client: PoolClient | undefined 15 | 16 | constructor() { 17 | this.pool = new Pool(CONFIG) 18 | this.connection() 19 | } 20 | 21 | private async connection() { 22 | try { 23 | this.client = await this.pool.connect() 24 | } catch (error) { 25 | console.error(`Error connecting to database: ${error}`) 26 | 27 | throw new Error(`Error connecting to database:${error}`) 28 | } 29 | } 30 | 31 | get clientInstance() { 32 | return this.client 33 | } 34 | } 35 | 36 | export const database = new Database() 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "standard", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | 17 | "plugins": ["@typescript-eslint"], 18 | "rules": { 19 | "prettier/prettier": [ 20 | "error", 21 | { 22 | "printWidth": 80, 23 | "tabWidth": 2, 24 | "singleQuote": true, 25 | "trailingComma": "all", 26 | "arrowParens": "always", 27 | "semi": false 28 | } 29 | ], 30 | "camelcase":"off", 31 | "no-useless-constructor":"off" 32 | }, 33 | "settings": { 34 | "import/parsers": { 35 | "@typescript-eslint/parser": [".ts", ".d.ts"] 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/lib/typeorm/typeorm.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm' 2 | 3 | import { env } from '@/env' 4 | 5 | import { Product } from '@/entities/product.entity' 6 | import { Category } from '@/entities/category.entity' 7 | import { ProductAutoGenerateUUID1714090143497 } from './migrations/1714090143497-ProductAutoGenerateUUID' 8 | 9 | export const appDataSource = new DataSource({ 10 | type: 'postgres', 11 | host: env.DATABASE_HOST, 12 | port: env.DATABASE_PORT, 13 | username: env.DATABASE_USER, 14 | password: env.DATABASE_PASSWORD, 15 | database: env.DATABASE_NAME, 16 | entities: [Category, Product], 17 | migrations: [ProductAutoGenerateUUID1714090143497], 18 | logging: env.NODE_ENV === 'development', 19 | }) 20 | 21 | appDataSource 22 | .initialize() 23 | .then(() => { 24 | console.log('Database with typeorm connected') 25 | }) 26 | .catch((error) => { 27 | console.error('Error connecting to database with typeorm', error) 28 | }) 29 | -------------------------------------------------------------------------------- /src/http/controllers/address/find-address.ts: -------------------------------------------------------------------------------- 1 | import { makeFindAddressByPersonUseCase } from '@/use-cases/factory/make-find-address-by-person-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function findAddress( 6 | request: FastifyRequest, 7 | reply: FastifyReply, 8 | ) { 9 | const registerParamsSchema = z.object({ 10 | personId: z.coerce.number(), 11 | }) 12 | 13 | const registerQuerySchema = z.object({ 14 | page: z.coerce.number(), 15 | limit: z.coerce.number(), 16 | }) 17 | 18 | const { personId } = registerParamsSchema.parse(request.params) 19 | const { page, limit } = registerQuerySchema.parse(request.query) 20 | 21 | const findAddressByPersonUseCase = makeFindAddressByPersonUseCase() 22 | 23 | const address = await findAddressByPersonUseCase.handler( 24 | personId, 25 | page, 26 | limit, 27 | ) 28 | 29 | return reply.status(200).send(address) 30 | } 31 | -------------------------------------------------------------------------------- /src/http/controllers/user/signin.ts: -------------------------------------------------------------------------------- 1 | import { InvalidCredentialsError } from '@/use-cases/errors/invalid-credentials-error' 2 | import { makeSigninUseCase } from '@/use-cases/factory/make-signin-use-case' 3 | import { compare } from 'bcryptjs' 4 | import { FastifyReply, FastifyRequest } from 'fastify' 5 | import { z } from 'zod' 6 | 7 | export async function signin(request: FastifyRequest, reply: FastifyReply) { 8 | const registerBodySchema = z.object({ 9 | username: z.string(), 10 | password: z.string(), 11 | }) 12 | 13 | const { username, password } = registerBodySchema.parse(request.body) 14 | 15 | const signinUseCase = makeSigninUseCase() 16 | 17 | const user = await signinUseCase.handler(username) 18 | 19 | const doestPasswordMatch = await compare(password, user.password) 20 | 21 | if (!doestPasswordMatch) { 22 | throw new InvalidCredentialsError() 23 | } 24 | 25 | const token = await reply.jwtSign({ username }) 26 | 27 | return reply.status(200).send({ token }) 28 | } 29 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import '@/lib/typeorm/typeorm' 3 | import fastify from 'fastify' 4 | import { personRoutes } from '@/http/controllers/person/routes' 5 | import { userRoutes } from './http/controllers/user/routes' 6 | import { globalErrorHandler } from './utils/global-error-handler' 7 | import { addressRoutes } from './http/controllers/address/routes' 8 | import { productRoutes } from './http/controllers/product/routes' 9 | import { categoryRoutes } from './http/controllers/category/routes' 10 | import fastifyJwt from '@fastify/jwt' 11 | import { env } from './env' 12 | import { validateJwt } from './http/middlewares/jwt-validate' 13 | 14 | export const app = fastify() 15 | 16 | app.register(fastifyJwt, { 17 | secret: env.JWT_SECRET, 18 | sign: { expiresIn: '10m' }, 19 | }) 20 | 21 | app.addHook('onRequest', validateJwt) 22 | 23 | app.register(personRoutes) 24 | app.register(userRoutes) 25 | app.register(addressRoutes) 26 | app.register(productRoutes) 27 | app.register(categoryRoutes) 28 | 29 | app.setErrorHandler(globalErrorHandler) 30 | -------------------------------------------------------------------------------- /src/http/controllers/product/update.ts: -------------------------------------------------------------------------------- 1 | import { makeUpdateProductUseCase } from '@/use-cases/factory/make-update-product-use-case' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { z } from 'zod' 4 | 5 | export async function update(request: FastifyRequest, reply: FastifyReply) { 6 | const registerParamsSchema = z.object({ 7 | id: z.coerce.string(), 8 | }) 9 | 10 | const { id } = registerParamsSchema.parse(request.params) 11 | 12 | const registerBodySchema = z.object({ 13 | name: z.string(), 14 | description: z.string(), 15 | image: z.string(), 16 | price: z.coerce.number(), 17 | categories: z 18 | .array( 19 | z.object({ 20 | id: z.coerce.number(), 21 | name: z.string(), 22 | }), 23 | ) 24 | .optional(), 25 | }) 26 | 27 | const { name, description, image, price, categories } = 28 | registerBodySchema.parse(request.body) 29 | 30 | const updateProductUseCase = makeUpdateProductUseCase() 31 | 32 | const product = await updateProductUseCase.handler({ 33 | id, 34 | name, 35 | description, 36 | image, 37 | price, 38 | categories: categories || [], 39 | }) 40 | 41 | return reply.status(200).send(product) 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pettech", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start:dev": "tsx watch src/server.ts", 8 | "start": "node build/server.js", 9 | "build": "tsup src --out-dir build" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/bcryptjs": "2.4.6", 16 | "@types/node": "20.12.7", 17 | "@types/pg": "8.11.5", 18 | "@typescript-eslint/eslint-plugin": "6.21.0", 19 | "@typescript-eslint/parser": "6.21.0", 20 | "eslint": "8.57.0", 21 | "eslint-config-prettier": "9.1.0", 22 | "eslint-config-standard": "17.1.0", 23 | "eslint-plugin-import": "2.29.1", 24 | "eslint-plugin-n": "16.6.2", 25 | "eslint-plugin-prettier": "5.1.3", 26 | "eslint-plugin-promise": "6.1.1", 27 | "prettier": "3.2.5", 28 | "tsup": "8.0.2", 29 | "tsx": "4.7.2", 30 | "typescript": "5.4.5" 31 | }, 32 | "dependencies": { 33 | "@fastify/jwt": "8.0.0", 34 | "bcryptjs": "2.4.3", 35 | "dotenv": "16.4.5", 36 | "fastify": "4.26.2", 37 | "node-fetch": "3.3.2", 38 | "pg": "8.11.5", 39 | "reflect-metadata": "0.2.2", 40 | "typeorm": "0.3.20", 41 | "zod": "3.23.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/global-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@/env' 2 | import { FastifyReply, FastifyRequest } from 'fastify' 3 | import { ZodError } from 'zod' 4 | 5 | interface ErrorHandlerMap { 6 | [key: string]: ( 7 | error: Error | ZodError, 8 | request: FastifyRequest, 9 | reply: FastifyReply, 10 | ) => void 11 | } 12 | 13 | export const errorHandlerMap: ErrorHandlerMap = { 14 | ZodError: (error, _, reply) => { 15 | return reply.status(400).send({ 16 | message: 'Validation error', 17 | ...(error instanceof ZodError && { error: error.format() }), 18 | }) 19 | }, 20 | ResourceNotFoundError: (error, __, reply) => { 21 | return reply.status(404).send({ message: error.message }) 22 | }, 23 | InvalidCredentialsError: (error, __, reply) => { 24 | return reply.status(404).send({ message: error.message }) 25 | }, 26 | } 27 | 28 | export const globalErrorHandler = ( 29 | error: Error, 30 | _: FastifyRequest, 31 | reply: FastifyReply, 32 | ) => { 33 | if (env.NODE_ENV === 'development') { 34 | console.error(error) 35 | } 36 | 37 | const handler = errorHandlerMap[error.constructor.name] 38 | 39 | if (handler) return handler(error, _, reply) 40 | 41 | return reply.status(500).send({ message: 'Internal server error' }) 42 | } 43 | -------------------------------------------------------------------------------- /src/repositories/typeorm/product.repository.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/entities/models/product.interface' 2 | import { IProductRepository } from '../product.repository.interface' 3 | import { Repository } from 'typeorm' 4 | import { Product } from '@/entities/product.entity' 5 | import { appDataSource } from '@/lib/typeorm/typeorm' 6 | 7 | export class ProductRepository implements IProductRepository { 8 | private repository: Repository 9 | 10 | constructor() { 11 | this.repository = appDataSource.getRepository(Product) 12 | } 13 | 14 | async findAll(page: number, limit: number): Promise { 15 | return this.repository.find({ 16 | relations: ['categories'], 17 | skip: (page - 1) * limit, 18 | take: limit, 19 | }) 20 | } 21 | 22 | async findById(id: string): Promise { 23 | return this.repository.findOne({ 24 | relations: ['categories'], 25 | where: { id }, 26 | }) 27 | } 28 | 29 | async create(product: IProduct): Promise { 30 | return this.repository.save(product) 31 | } 32 | 33 | async update(product: IProduct): Promise { 34 | return this.repository.save(product) 35 | } 36 | 37 | async delete(id: string): Promise { 38 | await this.repository.delete(id) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | JoinTable, 5 | ManyToMany, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm' 8 | import { IProduct } from './models/product.interface' 9 | import { ICategory } from './models/category.interface' 10 | import { Category } from './category.entity' 11 | 12 | @Entity({ 13 | name: 'product', 14 | }) 15 | export class Product implements IProduct { 16 | @PrimaryGeneratedColumn('uuid', { 17 | name: 'id', 18 | }) 19 | id?: string | undefined 20 | 21 | @Column({ 22 | name: 'name', 23 | type: 'varchar', 24 | }) 25 | name: string 26 | 27 | @Column({ 28 | name: 'description', 29 | type: 'text', 30 | }) 31 | description: string 32 | 33 | @Column({ 34 | name: 'image_url', 35 | type: 'varchar', 36 | }) 37 | image: string 38 | 39 | @Column({ 40 | name: 'price', 41 | type: 'double precision', 42 | }) 43 | price: number 44 | 45 | @ManyToMany(() => Category, { 46 | cascade: true, 47 | }) 48 | @JoinTable({ 49 | name: 'product_category', 50 | joinColumn: { 51 | name: 'product_id', 52 | referencedColumnName: 'id', 53 | }, 54 | inverseJoinColumn: { 55 | name: 'category_id', 56 | referencedColumnName: 'id', 57 | }, 58 | }) 59 | categories?: ICategory[] | undefined 60 | } 61 | -------------------------------------------------------------------------------- /src/repositories/pg/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { database } from '@/lib/pg/db' 2 | import { IUserRepository } from '../user.repository.interface' 3 | import { IUser } from '@/entities/models/user.interface' 4 | import { IPerson } from '@/entities/models/person.interface' 5 | 6 | export class UserRepository implements IUserRepository { 7 | async findByUsername(username: string): Promise { 8 | const result = await database.clientInstance?.query( 9 | `SELECT * FROM "user" WHERE "user".username = $1`, 10 | [username], 11 | ) 12 | 13 | return result?.rows[0] 14 | } 15 | 16 | public async create({ 17 | username, 18 | password, 19 | }: IUser): Promise { 20 | const result = await database.clientInstance?.query( 21 | `INSERT INTO "user" (username, password) VALUES ($1, $2) RETURNING *`, 22 | [username, password], 23 | ) 24 | 25 | return result?.rows[0] 26 | } 27 | 28 | public async findWithPerson( 29 | userId: number, 30 | ): Promise<(IUser & IPerson) | undefined> { 31 | const result = await database.clientInstance?.query( 32 | `SELECT * FROM "user" 33 | LEFT JOIN person ON "user".id = person.user_id 34 | WHERE "user".id = $1`, 35 | [userId], 36 | ) 37 | return result?.rows[0] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/http/controllers/product/create.ts: -------------------------------------------------------------------------------- 1 | import { makeCreateProductUseCase } from '@/use-cases/factory/make-create-product-use-case' 2 | import { createProductInStock } from '@/utils/client-http' 3 | import { FastifyReply, FastifyRequest } from 'fastify' 4 | import { z } from 'zod' 5 | 6 | export async function create(request: FastifyRequest, reply: FastifyReply) { 7 | const registerBodySchema = z.object({ 8 | name: z.string(), 9 | description: z.string(), 10 | image_url: z.string(), 11 | price: z.coerce.number(), 12 | categories: z 13 | .array( 14 | z.object({ 15 | id: z.coerce.number().optional(), 16 | name: z.string(), 17 | }), 18 | ) 19 | .optional(), 20 | }) 21 | 22 | const { name, description, image_url, price, categories } = 23 | registerBodySchema.parse(request.body) 24 | 25 | const createProductUseCase = makeCreateProductUseCase() 26 | 27 | const product = await createProductUseCase.handler({ 28 | name, 29 | description, 30 | image: image_url, 31 | price, 32 | categories, 33 | }) 34 | 35 | await createProductInStock( 36 | { 37 | name: product.name, 38 | quantity: 0, 39 | relationId: String(product.id), 40 | }, 41 | request.headers.authorization as string, 42 | ) 43 | 44 | return reply.status(201).send(product) 45 | } 46 | -------------------------------------------------------------------------------- /src/repositories/pg/address.repository.ts: -------------------------------------------------------------------------------- 1 | import { IAddressRepository } from '../address.repository.interface' 2 | import { database } from '@/lib/pg/db' 3 | import { IAddress } from '@/entities/models/address.interface' 4 | import { IPerson } from '@/entities/models/person.interface' 5 | 6 | export class AddressRepository implements IAddressRepository { 7 | async findAddressByPersonId( 8 | personId: number, 9 | page: number, 10 | limit: number, 11 | ): Promise<(IAddress & IPerson)[]> { 12 | const offset = (page - 1) * limit 13 | 14 | const query = ` 15 | SELECT address.*, person.* 16 | FROM address 17 | JOIN person ON address.person_id = person.id 18 | WHERE person.id = $1 19 | LIMIT $2 OFFSET $3 20 | ` 21 | const result = await database.clientInstance?.query( 22 | query, 23 | [personId, limit, offset], 24 | ) 25 | 26 | return result?.rows || [] 27 | } 28 | 29 | async create({ 30 | street, 31 | city, 32 | state, 33 | zip_code, 34 | person_id, 35 | }: IAddress): Promise { 36 | const result = await database.clientInstance?.query( 37 | ` 38 | INSERT INTO "address" (street, city, state, zip_code, person_id) VALUES 39 | ($1, $2, $3, $4, $5) RETURNING * 40 | `, 41 | [street, city, state, zip_code, person_id], 42 | ) 43 | 44 | return result?.rows[0] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # School Blog 2 | 3 | Este projeto foi desenvolvido com [Nest.JS](https://nestjs.com/) como trabalho final do módulo na [Pós Tech da FIAP](https://postech.fiap.com.br/). 4 | 5 | ### Integrantes do Grupo 6 | Carolina, 7 | Jéssica, 8 | Tiago, 9 | Arthur, 10 | 11 | 12 | 13 | # Preparando o ambiente 14 | 15 | Para rodar este projeto você deve ter a versão do node >18. 16 | 17 | ## Instalando as dependências 18 | 19 | ```yarn install``` 20 | ou 21 | ```npm i``` 22 | 23 | ## Para rodar o projeto 24 | 25 | ```yarn start``` 26 | ou 27 | ```npm start``` 28 | 29 | ## Docker 30 | É necessário ter o ambiente docker rodando localmente. 31 | 32 | ```yarn docker:up``` 33 | ou 34 | ```npm run docker:up``` 35 | 36 | 37 | # Rotas 38 | _TODO_ 39 | Veja a documentação completa das rotas com Swagger 40 | ```yarn swagger``` 41 | 42 | 43 | ## Rotas privadas 44 | Algumas rotas da aplicação são privadas (ex.: criação de posts), para ter acesso a funcionalidade é necessário incluir o token da autenticação nos Headers da requisição. 45 | 46 | 47 | Para gerar o token, faça o login pela rota ```users/login``` 48 | 49 | _(TODO)_ Utilize nosso usuário padrão: 50 | ``` 51 | username: admin 52 | password: admin 53 | ``` 54 | 55 | Ou crie um novo usuário na rota ```/users``` 56 | 57 | 58 | Em seguida adicione o token retornado no header para requisições em rotas privadas. 59 | A autenticação ficará válida por até 15 minutos. 60 | 61 | 62 | ## Script DB 63 | 64 | ```sql 65 | CREATE TABLE product ( 66 | id UUID PRIMARY KEY, 67 | name VARCHAR(255) NOT NULL, 68 | description TEXT, 69 | image_url VARCHAR(255), 70 | price DOUBLE PRECISION 71 | ); 72 | 73 | CREATE TABLE category ( 74 | id SERIAL PRIMARY KEY, 75 | name VARCHAR(255) NOT NULL, 76 | creation_date TIMESTAMP WITHOUT TIME ZONE 77 | ); 78 | 79 | CREATE TABLE product_category ( 80 | product_id UUID NOT NULL, 81 | category_id SERIAL NOT NULL, 82 | PRIMARY KEY (product_id, category_id), 83 | FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE, 84 | FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE CASCADE 85 | ); 86 | 87 | CREATE TABLE address ( 88 | id SERIAL PRIMARY KEY, 89 | street VARCHAR(255) NOT NULL, 90 | city VARCHAR(255) NOT NULL, 91 | state VARCHAR(2) NOT NULL, 92 | zip_code VARCHAR(10) NOT NULL 93 | ); 94 | 95 | CREATE TABLE person ( 96 | id BIGSERIAL PRIMARY KEY, 97 | cpf VARCHAR(11) not null, 98 | name VARCHAR(100) not null, 99 | birth DATE not null, 100 | email varchar(255) not null 101 | ); 102 | 103 | alter table address 104 | add column person_id bigint not null; 105 | 106 | alter table address 107 | add constraint fk_address_person 108 | foreign key (person_id) 109 | references person(id); 110 | 111 | CREATE TABLE user ( 112 | id SERIAL PRIMARY KEY, 113 | username VARCHAR(255) NOT NULL, 114 | password VARCHAR(255) NOT NULL 115 | ); 116 | 117 | alter table person 118 | add column user_id int unique, 119 | add constraint fk_user_id foreign key (user_id) references user(id); 120 | ``` 121 | - 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | "paths": { 33 | "@/*": ["./src/*"] 34 | }, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 61 | // "removeComments": true, /* Disable emitting comments. */ 62 | // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 65 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 66 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 70 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 71 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 74 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 76 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 77 | 78 | /* Interop Constraints */ 79 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 80 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 81 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 82 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 83 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 84 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 85 | 86 | /* Type Checking */ 87 | "strict": true, /* Enable all strict type-checking options. */ 88 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 89 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 90 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 91 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 92 | "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | } 111 | } 112 | --------------------------------------------------------------------------------