diff --git a/package-lock.json b/package-lock.json index 020009d..2851a25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fastify/sensible": "^6.0.1", "@fastify/type-provider-json-schema-to-ts": "^4.0.1", "dotenv": "^16.4.5", "fastify": "^5.0.0", + "fastify-plugin": "^5.0.1", "pg": "^8.13.1", "pg-format": "^1.0.4" }, @@ -728,6 +730,21 @@ "fast-deep-equal": "^3.1.3" } }, + "node_modules/@fastify/sensible": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@fastify/sensible/-/sensible-6.0.1.tgz", + "integrity": "sha512-D0rN0kMeZKP23f4w9MoCI9e4+n7vASFAsGoRNn9bondSbplLeIfR2HcjCbyElAM04jGrPRLi/edyThEPOyC9cQ==", + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "dequal": "^2.0.3", + "fastify-plugin": "^5.0.0-pre.fv5.1", + "forwarded": "^0.2.0", + "http-errors": "^2.0.0", + "type-is": "^1.6.18", + "vary": "^1.1.2" + } + }, "node_modules/@fastify/type-provider-json-schema-to-ts": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@fastify/type-provider-json-schema-to-ts/-/type-provider-json-schema-to-ts-4.0.1.tgz", @@ -1212,6 +1229,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2224,6 +2250,24 @@ "node": ">=0.10.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2549,6 +2593,12 @@ "toad-cache": "^3.7.0" } }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2845,6 +2895,22 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2928,7 +2994,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ipaddr.js": { @@ -3931,6 +3996,15 @@ "tmpl": "1.0.5" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3952,6 +4026,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4935,6 +5030,12 @@ "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==", "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5054,6 +5155,15 @@ "node": ">=10" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5257,6 +5367,15 @@ "node": ">=12" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -5389,6 +5508,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", @@ -5470,6 +5602,15 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index dfe7a81..deb3638 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "license": "ISC", "description": "", "dependencies": { + "@fastify/sensible": "^6.0.1", "@fastify/type-provider-json-schema-to-ts": "^4.0.1", "dotenv": "^16.4.5", "fastify": "^5.0.0", + "fastify-plugin": "^5.0.1", "pg": "^8.13.1", "pg-format": "^1.0.4" }, diff --git a/requests.http b/requests.http index 5615710..72c983f 100644 --- a/requests.http +++ b/requests.http @@ -6,6 +6,11 @@ GET {{host}}/api/pets ### +GET {{host}}/api/pets/399 + +### + + POST {{host}}/api/pets Content-Type: application/json @@ -13,7 +18,7 @@ Content-Type: application/json ### -PUT {{host}}/api/owners/1/pets/24 +PUT {{host}}/api/owners/1/pets/3999 ### diff --git a/src/controller/app.ts b/src/controller/app.ts index 49c6f64..343ad23 100644 --- a/src/controller/app.ts +++ b/src/controller/app.ts @@ -1,18 +1,28 @@ import fastify from 'fastify'; -import { PetService } from '../service/pet.service'; +import { PetNotFoundError, PetService, PetTakenError } from '../service/pet.service'; import { PetRepository } from '../repository/pet.repository'; import { DbClient } from '../db'; -import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' -import { getPetByIdSchema, getPetsSchema, postPetsSchema, putPetsToOwnersSchema } from './pet.schemas'; import { OwnerRepository } from '../repository/owner.repository'; import { OwnerService } from '../service/owner.service'; -import { getOwnerByIdSchema, getOwnersSchema, postOwnerSchema } from './owner.schemas'; +import { createPetRoutes } from './routes/pet/pet.routes'; +import { createOwnerRoutes } from './routes/owner/owner.routes'; +import createGreeterPlugin from './plugins/greeter'; +import { httpErrors } from '@fastify/sensible'; +import { log } from 'console'; type Dependencies = { dbClient: DbClient; } -export default function createApp(options = {}, dependencies: Dependencies) { +declare module 'fastify' { + interface FastifyInstance { + petService: PetService, + ownerService: OwnerService + } +} + + +export default async function createApp(options = {}, dependencies: Dependencies) { const { dbClient } = dependencies; const petRepository = new PetRepository(dbClient); @@ -20,75 +30,24 @@ export default function createApp(options = {}, dependencies: Dependencies) { const ownerRepository = new OwnerRepository(dbClient); const ownerService = new OwnerService(ownerRepository); - const app = fastify(options) - .withTypeProvider() - - app.get( - '/api/pets', - { schema: getPetsSchema }, - async () => { - const pets = await petService.getAll(); - return pets; - }) - - app.get( - '/api/pets/:id', - { schema: getPetByIdSchema }, - async (request) => { - const { id } = request.params; - const pets = await petService.getById(id); - return pets; - }) - - - app.post( - '/api/pets', - { schema: postPetsSchema }, - async (request, reply) => { - const { body: petToCreate } = request; - - const created = await petService.create(petToCreate); - reply.status(201); - return created; - }) - - app.put( - '/api/owners/:ownerId/pets/:petId', - { schema: putPetsToOwnersSchema }, - async (request) => { - const { petId, ownerId } = request.params; - const updated = await petService.adopt(petId, ownerId); - return updated; - } - ) - - app.get( - '/api/owners', - { schema: getOwnersSchema }, - async () => { - return await ownerService.getAll(); + const app = fastify(options); + + app.decorate('petService', petService); + app.decorate('ownerService', ownerService) + app.setErrorHandler((error) => { + app.log.error(error); + if(error instanceof PetNotFoundError) { + return httpErrors.notFound('Pet not found.') + } else if (error instanceof PetTakenError) { + return httpErrors.badRequest('Pet has been already taken.') + } else { + return httpErrors.internalServerError('Something went wrong. 😥') } - ) + }) - app.get( - '/api/owners/:id', - { schema: getOwnerByIdSchema }, - async (request) => { - const { id } = request.params; - return await ownerService.getById(id); - } - ) - - app.post( - '/api/owners', - { schema: postOwnerSchema }, - async (request, reply) => { - const ownerProps = request.body; - const created = await ownerService.create(ownerProps); - reply.status(201); - return created; - } - ) + await app.register(createGreeterPlugin, { message: 'Hi' }) + await app.register(createPetRoutes, { prefix: '/api/pets' }); + await app.register(createOwnerRoutes, { prefix: '/api/owners' }); return app; } \ No newline at end of file diff --git a/src/controller/plugins/greeter.ts b/src/controller/plugins/greeter.ts new file mode 100644 index 0000000..bc6552f --- /dev/null +++ b/src/controller/plugins/greeter.ts @@ -0,0 +1,30 @@ +import { FastifyPluginAsync } from 'fastify'; +import fastifyPlugin from 'fastify-plugin'; + +type PluginOptions = { + message: string +} + +declare module 'fastify' { + interface FastifyRequest { + message: string + } +} + +const createGreeterPlugin: FastifyPluginAsync = async ( + app, + options +) => { + app.decorateRequest('message', '') + + app.addHook('onRequest', async (request) => { + const { message } = options; + request.message = message + }) + + app.addHook('onResponse', async (request) => { + console.log(`Message: ${request.message}`) + }) +} + +export default fastifyPlugin(createGreeterPlugin) \ No newline at end of file diff --git a/src/controller/routes/owner/owner.routes.ts b/src/controller/routes/owner/owner.routes.ts new file mode 100644 index 0000000..3eac839 --- /dev/null +++ b/src/controller/routes/owner/owner.routes.ts @@ -0,0 +1,51 @@ +import { FastifyPluginAsync } from 'fastify' +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' +import { putPetsToOwnersSchema } from '../pet/pet.schemas'; +import { getOwnerByIdSchema, getOwnersSchema, postOwnerSchema } from './owner.schemas'; +import { PetNotFoundError, PetTakenError } from '../../../service/pet.service'; +import { httpErrors } from '@fastify/sensible'; + +export const createOwnerRoutes: FastifyPluginAsync = async ( + app, +) => { + const appWithProvider = app.withTypeProvider() + + appWithProvider.put( + '/:ownerId/pets/:petId', + { schema: putPetsToOwnersSchema }, + async (request) => { + const { petId, ownerId } = request.params; + const updated = await app.petService.adopt(petId, ownerId); + return updated; + } + ) + + appWithProvider.get( + '/', + { schema: getOwnersSchema }, + async () => { + return await app.ownerService.getAll(); + } + ) + + appWithProvider.get( + '/:id', + { schema: getOwnerByIdSchema }, + async (request) => { + const { id } = request.params; + return await app.ownerService.getById(id); + } + ) + + appWithProvider.post( + '/', + { schema: postOwnerSchema }, + async (request, reply) => { + const ownerProps = request.body; + const created = await app.ownerService.create(ownerProps); + reply.status(201); + return created; + } + ) + +} \ No newline at end of file diff --git a/src/controller/owner.schemas.ts b/src/controller/routes/owner/owner.schemas.ts similarity index 100% rename from src/controller/owner.schemas.ts rename to src/controller/routes/owner/owner.schemas.ts diff --git a/src/controller/routes/pet/pet.routes.ts b/src/controller/routes/pet/pet.routes.ts new file mode 100644 index 0000000..9533d0d --- /dev/null +++ b/src/controller/routes/pet/pet.routes.ts @@ -0,0 +1,39 @@ +import { FastifyPluginAsync } from 'fastify' +import { getPetByIdSchema, getPetsSchema, postPetsSchema } from './pet.schemas'; +import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' + +export const createPetRoutes: FastifyPluginAsync = async ( + app, +) => { + + const appWithProvider = app.withTypeProvider() + + appWithProvider.get( + '/', + { schema: getPetsSchema }, + async () => { + const pets = await app.petService.getAll(); + return pets; + }) + + appWithProvider.get( + '/:id', + { schema: getPetByIdSchema }, + async (request) => { + const { id } = request.params; + const pets = await app.petService.getById(id); + return pets; + }) + + appWithProvider.post( + '/', + { schema: postPetsSchema }, + async (request, reply) => { + const { body: petToCreate } = request; + + const created = await app.petService.create(petToCreate); + reply.status(201); + return created; + }) + +} \ No newline at end of file diff --git a/src/controller/pet.schemas.ts b/src/controller/routes/pet/pet.schemas.ts similarity index 100% rename from src/controller/pet.schemas.ts rename to src/controller/routes/pet/pet.schemas.ts diff --git a/src/server.ts b/src/server.ts index 2c6a37b..c27b680 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,12 +16,16 @@ const options = { }, }; -const app = createApp(options, { dbClient }); +async function main() { + const app = await createApp(options, { dbClient }) + + app.listen({ port: PORT }, (error, address) => { + if (error) { + app.log.error(error); + process.exit(1); + } + app.log.info(`Server is started successfully.`) + }); +} -app.listen({ port: PORT }, (error, address) => { - if (error) { - app.log.error(error); - process.exit(1); - } - app.log.info(`Server is started successfully.`) -}); \ No newline at end of file +main(); diff --git a/src/service/pet.service.ts b/src/service/pet.service.ts index f9134d0..dbc6c10 100644 --- a/src/service/pet.service.ts +++ b/src/service/pet.service.ts @@ -1,6 +1,10 @@ import { PetToCreate } from "../entity/pet.type"; import { PetRepository } from "../repository/pet.repository" + +export class PetNotFoundError extends Error {}; +export class PetTakenError extends Error {}; + export class PetService { private readonly repository; @@ -15,7 +19,7 @@ export class PetService { async getById(id: number) { const pet = await this.repository.readById(id); if(!pet) { - throw new Error('Pet not found'); + throw new PetNotFoundError(); } return pet; } @@ -27,14 +31,14 @@ export class PetService { async adopt(petId: number, ownerId: number) { const pet = await this.repository.readById(petId); if (!pet) { - throw new Error('Pet does not exists.'); + throw new PetNotFoundError(); } if(pet.ownerId !== null) { - throw new Error('Pet has already have an owner.') + throw new PetTakenError(); } const adopted = await this.repository.update(petId, { ownerId }) if (!adopted) { - throw new Error('Pet could not be adopted, because it is disappeared.'); + throw new PetNotFoundError(); } return adopted; }