From 60837cafad5b2c7807de9c37da60723462f902d9 Mon Sep 17 00:00:00 2001 From: Javierson Date: Wed, 26 May 2021 22:22:46 -0600 Subject: [PATCH] GraphQL API --- Resources/Controller/index.js | 102 +++++++++++++++++++++++++ Resources/Database/Schema/Category.js | 16 ++++ Resources/Database/Schema/Product.js | 22 ++++++ Resources/Database/index.js | 10 +++ Resources/Schema/Resolvers/index.js | 80 ++++++++++++++++++++ Resources/Schema/Typedefs.graphql | 104 ++++++++++++++++++++++++++ index.js | 52 +++++++++++++ package.json | 45 ++++++----- 8 files changed, 407 insertions(+), 24 deletions(-) create mode 100644 Resources/Controller/index.js create mode 100644 Resources/Database/Schema/Category.js create mode 100644 Resources/Database/Schema/Product.js create mode 100644 Resources/Database/index.js create mode 100644 Resources/Schema/Resolvers/index.js create mode 100644 Resources/Schema/Typedefs.graphql create mode 100644 index.js diff --git a/Resources/Controller/index.js b/Resources/Controller/index.js new file mode 100644 index 00000000..aa6bf8ae --- /dev/null +++ b/Resources/Controller/index.js @@ -0,0 +1,102 @@ + + +require ('json-bigint-patch') +import { ApolloError, UserInputError } from 'apollo-server-express' + + +class Controller { + + + constructor ( Data, Fields, Schema ) { + + this.Data = Data + this.Fields = Fields + this.Model = Schema + + } + + + async Create ( Exception = 'Excepcion en el guardado de datos' ) { + + try { + + return await this.Model(this.Data).save() + + } catch ({ message: m }) { + + throw await new ApolloError ( m || Exception ) + + } + + } + + + async FindOne ( Filter, Excepcion = 'Excepcion en la consulta' ) { + + try { + + return await this.Model.findOne(JSON.parse(JSON.stringify(Filter))) + + } catch ({ message: m }) { + + throw new ApolloError ( m || Excepcion ) + + } + + } + + + async Find ( Limit = 1, OffSet, Excepcion = 'Excepcion en la consulta' ) { + + try { + + return await this.Model.find(this.Data && JSON.parse(JSON.stringify(this.Data)), this.Fields).limit(Limit).skip(OffSet) + + } catch ({ message: m }) { + + throw new ApolloError ( m || Excepcion ) + + } + + } + + + async FindOneAndUpdateBy ( Filter, Exception = 'Excepcion en la actualizacion del dato', UpSert ) { + + try { + + await this.Model.updateOne(JSON.parse(JSON.stringify(Filter)), this.Data, { upsert: UpSert, runValidators: true }) + + console.log(await this.FindOne(Object.assign(Filter, this.Data))) + + return await this.FindOne(Object.assign(Filter, this.Data)) || new UserInputError (Exception) + + } catch ({ message: m }) { + + throw await new ApolloError ( m || Exception ) + + } + + } + + + async FindOneAndRemove ( Filter, Exception = 'Excepcion en la eliminacion del dato' ) { + + try { + + return await this.Model.findOneAndRemove(JSON.parse(JSON.stringify(Filter))) || new UserInputError ('El elemento a eleminar no se encontro') + + } catch ({ message: m }) { + + throw await new ApolloError ( m || Exception ) + + } + + } + + +} + + +export { Controller } + diff --git a/Resources/Database/Schema/Category.js b/Resources/Database/Schema/Category.js new file mode 100644 index 00000000..d4304d3e --- /dev/null +++ b/Resources/Database/Schema/Category.js @@ -0,0 +1,16 @@ + + +import { Schema, model } from 'mongoose' + +require ('dotenv').config() + +const { Category } = process.env, { ObjectId } = Schema.Types + +export default model ( Category, new Schema ({ + + Name: { type: String, unique: true, trim: true, maxlength: 20, required: true }, + + Image: { type: String, trim: true, maxlength: 100, required: true }, + +}, { timestamps: true, versionKey: false }), Category ) + diff --git a/Resources/Database/Schema/Product.js b/Resources/Database/Schema/Product.js new file mode 100644 index 00000000..2d624217 --- /dev/null +++ b/Resources/Database/Schema/Product.js @@ -0,0 +1,22 @@ + + +import { Schema, model } from 'mongoose' + +require ('dotenv').config() + +const { Product, Category } = process.env, { ObjectId } = Schema.Types + +export default model ( Product, new Schema ({ + + Name: { type: String, unique: true, trim: true, maxlength: 20, required: true }, + + Price: { type: Number, min: 1, required: true }, + + Description: { type: String, trim: true, maxlength: 40 }, + + CategoryID: { type: ObjectId, trim: true, ref: Category, required: true }, + + Image: { type: String, trim: true, maxlength: 100, required: true }, + +}, { timestamps: true, versionKey: false }), Product ) + diff --git a/Resources/Database/index.js b/Resources/Database/index.js new file mode 100644 index 00000000..7df12887 --- /dev/null +++ b/Resources/Database/index.js @@ -0,0 +1,10 @@ + + +import Category from './Schema/Category' + + +import Product from './Schema/Product' + + +export { Category, Product } + diff --git a/Resources/Schema/Resolvers/index.js b/Resources/Schema/Resolvers/index.js new file mode 100644 index 00000000..1c262aab --- /dev/null +++ b/Resources/Schema/Resolvers/index.js @@ -0,0 +1,80 @@ + + +require ('dotenv').config() +import { hash, compare } from 'bcryptjs' +import { sign, verify } from 'jsonwebtoken' +import { Controller as C } from '../../Controller' + +import { UserInputError, AuthenticationError } from 'apollo-server-express' + + +import { Category, Product } from '../../Database' + + +import { + ObjectIDResolver as ObjectID, + TimestampResolver as Timestamp, + DateResolver as Date, + BigIntResolver as BigInt, + NonEmptyStringResolver as NonEmptyString, + EmailAddressResolver as EmailAddress, + PositiveIntResolver as PositiveInt, + PositiveFloatResolver as PositiveFloat +} from 'graphql-scalars' + + +const ExecuteCategory = (Data, Fields) => new C ( Data, Fields, Category ), +ExecuteProduct = (Data, Fields) => new C ( Data, Fields, Product ) + + +export default { + + + Query: { + + _: async () => await true, + + // Categoria + + getCategory: async (_, { ID: _id }) => await ExecuteCategory().FindOne({ _id }), + getCategories: async (_, { Query: { Limit, OffSet } = { } }) => await ExecuteCategory().Find(Limit, OffSet), + + // Producto + + getProduct: async (_, { ID: _id }) => await ExecuteProduct().FindOne({ _id }), + getProducts: async (_, { Query: { Limit, OffSet } = { } }) => await ExecuteProduct().Find(Limit, OffSet), + + }, + + + Mutation: { + + _: async () => await true, + + // Producto + + createProduct: async (_, { Producto: { Name, Price, Description, CategoryID, Image } = { } }) => await ExecuteProduct({ Name, Price, Description, CategoryID, Image }).Create(), + + updateProduct: async (_, { ID: _id, Producto: { Name, Price, Description, CategoryID, Image } = { } }) => await ExecuteProduct({ Name, Price, Description, CategoryID, Image }).FindOneAndUpdateBy({ _id: _id.trim() }), + + deleteProduct: async (_, { ID: _id }) => await ExecuteProduct().FindOneAndRemove({ _id: _id.trim() }), + + // Categoria + + createCategory: async (_, { Categoria: { Name, Image } = { } }) => await ExecuteCategory({ Name, Image }).Create(), + + updateCategory: async (_, { ID: _id, Categoria: { Name, Image } = { } }) => await ExecuteCategory({ Name, Image }).FindOneAndUpdateBy({ _id: _id.trim() }), + + deleteCategory: async (_, { ID: _id }) => await ExecuteCategory().FindOneAndRemove({ _id: _id.trim() }) + + }, + + + Producto: { CategoryID: async ({ CategoryID: _id }) => await ExecuteCategory().FindOne({ _id }) }, + + + ObjectID, Date, Timestamp, BigInt, NonEmptyString, EmailAddress, PositiveInt, PositiveFloat + + +} + diff --git a/Resources/Schema/Typedefs.graphql b/Resources/Schema/Typedefs.graphql new file mode 100644 index 00000000..4a445e76 --- /dev/null +++ b/Resources/Schema/Typedefs.graphql @@ -0,0 +1,104 @@ + + +type Query { + + + "Verifica si el servidor reponde" + _: Boolean! + + getProduct (ID: ObjectID!): Producto + getProducts (Query: QueryInput!): [ Producto ] + + getCategory (ID: ObjectID!): Categoria + getCategories (Query: QueryInput!): [ Categoria ] + +} + + +type Mutation { + + + "Verifica si las modificaciones responden" + _: Boolean! + + createProduct (Producto: ProductInput!): Producto + updateProduct (ID: ObjectID! Producto: ProductOptionalInput!): Producto + deleteProduct (ID: ObjectID!): Producto + + createCategory (Categoria: CategoryInput!): Categoria + updateCategory (ID: ObjectID! Categoria: CategoryInput!): Categoria + deleteCategory (ID: ObjectID!): Categoria + + +} + + +scalar ObjectID scalar Timestamp scalar Date scalar BigInt scalar NonEmptyString scalar EmailAddress scalar PositiveInt scalar PositiveFloat + + +input QueryInput { + + Limit: PositiveInt! + OffSet: PositiveInt + +} + + +type Producto { + + id: ObjectID! + Name: NonEmptyString! + Price: PositiveFloat! + Description: NonEmptyString + CategoryID: Categoria + Image: NonEmptyString! + +} + + +input ProductInput { + + Name: NonEmptyString! + Price: PositiveFloat! + Description: NonEmptyString + CategoryID: ObjectID! + Image: NonEmptyString! + +} + + +input ProductOptionalInput { + + Name: NonEmptyString + Price: PositiveFloat + Description: NonEmptyString + CategoryID: ObjectID + Image: NonEmptyString + +} + + +type Categoria { + + id: ObjectID! + Name: NonEmptyString! + Image: NonEmptyString! + +} + + +input CategoryInput { + + Name: NonEmptyString! + Image: NonEmptyString! + +} + + +input CategoryOptionalInput { + + Name: NonEmptyString + Image: NonEmptyString + +} + diff --git a/index.js b/index.js new file mode 100644 index 00000000..bda64edc --- /dev/null +++ b/index.js @@ -0,0 +1,52 @@ + + +import express from 'express' + +import { ApolloServer } from 'apollo-server-express' + +import { loadSchemaSync } from '@graphql-tools/load' + +import { addResolversToSchema } from '@graphql-tools/schema' + +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader' + +import { composeResolvers } from '@graphql-tools/resolvers-composition' + +// import Contrains from './Resources/Schema/Resolvers/Constraints' + +import resolvers from './Resources/Schema/Resolvers' + + +const { error } = require ('dotenv').config(), { connect, set } = require ('mongoose'), +app = express(), { PORT, MONGO_HOST, MONGO_PORT, MONGO_DB_NAME } = process.env, +server = new ApolloServer({ schema: addResolversToSchema({ schema: loadSchemaSync(`${ __dirname }/Resources/Schema/Typedefs.graphql`, { loaders: [ new GraphQLFileLoader() ] }), resolvers: composeResolvers(resolvers /* Contrains */ ) }), context: async ({ req: { headers: { auth: Auth } = { Auth: undefined } } }) => await ({ Auth }), tracing: true }) + + +app.disable('x-powered-by') + + +server.applyMiddleware({ app }) + + +app.listen({ port: PORT }, async () => { + + try { + + if (error) + throw await new Error ('Excepcion importando las variables de entorno') + + set('useCreateIndex', true) + + const MongoAddress = `mongodb://${ MONGO_HOST }:${ MONGO_PORT }/${ MONGO_DB_NAME }` + + await connect(MongoAddress, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }, async e => await console.log(`${ !e ? 'Conectado a mongo' : 'Excepcion en la conexion de mongo' }\n${ MongoAddress }\nServidor iniciado en localhost:${ PORT + server.graphqlPath }`) ) + + + } catch (E) { + + await console.log(E) + + } + +}) + diff --git a/package.json b/package.json index 279a5416..d1cbb8fb 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,30 @@ { - "name": "escuelajs-reto-09", + "name": "proyecto", "version": "1.0.0", - "description": "Reto 9 Octubre 26: Curso de Backend con Node", + "description": "", "main": "index.js", + "module": "main.js", + "scripts": { + "start": "nodemon -e js,graphql -r esm", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Javierson", + "license": "MIT", "dependencies": { - "cors": "^2.8.5", + "apollo-server-express": "^2.9.15", + "bcryptjs": "^2.4.3", "dotenv": "^8.2.0", + "esm": "^3.2.25", "express": "^4.17.1", - "mongodb": "^3.6.6" + "graphql": "^14.5.8", + "graphql-scalars": "^1.5.0", + "graphql-tools": "^7.0.1", + "json-bigint-patch": "0.0.4", + "jsonwebtoken": "^8.5.1", + "mongoose": "^5.8.4", + "mongoose-long": "^0.3.2" }, "devDependencies": { - "jest": "^26.6.3", - "nodemon": "^1.19.4", - "supertest": "^6.1.3" - }, - "scripts": { - "dev": "DEBUG=app:* nodemon src/index.js", - "start": "NODE_ENV=production node src/index.js", - "test:e2e": "jest --forceExit --config ./e2e/jest-e2e.json" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/platzi/escuelajs-reto-09.git" - }, - "keywords": [], - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/platzi/escuelajs-reto-09/issues" - }, - "homepage": "https://github.com/platzi/escuelajs-reto-09#readme" + "nodemon": "^2.0.2" + } }