From 87dbde95525b3b2d618b20df45ceb1dc169a9b0a Mon Sep 17 00:00:00 2001 From: Oscar Reyes Date: Wed, 28 Aug 2024 11:45:56 -0600 Subject: [PATCH] feat: Graphql layer (#42) --- api/package-lock.json | 67 ++++++++++++++++++++++ api/package.json | 1 + api/src/api.ts | 4 ++ api/src/graphql/create.resolver.ts | 9 +++ api/src/graphql/get.resolver.ts | 20 +++++++ api/src/graphql/import.resolver.ts | 13 +++++ api/src/graphql/resolvers.ts | 9 +++ api/src/repositories/pokemon.repository.ts | 2 - api/src/schema.ts | 31 ++++++++++ 9 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 api/src/graphql/create.resolver.ts create mode 100644 api/src/graphql/get.resolver.ts create mode 100644 api/src/graphql/import.resolver.ts create mode 100644 api/src/graphql/resolvers.ts create mode 100644 api/src/schema.ts diff --git a/api/package-lock.json b/api/package-lock.json index 8d0c2c3..97fc45a 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -33,6 +33,7 @@ "kafkajs": "^2.2.4", "koa": "^2.14.2", "koa-bodyparser": "^4.4.0", + "koa-graphql": "^0.12.0", "koa-logger": "^3.2.1", "koa-mount": "^4.0.0", "koa-static": "^5.0.0", @@ -5123,6 +5124,47 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/express-graphql": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz", + "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==", + "deprecated": "This package is no longer maintained. We recommend using `graphql-http` instead. Please consult the migration document https://github.com/graphql/graphql-http#migrating-express-grpahql.", + "dependencies": { + "accepts": "^1.3.7", + "content-type": "^1.0.4", + "http-errors": "1.8.0", + "raw-body": "^2.4.1" + }, + "engines": { + "node": ">= 10.x" + }, + "peerDependencies": { + "graphql": "^14.7.0 || ^15.3.0" + } + }, + "node_modules/express-graphql/node_modules/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-graphql/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5512,6 +5554,15 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/graphql": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.9.0.tgz", + "integrity": "sha512-GCOQdvm7XxV1S4U4CGrsdlEN37245eC8P9zaYCMr6K1BG0IPGy5lUwmJsEOGyl1GD6HXjOtl2keCP9asRBwNvA==", + "peer": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -6290,6 +6341,22 @@ "node": ">= 10" } }, + "node_modules/koa-graphql": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/koa-graphql/-/koa-graphql-0.12.0.tgz", + "integrity": "sha512-c1G5qcE7hIBCP+21fiUX7Z0CjbI3+mn4vDuAxQVP9sfpTDW8O+7Mckei/Ar7dY/w6smrHcMxmt8/KXc1d76ONg==", + "dependencies": { + "@types/koa": "^2.13.4", + "express-graphql": "0.12.0", + "http-errors": "^1.7.3" + }, + "engines": { + "node": ">= 10.x" + }, + "peerDependencies": { + "graphql": "^14.7.0 || ^15.3.0" + } + }, "node_modules/koa-logger": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-3.2.1.tgz", diff --git a/api/package.json b/api/package.json index 1b92a90..6be6fef 100644 --- a/api/package.json +++ b/api/package.json @@ -62,6 +62,7 @@ "kafkajs": "^2.2.4", "koa": "^2.14.2", "koa-bodyparser": "^4.4.0", + "koa-graphql": "^0.12.0", "koa-logger": "^3.2.1", "koa-mount": "^4.0.0", "koa-static": "^5.0.0", diff --git a/api/src/api.ts b/api/src/api.ts index 4529130..eaf9e4c 100644 --- a/api/src/api.ts +++ b/api/src/api.ts @@ -17,6 +17,9 @@ import updateHandler from '@pokemon/handlers/update.handler'; import healthcheckHandler from '@pokemon/handlers/healthcheck.handler'; import { setupSequelize } from '@pokemon/utils/db'; import { instrumentRoute } from '@pokemon/middlewares/instrumentation'; +import { graphqlHTTP } from 'koa-graphql'; +import schema from './schema'; +import resolvers from './graphql/resolvers'; const { APP_PORT = 8081 } = process.env; @@ -53,6 +56,7 @@ async function startApp() { ui.use(serve(resolve(__dirname, './ui'))); app.use(mount('/', ui)); + app.use(mount('/graphql', graphqlHTTP({ schema, rootValue: resolvers, graphiql: true }))); console.log(`Starting server on port ${APP_PORT}`); app.listen(APP_PORT); diff --git a/api/src/graphql/create.resolver.ts b/api/src/graphql/create.resolver.ts new file mode 100644 index 0000000..c23ded2 --- /dev/null +++ b/api/src/graphql/create.resolver.ts @@ -0,0 +1,9 @@ +import { getPokemonRepository, Pokemon } from '@pokemon/repositories'; + +const create = async (raw: Pokemon): Promise => { + const repository = getPokemonRepository(); + + return repository.create(new Pokemon(raw)); +}; + +export default create; diff --git a/api/src/graphql/get.resolver.ts b/api/src/graphql/get.resolver.ts new file mode 100644 index 0000000..5ff7658 --- /dev/null +++ b/api/src/graphql/get.resolver.ts @@ -0,0 +1,20 @@ +import { getPokemonRepository, Pokemon } from '@pokemon/repositories'; +import { SearchOptions } from '../repositories/pokemon.repository'; + +type PokemonList = { + items: Pokemon[]; + totalCount: number; +}; + +const get = async (query: SearchOptions): Promise => { + const repository = getPokemonRepository(); + + const [items, totalCount] = await Promise.all([repository.findMany(query), repository.count()]); + + return { + items, + totalCount, + }; +}; + +export default get; \ No newline at end of file diff --git a/api/src/graphql/import.resolver.ts b/api/src/graphql/import.resolver.ts new file mode 100644 index 0000000..c1eea4b --- /dev/null +++ b/api/src/graphql/import.resolver.ts @@ -0,0 +1,13 @@ +import PokeAPIService from '@pokemon/services/pokeApi.service'; +import PokemonSyncronizer from '@pokemon/services/pokemonSyncronizer.service'; + +const pokeApiService = new PokeAPIService(); +const pokemonSyncronizer = PokemonSyncronizer(pokeApiService); + +const importPokemon = async ({ id = 0 }) => { + await pokemonSyncronizer.queue({ id }); + + return { id }; +}; + +export default importPokemon; diff --git a/api/src/graphql/resolvers.ts b/api/src/graphql/resolvers.ts new file mode 100644 index 0000000..84161de --- /dev/null +++ b/api/src/graphql/resolvers.ts @@ -0,0 +1,9 @@ +import createPokemon from './create.resolver'; +import getPokemonList from './get.resolver'; +import importPokemon from './import.resolver'; + +export default { + getPokemonList, + createPokemon, + importPokemon, +}; diff --git a/api/src/repositories/pokemon.repository.ts b/api/src/repositories/pokemon.repository.ts index 91fe8cc..fdad108 100644 --- a/api/src/repositories/pokemon.repository.ts +++ b/api/src/repositories/pokemon.repository.ts @@ -1,5 +1,3 @@ -import { PokemonModel } from './pokemon.sequelize.repository'; - export class Pokemon { public id?: number; public name: string; diff --git a/api/src/schema.ts b/api/src/schema.ts new file mode 100644 index 0000000..c892e6d --- /dev/null +++ b/api/src/schema.ts @@ -0,0 +1,31 @@ +import { buildSchema } from 'graphql'; + +const schema = buildSchema(` + type Pokemon { + id: Int + name: String! + type: String! + isFeatured: Boolean! + imageUrl: String + } + + type PokemonList { + items: [Pokemon] + totalCount: Int + } + + type ImportPokemon { + id: Int! + } + + type Query { + getPokemonList(where: String, skip: Int, take: Int): PokemonList + } + + type Mutation { + createPokemon(name: String!, type: String!, isFeatured: Boolean!, imageUrl: String): Pokemon! + importPokemon(id: Int!): ImportPokemon! + } +`); + +export default schema;