
Compartir:
Temiloluwa is a passionate learner focused on building accessible and user-centred solutions that solve global problems. He works with React, Nodejs and Graphql and has basic experience working with Java. He loves exploring new technologies and languages and has a penchant for breaking complex topics into small chunks of easy to understand bits. He is currently open to opportunities that provide the right mix of challenge and growth.
Creación de una aplicación de chat GraphQL en tiempo real con notificaciones por SMS
Tiempo de lectura: 15 minutos
Con la aparición de GraphQL surgió una nueva forma de desarrollar aplicaciones cliente/servidor. Las ventajas de desarrollar aplicaciones GraphQL son numerosas, desde solicitar explícitamente al servidor lo que se necesita hasta la comunicación en tiempo real basada en eventos mediante suscripción. Este artículo destaca GraphQL code-first y sus superpoderes. El artículo también explica cómo desarrollar una aplicación de chat basada en Next.js y Apollo en el frontend, y Prisma 2, Graphql-yoga y notificación por SMS utilizando la excelente API de SMS de Vonage en el backend.
GraphQL Código Primero
Code-first GraphQL es un enfoque para desarrollar servidores GraphQL escribiendo tus resolvers y subcontratando la definición del esquema para que se genere mediante programación. A menudo se denomina "resolver primero". La generación del esquema es manejada por una herramienta que recorre sus resolvers y genera el esquema. Schema first es lo contrario de code-first-approach, e implica definir los tipos, la respuesta, etc., de su servidor.
Desarrollo backend
Vamos a crear una aplicación de chat en tiempo real con notificaciones por SMS.
Para empezar, clone este repositorio. Contiene la configuración básica que necesitas para seguir este artículo.
Requisitos previos
Node.js >=10.0.0
Conocimientos previos de Prisma
Conocimiento de GraphQL
Una base de datos, por ejemplo MySQL
Prisma CLI
Una cuenta de Vonage
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
Ahora, vamos a entender el directorio del proyecto.
Hay dos carpetas contenidas en el directorio raíz. El directorio backend contiene Una carpeta Prisma que contiene la configuración de Prisma. Hay un archivo schema.prisma en la carpeta Prisma que incluye la configuración de la base de datos y una base de datos SQLite llamada dev.db. Navegue hasta el directorio backend y ejecute npm install para instalar todas las dependencias necesarias.
Además, crea un archivo .env en el directorio backend; este contendría las variables de entorno necesarias como la URL de la base de datos y variables y todo eso. Para la URL de la base de datos, pegue esto en el archivo env:
DATABASE_URL="file:./dev.db"El directorio pages en la carpeta frontend es donde Next.js servirá las páginas de la aplicación. El directorio pages contiene un _app.js que ha sido configurado para trabajar con Bootstrap. Navega hasta el directorio frontend y ejecuta npm install. Esta carpeta también incluye un directorio src con subdirectorios assets, components y utils.
A continuación, vaya al archivo prisma/schema.prisma. Necesitamos dos modelos, uno para Usuario y otro para Chat. A continuación se muestra la configuración del cliente generador:
model User {
id Int @id @default(autoincrement())
name String
email String? @unique
password String
phone String @unique
isAdmin Boolean @default(false)
messages Chat[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
Chat Chat[] @relation("RecieverOfChat")
}
model Chat {
id Int @id @default(autoincrement())
receiverId Int
receiver User @relation("RecieverOfChat", fields: [receiverId], references: [id])
sender User @relation(fields: [senderId], references: [id])
senderId Int
message String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
}El modelo representa el nombre de la tabla que se creará en la base de datos, mientras que los campos representan los nombres de las columnas y los tipos de datos que se almacenarán en ellas. Además de los tipos de datos disponibles en Prisma, un modelo también puede ser un tipo de datos. Esto es lo que define la relación entre dos o más modelos o una auto-relación para un modelo. Anotamos cada modelo con palabras clave Prisma. Si no comprende las palabras clave utilizadas, consulte la documentación Prisma documentación.
Ejecutar prisma migrate save --experimental. Asigne un nombre a su migración y ejecútela prisma migrate up --experimental. El comando creará las tablas basándose en las definiciones de modelo del esquema. Por último, ejecute prisma generate para exponer el esquema de base de datos mapeado a métodos Prisma y características que permiten funcionalidades CRUD.
Navegue hasta el directorio src/tipos y crea un archivo User y Chat. Ya existen cuatro archivos, Mutation.js, Query.js, Subscription.js y un archivo index.js que combina todos los resolvers como uno.
En el archivo User.js, añade:
const { objectType } = require("@nexus/schema");
const User = objectType({
name: "User",
definition(t) {
t.model.id();
t.model.name();
t.model.email();
t.model.phone();
t.model.isAdmin();
t.model.messages();
t.model.createdAt();
t.model.updatedAt();
},
});
module.exports = {
User,
};En el archivo Chat.js, añade:
const { objectType } = require("@nexus/schema");
const Chat = objectType({
name: "Chat",
definition(t) {
t.model.id();
t.model.receiver();
t.model.sender();
t.model.message();
t.model.createdAt();
t.model.updatedAt();
},
});
module.exports = {
Chat,
};Importamos un objectType de nexus porque los modelos User y Chat son de tipo object. Accedemos a los campos que definimos en nuestro esquema utilizando el método model y el nombre del campo. Esto es posible gracias a la función nexus-plugin-prisma que hemos instalado. Esto nos ayuda a no tener que empezar a definir cada campo uno a uno y configurarlo. El siguiente código es un ejemplo de configuración manual:
t.id("id", { description: "The ID (Primary Key) of the table or model" });
t.string("name");
t.boolean("isAdmin");
// ... the restEn el archivo de mutación, vamos a manejar el inicio de sesión y registro. Crea un nuevo archivo en el directorio src/types llamado AuthPayload.js. Este es un tipo de objeto que representa qué tipo de carga útil de autenticación devolvería al cliente.
En AuthPayload.js, añade:
const { objectType } = require("@nexus/schema'");
const AuthPayload = objectType({
name: "AuthPayload",
definition(t) {
t.string("token");
t.field("user", { type: "User" });
},
});
module.exports = { AuthPayload };En el directorio src/utils, crea un archivo helper.js y crea estos métodos.
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const getUserId = async (ctx) => {
let Authorization = ctx.request
? ctx.request.get("Authorization")
: ctx.connection.context.Authorization;
if (Authorization === undefined && ctx.connection) {
Authorization = ctx.connection.context.headers.Authorization;
} else if (Authorization === undefined && ctx.request.cookies) {
Authorization = ctx.request.cookies.token;
} else {
// it means no authorization header was sent
}
if (Authorization) {
const token = Authorization.replace("Bearer", "");
return token.length === 0
? null
: jwt.verify(token.trim(), process.env.JWT_SECRET).userId;
}
return null;
};
const genToken = userId => {
return jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: "364 days",
});
};
const hashPassword = password => {
if (password.length < 6) {
throw new Error("Password must be 8 characters or longer");
}
return bcrypt.hash(password, Number(process.env.SALTROUND));
};
module.exports = {
getUserId,
genToken,
hashPassword,
};NOTA: En este momento usted debe tener variables de entorno establecidas en su archivo .env para estos métodos.
Ahora, tenemos una forma de obtener el ID del usuario usando un método getUser. Vamos a modificar el archivo script.js. Importa el método getUser de src/utils/helpers y descomenta esta parte del método context en el archivo de configuración del servidor.
// const userId = getUserId(sConfig);
// const user = await prisma.user.findOne({ where: { id: Number(userId) } });
// if (user) {
// data.user = user;
// }En el archivo mutation.js, añade el código para registrarse:
const { idArg, mutationType, stringArg, booleanArg } = require("@nexus/schema");
const { compare } = require("bcryptjs");
const { genToken, hashPassword } = require("../utils/helpers");
const Mutation = mutationType({
definition(t) {
t.field("signup", {
type: "AuthPayload",
args: {
name: stringArg({nullable:false}),
email: stringArg({ nullable: true }),
password: stringArg({nullable:false}),
phone: stringArg({nullable:false}),
isAdmin: booleanArg({ nullable: true, default: false }),
},
resolve: async (parent, args, ctx) => {
const emailAddress = args.email ? args.email.toLowerCase() :""
const existingUser = await ctx.prisma.user.findFirst({
where: {
OR: [
{
email: emailAddress,
},
{
phone: args.phone,
},
],
},
});
if (existingUser) {
if (existingUser.email.toLowerCase() === emailAddress) {
throw new Error("A user with this email address currently exist");
} else {
throw new Error("A user with this phone number currently exist");
}
}
const hashedPassword = await hashPassword(args.password);
let token;
let user = await ctx.prisma.user.create({
data: {
...args,
password: hashedPassword,
email: emailAddress,
},
});
token = genToken(user.id);
ctx.sConfig.response.cookie("token", token, {
maxAge: 1000 * 60 * 60 * 24 * 365,
path: "/",
sameSite: "none",
secure: true,
});
return {
token,
user,
};
},
});
},
});Primero comprobamos si la base de datos contiene un usuario con el email o el teléfono, lanzamos un error si el usuario existe, sino creamos el usuario, generamos un token usando el id del usuario y establecemos el token como cookie. También devolvemos el payload del registro.
Añade lo siguiente a login.js, justo debajo del resolver de registro:
t.field("login", {
type: "AuthPayload",
args: {
username: stringArg({ nullable: false }),
password: stringArg(),
},
resolve: async (parent, { username, password }, ctx) => {
const user = await ctx.prisma.user.findFirst({
where: {
OR: [
{
email: username,
},
{
phone: username,
},
],
},
});
if (!user) {
throw new Error("Invalid login credentials provided");
}
const passwordValid = await compare(password, user.password);
if (!passwordValid) {
throw new Error("Invalid password");
}
let token;
token = genToken(user.id);
ctx.sConfig.response.cookie("token", token, {
maxAge: 1000 * 60 * 60 * 24 * 365,
path: "/",
sameSite: "none",
secure: true,
});
const textMessage = `Hi ${user.name}. Just confirming that this is you. If it's not, please reset your password immediately. `;
await ctx.vonage.message.sendSms(
process.env.ADMIN_PHONE,
user.phone,
textMessage,
{
type: "unicode",
},
(err, response) => {
if (err) {
console.log(err);
} else {
console.log(response, "eerr");
if (response.messages[0]["status"] === "0") {
console.log("Message sent successfully.");
} else {
console.log(
`Message failed with error: ${response.messages[0]["error-text"]}`
);
}
}
}
);
return {
token,
user,
};
},
});Verificamos que el usuario exista, validamos sus credenciales de inicio de sesión, establecemos el token como una cookie y devolvemos el tipo de mutación de la carga útil de autenticación. También les enviamos una notificación por SMS a través de la instancia de la SMS API de Vonage que creamos en la configuración del servidor.
Para gestionar las notificaciones por SMS a los usuarios de la aplicación, he comprado un número virtual a Vonage. Debería seguir este artículo para comenzar a crear una aplicación SMS de Vonage. Una vez que hayas creado una aplicación, se descargará automáticamente un archivo de clave privada en tu computadora. Mueve este archivo al directorio backend. También deberías tener una variable de entorno ADMIN_PHONE configurada en tu archivo .env, el número virtual que compré de la cuenta de Vonage.
Vaya al archivo archivo types/index.js y comenta las importaciones Query y Subscription ya que no tenemos nada ahí. Por ahora, importa tus archivos User.js y Chat.js.
Vamos a añadir un Query resolver para permitir a un usuario consultar los detalles de su Account. En Query.js, añade:
const Query = queryType({
definition(t) {
t.field("myAccount", {
type: "User",
resolve: async (parent, args, ctx) => {
const myAccount = ctx.user;
if (!myAccount) {
throw new Error(
"You are not logged in. Please login to view your account information"
);
}
return myAccount;
},
});
}
})Como hemos manejado la consulta del usuario en cada petición que recibimos en el campo context de la configuración del servidor, podemos acceder al usuario si existe en el objeto ctx. Si el usuario no existe en el objeto ctx, significa que el usuario necesita iniciar sesión. En el archivo types/index.js, descomenta el archivo Query.js importado.
Vamos a crear otros dos resolvedores de consultas; Uno para consultar a un usuario y otro para consultar a múltiples usuarios. Estaremos usando las funcionalidades de nexus-plugin-prisma crud; esta es una característica experimental, así que necesitamos activarla. En el archivo script.js en el campo plugins, añada {experimentalCRUD: true} a la función nexusPrisma si no está añadida.
En Query.js, añade:
t.crud.user();
t.crud.users({
ordering: true,
filtering: true,
pagination: true,
});Para utilizar esta funcionalidad, asegúrese de nombrar sus modelos de esquema en singular como Usuario y no Usuarios. Utilicemos también esta función para gestionar los resolvers de actualización de un usuario y eliminación de un usuario en el archivo de mutación.
t.crud.updateOneUser({
alias:"updateUser"
})
t.crud.deleteOneUser({
alias:"deleteUser"
})Manejemos ahora los resolvers de Chat. También vamos a añadir dos archivos más, uno llamado Subscription.js y el otro SubscriptionPayload. Añade ambos archivos a tu lista de tipos (resolvers) en el archivo index.js.
SubscriptionPayload.js
const { objectType } = require("@nexus/schema");
const SubscriptionPayload = objectType({
name: "SubscriptionPayload",
definition(t) {
t.field("message", { type: "Chat" });
t.field("mutation", { type: "String" });
}
});
module.exports = { SubscriptionPayload };Para gestionar las suscripciones, utilizaremos el método PubSub que viene con el paquete Graphql-yoga.
En primer lugar, vamos a crear funcionalidades CRUD para un chat al que el solucionador de suscripciones escucharía para estos eventos CRUD. También vamos a crear una función sendNewMessageNotification para gestionar el envío de notificaciones a los destinatarios del chat cada vez que las conversaciones anteriores entre ellos tengan menos de una hora.
En el archivo helper.js, añade:
const sendNewMessageNotification = async (lastMessage, vonage) => {
const oneHour = 60 * 60 * 1000;
const messageTime = new Date(lastMessage.createdAt);
const anHourAgo = Date.now() - oneHour;
if (messageTime.getTime() < anHourAgo) {
const textMessage = `Hi ${lastMessage.receiver.name}. You have a new message from ${lastMessage.sender.name}. Login to your account to continue chatting with them.`;
await vonage.message.sendSms(
"AWESOME CHAT APP",
lastMessage.receiver.phone,
textMessage,
{
type: "unicode",
},
(err, response) => {
if (err) {
console.log(err);
} else {
console.log(response, "eerr");
if (response.messages[0]["status"] === "0") {
console.log("Message sent successfully.");
} else {
console.log(
`Message failed with error: ${response.messages[0]["error-text"]}`
);
}
}
}
);
}
};Este método comprueba que el último mensaje enviado tiene menos de 1 hora. Si lo es, enviamos al destinatario del mensaje una notificación por SMS, y si no, no hacemos nada. Exporta el método sendNewMessageNotification e impórtalo en el archivo Mutation.js. Manejemos ahora el resolvedor createChat.
En Mutation.js, añade:
t.field("createChat", {
type: "Chat",
args: {
receiverId: intArg({ nullable: false }),
message: stringArg({ nullable: false }),
},
resolve: async (parent, { receiverId, message }, ctx) => {
const sender = ctx.user;
if (!sender) {
throw new Error(errorMessage);
}
if (message.length === 0) {
throw new Error("Your message must not be empty");
}
const lastsentMessage = await ctx.prisma.chat.findFirst({
orderBy: [
{
createdAt: "desc",
},
],
where: {
OR: [
{
OR: [
{
senderId: sender.id,
},
{
receiverId,
},
],
},
{
OR: [
{
senderId: receiverId,
},
{
receiverId: sender.id,
},
],
},
],
},
include: {
sender: true,
receiver: true,
},
});
const newMessage = await ctx.prisma.chat.create({
data: {
message,
receiver: {
connect: {
id: receiverId,
},
},
sender: {
connect: {
id: sender.id,
},
},
},
});
if (lastsentMessage) {
await sendNewMessageNotification(lastsentMessage, ctx.vonage);
}
await ctx.pubSub.publish("CREATED", {
Chat: {
message: newMessage,
mutation: "CREATED",
},
senderId: sender.id,
receiverId,
});
return newMessage;
},
});Validamos que el remitente del mensaje ha iniciado sesión comprobando que el objeto usuario existe en el servidor y, a continuación, validamos que el mensaje no está vacío. Primero buscamos el último mensaje enviado o recibido por el usuario y luego creamos el nuevo mensaje. Si el mensaje anterior entre ellos es de hace menos de una hora, se dispara el método sendNewMessageNotification. Por último, vamos a manejar el aspecto de la suscripción.
En Subscription.js, añade:
const { intArg, subscriptionField } = require("@nexus/schema");
const { withFilter } = require("graphql-yoga");
const mutationType = ["CREATED", "UPDATED", "DELETED"];
const Subscription = subscriptionField("Chat", {
type: "SubscriptionPayload",
args: {
receiverId: intArg({
nullable: false,
}),
},
description: "Subscription For Chats",
subscribe: withFilter(
(parent, args, ctx) => {
const sender = ctx.user;
if (!sender) {
throw new Error("You are not logged in. Please login to your account.");
}
args.senderId = sender.id;
return ctx.pubSub.asyncIterator(mutationType);
},
(payload, variables) => {
if (
(Number(payload.senderId) === Number(variables.senderId) &&
Number(payload.receiverId) === Number(variables.receiverId)) ||
(Number(payload.senderId) === Number(variables.receiverId) &&
Number(payload.receiverId) === Number(variables.senderId))
) {
return true;
}
return false;
}
),
resolve: async (payload) => {
const { Chat } = await payload;
return Chat;
},
});
module.exports = {
Subscription,
};Importamos los objetos intArg y subScriptionFIeld de nexus/schema, y también importamos el método withFIlter del paquete graphql-yoga, que nos ayuda a asegurarnos de que sólo los usuarios correctos reciben payloads o eventos. El primer argumento es el subscribe resolver que devuelve el asyncIterator que queremos filtrar, y se le pasan los eventos que queremos escuchar, es decir CREADO, ACTUALIZADO, ELIMINADO. El segundo argumento es la condición que debe cumplirse para que ese evento pase. Para nuestro caso de uso, este evento debe pasar sólo al remitente y al destinatario de los datos del mensaje. Por curiosidad, comenta ese campo y pruébalo. Deberías notar que enviar un mensaje notifica a todos los usuarios de la aplicación que escuchan el resolver createChat.
Ahora, vamos a añadir las mutaciones updateChat y deleteChat- que es ligeramente diferente de la creación de un chat. En primer lugar, tenemos que comprobar que el usuario está autenticado. En segundo lugar, tenemos que comprobar que el mensaje existe; por último, tenemos que comprobar que es el remitente del mensaje el que puede actualizarlo o borrarlo. Un usuario que no haya enviado el mensaje no debería tener acceso a borrarlo. Si se cumplen estas condiciones, actualizamos o borramos el chat y se lo notificamos a nuestros suscriptores.
Para la mutación updateChat, añadir:
t.field("updateChat", {
type: "Chat",
args: {
messageId: intArg({ nullable: false }),
message: stringArg({ nullable: false }),
},
resolve: async (parent, { messageId, message }, ctx) => {
const sender = ctx.user;
if (!sender) {
throw new Error(
"You are not logged in. Please login to your account."
);
}
const sentMessage = await ctx.prisma.chat.findOne({
where: {
id: Number(messageId)
}
});
if (!sentMessage) {
throw new Error("This message does not exist.");
}
if (Number(sentMessage.senderId) !== Number(sender.id)) {
throw new Error("You don't have the permission to delete this message. You can only delete messages created by you.")
}
const updatedMessage = await ctx.prisma.chat.update({
where: {
id: sentMessage.id
},
data: {
message
}
});
await ctx.pubSub.publish("UPDATED", {
Chat: {
message: updatedMessage,
mutation: "UPDATED",
},
senderId: sender.id,
receiverId: updatedMessage.receiverId,
});
return updatedMessage;
},
});Para la mutación deleteChat, añade:
t.field("deleteChat", {
type: "Chat",
args: {
messageId: intArg({ nullable: false }),
},
resolve: async (parent, { messageId }, ctx) => {
const sender = ctx.user;
if (!sender) {
throw new Error(
"You are not logged in. Please login to your account."
);
}
const sentMessage = await ctx.prisma.chat.findOne({
where: {
id: Number(messageId)
}
});
if (!sentMessage) {
throw new Error("This message does not exist.");
}
if (Number(sentMessage.senderId) !== Number(sender.id)) {
throw new Error("You don't have the permission to delete this message. You can only delete messages created by you.")
}
const deletedMessage = await ctx.prisma.chat.delete({
where: {
id: sentMessage.id
}
});
await ctx.pubSub.publish("DELETED", {
Chat: {
message: deletedMessage,
mutation: "DELETED",
},
senderId: sender.id,
receiverId : deletedMessage.receiverId,
});
return deletedMessage;
},
});Ahora vamos a aprovechar la funcionalidad CRUD para la consulta de chat y chats. En el archivo Query.js, añade:
t.crud.chat();
t.crud.chats({
ordering: true,
filtering: true,
pagination: true,
});Hemos estado trabajando en los aspectos de mutación, consulta y suscripción de la aplicación. Hemos creado permisos de bajo nivel y mecanismos de autorización para garantizar que algunas características de la aplicación son seguras, pero ahora es el momento de proteger nuestra API. Vamos a trabajar en los permisos.
Idealmente, no queremos que todas las funciones de la aplicación sean privadas o inaccesibles para los usuarios no autenticados. Tampoco queremos que todo sea accesible, así que ¿cómo resolvemos esto?
Por ahora, haremos que las consultas a la lista de usuarios sean accesibles para usuarios no autenticados, mientras que otras funciones estarán protegidas. También vamos a añadir permisos adicionales para algunos resolvers para asegurar que sólo los administradores pueden realizar ciertas operaciones como la eliminación de un usuario. Empecemos. Haremos uso de Graphql-shield. Aquí hay un buen tutorial que cubre los fundamentos de Graphql-shield.
En permissions/rules.js, añade:
const { rule } = require("graphql-shield");
const errorMessage = "You are not logged in, please login to your account";
const rules = {
isAuthenticatedUser: rule({ cache: "contextual" })(
async (parent, args, ctx) => {
const loggedInUser = ctx.user;
if (!loggedInUser) {
return new Error(errorMessage);
}
return true;
}
),
isAdmin: rule({ cache: "contextual" })(async (parent, args, ctx) => {
const loggedInUser = ctx.user;
if (!loggedInUser) {
return new Error(errorMessage);
}
if (!loggedInUser.isAdmin) {
return new Error(
"You don't have the right permission to make this request"
);
}
return loggedInUser.isAdmin;
}),
isChatOwner: rule({ cache: "strict" })(async (parent, args, ctx) => {
const loggedInUser = ctx.user;
if (!loggedInUser) {
return new Error(errorMessage);
}
const chatOwner = await ctx.prisma.chat
.findOne({
where: {
id: Number(args.messageId),
},
})
.sender();
if (chatOwner.id !== loggedInUser.id) {
return new Error(
"You don't have the right permission to make this request. "
);
}
return true;
}),
};
module.exports = rules;Luego, en el archivo permissions/index.js, aplicamos nuestras reglas definidas a cada uno de nuestros resolvers.
const { shield, or } = require("graphql-shield");
const rules = require("./rules");
const permissions = shield(
{
Query: {
myAccount: rules.isAuthenticatedUser,
user: or(rules.isAuthenticatedUser, rules.isAdmin),
chat: or(rules.isAuthenticatedUser, rules.isAdmin),
chats: or(rules.isAuthenticatedUser, rules.isAdmin),
},
Mutation: {
updateUser: or(rules.isAuthenticatedUser, rules.isAdmin),
deleteUser: rules.isAdmin,
createChat: rules.isAuthenticatedUser,
updateChat: or(rules.isAdmin, rules.isChatOwner),
deleteChat: or(rules.isAdmin, rules.isChatOwner),
},
},
{
allowExternalErrors: true,
}
);
module.exports = permissions;Por último, en el archivo de configuración del servidor de script.js, descomente el campo middleware para activar los permisos.
Desarrollo Frontend
El repositorio de GitHub ya viene con los paquetes necesarios y la configuración predeterminada necesaria para codificar. Ejecute npm i para instalar las dependencias necesarias.
Navega a la carpeta pages y crea un archivo login, signup.js e index.js.
Ahora, vamos a trabajar con los usuarios que se registran y se dan de alta. Antes de continuar, vamos a crear un archivo Layout.js para envolver nuestras páginas con funcionalidades reutilizables como el título del sitio, favicon, etc.
En la carpeta components, crea un archivo Layout.js.
import Head from "next/head";
import { Nav, Navbar } from "react-bootstrap";
import Image from "next/image";
import Link from "next/link";
import Router from "next/router";
export const logout = () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
document.cookie = `token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`;
document.cookie = `user=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT`;
Router.replace("/login");
};
const Layout = (props) => {
const {
title = "Awesome Web App",
navHidden = false,
height = "100vh",
} = props;
return (
<>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
<meta httpequiv="content-type" content="text/html;charset=UTF-8">
<meta charset="utf-8">
<link rel="icon" href="/assets/img/logo.png" type="image/png" sizes="16x16">
<title>{title}</title>
{!navHidden && (
<header>
<navbar bg="dark" expand="lg" classname="mb-40">
<navbar.brand href="#home">
<img src="/assets/img/logo.png" width="100" height="100" classname="d-inline-block align-top" alt="">
</navbar.brand>
<nav classname="mr-auto flex-row">
<link href="/">
<a classname="text-white mr-2 nav-link">Home</a>
<link href="/profile">
<a classname="text-white mr-2">Profile</a>
<nav.link classname="text-white" onclick="{(e)" ==""> logout()}>
Logout
</nav.link>
</nav>
</navbar>
</header>
)}
<main style="{{" display:="" "flex",="" justifycontent:="" "center",="" alignitems:="" height,="" position:="" "relative",="" flex:="" 1,="" }}="">
{props.children}
</main>
);
};
export default Layout;En login.js, añade:
import { useState } from "react";
import { Form, Row, Col } from "react-bootstrap";
import { useMutation } from "@apollo/client";
import Mutation from "../src/gql/Mutation";
import { setToken } from "../src/utils/tokenUtils";
import { useRouter } from "next/router";
import Link from "next/link";
import Layout from "../src/components/Layout";
import { getToken } from "../src/utils/tokenUtils";
const Login = (props) => {
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const redirectTo = router.query?.redirectTo ?? "/";
const [login, { loading, error }] = useMutation(Mutation.login, {
variables: {
password,
username,
},
errorPolicy: "all",
onCompleted({ login }) {
if (login) {
setToken(login);
setUsername("");
setPassword("");
router.push(redirectTo);
}
},
});
const handleSubmit = (event) => {
event.preventDefault();
login();
};
return (
<layout navhidden="{true}">
<row>
<row classname="align-items-center m-h-100">
<div classname="pb-2 text-center">
<h4 classname=" d-block">Awesome Chat App</h4>
</div>
<h5 classname="text-center fw-400 p-b-20">Login</h5>
<form method="post" onsubmit="{(e)" ==""> handleSubmit(e)}>
<form.row>
<form.group as="{Col}" md="12" controlid="validationCustom01">
<form.control required="" size="lg" type="text" value="{username}" placeholder="phone or email" isinvalid="{Boolean(error" &&="" error.message)}="" onchange="{(e)" ==""> setUsername(e.target.value)}
disabled={loading}
/>
</form.control></form.group>
<form.group as="{Col}" md="12" controlid="validationCustom02">
<form.control required="" size="lg" type="password" placeholder="password" value="{password}" isinvalid="{Boolean(error" &&="" error.message)}="" onchange="{(e)" ==""> setPassword(e.target.value)}
disabled={loading}
/>
<form.control.feedback type="{"invalid"}">
{error && error.message}
</form.control.feedback>
</form.control></form.group>
<button type="submit" classname="col-md-12 mb-3 btn btn-danger" size="lg">
{loading ? "Logging you in" : "Login"}
</button>
</form.row>
</form>
<row>
<link href="/signup">
<a href="#!" classname="text-underline">
Create Account?
</a>
<a href="#!" classname=" float-right text-underline">
Forgot Password?
</a>
</row>
</row>
</row>
</layout>
);
};
export async function getServerSideProps(context) {
const token = getToken(context);
if (token) {
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
return {
props: {},
};
}
export default Login;En el getServerSideProps, comprobamos que el token existe. Si el token existe, significa que el usuario no ha iniciado sesión, y redirigimos al usuario a la página de inicio. Si el token no existe, procedemos. Las importaciones requeridas como setToken, Mutation y useMutation son importadas. Definimos una interfaz de usuario básica para iniciar sesión y proporcionar un campo de nombre de usuario y contraseña. Este nombre de usuario acepta un correo electrónico o un número de teléfono. Si obtenemos una respuesta satisfactoria del servidor, establecemos el token y dirigimos al usuario a la URL necesaria definida en la variable redirecTo variable. De lo contrario, si las credenciales de acceso son incorrectas, mostramos este mensaje al usuario. Si no entiendes cómo funciona el hook useMutation, por favor lee la documentación del cliente Apollo aquí.
En Signup.js, añade:
import { useState } from "react";
import { Form, Row, Col } from "react-bootstrap";
import { useMutation } from "@apollo/client";
import Mutation from "../src/gql/Mutation";
import { setToken } from "../src/utils/tokenUtils";
import { useRouter } from "next/router";
import Link from "next/link";
import Layout from "../src/components/Layout";
const SignUp = (props) => {
const router = useRouter();
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [signup, { loading, error }] = useMutation(Mutation.signup, {
variables: {
email,
phone,
password,
name,
},
errorPolicy: "all",
onCompleted({ signup }) {
if (signup) {
setToken(signup);
setEmail("");
setName("");
setPhone("");
setPassword("");
router.push(`/`);
}
},
});
const handleSubmit = (event) => {
event.preventDefault();
signup();
};
return (
<layout navhidden="{true}">
<row classname="align-items-center m-h-100">
<div classname="pb-2 text-center">
<h4 classname=" d-block">Awesome Chat App</h4>
</div>
<h5 classname="text-center fw-400 p-b-20">Signup</h5>
<form method="post" onsubmit="{(e)" ==""> handleSubmit(e)}>
<form.row>
<form.group as="{Col}" md="12" controlid="validationCustom01">
<form.control required="" size="lg" type="text" value="{name}" placeholder="Full Name" onchange="{(e)" ==""> setName(e.target.value)}
disabled={loading}
isInvalid={Boolean(error && error.message)}
/>
</form.control></form.group>
<form.group as="{Col}" md="12" controlid="validationCustom02">
<form.control required="" size="lg" type="text" value="{phone}" placeholder="Phone" onchange="{(e)" ==""> setPhone(e.target.value)}
disabled={loading}
isInvalid={Boolean(error && error.message)}
/>
</form.control></form.group>
<form.group as="{Col}" md="12" controlid="validationCustom03">
<form.control size="lg" type="email" value="{email}" placeholder="Email" onchange="{(e)" ==""> setEmail(e.target.value)}
disabled={loading}
isInvalid={Boolean(error && error.message)}
/>
</form.control></form.group>
<form.group as="{Col}" md="12" controlid="validationCustom04">
<form.control required="" size="lg" type="password" placeholder="Password" value="{password}" onchange="{(e)" ==""> setPassword(e.target.value)}
disabled={loading}
isInvalid={Boolean(error && error.message)}
/>
<form.control.feedback type="{"invalid"}">
{error && error.message}
</form.control.feedback>
</form.control></form.group>
<button type="submit" classname="col-md-12 mb-3 btn btn-danger" size="lg">
{loading ? "Signing you up..." : "Signup"}
</button>
</form.row>
</form>
<row>
<link href="/login">
<a href="#!" classname="text-underline">
Have an account? Sign In.
</a>
</row>
</row>
</layout>
);
};
export default SignUp;Ahora, navega por tu navegador y prueba a iniciar sesión y registrarte.
A continuación, crea un archivo index.js y chat.js. En index.js, añade:
import { useState, useMemo } from "react";
import { Card, Row, Col } from "react-bootstrap";
import Layout from "../src/components/Layout";
import Query from "../src/gql/Query";
import { initializeApollo } from "../src/utils/apolloClient";
import Link from "next/link";
import cookies from "next-cookies";
import { getToken } from "../src/utils/tokenUtils";
const UserPage = (props) => {
const [users] = useState(props.users);
const [search, setSearch] = useState("");
const searchableUsers = users.filter((user) => {
return (
user?.name?.toLowerCase().includes(search) ||
user?.email?.toLowerCase().includes(search) ||
user?.phone?.toLowerCase().includes(search)
);
});
const memoizedUsers = useMemo(() => {
return searchableUsers.map(
(user, i) => {
return (
<row classname="border-bottom mb-2" key="{i}">
<p>{user.name}</p>
<link href="{`/chat?receiverId=${user.id}`}">
<a classname="btn btn-link">Start chatting</a>
</row>
);
},
[searchableUsers]
);
});
// View Layer
return (
<layout title="All Users">
<card classname="md-width">
<card.header classname="bg-danger text-white">All Users </card.header>
<card.body>
<row classname="mb-3 pb-2 border-dark border-bottom">
<input type="text" classname="form-control w-100" placeholder="search for a user by name, phone or email address" onchange="{(e)" ==""> setSearch(e.target.value.toLowerCase())}
/>
</row>
<div classname="overflow-auto">{memoizedUsers}</div>
</card.body>
</card>
<style jsx="" global="">
{`
.md-width {
width: 100%;
height: 600px;
margin-top: -60px;
overflow: auto;
}
.overflow-auto {
overflow: auto;
height: 90%;
}
@media (min-width: 768px) {
.md-width {
max-width: 800px !important;
margin-top: -190px;
}
}
`}
</style>
</layout>
);
};
export async function getServerSideProps(context) {
const token = getToken(context);
if (!token) {
return {
redirect: {
permanent: false,
destination: "/login?redirectTo=/",
},
};
}
const apolloClient = initializeApollo();
const { data, error } = await apolloClient.query({
query: Query.users,
});
const user = cookies(context).user;
const users =
data?.users?.filter((val) => Number(val.id) !== Number(user?.id)) ?? [];
if (!error) {
return {
props: {
users,
},
};
}
return {
props: {
users: [],
error,
},
};
}
export default UserPage;En el getServerSideProps, comprobamos si existe la cookie del token. Si el token no existe, el usuario es redirigido a la página de login. Si el token existe, el usuario logueado es filtrado antes de devolver los datos como props. El método array filter se utiliza para manejar la búsqueda en el lado del cliente, y el gancho useMemo React para memorizar los datos del usuario para evitar la renderización innecesaria.
Puede encontrar las consultas GraphQL utilizadas para estas páginas en la carpeta gql del directorio del proyecto.
A continuación, crea un componente ChatBubble en la carpeta de componentes. Utilizaremos un paquete day.js para gestionar las marcas de tiempo de los mensajes. Instala el paquete npm install dayjs y crea un archivo formatDate en utils. En formatDate.js, añade:
import dayjs from "dayjs";
import Calendar from "dayjs/plugin/calendar";
import updateLocale from "dayjs/plugin/updateLocale";
dayjs().format();
dayjs.extend(Calendar);
dayjs.extend(updateLocale);
dayjs().calendar();
export const formateDate = (date = "") => {
const sameElse = "D/M/YYYY h:mm A";
const dateStyle = {
sameElse,
lastDay: "[Yesterday] h:mm A",
sameDay: "[Today] h:mm A",
nextDay: "[Tomorrow] h:mm A",
lastWeek: sameElse,
nextWeek: sameElse,
};
// let defaultDate;
dayjs.updateLocale('en', {
calendar: dateStyle
});
if (!Boolean(date) || date.length === 0) {
return dayjs().calendar();
}
return dayjs(date).calendar();
};Luego, en ChatBubble.js, inserte:
import React from "react";
import { formateDate } from "../utils/dateFormatter";
const ChatBubble = React.forwardRef((props, ref) => {
const { chat = {} } = props;
const isMyMessage = props.receiverId !== chat?.sender?.id;
return (
<section>
<div style="{{" display:="" "flex",="" flexdirection:="" ismymessage="" ?="" "row-reverse"="" :="" "row",="" }}="" ref="{ref}">
<div classname="bubble bubble-bottom-left">
{chat?.message}
<p classname="{`${" ismymessage="" ?="" "text-muted"="" :="" ""="" }="" text-right="" mt-4="" font-small`}="">
{formateDate(chat?.createdAt ?? "")}
</p>
</div>
</div>
<style jsx="">
{`
.bubble {
position: relative;
font-family: sans-serif;
font-size: 18px;
line-height: 24px;
min-width: 200px;
max-width: 80%;
background: ${isMyMessage ? "#000" : "#DC3545"};
opacity: 0.7;
color: #fff;
border-radius: 40px;
padding: 18px;
text-align: center;
margin-bottom: 50px;
min-height: 20px;
}
.bubble-bottom-left:before {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 24px solid ${isMyMessage ? "#000" : "#DC3545"};
border-right: 12px solid transparent;
border-top: 12px solid ${isMyMessage ? "#000" : "#DC3545"};
border-bottom: 20px solid transparent;
${!isMyMessage ? `left: 32px;` : "right: 32px;"}
bottom: -24px;
}
.font-small {
font-size: 12px;
}
`}
</style>
</section>
);
});
export default ChatBubble;En función del objeto de chat recibido, aplicamos varios estilos, como la dirección de la burbuja de chat si el remitente es el usuario conectado en ese momento.
En chat.js, añade:
import { useState, useEffect, useRef } from "react";
import { Card, Row, Col, Button } from "react-bootstrap";
import Layout from "../src/components/Layout";
import Query from "../src/gql/Query";
import Mutation from "../src/gql/Mutation";
import Subscription from "../src/gql/Subscription";
import ChatBubble from "../src/components/ChatBubble";
import { useRouter } from "next/router";
import { useQuery, useMutation } from "@apollo/client";
import { getToken } from "../src/utils/tokenUtils";
import cookies from "next-cookies";
const ChatPage = (props) => {
const router = useRouter();
const myRef = useRef(null);
const [search, setSearch] = useState("");
const [message, setMessage] = useState("");
const receiverId = Number(router.query?.receiverId);
const { subscribeToMore, data, loading } = useQuery(Query.chats, {
variables: {
senderId: Number(props?.user?.id),
receiverId,
},
});
const [createChat] = useMutation(Mutation.createChat);
// subscription hook
useEffect(() => {
subscribeToMore({
document: Subscription.chats,
variables: {
receiverId: Number(router.query?.receiverId),
},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) {
return prev;
}
const newMessage = subscriptionData.data.Chat.message;
return {
chats: [...prev.chats, newMessage],
};
},
});
}, []);
// scroll to bottom hook
useEffect(() => {
if (myRef) {
myRef?.current?.scrollIntoView();
}
}, [data?.chats, myRef]);
if (loading) {
return <div>loading</div>;
} else {
const searchableChats =
data?.chats?.filter((chat) => {
return chat?.message.includes(search);
}) ?? [];
// profile of the receiver
const receiverProfile = data?.chats.find(
(chat) => Number(chat.receiver.id) === receiverId
);
// View Layer
return (
<Layout title={`Chatting with: ${receiverProfile.receiver.name} `}>
<Card className="md-width border-bottom-0">
<Card.Header className="py-4 bg-danger text-white">
Chatting with: {receiverProfile.receiver.name}
</Card.Header>
<Card.Body>
<Row className="mb-3 pb-2 border-dark border-bottom">
<input
type="text"
className="form-control w-100"
placeholder="search for chat..."
onChange={(e) => setSearch(e.target.value.toLowerCase())}
/>
</Row>
<Row className="overflow-auto">
{searchableChats.map((chat, i) => {
return (
<Col xs="12" key={i}>
<ChatBubble
chat={chat}
receiverId={receiverId}
ref={myRef}
/>
</Col>
);
})}
</Row>
<Row className="chat-message clearfix">
<Col xs="12" md="10">
<div>
<textarea
name="message-to-send"
value={message}
id="message-to-send"
placeholder="Type your message"
rows="3"
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(event) => {
if (event.key === "Enter") {
createChat({
variables: {
message,
receiverId,
},
});
setMessage("");
}
}}
/>
</div>
</Col>
<Col md="2" className="align-self-center">
<Button
size="lg"
variant="danger"
onClick={(e) => {
createChat({
variables: {
message,
receiverId,
},
});
setMessage("");
}}
>
Send
</Button>
</Col>
</Row>
</Card.Body>
</Card>
<style jsx global>
{`
.md-width {
width: 100%;
height: 600px;
margin-top: -60px;
}
.overflow-auto {
overflow: auto;
height: 90%;
}
@media (min-width: 768px) {
.md-width {
max-width: 800px !important;
margin-top: -190px;
}
}
.chat-message textarea {
width: 100%;
padding: 10px 20px;
font: 14px/22px "Lato", Arial, sans-serif;
margin-bottom: 10px;
border-radius: 5px;
resize: none;
margin-top: 10px;
}
.chat-message button:hover {
color: #75b1e8;
}
`}
</style>
</Layout>
);
}
};
export async function getServerSideProps(context) {
const token = getToken(context);
if (!token) {
return {
redirect: {
permanent: false,
destination: `/login?redirectTo=chat?receiverId=${context.query.receiverId}`,
},
};
}
const user = cookies(context).user;
return {
props: {
user,
},
};
}
export default ChatPage;Así que vamos a entender lo que está pasando aquí. Primero comprobamos que existe una cookie token. Si no existe, redirigimos al usuario a la página de inicio de sesión y pasamos la página de chat como una función redirectTo. Si existe un token, el objeto de usuario se añade a la prop, y los datos del chat se obtienen en el cliente usando el hook useQuery. Esto nos da acceso a un método llamado subscribeToMore que llamamos en useEffect hook para gestionar la suscripción a más mensajes para recibir las actualizaciones al instante. El método subscribeToMore acepta la consulta de suscripción, las variables necesarias y un método updateQuery que le indica cómo manejar el nuevo mensaje recibido. Para más información sobre cómo funciona esto, la documentación tiene una guía útil. El hook useMutation maneja la creación de un nuevo mensaje.
Ahora, vamos a trabajar en una página de perfil donde el usuario puede ver y actualizar su perfil antes de que manejemos el despliegue a la nube.
Para empezar, crea el archivo profile.js. A continuación, añadir lo siguiente a la misma:
import { useState } from "react";
import { Card, Form, Toast } from "react-bootstrap";
import Layout, { logout } from "../src/components/Layout";
import Mutation from "../src/gql/Mutation";
import cookies from "next-cookies";
import { getToken } from "../src/utils/tokenUtils";
import { useMutation } from "@apollo/client";
const ToastComponent = (props) => {
return (
<Toast
style={{
minHeight: "100px",
minWidth: "300px",
position: "absolute",
top: 0,
right: 0,
zIndex: 1,
marginRight: 40,
}}
>
<Toast.Header>
<img
src="/assets/img/logo.png"
width="60"
className="rounded mr-2"
alt=""
/>
<strong className="mr-auto">{props.type}</strong>
</Toast.Header>
<Toast.Body>{props.message}</Toast.Body>
</Toast>
);
};
const ProfilePage = (props) => {
const [name, setName] = useState(props.user.name);
const [email, setEmail] = useState(props.user.email);
const [phone] = useState(props.user.phone);
const [disabled, setDisabled] = useState(true);
const [showToast, setShowToast] = useState(false);
const [updateUser, { error }] = useMutation(Mutation.updateUser, {
variables: {
name: {
set: name,
},
phone: {
set: phone,
},
email: {
set: email,
},
id: props.user.id,
},
errorPolicy: "all",
onCompleted({ updateUser }) {
if (updateUser) {
setShowToast(!showToast);
setTimeout(() => {
logout();
}, 1300);
}
},
});
// View Layer
return (
<div className="position-relative">
{showToast && (
<ToastComponent
type={error ? "Error" : "Success"}
message={
error
? error.message
: "Updated Sucessfully, you would be redirected to the login page"
}
/>
)}
<Layout title={`${props.user.name}'s profle`}>
<Form
method="post"
onSubmit={(e) => {
e.preventDefault();
updateUser();
}}
>
<Card className="md-width">
<Card.Header className="bg-danger text-white">
<p>
Once you change your details, you would be redirected to the
login page to login with your new credentials.
</p>
</Card.Header>
<Card.Body>
<fieldset disabled={disabled}>
<Form.Row>
<Form.Group className="w-100">
<Form.Control
defaultValue={name}
name="name"
onChange={(e) => setName(e.target.value)}
/>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group className="w-100">
<Form.Control
defaultValue={email}
name="email"
onChange={(e) => setEmail(e.target.value)}
required={Boolean(email)}
/>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group className="w-100">
<Form.Control defaultValue={phone} name="phone" readOnly />
</Form.Group>
</Form.Row>
</fieldset>
<Form.Group className="text-center">
<a
className="btn btn-link text-danger px-3 mx-3"
onClick={(e) => setDisabled(!disabled)}
>
Edit
</a>
<button
className="btn btn-success px-3"
type="submit"
disabled={disabled}
onClick={(e) => setDisabled(false)}
>
Save
</button>
</Form.Group>
</Card.Body>
</Card>
</Form>
<style jsx global>
{`
.md-width {
width: 100%;
min-height: 300px;
margin-top: -60px;
overflow: auto;
}
@media (min-width: 768px) {
.md-width {
max-width: 400px !important;
margin-top: -190px;
}
}
`}
</style>
</Layout>
</div>
);
};
export async function getServerSideProps(context) {
const token = getToken(context);
if (!token) {
return {
redirect: {
permanent: false,
destination: "/login?redirectTo=/profile",
},
};
}
const user = cookies(context).user;
return {
props: {
user,
},
};
}
export default ProfilePage;Sólo hacemos editables los campos de nombre y correo electrónico. Si la actualización se realiza correctamente, mostramos al usuario un mensaje de brindis y lo redirigimos a la página de inicio de sesión transcurridos 1,3 segundos para que inicie sesión con las nuevas credenciales.
Despliegue
Nos hemos encargado de crear el servidor GraphQL, y también hemos desarrollado una aplicación construida con NextJS para consumir estos endpoints. Las APIs del servidor están protegidas, y las páginas también. Como hemos estado desarrollando localmente, ahora es el momento de exponer la aplicación al mundo. Vamos a desplegar el servidor en Heroku y la aplicación cliente en Vercel. Te mostraré dos formas de desplegar la aplicación. Una usando la interfaz de línea de comandos y la segunda usando el dashboard de Vercel. Crea una Account Vercel aquí. Crear una Account Heroku aquí.
Además, instale Vercel CLI con npm i -g vercel. Instale el Heroku CLI siguiendo las instrucciones aquí.
Crea dos nuevas ramas. Una llamada producción/servidor y otra llamada producción/cliente. Vamos a trabajar en el despliegue del servidor en primer lugar.
Borremos los archivos que no necesitamos que estén en producción. En la carpeta Prisma, elimina el archivo dev.db y también la carpeta de migración. Vamos a hacer uso de una base de datos Postgres en su lugar para la producción. Podemos obtener una base de datos PostgreSQL gratis de ElephantSql. Crea una Account y crear una nueva base de datos Postgres. Navega al directorio backend y copia la URL de la base de datos y reemplázala en tu archivo .env. En el archivo prisma/schema.prisma, reemplace la fuente de datos db setup con:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// rest of the codeTenemos una nueva base de datos, es hora de ejecutar las migraciones. Ejecutar
npx prisma migrate save --experimental`** and **`npx prisma migrate up --experimentalEn el terminal, ejecuta heroku login e introduce tus credenciales. A continuación, ejecuta
heroku create < name-of-your-app >. Establezcamos las variables de entorno utilizadas por la aplicación. Para establecer una variable de configuración, utilice el comando heroku config:set < name >= < value > -a < namme-of-your-app> p.e. para mi variable SALTROUND
heroku config:set SALTROUND=10 -a vongage-graphql-apiDado que nuestro directorio raíz contiene tanto el código del cliente como el del servidor, y Heroku no soporta el despliegue de subdirectorios fácilmente, tendremos que desplegar usando un buildpack diferente. Hazlo siguiendo este artículo aquí. Después de añadir las variables de configuración necesarias, ejecuta:
heroku buildpacks:clear
heroku buildpacks:set https://github.com/timanovsky/subdir-heroku-buildpack
heroku buildpacks:add heroku/nodejs
heroku config:set PROJECT_PATH=BackendA continuación, añada esto a sus scripts en el archivo package.json:
"heroku-postbuild": "npm run postinstall"El script que acabamos de añadir asegura que después de instalar las dependencias para el proyecto, Heroku genera la instancia de base de datos Prisma necesaria.
Confirme que Heroku forma parte del repositorio remoto ejecutando git remote -v. Si no ves un remoto llamado Heroku, añádelo ejecutando heroku git:remote -a <app-name>.
Por último, ejecuta git git push heroku master. Esto empujará tu código y lo construirá. Si tienes algún problema, asegúrate de que has configurado todas las variables env que utiliza tu aplicación y de que sigues las instrucciones al pie de la letra. A continuación, ejecuta heroku apps:open /playground.
Frontend
Asegúrate de que has enviado todos los cambios a GitHub en la rama producción/servidor. Crea una nueva rama a partir de ahí llamada production/client y sustituye los respectivos puntos finales de Graphql para las consultas y la mutación por tu URL de producción. Para las suscripciones, sustituye el protocolo HTTPS por WSS. Empuje los cambios a GitHub y navegue a su cuenta Vercel.
En tu salpicadero,
Haz clic en importar proyecto, copia el enlace de tu repositorio GitHub y pega
Elige el directorio frontend como directorio raíz de tu proyecto y un nombre
Debería detectar automáticamente NextJS como el framework elegido. No estamos usando ninguna variable env, así que deja esa parte vacía. A continuación, haga clic en desplegar.
Una vez que su proyecto se haya desplegado completamente, vaya a la pestaña de configuración. En git cambia la rama de despliegue de main a production/client.
Navega hasta tu código y haz un cambio en tus archivos, confírmalo y envíalo a la rama de producción/cliente. Automáticamente se construirá tu proyecto y se iniciará.
Una vez completado el despliegue, navega a tu panel de control de Heroku. En tu aplicación de servidor, navega a la pestaña de configuración y cambia la URL FRONTEND_ORIGIN a la URL del cliente de producción que acabamos de desplegar en Vercel. Asegúrate de no añadir una barra al final de la URL. Por ejemplo, debería ser www.example.com, no www.example.com/
Voilà, ya está.
Conclusión
Un artículo largo, pero estoy seguro de que valdrá la pena probarlo. Para resumir, hemos cubierto cómo crear un servidor GraphQL que también funciona con suscripciones utilizando el enfoque code-first. Nos sumergimos en la SMS API de Vonage y la utilizamos para enviar notificaciones por SMS a los usuarios. Utilizamos la fantástica base de datos Prisma 2 ORM para manejar las consultas db. No hay que olvidar que consumimos los endpoints utilizando Apollo y NextJS. Finalmente, desplegamos en el servicio de hosting Vercel y Heroku usando el CLI y el GUI. Creo firmemente que te he armado con todo lo que necesitas para construir esa próxima gran idea usando estas herramientas. Mi reto para ti es llevar esta aplicación al siguiente nivel mediante la adición de más funcionalidades como el restablecimiento de contraseña, la adición de amigos, mensajes, etc.
Gracias por tomarte el tiempo de probar este tutorial. Si te quedas atascado, no dudes en ponerte en contacto conmigo en Twitter en @codekagei o déjame un comentario. ¡Feliz hacking!
Consulta el código:
Compartir:
Temiloluwa is a passionate learner focused on building accessible and user-centred solutions that solve global problems. He works with React, Nodejs and Graphql and has basic experience working with Java. He loves exploring new technologies and languages and has a penchant for breaking complex topics into small chunks of easy to understand bits. He is currently open to opportunities that provide the right mix of challenge and growth.
