NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' course
Github Repositories
Source code for the NestJs and Nuxt
version of the Full-Stack Vue with GraphQL - The Ultimate Guide Udemy course
Within the code you can see how to:
- Use NestJs with GraphQL and MongoDb to create powerful back ends applicaction.
- Use Apollo Boost to create powerful full-stack apps.
- Handle errors on the client and server with Apollo / GraphQL
- Implement session-based JWT authentication to GraphQL applications
- Integrate Apollo with Nuxt Vuex for more reliable and scalable state management
- Implement infinite scrolling functionality using Vue-Apollo
- Deploy full-stack JavaScript / GraphQL applications using Heroku.
- Learn how to write queries and mutations in the GraphQL language on both the client and server
- Make use of many useful MongoDB methods and features
- Be able to create attractive, sophisticated UIs using the Vuetify CSS framework
- Become more familiar with all the best ES6 / 7 features such as async / await, destructuring, spread operators, arrow functions, etc
- Create a multi-language application using nuxt-i18n
Table of contents
- Section 3-4: Setting up the Server project.
- Section 5: Create Vue Frontend with NuxtJs
- Section 6: Using Vue Apollo 0 / 4|24min
- Section 7: Integrate Vuex with ApolloClient
- Section 8: JWT Authentication for Signin / Signup
- Section 9: Error Handling and Form Validation
- Section 10: Add Post / Infinite Scroll Components
- Section 11: Post Component
- Section 12: Like / Unlike Post
- Section 13: Search Posts
- Section 14: Profile Page, Update / Delete Posts
- Section 15: Preparing for Deployment
- Section 16: Deployment with Heroku
You can find more information on Full-Stack Vue with GraphQL - The Ultimate Guide.
This is the Nuxt
version of the Full-Stack Vue with GraphQL - The Ultimate Guide Udemy course.
Server
project.
Section 3-4: Setting up the I.-Create the main folder for the solution
- Create the main
full-stack-vue-with-graphql-the-ultimate-guide-nuxt
folder
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs
$ mkdir full-stack-vue-with-graphql-the-ultimate-guide-nuxt
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs
$ cd full-stack-vue-with-graphql-the-ultimate-guide-nuxt
- Initialize
Git
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt
$ git init
Initialized empty Git repository in C:/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/.git/
- Copy the
README.md
and.gitignore
from the originalfull-stack-vue-with-graphql-the-ultimate-guide
project.
server
project using NetsJS
II.-Create the - We are now going to use the NestJS CLI to scaffold the project. Nest is a progressive
Node.js
framework for building efficient, reliable and scalable server-side applications.
- We need to ensure
NestJs CLI
is already installed.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ nest -V
6.5.0
- If it is not installed we can install it by using
npm i -g @nestjs/cli
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/Typescript
$ npm i -g @nestjs/cli
C:\Users\juan.pablo.perez\AppData\Roaming\npm\nest -> C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@nestjs\cli\bin\nest.js
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\@nestjs\cli\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @nestjs/cli@6.5.0
added 255 packages from 174 contributors in 55.819s
- We are going to create the
server
project using theNestJS CLI
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ nest new server
⚡ We will scaffold your app in a few seconds..
CREATE /server/.prettierrc (51 bytes)
CREATE /server/nest-cli.json (84 bytes)
CREATE /server/nodemon-debug.json (163 bytes)
CREATE /server/nodemon.json (67 bytes)
CREATE /server/package.json (1802 bytes)
CREATE /server/README.md (3370 bytes)
CREATE /server/tsconfig.build.json (97 bytes)
CREATE /server/tsconfig.json (325 bytes)
CREATE /server/tslint.json (426 bytes)
CREATE /server/src/app.controller.spec.ts (617 bytes)
CREATE /server/src/app.controller.ts (274 bytes)
CREATE /server/src/app.module.ts (249 bytes)
CREATE /server/src/app.service.ts (142 bytes)
CREATE /server/src/main.ts (208 bytes)
CREATE /server/test/app.e2e-spec.ts (561 bytes)
CREATE /server/test/jest-e2e.json (183 bytes)
? Which package manager would you ❤️ to use? npm
▹▹▹▸▹ Installation in progress... ☕
� Successfully created project server
� Get started with the following commands:
$ cd server
$ npm run start
Thanks for installing Nest �
Please consider donating to our open collective
to help us maintain this package.
� Donate: https://opencollective.com/nest
- We can test if it has been installed properly by executing
npm run start
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ cd server
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm run start
> server@0.0.1 start C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server
> ts-node -r tsconfig-paths/register src/main.ts
[Nest] 22508 - 06/17/2019, 5:20 AM [NestFactory] Starting Nest application...
[Nest] 22508 - 06/17/2019, 5:20 AM [InstanceLoader] AppModule dependencies initialized +23ms
[Nest] 22508 - 06/17/2019, 5:20 AM [RoutesResolver] AppController {/}: +13ms
[Nest] 22508 - 06/17/2019, 5:20 AM [RouterExplorer] Mapped {/, GET} route +7ms
[Nest] 22508 - 06/17/2019, 5:20 AM [NestApplication] Nest application successfully started +8ms
- Browse http://localhost:3000/
- Let's see what has been created.
III.-Setting up Configuration (using environment variables)
- We are going to use dotenv, so we need to install the
dotenv
and@types/dotenv
.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i dotenv
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ dotenv@8.0.0
added 1 package and audited 22998 packages in 21.757s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
$ npm i --save-dev @types/dotenv
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @types/dotenv@6.1.1
added 1 package from 4 contributors and audited 23000 packages in 15.297s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
V.-Setting up MongoDb
- As we can see on NestJS Mongo we have to install the
@nestjs/mongoose
andmongoose
packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm install --save @nestjs/mongoose mongoose
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ mongoose@5.6.0
+ @nestjs/mongoose@6.1.2
added 19 packages from 12 contributors and audited 22997 packages in 23.456s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
- We also need to install the
@types/mongoose
for the dev environment
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i --save-dev @types/mongoose
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @types/mongoose@5.5.6
added 3 packages from 47 contributors and audited 23006 packages in 16.131s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
- We are going to have the URI connection in a
.dev
document thatdotenv
will use.
.dev
MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
- We are going to create the
src/config
folder with theconfig.module.ts
andconfig.service.ts
documents. It will be used to manage ourconfig
variables.
src/config/config.service.ts
import * as dotenv from "dotenv";
import * as fs from "fs";
export class ConfigService {
MONGODB_URI: string;
private readonly envConfig: { [key: string]: string };
constructor() {
if (
process.env.NODE_ENV === "production" ||
process.env.NODE_ENV === "staging"
) {
this.envConfig = {
MONGO_URI: process.env.MONGO_URI
};
} else {
this.envConfig = dotenv.parse(fs.readFileSync(".env"));
}
}
get(key: string): string {
return this.envConfig[key];
}
}
src/config/config.module.ts
import { Module } from "@nestjs/common";
import { ConfigService } from "./config.service";
@Module({
providers: [ConfigService],
exports: [ConfigService]
})
export class ConfigModule {}
- We are going to create the
src/database
folder with thedatabase.providers.ts
anddatabase.module.ts
documents that are going to be used to set upMongoDb
src/config/database.providers.ts
import { ConfigModule } from "../config/config.module";
import { ConfigService } from "../config/config.service";
import { MongooseModule } from "@nestjs/mongoose";
export const databaseProviders = [
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
uri: config.get("MONGO_URI"),
useNewUrlParser: true
})
})
];
src/config/database.module.ts
import { Module } from "@nestjs/common";
import { databaseProviders } from "./database.providers";
@Module({
imports: [...databaseProviders],
exports: [...databaseProviders]
})
export class DatabaseModule {}
- We also need to modify the
app.module.ts
document to import theDatabaseModule
.
app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
@Module({
imports: [DatabaseModule],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
- We can test if everything is working without issues.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm run start:dev
> server@0.0.1 start:dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server
> concurrently --handle-input "wait-on dist/main.js && nodemon" "tsc -w -p tsconfig.build.json"
[1]
[1]
6:40:04 PM - Starting compilation in watch mode...
[1]
[0] [nodemon] 1.19.1
[0] [nodemon] to restart at any time, enter `rs`
[0] [nodemon] watching: C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\dist/**/*
[0] [nodemon] starting `node dist/main`
[0] [nodemon] restarting due to changes...
[1]
[1] 6:40:09 PM - Found 0 errors. Watching for file changes.
[0] [nodemon] restarting due to changes...
[0] [nodemon] starting `node dist/main`
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [NestFactory] Starting Nest application...
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [InstanceLoader] DatabaseModule dependencies initialized +49ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [InstanceLoader] MongooseModule dependencies initialized +2ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [InstanceLoader] ConfigModule dependencies initialized +2ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [InstanceLoader] AppModule dependencies initialized +12ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [InstanceLoader] MongooseCoreModule dependencies initialized +866ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [RoutesResolver] AppController {/}: +7ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [RouterExplorer] Mapped {/, GET} route +3ms
[0] [Nest] 15368 - 06/20/2019, 6:40 PM [NestApplication] Nest application successfully started +3ms
GraphQL
with Apollo Server
IV.-Setting up - As we can see on NestJs GraphQL Quick Start we need to install the
@nestjs/graphql
,apollo-server-express
,graphql-tools
andgraphql
packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i --save @nestjs/graphql apollo-server-express graphql-tools graphql
> core-js@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\apollo-env\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> protobufjs@6.8.8 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\protobufjs
> node scripts/postinstall
> type-graphql@0.17.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\type-graphql
> node ./dist/postinstall || exit 0
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ graphql@14.3.1
+ graphql-tools@4.0.4
+ apollo-server-express@2.6.3
+ @nestjs/graphql@6.2.4
added 150 packages from 149 contributors and audited 22927 packages in 36.49s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
We are going to apply Code First approach for managing
GraphQL
withNestJS
We need to install the
type-graphql
package
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i type-graphql
> type-graphql@0.17.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\node_modules\type-graphql
> node ./dist/postinstall || exit 0
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ type-graphql@0.17.4
updated 1 package and audited 22957 packages in 15.604s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
We need to modify
src\app.module.ts
document to registerGraphQLModule
and to addautoSchemaFile
property to the options object. As we have chosen theCode First
approach, theGraphQL
schema is going to be generated automatically byNestJS
.schema.gpl
is the name of the schema file that is going to be created.We are going to get rid of the
app.controller.ts
document as we are not going to use controllers butGrahpQL
.
src\app.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {}
src\app.module.ts
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
import { GraphQLModule } from "@nestjs/graphql";
@Module({
imports: [
DatabaseModule,
GraphQLModule.forRoot({
autoSchemaFile: "schema.gql"
})
],
providers: [AppService]
})
export class AppModule {}
User
model.
VII.Create the database schema
: This is an organization of data as a blueprint for defining the structure and the types of data that the database needs to store.data transfer object
: This is an object that defines how data will be sent over the network and carries the data between processes.interfaces: TypeScript interfaces are used for type-checking
. It can be used to define the types of data that should be passed for an application.
nestjs-typegoose
package to avoid having to create a schema and an interface
1.Use the - We need to install the typegoose and the nestjs-typegoose packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i typegoose nestjs-typegoose
npm WARN ts-jest@24.0.2 requires a peer of jest@>=24 <25 but none is installed. You must install peer dependencies yourself.
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ nestjs-typegoose@5.2.1
+ typegoose@5.7.2
added 3 packages from 3 contributors and audited 23011 packages in 26.646s
found 62 low severity vulnerabilities
run `npm audit fix` to fix them, or `npm audit` for details
model
folder and the model
documents.
2.Create the - Create the
users
folder and inside it themodel
folder. In this folder we are going to put the documents related to the databaseusers
colection. Theuser.model.ts
document must be created with the definition of theUser
model.
src/users/model/users.model.ts
import * as mongoose from "mongoose";
import { prop, Typegoose } from "typegoose";
import { IsString, IsDate, IsArray } from "class-validator";
export class User extends Typegoose {
@IsString()
@prop({ required: true, unique: true, trim: true })
username: string;
@IsString()
@prop({ required: true, trim: true })
email: string;
@IsString()
@prop({ required: true, trim: true })
password: string;
@IsString()
avatar?: string;
@IsDate()
@prop({ default: Date.now })
joinDate: Date;
@IsArray()
@prop({ required: true, ref: "Post" })
favorites: [mongoose.Schema.Types.ObjectId];
}
- We are going to create the
dtos
folder where we are going to put theData Transfer Objects
used to send data to the collections. We are going to create thecreate-user.dto.ts
document that will be used to create a newuser
.
src/users/model/dto/create-user.dto.ts
export class CreateUserDto {
readonly username: string;
readonly email: string;
readonly password: string;
}
grahpql
folder with the graphql
documents
3.Create the We need to create
graphql
folder with theinputs
andtypes
subfolders.We need to create the
users.resolver.ts
with theGraphQL resolver
for the user.
src/users/graphql/users.resolver.ts
import { Resolver, Query, Mutation, Args } from "@nestjs/graphql";
import { UsersService } from "../users.service";
import { UserInput } from "./inputs/user.input";
import { User } from "./types/user.type";
@Resolver()
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User])
async getUsers() {
return await this.usersService.getUsers();
}
@Mutation(() => User)
async signupUserWithInput(@Args("input") input: UserInput) {
return await this.usersService.signupUserWithInput(input);
}
@Mutation(() => User)
async signupUser(
@Args("username") username: string,
@Args("email") email: string,
@Args("password") password: string
) {
return await this.usersService.signupUser({ username, email, password });
}
}
- The
inputs
subfolder will contain the classes used to define thegraphQL
inputs used byQueries
andMutations
. We are going to create theuser.input.ts
document that will be used to create a new user when they sign up.
users//inputs/user.input.ts
import { InputType, Field, Int } from "type-graphql";
@InputType()
export class UserInput {
@Field()
readonly username: string;
@Field()
readonly email: string;
@Field()
readonly password: string;
}
- The
types
subfolder will containt the classes used to define the types returned by thegraphQL Queries and Mutations
. We are going to create theuser.type.ts
document that will be used to return the data from theUser
collection.
users//types/user.type.ts
import { Field, ObjectType, ID } from "type-graphql";
@ObjectType()
export class User {
@Field(() => ID)
readonly _id: string;
@Field()
readonly username: string;
@Field()
readonly email: string;
@Field()
readonly password: string;
@Field({ nullable: true })
readonly avatar: string;
@Field()
readonly joinDate: Date;
@Field(() => [ID], { nullable: true })
readonly favorites: [string];
}
users service
4.Create the - We are going to use the
NestJs CLI
to create theusers
service.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate service users
CREATE /src/users/users.service.spec.ts (453 bytes)
CREATE /src/users/users.service.ts (89 bytes)
UPDATE /src/users/users.module.ts (159 bytes)
src/users/users.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class UsersService {}
src/users/users.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { UsersService } from "./users.service";
describe("UsersService", () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService]
}).compile();
service = module.get<UsersService>(UsersService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
});
src/users/users.module.ts
import { Module } from "@nestjs/common";
@Module()
export class UsersModule {}
- We need to modify the new service to define the methods used to manage the access to the
users
collection.
src/users/users.service.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "nestjs-typegoose";
import { User } from "./model/user.model";
import { CreateUserDto } from "./model/dtos/create-user.dto";
import { ModelType } from "typegoose";
@Injectable()
export class UsersService {
constructor(@InjectModel(User) private readonly userModel: ModelType<User>) {}
async getUsers(): Promise<User[] | null> {
return await this.userModel.find().exec();
}
async signupUserWithInput(createUserDto: CreateUserDto): Promise<User> {
const username = createUserDto.username;
const user = await this.userModel.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const createdUser = new this.userModel(createUserDto);
return await createdUser.save();
}
async signupUser({ username, email, password }): Promise<User> {
const user = await this.userModel.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const createdUser = new this.userModel({ username, email, password });
return await createdUser.save();
}
}
users module
5.Create the - We are going to use the
NestJs CLI
to create theusers
module.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate module users
CREATE /src/users/users.module.ts (82 bytes)
UPDATE /src/app.module.ts (438 bytes)
src/users/users.module.ts
import { Module } from "@nestjs/common";
@Module()
export class UsersModule {}
- We need to modify the
module
created to define the imports and provides used forUsers
src/users/users.module.ts
import { Module } from "@nestjs/common";
import { TypegooseModule } from "nestjs-typegoose";
import { User } from "./model/user.model";
import { UsersService } from "./users.service";
import { UsersResolver } from "./graphql/users.resolver";
@Module({
imports: [TypegooseModule.forFeature([User])],
providers: [UsersService, UsersResolver]
})
export class UsersModule {}
app.module.ts
6.-Modify the - We need to modify the
app.module.ts
to include theUsersModule
and theGraphQLModule
.
src/app.module.ts
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
import { GraphQLModule } from "@nestjs/graphql";
import { UsersModule } from './users/users.module';
@Module({
imports: [
DatabaseModule,
UsersModule,cd ..
GraphQLModule.forRoot({
autoSchemaFile: "schema.gql"
}),
],
providers: [AppService]
})
export class AppModule { }
7.-Examples
request
{
getUsers {
_id
username
email
password
avatar
joinDate
favorites
}
}
response
{
"data": {
"getUsers": [
{
"_id": "5d04cf30deb8673e8c38fc2d",
"username": "John",
"email": "john@gmail.com",
"password": "Password",
"avatar": null,
"joinDate": "2019-06-15T10:57:52.586Z",
"favorites": null
},
{
"_id": "5d0da3d023e0c02b9c90ef2a",
"username": "Paul",
"email": "paul@gmail.com",
"password": "Password",
"avatar": null,
"joinDate": "2019-06-22T03:43:12.472Z",
"favorites": null
},
{
"_id": "5d0da5312ce4d742386276af",
"username": "Sarah",
"email": "sarah@gmail.com",
"password": "Password",
"avatar": null,
"joinDate": "2019-06-22T03:49:05.357Z",
"favorites": null
},
{
"_id": "5d0da82b9b5def3308ad242a",
"username": "Mary",
"email": "mary@gmail.com",
"password": "Password",
"avatar": null,
"joinDate": "2019-06-22T04:01:47.521Z",
"favorites": null
},
{
"_id": "5d0e10127dd57444d060778e",
"username": "Jess",
"email": "jess@gmail.com",
"password": "Password",
"avatar": null,
"joinDate": "2019-06-22T11:25:06.222Z",
"favorites": null
},
{
"_id": "5d0e10297dd57444d060778f",
"username": "Juan",
"email": "juan@gmail.com",
"password": "Password",
"avatar": null,
"joinDate": "2019-06-22T11:25:29.692Z",
"favorites": null
}
]
}
}
request
mutation {
signupUser(username: "Juan", email: "juan@gmail.com", password: "Password") {
_id
username
email
avatar
password
joinDate
}
}
response
{
"data": {
"signupUser": {
"_id": "5d0e10297dd57444d060778f",
"username": "Juan",
"email": "juan@gmail.com",
"avatar": null,
"password": "Password",
"joinDate": "2019-06-22T11:25:29.692Z"
}
}
}
request
mutation {
signupUserWithInput(
input: { username: "Jess", email: "jess@gmail.com", password: "Password" }
) {
_id
username
email
avatar
password
joinDate
}
}
response
{
"data": {
"signupUserWithInput": {
"_id": "5d0e10127dd57444d060778e",
"username": "Jess",
"email": "jess@gmail.com",
"avatar": null,
"password": "Password",
"joinDate": "2019-06-22T11:25:06.222Z"
}
}
}
Post
model.
VI.-Create the posts
folder with a module
and a `service.
1.-Create the - We need to create the
usr/posts
folder and use theNestJs CLI
to create themodule
and theservice
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate module posts
CREATE /src/posts/posts.module.ts (82 bytes)
UPDATE /src/app.module.ts (576 bytes)
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ nest generate service posts
CREATE /src/posts/posts.service.spec.ts (453 bytes)
CREATE /src/posts/posts.service.ts (89 bytes)
UPDATE /src/posts/posts.module.ts (507 bytes)
- The
src/app.module.ts
document is updated automatically
import { Module } from "@nestjs/common";
import { AppService } from "./app.service";
import { DatabaseModule } from "./database/database.module";
import { GraphQLModule } from "@nestjs/graphql";
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [
DatabaseModule,
GraphQLModule.forRoot({
autoSchemaFile: "schema.gql"
}),
UsersModule,
PostsModule,
],
providers: [AppService]
})
export class AppModule { }
model
folder and the model
documents.
2.-Create the - We need to create the
src/posts/model
andsrc/posts/model/dtos
folders and thepost.model.ts
andcreate-post.dto.ts
documents.
src/posts/model/post.model.ts
import * as mongoose from "mongoose";
import { prop, Typegoose } from 'typegoose';
import { IsString, IsDate, IsArray, IsInt } from 'class-validator';
import { User } from '../../users/model/user.model'
class Message {
@IsString()
@prop({ required: true })
messageBody: string;
@IsDate()
@prop({ default: Date.now })
messageDate: Date;
@IsString()
@prop({ required: true, ref: User })
messageUser: mongoose.Schema.Types.ObjectId
}
export class Post extends Typegoose {
@IsString()
@prop({ required: true })
title: string;
@IsString()
@prop({ required: true })
imageUrl: string;
@IsString()
@prop({ required: true })
categories: string[];
@IsString()
@prop({ required: true })
description: string;
@IsDate()
@prop({ default: Date.now })
createdDate: Date
@IsInt()
@prop({ default: 0 })
likes: number;
@IsString()
@prop({ required: true, ref: User })
createdBy: mongoose.Schema.Types.ObjectId
@IsArray()
@prop({ ref: Message })
messages: [Message]
}
src/posts/model/dtos/create-post.dto.ts
export class CreatePostDto {
readonly title: string;
readonly imageUrl: string;
readonly categories: string[];
readonly description: string;
readonly createdBy: string;
}
grahpql
folder with the graphql
documents.
3.-Create the - We need to create the
src/posts/graphql
,src/posts/graphql/inputs
andsrc/posts/graphql/types
folders and theusers.resolver.ts
,post.input.ts
andpost.type.ts
documents.
src/posts/graphql/post.resolver.ts
import { Resolver, Query, Mutation, Args } from "@nestjs/graphql";
import { PostsService } from "../posts.service";
import { PostInput } from "./inputs/post.input";
import { Post } from "./types/post.type";
@Resolver()
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query(() => [Post])
async getPosts() {
return await this.postsService.getPosts();
}
@Mutation(() => Post)
async addPostWithInput(@Args("input") input: PostInput) {
const createdBy = input.creatorId;
const { title, imageUrl, categories, description } = input;
return await this.postsService.addPost({
title,
imageUrl,
categories,
description,
createdBy
});
}
@Mutation(() => Post)
async addPost(
@Args("title") title: string,
@Args("imageUrl") imageUrl: string,
@Args({ name: "categories", type: () => [String] }) categories: [string],
@Args("description") description: string,
@Args("creatorId") creatorId: string
) {
const createdBy = creatorId;
return await this.postsService.addPost({
title,
imageUrl,
categories,
description,
createdBy
});
}
}
src/posts/graphql/inputs/post.input.ts
import { InputType, Field, ID } from 'type-graphql';
@InputType()
export class PostInput {
@Field()
readonly title: string;
@Field()
readonly imageUrl: string;
@Field(() => [String], { nullable: "items" })
readonly categories: string[];
@Field()
readonly description: string;
@Field(() => ID)
readonly creatorId: string;
}
src/posts/graphql/inputs/post.type.ts
import { Field, ObjectType, ID, Int } from 'type-graphql';
import { User } from '../../../users/graphql/types/user.type'
@ObjectType()
class Message {
@Field(() => ID)
readonly _id: string
@Field()
readonly messageBody: string;
@Field()
readonly messageDate: Date;
@Field()
readonly messageUser: string;
}
@ObjectType()
export class Post {
@Field(() => ID)
readonly _id: string
@Field()
readonly title: string;
@Field()
readonly imageUrl: string;
@Field(() => [String], { nullable: "items" })
readonly categories: string[];
@Field()
readonly description: string;
@Field()
readonly createdDate: Date
@Field(() => Int, { nullable: true })
likes: number;
@Field(() => User)
readonly createdBy: User
@Field(() => [Message], { nullable: true })
readonly messages: [Message]
}
module
and service
documents.
4.-Modify the - We need to modify the
module
created to define the imports and provides used forposts
src/users/posts.module.ts
import { Module } from "@nestjs/common";
import { TypegooseModule } from "nestjs-typegoose";
import { Post } from "./model/post.model";
import { PostsService } from "./posts.service";
import { PostsResolver } from "./graphql/posts.resolver";
@Module({
imports: [TypegooseModule.forFeature([Post])],
providers: [PostsService, PostsResolver]
})
export class PostsModule {}
- We need to modify the
service
to define the methods used to manage the access to theposts
collection.
src/users/posts.service.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "nestjs-typegoose";
import { Post } from "./model/post.model";
import { CreatePostDto } from "./model/dtos/create-post.dto";
import { ModelType } from "typegoose";
@Injectable()
export class PostsService {
constructor(@InjectModel(Post) private readonly postModel: ModelType<Post>) {}
async getPosts(): Promise<Post[] | null> {
const posts = await this.postModel
.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
}
async addPost(createPostDto: CreatePostDto): Promise<Post> {
const newPost = new this.postModel(createPostDto);
return await newPost.save();
}
}
5.-Examples
request
{
getPosts {
_id
title
imageUrl
categories
description
createdDate
likes
createdBy {
_id
username
email
avatar
password
joinDate
}
}
}
response
{
"data": {
"getPosts": [
{
"_id": "5d0e0fdc7dd57444d060778d",
"title": "Credenza",
"imageUrl": "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567",
"categories": ["Furniture"],
"description": "A piece of furniture I want to buy",
"createdDate": "2019-06-22T11:24:12.972Z",
"likes": 0,
"createdBy": {
"_id": "5d0da3d023e0c02b9c90ef2a",
"username": "Paul",
"email": "paul@gmail.com",
"avatar": null,
"password": "Password",
"joinDate": "2019-06-22T03:43:12.472Z"
}
},
{
"_id": "5d04d377deb8673e8c38fc2f",
"title": "Mona lisa",
"imageUrl": "https://images.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.readingpublicmuseum.org%2Fexhibit_secrets-of-mona-lisa-4.jpg&f=1",
"categories": ["Art"],
"description": "A painting",
"createdDate": "2019-06-15T11:16:07.195Z",
"likes": 0,
"createdBy": {
"_id": "5d04cf30deb8673e8c38fc2d",
"username": "John",
"email": "john@gmail.com",
"avatar": null,
"password": "Password",
"joinDate": "2019-06-15T10:57:52.586Z"
}
}
]
}
}
request
mutation {
addPost(
title: "Credenza"
imageUrl: "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567"
categories: ["Furniture"]
description: "A piece of furniture I want to buy"
creatorId: "5d0da3d023e0c02b9c90ef2a"
) {
title
imageUrl
categories
description
createdDate
likes
}
}
response
{
"data": {
"addPost": {
"title": "Credenza",
"imageUrl": "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567",
"categories": ["Furniture"],
"description": "A piece of furniture I want to buy",
"createdDate": "2019-06-22T11:24:12.972Z",
"likes": 0
}
}
}
request
mutation {
addPostWithInput(
input: {
title: "Tasty coffee"
imageUrl: "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
categories: ["Art", "Food"]
description: "Some nice coffee artwork"
creatorId: "5d0da3d023e0c02b9c90ef2a"
}
) {
title
imageUrl
categories
description
createdDate
likes
}
}
response
{
"data": {
"addPostWithInput": {
"title": "Tasty coffee",
"imageUrl": "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
"categories": ["Art", "Food"],
"description": "Some nice coffee artwork",
"createdDate": "2019-06-22T11:27:58.416Z",
"likes": 0
}
}
}
Section 5: Create Vue Frontend with NuxtJs
NuxtJs
app using the create-nuxt-app
CLI
I.Create the Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npx create-nuxt-app client
npx: installed 379 in 37.171s
> Generating Nuxt.js project in C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client
? Project name client
? Project description Frontend for the Nuxt version of [Full-Stack Vue with GraphQL - The Ultimate Guide
? Use a custom server framework none
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier
? Use a custom UI framework vuetify
? Use a custom test framework jest
? Choose rendering mode Universal
? Author name Juan Pablo Perez
? Choose a package manager npm
\ Installing packages with npm
Choose a package manager npm
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0
npm
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0
Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js-pure@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\nodemon
> node bin/postinstall || exit 0
Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate
> client@1.0.0 lint C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client
> eslint --ext .js,.vue --ignore-path .gitignore . "--fix"
To get started:
cd client
npm run dev
To build & start for production:
cd client
npm run build
npm start
To test:
cd client
npm run test
- Run de app to check if it works properly
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm run dev
> client@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client
> nuxt
╭─────────────────────────────────────────────╮
│ │
│ Nuxt.js v2.8.1 │
│ Running in development mode (universal) │
│ │
│ Listening on: http://localhost:3000/ │
│ │
╰─────────────────────────────────────────────╯
i Preparing project for development 16:34:20
i Initial build may take a while 16:34:20
√ Builder initialized 16:34:21
√ Nuxt files generated 16:34:21
√ Client
Compiled successfully in 14.51s
√ Server
Compiled successfully in 11.43s
ERROR (node:13864) DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead 16:34:25
i Waiting for file changes 16:34:40
i Memory usage: 235 MB (RSS: 337 MB)
- Browse to http://localhost:3000/
II.Create a script to run server and client at the same time
We are going to run server and client at the same time by using the concurrently package.
We need to create a new empty
package.json
file withnpm init -y
at the main folder.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm init -y
Wrote to C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\package.json:
{
"name": "full-stack-vue-with-graphql-the-ultimate-guide-nuxt",
"version": "1.0.0",
"description": "Source code for the NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' Udemy course.", "main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/issues"
},
"homepage": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt#readme"
}
- We need to install the
concurrently
package.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm i --save-dev concurrently
npm notice created a lockfile as package-lock.json. You should commit this file.
+ concurrently@4.1.0
added 86 packages from 48 contributors and audited 103 packages in 21.691s
found 0 vulnerabilities
- Change the
server/src/main.ts
to run the server on port4000
server/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT || 4000);
}
bootstrap();
- Modify the
package.json
document to create the scripts needed to run theserver
and theclient
at the same time
package.json
{
"name": "full-stack-vue-with-graphql-the-ultimate-guide-nuxt",
"version": "1.0.0",
"description": "Source code for the NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' Udemy course.",
"main": "index.js",
"scripts": {
"server": "cd server && npm run start:dev",
"client": "cd client && npm run dev",
"dev": "concurrently --names \"server,client\" \"npm run server --silent\" \"npm run client --silent\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/issues"
},
"homepage": "https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt#readme",
"devDependencies": {
"concurrently": "^4.1.0"
}
}
- Execute the
npm run dev
script.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm run dev
> full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt
> concurrently --names "server,client" "npm run server --silent" "npm run client --silent"
5:16:48 PM - Starting compilation in watch mode...
[server] [1]
[server] [0] [nodemon] 1.19.1
[server] [0] [nodemon] to restart at any time, enter `rs`
[server] [0] [nodemon] watching: C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\server\dist/**/*
[server] [0] [nodemon] starting `node dist/main`
[client] i Listening on: http://localhost:3000/
[client] i Preparing project for development
[client] i Initial build may take a while
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [NestFactory] Starting Nest application...
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] AppModule dependencies initialized +97ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] DatabaseModule dependencies initialized +2ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] TypegooseModule dependencies initialized +1ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] ConfigModule dependencies initialized +2ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] GraphQLModule dependencies initialized +24ms
[server] [1] node_modules/apollo-server-express/dist/ApolloServer.d.ts(1,8): error TS1192: Module '"C:/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server/node_modules/@types/express/index"' has no default export.
[server] [1] node_modules/apollo-server-express/dist/ApolloServer.d.ts(2,8): error TS1192: Module '"C:/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server/node_modules/@types/cors/index"' has no default export.
[server] [1]
[server] [1] 5:17:13 PM - Found 2 errors. Watching for file changes.
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] TypegooseCoreModule dependencies initialized +1552ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] TypegooseModule dependencies initialized +2ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] TypegooseModule dependencies initialized +2ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] UsersModule dependencies initialized +3ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [InstanceLoader] PostsModule dependencies initialized +2ms
[server] [0] [Nest] 17600 - 06/22/2019, 5:17 PM [NestApplication] Nest application successfully started +641ms
[client] √ Builder initialized
[client] √ Nuxt files generated
[client] i Compiling Client
[client] i Compiling Server
- Browse to http://localhost:3000/ to see the client
- Browse to http://localhost:4000/graphql to see the server
graphql
Vuetify
Theme
III.-Set up the - Modify the
nuxt.config.js
document to change thevuetify.theme
to use the same colours as the ones from the 'original` Vue app.
/client/nuxt.config.js
// import colors from 'vuetify/es5/util/colors'
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js modules
*/
modules: ['@nuxtjs/vuetify', '@nuxtjs/pwa', '@nuxtjs/eslint-module'],
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
// primary: colors.blue.darken2,
// accent: colors.grey.darken3,
// secondary: colors.amber.darken3,
// info: colors.teal.lighten1,
// warning: colors.amber.base,
// error: colors.deepOrange.accent4,
// success: colors.green.accent3
primary: '#3B125F',
secondary: '#8B5FBF',
accent: '#BF653F',
error: '#722530',
warning: '#A37513',
info: '#396893',
success: '#4caf50'
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
Pages
(and remove the test code)
IV.-Create the - Modify the
client/layouts/default.vue
document to create the layout based on the originalApp.vue
document.
client/layouts/default.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer v-model="sideNav" fixed app temporary>
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<nuxt-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</nuxt-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
v-for="(item, i) in sideNavItems"
:key="i"
:to="item.link"
ripple
>
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{ item.title }}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link to="/" tag="span" style="cursor: pointer">
VueShare
</nuxt-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
v-for="(item, i) in horizontalNavItems"
:key="i"
:to="item.link"
flat
>
<v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
</v-app>
</template>
<script>
export default {
data() {
return {
sideNav: false
}
},
computed: {
horizontalNavItems() {
return [
{ icon: 'chat', title: 'Posts', link: '/posts' },
{ icon: 'lock_open', title: 'Sign In', link: '/signin' },
{ icon: 'create', title: 'Sign Up', link: '/signup' }
]
},
sideNavItems() {
return [
{ icon: 'chat', title: 'Posts', link: '/posts' },
{ icon: 'lock_open', title: 'Sign In', link: '/signin' },
{ icon: 'create', title: 'Sign Up', link: '/signup' }
]
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav
}
}
}
</script>
- Modify the
client/pages/index.vue
document copy the content from the original/components/Home.vue
client/pages/index.vue
<template>
<v-container>
<h1>Home</h1>
</v-container>
</template>
<script>
export default {
name: 'Home'
}
</script>
- Create the
post
folder with theadd.vue
andindex.vue
document.
client/pages/posts/add.vue
<template>
<v-container>
<h1>Add Post</h1>
</v-container>
</template>
<script>
export default {
name: 'AddPost'
}
</script>
client/pages/posts/index.vue
<template>
<v-container>
<h1>Posts</h1>
</v-container>
</template>
<script>
export default {
name: 'Posts'
}
</script>
- Create the
profile
folder with theindex.vue
document.
client/pages/profile/index.vue
<template>
<v-container>
<h1>Profile</h1>
</v-container>
</template>
<script>
export default {
name: 'Profile'
}
</script>
- Create the
signin
folder with theindex.vue
document.
client/pages/signin/index.vue
<template>
<v-container>
<h1>Signin</h1>
</v-container>
</template>
<script>
export default {
name: 'Signin'
}
</script>
- Create the
signup
folder with theindex.vue
document.
client/pages/signup/index.vue
<template>
<v-container>
<h1>Signup</h1>
</v-container>
</template>
<script>
export default {
name: 'Signup'
}
</script>
- Check if the application works the same way as the original application.
Typescript
V.-Sep up - We are going to follow the TypeScript Support documentation to set up the use of
Typescript
with ourNuxt
application
Typescript
packages
1.Install the needed - We need to install
@nuxt/typescript
indevDependencies
andts-node
independencies
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i -D @nuxt/typescript
npm WARN deprecated @types/chokidar@2.1.3: This is a stub types definition. chokidar provides its own type definitions, so you do not need this installed.
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fork-ts-checker-webpack-plugin\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @nuxt/typescript@2.8.1
added 55 packages from 162 contributors and audited 884211 packages in 72.918s
found 0 vulnerabilities
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i ts-node
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fork-ts-checker-webpack-plugin\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ ts-node@8.3.0
added 5 packages from 4 contributors and audited 884219 packages in 54.323s
found 0 vulnerabilities
tsconfig.json
document
2.-Create the - We need to create an empty
tsconfig.json
file in the root project folder.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ touch tsconfig.json
' Note:
The tsconfig.json
file will automatically update with default values the first time we run the nuxt
command.
nuxt.config.js
document to nuxt.config.ts
and modify it.
3.-rename the - To use TypeScript in the configuration file, we need to rename
nuxt.config.js
tonuxt.config.ts
and change the document.
nuxt.config.ts
// import colors from 'vuetify/es5/util/colors'
import NuxtConfiguration from '@nuxt/config'
const config: NuxtConfiguration = {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js modules
*/
modules: ['@nuxtjs/vuetify', '@nuxtjs/pwa', '@nuxtjs/eslint-module'],
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
// primary: colors.blue.darken2,
// accent: colors.grey.darken3,
// secondary: colors.amber.darken3,
// info: colors.teal.lighten1,
// warning: colors.amber.base,
// error: colors.deepOrange.accent4,
// success: colors.green.accent3
primary: '#3B125F',
secondary: '#8B5FBF',
accent: '#BF653F',
error: '#722530',
warning: '#A37513',
info: '#396893',
success: '#4caf50'
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
export default config;
Linting
with ESLint
4.-Set up - We need to install
@typescript-eslint/eslint-plugin
and@typescript-eslint/parser
.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fork-ts-checker-webpack-plugin\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @typescript-eslint/parser@1.10.2
+ @typescript-eslint/eslint-plugin@1.10.2
added 8 packages from 5 contributors and audited 884247 packages in 64.087s
found 0 vulnerabilities
- We need to edit the ESLint configuration
.eslintrc.js
document by adding the@typescript-eslint
plugin and making@typescript-eslint/parser
the default parser.
.eslintrc.js (before changes)
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'plugin:prettier/recommended',
'prettier',
'prettier/vue'
],
plugins: [
'prettier'
],
// add your custom rules here
rules: {
"vue/component-name-in-template-casing": ["error", "PascalCase"],
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
},
}
.eslintrc.js (after changes)
module.exports = {
plugins: ['@typescript-eslint','prettier'],
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: '@typescript-eslint/parser'
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'plugin:prettier/recommended',
'prettier',
'prettier/vue'
],
// add your custom rules here
rules: {
'@typescript-eslint/no-unused-vars': 'error',
"vue/component-name-in-template-casing": ["error", "PascalCase"],
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
},
}
- Finally, add or edit the lint script of the
package.json
document.
. package.json
{
"name": "client",
"version": "1.0.0",
"description": "Frontend for the Nuxt version of [Full-Stack Vue with GraphQL - The Ultimate Guide",
"author": "Juan Pablo Perez",
"private": true,
"scripts": {
"lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
"precommit": "npm run lint",
"test": "jest",
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
},
"dependencies": {
"@nuxtjs/pwa": "^2.6.0",
"@nuxtjs/vuetify": "0.5.5",
"nuxt": "^2.0.0",
"nuxt-i18n": "^5.12.7",
"ts-node": "^8.3.0"
},
"devDependencies": {
"@nuxt/typescript": "^2.8.1",
"@nuxtjs/eslint-config": "^0.0.1",
"@nuxtjs/eslint-module": "^0.0.1",
"@typescript-eslint/eslint-plugin": "^1.10.2",
"@typescript-eslint/parser": "^1.10.2",
"@vue/test-utils": "^1.0.0-beta.27",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
"eslint": "^5.15.1",
"eslint-config-prettier": "^4.1.0",
"eslint-config-standard": ">=12.0.0",
"eslint-plugin-import": ">=2.16.0",
"eslint-plugin-jest": ">=22.3.0",
"eslint-plugin-node": ">=8.0.1",
"eslint-plugin-nuxt": ">=0.4.2",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-promise": ">=4.0.1",
"eslint-plugin-standard": ">=4.0.0",
"eslint-plugin-vue": "^5.2.2",
"jest": "^24.1.0",
"nodemon": "^1.18.9",
"prettier": "^1.16.4",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"vue-jest": "^3.0.3"
}
}
pages
to use TypeScript
5.-Modify the - We need to install the
vue-property-decorator
package that is going to help usingTypeScripts
inVue
documents.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm i vue-property-decorator
npm WARN full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 scripts['server'] should probably be scripts['start'].
+ vue-property-decorator@8.2.1
added 3 packages from 2 contributors and audited 106 packages in 6.878s
found 0 vulnerabilities
layouts/default.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer v-model="sideNav" fixed app temporary>
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<nuxt-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</nuxt-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
v-for="(item, i) in sideNavItems"
:key="i"
:to="item.link"
ripple
>
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{ item.title }}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link to="/" tag="span" style="cursor: pointer">
VueShare
</nuxt-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
v-for="(item, i) in horizontalNavItems"
:key="i"
:to="item.link"
flat
>
<v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-content>
<v-container>
<nuxt />
</v-container>
</v-content>
</v-app>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Layout extends Vue {
sideNav: boolean = false
// computed
get horizontalNavItems() {
return [
{ icon: 'chat', title: 'Posts', link: '/posts' },
{ icon: 'lock_open', title: 'Sign In', link: '/signin' },
{ icon: 'create', title: 'Sign Up', link: '/signup' }
]
}
get sideNavItems() {
return [
{ icon: 'chat', title: 'Posts', link: '/posts' },
{ icon: 'lock_open', title: 'Sign In', link: '/signin' },
{ icon: 'create', title: 'Sign Up', link: '/signup' }
]
}
// methods
toggleSideNav() {
this.sideNav = !this.sideNav
}
}
</script>
layout/error.vue (It hasn't been changed)
<template>
<v-app dark>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
<h1 v-else>
{{ otherError }}
</h1>
<NuxtLink to="/">
Home page
</NuxtLink>
</v-app>
</template>
<script>
export default {
layout: 'empty',
props: {
error: {
type: Object,
default: null
}
},
head() {
const title =
this.error.statusCode === 404 ? this.pageNotFound : this.otherError
return {
title
}
},
data() {
return {
pageNotFound: '404 Not Found',
otherError: 'An error occurred'
}
}
}
</script>
<style scoped>
h1 {
font-size: 20px;
}
</style>
pages/posts/add.Vue
<template>
<v-container>
<h1>Add Post</h1>
</v-container>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class AddPostPage extends Vue {
name: string = 'AddPost'
}
</script>
pages/posts/index.Vue
<template>
<v-container>
<h1>Posts</h1>
</v-container>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
name: string = 'Posts'
}
</script>
pages/profile/index.Vue
<template>
<v-container>
<h1>Posts</h1>
</v-container>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
name: string = 'Posts'
}
</script>
pages/signin/index.Vue
<template>
<v-container>
<h1>Posts</h1>
</v-container>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
name: string = 'Posts'
}
</script>
pages/signup/index.Vue
<template>
<v-container>
<h1>Posts</h1>
</v-container>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class Posts extends Vue {
name: string = 'Posts'
}
</script>
pages/index.Vue
<template>
<v-container>
<h1>Home</h1>
</v-container>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
export default class HomePage extends Vue {
name: string = 'Home'
}
</script>
6.-Run the application to ensure it's working
- Browse to http://localhost:3000/
- It doesn't work. It seems as if the
layout/default.vue
is not loaded.
typescript
branch to keep the changes
7.-Create the - We are going to create a new branch to keep the changes applied but we are not going to use
TypeScript
at the moment. Follow the Move existing, uncommitted work to a new branch in Git to create the new branch.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ git checkout -b typescript
Switched to a new branch 'typescript'
M client/.eslintrc.js
M client/layouts/default.vue
D client/nuxt.config.js
M client/package-lock.json
M client/package.json
M client/pages/index.vue
M client/pages/posts/add.vue
M client/pages/posts/index.vue
M client/pages/profile/index.vue
M client/pages/signin/index.vue
M client/pages/signup/index.vue
M package-lock.json
M package.json
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git add .
warning: LF will be replaced by CRLF in client/.eslintrc.js.
The file will have its original line endings in your working directory
.
.
.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git status
On branch typescript
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: client/.eslintrc.js
modified: client/layouts/default.vue
renamed: client/nuxt.config.js -> client/nuxt.config.ts
modified: client/package-lock.json
modified: client/package.json
modified: client/pages/index.vue
modified: client/pages/posts/add.vue
modified: client/pages/posts/index.vue
modified: client/pages/profile/index.vue
modified: client/pages/signin/index.vue
modified: client/pages/signup/index.vue
new file: client/tsconfig.json
modified: package-lock.json
modified: package.json
- Push the changes
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git push --set-upstream origin typescript
Enumerating objects: 42, done.
Counting objects: 100% (42/42), done.
Delta compression using up to 4 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (23/23), 12.30 KiB | 286.00 KiB/s, done.
Total 23 (delta 12), reused 0 (delta 0)
remote: Resolving deltas: 100% (12/12), completed with 6 local objects.
remote:
remote: Create a pull request for 'typescript' on GitHub by visiting:
remote: https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/pull/new/typescript
remote:
To https://github.com/peelmicro/full-stack-vue-with-graphql-the-ultimate-guide-nuxt.git
* [new branch] typescript -> typescript
Branch 'typescript' set up to track remote branch 'typescript' from 'origin'.
- Go back to the
master
branch
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (typescript)
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
multi-language
VI.-Set up multi-language
on the A simple multi-language site with Nuxt.js and nuxt-i18n articule
1.-Base the - We are going to follow the A simple multi-language site with Nuxt.js and nuxt-i18n articule and the nuxt-i18n web site.
nuxt-i18n
package
2.-Install the Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i nuxt-i18n --save
npm WARN @nuxtjs/vuetify@0.5.5 requires a peer of vuetify-loader@^1.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ nuxt-i18n@5.12.7
added 7 packages from 6 contributors and audited 880983 packages in 37.288s
found 0 vulnerabilities
- Fix the
warn
detected
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i vuetify-loader
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ vuetify-loader@1.2.2
added 1 package and audited 880989 packages in 44.82s
found 0 vulnerabilities
lang
folder and the translations documents
3.-Create the - We need to create the
lang
folder and thees-ES.js
for theSpanish
translations and theen-US.js
for theEnglish
translations.
client/lang/es-ES.js
export default {
posts: 'Entradas',
signin: 'Iniciar sesión',
signup: 'Inscripción',
searchposts: 'Buscar entradas',
home: 'Inicio'
}
client/lang/en-US.js
export default {
posts: 'Posts',
signin: 'Sign in',
signup: 'Sign up',
searchposts: 'Search Posts',
home: 'Home'
}
nuxt.config.js
document to set up the pluging and the initial languages
4.-Modify the nuxt.config.js
import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js modules
*/
modules: [
[
'nuxt-i18n',
{
seo: false,
locales: [
{
name: 'ESP',
code: 'es',
iso: 'es-ES'
},
{
name: 'ENG',
code: 'en',
iso: 'en-US'
}
],
strategy: 'prefix_and_default',
langDir: 'lang/',
defaultLocale: 'es',
vueI18n: {
fallbackLocale: 'es',
messages: { es, en }
}
}
],
'@nuxtjs/vuetify',
'@nuxtjs/pwa',
'@nuxtjs/eslint-module'
],
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
// primary: colors.blue.darken2,
// accent: colors.grey.darken3,
// secondary: colors.amber.darken3,
// info: colors.teal.lighten1,
// warning: colors.amber.base,
// error: colors.deepOrange.accent4,
// success: colors.green.accent3
primary: '#3B125F',
secondary: '#8B5FBF',
accent: '#BF653F',
error: '#722530',
warning: '#A37513',
info: '#396893',
success: '#4caf50'
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
lang-switcher
button.
5.-Modify the layout to include a client/layouts/default.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer v-model="sideNav" fixed app temporary>
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</nuxt-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
v-for="(item, i) in sideNavItems"
:key="i"
:to="localePath(item.link)"
ripple
>
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{ item.title }}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
VueShare
</nuxt-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
color="accent"
single-line
hide-details
:placeholder="$t('searchposts')"
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
v-for="(item, i) in horizontalNavItems"
:key="i"
:to="localePath(item.link)"
flat
>
<v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
</v-toolbar-items>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link
v-for="(locale, i) in showLocales"
:key="i"
tag="span"
style="cursor: pointer"
class="lang-switcher"
:to="switchLocalePath(locale.code)"
>
{{ locale.name }}
</nuxt-link>
</v-toolbar-title>
</v-toolbar>
<v-content>
<v-container class="mt-4">
<transition name="fade">
<nuxt />
</transition>
</v-container>
</v-content>
</v-app>
</template>
<script>
export default {
head() {
return this.$nuxtI18nSeo()
},
data() {
return {
sideNav: false
}
},
computed: {
horizontalNavItems() {
return [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{ icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
{ icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
]
},
sideNavItems() {
return [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{ icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
{ icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
]
},
showLocales() {
return this.$i18n.locales.filter(
locale => locale.code !== this.$i18n.locale
)
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav
}
}
}
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
multi-language
works with the changes applied
6.-Test if the Section 6: Using Vue Apollo 0 / 4|24min
I.-Setting up Apollo Client / Vue Apollo, Firing getPosts Query from Client
- We are going to use the @nuxtjs/apollo package. We need to install the
@nuxtjs/apollo
and graphql-tag packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i @nuxtjs/apollo graphql-tag
> core-js@3.1.4 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\apollo-env\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> protobufjs@6.8.8 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\client\node_modules\protobufjs
> node scripts/postinstall
npm WARN vue-cli-plugin-apollo@0.20.0 requires a peer of @vue/cli-shared-utils@^3.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN ts-node@8.3.0 requires a peer of typescript@>=2.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ graphql-tag@2.10.1
+ @nuxtjs/apollo@4.0.0-rc7
added 100 packages from 126 contributors and audited 883960 packages in 64.792s
found 0 vulnerabilities
- Create the
apollo
folder and theapollo/customErrorHandler.js
document.
apollo/customErrorHandler.js
export default (err, { error }) => {
console.log(err)
error({ statusCode: 304, message: 'Server error!' })
}
- Create the
gql
folder and thegql/getPosts.gql
document.
gql/getPosts.gql
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
- We are going to modify the
client/nuxt.config.js
document to set up the use of the@nuxtjs/apollo
package.
client/nuxt.config.js
import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js modules
*/
modules: [
[
'nuxt-i18n',
{
seo: false,
locales: [
{
name: 'ESP',
code: 'es',
iso: 'es-ES'
},
{
name: 'ENG',
code: 'en',
iso: 'en-US'
}
],
strategy: 'prefix_and_default',
langDir: 'lang/',
defaultLocale: 'es',
vueI18n: {
fallbackLocale: 'es',
messages: { es, en }
}
}
],
'@nuxtjs/vuetify',
'@nuxtjs/pwa',
'@nuxtjs/eslint-module',
'@nuxtjs/apollo'
],
apollo: {
errorHandler: '~/apollo/customErrorHandler.js',
clientConfigs: {
default: {
httpEndpoint:
process.env.HTTP_ENDPOINT || 'http://localhost:4000/graphql'
}
}
},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
// primary: colors.blue.darken2,
// accent: colors.grey.darken3,
// secondary: colors.amber.darken3,
// info: colors.teal.lighten1,
// warning: colors.amber.base,
// error: colors.deepOrange.accent4,
// success: colors.green.accent3
primary: '#3B125F',
secondary: '#8B5FBF',
accent: '#BF653F',
error: '#722530',
warning: '#A37513',
info: '#396893',
success: '#4caf50'
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
- We are going to modify the
client/pages/index.vue
document to include a call to thegetPosts
graphqlquery
from the server.
client/pages/index.vue
<template>
<v-container>
<h1>Home</h1>
<ul v-for="post in getPosts" :key="post._id">
<li>{{ post.title }} {{ post.imageUrl }} {{ post.description }}</li>
<li>{{ post.likes }}</li>
</ul>
</v-container>
</template>
<script>
import { getPosts } from '~/gql/getPosts.gql'
export default {
name: 'Home',
apollo: {
getPosts: {
query: getPosts
}
}
}
</script>
- We need top test if everything works correctly.
- We are going to modify the
client/pages/index.vue
document to include theCorousel Vuetify
components.
client/pages/index.vue
<template>
<v-container v-if="getPosts" text-xs-center>
<v-flex xs12>
<v-carousel v-bind="{ cycle: true }" interval="3000">
<v-carousel-item
v-for="post in getPosts"
:key="post._id"
:src="post.imageUrl"
>
<h1 id="carousel__title">{{ post.title }}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { getPosts } from '~/gql/getPosts.gql'
export default {
name: 'Home',
apollo: {
getPosts: {
query: getPosts
}
}
}
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>
Section 7: Integrate Vuex with ApolloClient
GraphQL getPosts
query
I.Create the store to manage the client\store\index.js
import { getPosts } from '~/gql/getPosts.gql'
export const state = () => ({
posts: [],
loading: false
})
export const mutations = {
setPosts: (state, payload) => {
state.posts = payload
},
setLoading: (state, payload) => {
state.loading = payload
}
}
export const actions = {
async nuxtServerInit({ dispatch }) {
await dispatch('getPosts')
},
async getPosts({ commit }) {
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getPosts
})
commit('setPosts', result.data.getPosts)
} catch (error) {
console.log(error)
}
commit('setLoading', false)
}
}
export const getters = {
posts: state => state.posts,
loading: state => state.loading
}
Home
page to read the Posts from the store
II.-Modify the client\pages\index.vue
<template>
<v-container text-xs-center>
<v-layout row>
<v-dialog v-model="loading" persistent fullscreen>
<v-container fill-height>
<v-layout row justify-center align-center>
<v-progress-circular
indeterminate
:size="70"
:width="7"
color="secondary"
></v-progress-circular>
</v-layout>
</v-container>
</v-dialog>
</v-layout>
<v-flex xs12>
<v-carousel
v-if="!loading && posts.length > 0"
v-bind="{ cycle: true }"
interval="3000"
>
<v-carousel-item
v-for="post in posts"
:key="post._id"
:src="post.imageUrl"
>
<h1 id="carousel__title">{{ post.title }}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Home',
computed: {
...mapGetters(['loading', 'posts'])
}
}
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>
III.-Test the integration of Vuex with ApolloClient
Section 8: JWT Authentication for Signin / Signup
I.-Create Gravatar Avatar and Hash User Passwords on Signup
- We need to install the MD5 package, a JavaScript function for hashing messages with MD5, and the node.bcrypt.js package, A library to help you hash passwords.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm i md5 bcrypt
> bcrypt@3.0.6 install C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\node_modules\bcrypt
> node-pre-gyp install --fallback-to-build
node-pre-gyp WARN Using needle for node-pre-gyp https download
[bcrypt] Success: "C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide-nuxt\node_modules\bcrypt\lib\binding\bcrypt_lib.node" is installed via remote
npm WARN full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 scripts['server'] should probably be scripts['start'].
+ md5@2.2.1
+ bcrypt@3.0.6
added 65 packages from 51 contributors and audited 202 packages in 8.927s
found 0 vulnerabilities
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt (master)
$ npm --save-devnpm i --save-dev @types/md5 @types/bcrypt
npm WARN full-stack-vue-with-graphql-the-ultimate-guide-nuxt@1.0.0 scripts['server'] should probably be scripts['start'].
+ @types/md5@2.1.33
+ @types/bcrypt@3.0.0
added 3 packages from 41 contributors and audited 205 packages in 5.411s
found 0 vulnerabilities
- We need to modify the
User's Model
to put the@prop
attribute to theavatar
field.
server\src\users\model\user.model.ts
import * as mongoose from "mongoose";
import { prop, Typegoose } from 'typegoose';
import { IsString, IsDate, IsArray } from 'class-validator';
export class User extends Typegoose {
@IsString()
@prop({ required: true, unique: true, trim: true })
username: string;
@IsString()
@prop({ required: true, trim: true })
email: string;
@IsString()
@prop({ required: true, trim: true })
password: string;
@IsString()
@prop()
avatar?: string;
@IsDate()
@prop({ default: Date.now })
joinDate: Date
@IsArray()
@prop({ required: true, ref: "Post" })
favorites: [mongoose.Schema.Types.ObjectId]
}
- We need to modify the
User Service
to assign a ramndom avatar to the user and to store the password hashed.
server\src\users\users.service.ts
import * as mongoose from "mongoose";
import { prop, Typegoose } from 'typegoose';
import { IsString, IsDate, IsArray } from 'class-validator';
export class User extends Typegoose {
@IsString()
@prop({ required: true, unique: true, trim: true })
username: string;
@IsString()
@prop({ required: true, trim: true })
email: string;
@IsString()
@prop({ required: true, trim: true })
password: string;
@IsString()
@prop()
avatar?: string;
@IsDate()
@prop({ default: Date.now })
joinDate: Date
@IsArray()
@prop({ required: true, ref: "Post" })
favorites: [mongoose.Schema.Types.ObjectId]
}
- We can test if the request to sign up a new user is working properly now.
mutation {
signupUserWithInput(
input: { username: "John", email: "john@google.com", password: "John" }
) {
_id
username
email
password
avatar
joinDate
favorites
}
}
{
"data": {
"signupUserWithInput": {
"_id": "5d36a9e007bced421c3a2031",
"username": "John",
"email": "john@google.com",
"password": "$2b$10$OPv9/YQC1zZatdGEDw5YYOjiUE2omaBjuiSn5Bf9cJXahre7A/hNO",
"avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon",
"joinDate": "2019-07-23T06:32:00.328Z",
"favorites": null
}
}
}
II.-Modify the server app to include the authentication.
passport
and jwt
packaged needed to manage the authentication
1.-Install the - We need to install the @nestjs/jwt, @nestjs/passport, passport and passport-jwt packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i @nestjs/jwt @nestjs/passport passport passport-jwt
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ passport-jwt@4.0.0
+ passport@0.4.0
+ @nestjs/passport@6.1.0
+ @nestjs/jwt@6.1.1
added 20 packages from 17 contributors and audited 877489 packages in 75.148s
found 0 vulnerabilities
- We also need to install the @types/passport-jwt packagae.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/server (master)
$ npm i --save-dev @types/passport-jwt
npm WARN server@0.0.1 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\@nestjs\graphql\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @types/passport-jwt@3.0.1
added 3 packages from 12 contributors and audited 877533 packages in 15.893s
found 0 vulnerabilities
2.-Create the SECRET environment variable
- We need to create the new
SECRET
environment variable needed for theJWT
token:
server\fake.env
MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
SECRET=mysupersecretencryptionkey
- We need to modify the
config.service.ts
document to include the newSECRET
environment variable:
server\src\config\config.service.ts
import * as dotenv from 'dotenv'
import * as fs from 'fs'
export class ConfigService {
MONGODB_URI: string
private readonly envConfig: { [key: string]: string }
constructor() {
if (
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'staging'
) {
this.envConfig = {
MONGO_URI: process.env.MONGO_URI,
SECRET: process.env.SECRET
}
} else {
this.envConfig = dotenv.parse(fs.readFileSync('.env'))
}
}
get(key: string): string {
return this.envConfig[key]
}
}
3.-Create the Auth Service
We need to create the
Auth
folder to put inside theAuth Service
and everything related. This service is going to be used to manage theAuthentication
and theAuthorisation
of the GraphQLQueries
adnMutations
.We are going to create the
jwt-payload.interface.ts
to define the content of the 'jwt token`
server\src\auth\jwt-payload.interface.ts
export interface JwtPayload {
username: string
email: string
}
- We need to create the
graphql-auth.guard.ts
to create theGraphqlAuthGuard
guard derived from theNestJS AuthGuard
Guard to transform from theExceution Context
to aGraphQL context
server\src\auth\graphql-auth.guard.ts
import { Injectable, ExecutionContext } from "@nestjs/common";
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from "@nestjs/graphql";
@Injectable()
export class GraphqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
- We need to create the
jwt.strategy.ts
document with theJwtStrategy
class used to read theUser
information obtained from the token passed through theauthorization
header with any request. It is attached to the request on theuser
object.
server\src\auth\jwt.strategy.ts
import { PassportStrategy } from '@nestjs/passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtPayload } from './jwt-payload.interface'
import { InjectModel } from 'nestjs-typegoose'
import { User } from '../users/model/user.model'
import { ModelType } from 'typegoose'
import { ConfigService } from '../config/config.service'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectModel(User) private readonly userModel: ModelType<User>,
private readonly configService: ConfigService
) {
super({
jwtFromRequest: ExtractJwt.fromHeader("authorization"),
secretOrKey: configService.get('SECRET'),
})
}
async validate(payload: JwtPayload): Promise<User> {
const { username } = payload
const user = await this.userModel.findOne({
username: username
}).populate({
path: "favorites",
model: "Post"
})
if (!user) {
throw new UnauthorizedException()
}
return user
}
}
- We are going to create the
current-user.decorator.ts
to create theCurrentUser
custom decorator withUser
information obtained from the token passed through theauthentication
header with any request. Please, note it isauthentication
instead ofauthorisation
because it is the standard header for Apollo.
server\src\auth\current-user.decorator.ts
import { createParamDecorator } from '@nestjs/common'
export const CurrentUser = createParamDecorator(
(_data, [,, ctx]) => ctx.req.user,
)
- We are going to create the
auth.service.ts
document with theAuthService
service that has thesignUp
andsignIn
methods.
server\src\auth\auth.service.ts
import { Injectable, ConflictException, InternalServerErrorException, UnauthorizedException } from '@nestjs/common'
import { InjectModel } from 'nestjs-typegoose'
import { ModelType } from 'typegoose'
import { JwtService } from '@nestjs/jwt'
import { SigninUserDto } from '../users/model/dtos/signin-user.dto'
import { CreateUserDto } from '../users/model/dtos/create-user.dto'
import { Token, User } from '../users/model/user.model'
import { JwtPayload } from './jwt-payload.interface'
import md5 from 'md5'
import * as bcrypt from 'bcrypt'
@Injectable()
export class AuthService {
constructor(
@InjectModel(User) private readonly userModel: ModelType<User>,
private readonly jwtService: JwtService,
) { }
async signUp(createUserDto: CreateUserDto): Promise<Token> {
const username = createUserDto.username
const user = await this.userModel.findOne({ username })
if (user) {
throw new ConflictException("User already exists")
}
const avatar = `http://gravatar.com/avatar/${md5(username)}?d=identicon`
var salt = bcrypt.genSaltSync(10)
var password = bcrypt.hashSync(createUserDto.password, salt)
const currentUser = { ...createUserDto, avatar, password }
const createdUser = new this.userModel(currentUser)
try {
const newUser = await createdUser.save()
const token = this.getAccessToken(newUser)
return token
}
catch (error) {
throw new InternalServerErrorException()
}
}
async signIn(signinUserDto: SigninUserDto): Promise<Token> {
const { username, password } = signinUserDto
const user = await this.userModel.findOne({ username })
if (!user) {
throw new UnauthorizedException("Invalid credentials")
}
const isValidPassword = await bcrypt.compare(password, user.password)
if (!isValidPassword) {
throw new UnauthorizedException("Invalid credentials")
}
const token = this.getAccessToken(user)
return token
}
private async getAccessToken(user: User): Promise<Token> {
const { username, email } = user
const payload: JwtPayload = { username, email }
const token = await this.jwtService.sign(payload)
return { token }
}
}
- We finally need to create the
auth.module.ts
with theAuthModule
module. It needs to import thePassportModule
and theJwtModule
.
server\src\auth\auth.module.ts
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { JwtStrategy } from './jwt.strategy'
import { ConfigModule } from '../config/config.module';
import { ConfigService } from '../config/config.service';
import { TypegooseModule } from 'nestjs-typegoose'
import { User } from '../users/model/user.model';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('SECRET'),
signOptions: {
expiresIn: 216000, // 1 Hour: 60 * 60 * 60
},
}),
}),
TypegooseModule.forFeature([User]),
],
providers: [
JwtStrategy,
AuthService,
ConfigService
],
exports: [
AuthService
],
})
export class AuthModule {}
Users
Module
4.-Modify the files related to the - We need to modify the
app.module.ts
document to include thecontext
as part of theGraphQLModule
module.
server\src\app.module.ts
import { Module } from "@nestjs/common"
import { AppService } from "./app.service"
import { DatabaseModule } from "./database/database.module"
import { GraphQLModule } from "@nestjs/graphql"
import { UsersModule } from './users/users.module'
import { PostsModule } from './posts/posts.module'
@Module({
imports: [
DatabaseModule,
GraphQLModule.forRoot({
autoSchemaFile: "schema.gql",
context: ({ req }) => ({ req }),
}),
UsersModule,
PostsModule,
],
providers: [AppService]
})
export class AppModule { }
- We need to modify the
server\src\users\users.module.ts
document to inlcude theConfigService
provider adn theAuthModule
module.
server\src\users\users.module.ts
import { Module } from '@nestjs/common'
import { TypegooseModule } from 'nestjs-typegoose'
import { User } from './model/user.model'
import { UsersService } from './users.service'
import { UsersResolver } from "./graphql/users.resolver"
import { ConfigService } from '../config/config.service'
import { AuthModule } from '../auth/auth.module'
@Module({
imports: [
TypegooseModule.forFeature([User]),
AuthModule
],
providers: [UsersService, UsersResolver, ConfigService]
})
export class UsersModule { }
- We are going to create the new
SignUserDto
Data Transfer Object.
server\src\users\model\dtos\signin-user.dto.ts
export class SigninUserDto {
readonly username: string;
readonly password: string
}
- We are going to modify the
user.type.ts
anduser.model.ts
documents to include theToken
class.
server\src\users\graphql\types\user.type.ts
import { Field, ObjectType, ID } from 'type-graphql'
@ObjectType()
export class Token {
@Field()
readonly token: string
}
@ObjectType()
export class User {
@Field(() => ID)
readonly _id: string
@Field()
readonly username: string
@Field()
readonly email: string
@Field()
readonly password: string
@Field({ nullable: true })
readonly avatar: string
@Field()
readonly joinDate: Date
@Field(() => [ID], { nullable: true })
readonly favorites: string[]
}
server\src\users\model\user.model.ts
import * as mongoose from "mongoose"
import { prop, Typegoose } from 'typegoose'
import { IsString, IsDate, IsArray } from 'class-validator'
export class Token {
@IsString()
readonly token: string
}
export class User extends Typegoose {
@IsString()
@prop({ required: true, unique: true, trim: true })
username: string
@IsString()
@prop({ required: true, trim: true })
email: string
@IsString()
@prop({ required: true, trim: true })
password: string
@IsString()
@prop()
avatar?: string
@IsDate()
@prop({ default: Date.now })
joinDate: Date
@IsArray()
@prop({ required: true, ref: "Post" })
favorites: [mongoose.Schema.Types.ObjectId]
}
- We are going to modify the
users.service.ts
document to remove thesignupUser
andcreateToken
methods that are now part of theAuthService
.
server\src\users\users.service.ts
import { Injectable } from '@nestjs/common'
import { InjectModel } from 'nestjs-typegoose'
import { User } from './model/user.model'
import { ModelType } from 'typegoose'
@Injectable()
export class UsersService {
constructor(@InjectModel(User) private readonly userModel: ModelType<User>) { }
async getUsers(): Promise<User[] | null> {
return await this.userModel.find().exec()
}
}
- We are going to modify the
users.resolver.ts
document to include theAuthService
service and to include the newsigninUser
mutation, the newgetCurrentUser
query and to modify thesignupUser
mutation.
server\src\users\graphql\users.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'
import { UsersService } from '../users.service'
import { User, Token } from './types/user.type'
import { GraphqlAuthGuard } from '../../auth/graphql-auth.guard'
import { CurrentUser } from '../../auth/current-user.decorator'
import { AuthService } from '../../auth/auth.service'
import { UseGuards } from '@nestjs/common'
@Resolver()
export class UsersResolver {
constructor(
private readonly usersService: UsersService,
private readonly authService: AuthService,
) { }
@Query(() => User)
@UseGuards(GraphqlAuthGuard)
async getCurrentUser(@CurrentUser() currentUser: User) {
return currentUser
}
@Query(() => [User])
async getUsers() {
return await this.usersService.getUsers()
}
@Mutation(() => Token)
async signupUser(
@Args('username') username: string,
@Args('email') email: string,
@Args('password') password: string,
) {
return await this.authService.signUp({ username, email, password })
}
@Mutation(() => Token)
async signinUser(
@Args('username') username: string,
@Args('password') password: string,
) {
return await this.authService.signIn({ username, password })
}
}
- We are going to check if the new
signinUser
mutation works properly:
Request
mutation {
signinUser(username: "Fofo", password: "Fofo") {
token
}
}
Response
{
"data": {
"signinUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTY1MDIxMTQ5LCJleHAiOjE1NjUyMzcxNDl9.OtGTm2qHC5XT-hqNmb6eypFHrZg3DRwBj7ozac3CPCs"
}
}
}
- We are going to check if the
signupUser
mutation still works properly, but returning a Token instead of the user:
Request
mutation {
signupUser(username: "David", email: "david@gmail.com", password: "David") {
token
}
}
Response
{
"data": {
"signupUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkRhdmlkIiwiZW1haWwiOiJkYXZpZEBnbWFpbC5jb20iLCJpYXQiOjE1NjUwMTc4MzksImV4cCI6MTU2NTIzMzgzOX0.KcnzxLj4Wu9RkjT9KD6L55ILzBzYk0NNdnWp6o1-nFk"
}
}
}
- We are going to check if the new
getCurrentUser
query works properly:
Request
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites
}
}
Http header
{
"authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTY1MDIxMTQ5LCJleHAiOjE1NjUyMzcxNDl9.OtGTm2qHC5XT-hqNmb6eypFHrZg3DRwBj7ozac3CPCs"
}
Response
{
"data": {
"getCurrentUser": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"email": "Fofo@gmail.com",
"password": "$2b$10$qb.j.FtbHSfSgyH8ZYGGwOW.cIFj8voPEs1m/jcmdFrX/j/kP1zky",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon",
"joinDate": "2019-07-22T04:18:23.184Z",
"favorites": null
}
}
}
III.-Modify the client app to include the Authentication.
1.-Modify the Nuxt.Config to set up Apollo Authentication
- We need to modify the
nuxt.config.js
document to add theauthenticationType: '',
property to theapollo
tag. Otherwise it would included theBearer
prefix as the default value.
client\nuxt.config.js
import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js modules
*/
modules: [
[
'nuxt-i18n',
{
seo: false,
locales: [
{
name: 'ESP',
code: 'es',
iso: 'es-ES'
},
{
name: 'ENG',
code: 'en',
iso: 'en-US'
}
],
strategy: 'prefix_and_default',
langDir: 'lang/',
defaultLocale: 'es',
vueI18n: {
fallbackLocale: 'es',
messages: { es, en }
}
}
],
'@nuxtjs/vuetify',
'@nuxtjs/pwa',
'@nuxtjs/eslint-module',
'@nuxtjs/apollo'
],
apollo: {
errorHandler: '~/apollo/customErrorHandler.js',
authenticationType: '',
clientConfigs: {
default: {
httpEndpoint:
process.env.HTTP_ENDPOINT || 'http://localhost:4000/graphql'
}
}
},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
// primary: colors.blue.darken2,
// accent: colors.grey.darken3,
// secondary: colors.amber.darken3,
// info: colors.teal.lighten1,
// warning: colors.amber.base,
// error: colors.deepOrange.accent4,
// success: colors.green.accent3
primary: '#3B125F',
secondary: '#8B5FBF',
accent: '#BF653F',
error: '#722530',
warning: '#A37513',
info: '#396893',
success: '#4caf50'
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
gql
documents with queries and mutations
2.-Create the client\gql\getCurrentUser.gql
query getCurrentUser {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
client\gql\signinUser.gql
mutation signinUser($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
client\gql\signupUser.gql
mutation signupUser($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
localization
documents with new entries
3.-Update the client\lang\en-US.js
export default {
posts: 'Posts',
signin: 'Sign in',
signup: 'Sign up',
searchposts: 'Search Posts',
home: 'Home',
createPost: 'Create Post',
profile: 'Profile',
signout: 'Signout',
password: 'Password',
username: 'Username',
dontHaveAnAccount: "Don't have an account?",
welcomeBack: 'Welcome Back!'
}
client\lang\es-ES.js
export default {
posts: 'Entradas',
signin: 'Iniciar sesión',
signup: 'Inscripción',
searchposts: 'Buscar entradas',
home: 'Inicio',
createPost: 'Crear Entrada',
profile: 'Perfil',
signout: 'Cerrar sesión',
username: 'Usuario',
password: 'Contraseña',
dontHaveAnAccount: '¿No tiene una cuenta?',
welcomeBack: '¡Bienvenido de nuevo!'
}
store
to manaage the Authentication
4.-Update the client\store\index.js
import { getPosts } from '~/gql/getPosts.gql'
import { getCurrentUser } from '~/gql/getCurrentUser.gql'
import { signinUser } from '~/gql/signinUser.gql'
export const state = () => ({
posts: [],
user: null,
loading: false
})
export const mutations = {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null)
}
export const actions = {
async nuxtServerInit({ dispatch }) {
await dispatch('getCurrentUser')
await dispatch('getPosts')
},
async getPosts({ commit }) {
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getPosts
})
commit('setPosts', result.data.getPosts)
} catch (error) {
console.log(error)
}
commit('setLoading', false)
},
async getCurrentUser({ commit }) {
if (!this.app.$apolloHelpers.getToken()) {
commit('clearUser')
return
}
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getCurrentUser
})
// Add user data to state
commit('setUser', result.data.getCurrentUser)
} catch (error) {
console.error(error)
}
commit('setLoading', false)
},
async signinUser({ commit, dispatch }, payload) {
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.mutate({
mutation: signinUser,
variables: payload
})
await this.app.$apolloHelpers.onLogin(result.data.signinUser.token)
await dispatch('getCurrentUser')
commit('setLoading', false)
} catch (error) {
console.error(error)
commit('setLoading', false)
}
},
async signoutUser({ commit }) {
// clear user in state
commit('clearUser')
await this.app.$apolloHelpers.onLogout()
// redirect home - kick users out of private pages (i.e. profile)
this.app.router.push(this.app.localePath('index'))
}
}
export const getters = {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading
}
SignIn
page
5.-Update the client\pages\signin\index.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Signin Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1>{{ $t('welcomeBack') }}</h1>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card color="secondary" dark>
<v-container>
<v-form @submit.prevent="handleSigninUser">
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="username"
prepend-icon="face"
:label="$t('username')"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="password"
prepend-icon="extension"
:label="$t('password')"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn color="accent" type="submit">{{ $t('signin') }}</v-btn>
<h3>
{{ $t('dontHaveAnAccount') }}
<nuxt-link to="/signup">{{ $t('signup') }}</nuxt-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Signin',
data() {
return {
username: '',
password: ''
}
},
computed: {
...mapGetters(['user'])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push(this.localePath('index'))
}
}
},
methods: {
handleSigninUser() {
this.$store.dispatch('signinUser', {
username: this.username,
password: this.password
})
}
}
}
</script>
auth
middleware
6.-Create the client\middleware\auth.js
export default function({ store, redirect, app }) {
if (!store.getters.user) {
return redirect(app.localePath('signin'))
}
}
Profile
page to use the auth
middleware
7.-Update the client\pages\profile\index.vue
<template>
<v-container>
<h1>Profile</h1>
</v-container>
</template>
<script>
export default {
name: 'Profile',
middleware: 'auth'
}
</script>
Layout
with new options
8.-Update the client\layouts\default.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer v-model="sideNav" fixed app temporary>
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</nuxt-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
v-for="(item, i) in sideNavItems"
:key="i"
:to="localePath(item.link)"
ripple
>
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{ item.title }}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile v-if="user" @click="handleSignoutUser">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>{{ $t('signout') }}</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
VueShare
</nuxt-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
color="accent"
single-line
hide-details
:placeholder="$t('searchposts')"
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
v-for="(item, i) in horizontalNavItems"
:key="i"
:to="localePath(item.link)"
flat
>
<v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
</v-toolbar-items>
<!-- Profile Button -->
<v-btn v-if="user" flat to="profile">
<v-icon class="hidden-sm-only" left>account_box</v-icon>
<v-badge right color="blue darken-2">
<!-- <span slot="badge"></span> -->
{{ $t('profile') }}
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn v-if="user" flat @click="handleSignoutUser">
<v-icon class="hidden-sm-only" left>exit_to_app</v-icon>
{{ $t('signout') }}
</v-btn>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link
v-for="(locale, i) in showLocales"
:key="i"
tag="span"
style="cursor: pointer"
class="lang-switcher"
:to="switchLocalePath(locale.code)"
>
{{ locale.name }}
</nuxt-link>
</v-toolbar-title>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<nuxt />
</transition>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
head() {
return this.$nuxtI18nSeo()
},
data() {
return {
sideNav: false
}
},
computed: {
...mapGetters(['user']),
horizontalNavItems() {
let items = [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{ icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
{ icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
]
if (this.user) {
items = [{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' }]
}
return items
},
sideNavItems() {
let items = [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{ icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
{ icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
]
if (this.user) {
items = [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{
icon: 'stars',
title: this.$i18n.t('createPost'),
link: 'posts-add'
},
{ icon: 'account_box', title: this.$i18n.t('posts'), link: 'profile' }
]
}
return items
},
showLocales() {
return this.$i18n.locales.filter(
locale => locale.code !== this.$i18n.locale
)
}
},
methods: {
handleSignoutUser() {
this.$store.dispatch('signoutUser')
},
toggleSideNav() {
this.sideNav = !this.sideNav
}
}
}
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
9.-Check if the changes works properly
- We can try if we can access the profile without being authenticated
- Check how the cookie is created:
Section 9: Error Handling and Form Validation
ignoring expiration
of the jwt
token.
I.-Modify the server app to avoid - We need to modify the
jwt.strategy.ts
document to add the optionignoreExpiration: false
.
server\src\auth\jwt.strategy.ts
import { PassportStrategy } from '@nestjs/passport'
import { Strategy, ExtractJwt } from 'passport-jwt'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtPayload } from './jwt-payload.interface'
import { InjectModel } from 'nestjs-typegoose'
import { User } from '../users/model/user.model'
import { ModelType } from 'typegoose'
import { ConfigService } from '../config/config.service'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectModel(User) private readonly userModel: ModelType<User>,
private readonly configService: ConfigService
) {
super({
jwtFromRequest: ExtractJwt.fromHeader("authorization"),
ignoreExpiration: false,
secretOrKey: configService.get('SECRET'),
})
}
async validate(payload: any): Promise<User | null> {
const { username } = payload
const user = await this.userModel.findOne({
username: username
}).populate({
path: "favorites",
model: "Post"
})
if (!user) {
throw new UnauthorizedException()
}
return user
}
}
II.-Modify the client app to include Error Handling and Form Validation.
cookie-universal-nuxt
package
1.-Install the - We are going to install the cookie-universal-nuxt package to be able to remove cookies on both
client
andserver
sides.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide-nuxt/client (master)
$ npm i cookie-universal-nuxt
npm WARN ts-node@8.3.0 requires a peer of typescript@>=2.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.0.7 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.0.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\watchpack\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\nodemon\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\jest-haste-map\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ cookie-universal-nuxt@2.0.17
added 3 packages from 2 contributors and audited 897031 packages in 67.312s
found 0 vulnerabilities
FormAlert
Component and register it globally
2.-Create the - We need to create the
FormAlert.vue
Component document. Create theShared
folder fromclient\components
and theFormAlert.vue
document in it.
client\components\Shared\FormAlert.vue
<template>
<v-alert type="error" :value="true" transition="scale-transition" dismissible>
<h3>{{ message }}</h3>
</v-alert>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true
}
}
}
</script>
- We need to create the
shared-components.js
plugin component that will be used to register all the components from theclient\components\Shared
folder as Global components. It uses the WebPack require-context method.
client\plugins\shared-components.js
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
'~/components/Shared',
false,
/[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName)
const componentName = upperFirst(
camelCase(fileName.replace(/^\.\/(.*)\.\w+$/, '$1'))
)
Vue.component(componentName, componentConfig.default || componentConfig)
})
nuxt.config.js
document to add a new module and a new pugin
3.-Modify the - We need to modify the
nuxt.config.js
document to include the newcookie-universal-nuxt
module and to register theshared-components
plugin.
client\nuxt.config.js
import es from './lang/es-ES.js'
import en from './lang/en-US.js'
// import colors from 'vuetify/es5/util/colors'
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{
rel: 'stylesheet',
href:
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
}
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: ['@plugins/shared-components.js'],
/*
** Nuxt.js modules
*/
modules: [
[
'nuxt-i18n',
{
seo: false,
locales: [
{
name: 'ESP',
code: 'es',
iso: 'es-ES'
},
{
name: 'ENG',
code: 'en',
iso: 'en-US'
}
],
strategy: 'prefix_and_default',
langDir: 'lang/',
defaultLocale: 'es',
vueI18n: {
fallbackLocale: 'es',
messages: { es, en }
}
}
],
'@nuxtjs/vuetify',
'@nuxtjs/pwa',
'@nuxtjs/eslint-module',
'@nuxtjs/apollo',
`cookie-universal-nuxt`
],
apollo: {
authenticationType: '',
errorHandler: '~/apollo/customErrorHandler.js',
clientConfigs: {
default: {
httpEndpoint:
process.env.HTTP_ENDPOINT || 'http://localhost:4000/graphql'
}
}
},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
theme: {
// primary: colors.blue.darken2,
// accent: colors.grey.darken3,
// secondary: colors.amber.darken3,
// info: colors.teal.lighten1,
// warning: colors.amber.base,
// error: colors.deepOrange.accent4,
// success: colors.green.accent3
primary: '#3B125F',
secondary: '#8B5FBF',
accent: '#BF653F',
error: '#722530',
warning: '#A37513',
info: '#396893',
success: '#4caf50'
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
}
}
utils.js
document that will contain global functions
4.-Create a new - Create the
client\helpers
folder and theutils.js
document inside with global functions.
client\helpers\utils.js
const utils = {
isJwtTokenValid(jwtToken) {
if (!jwtToken) {
return false
}
const items = jwtToken.split('.')
if (!items) {
return false
}
return items.length > 2
},
getFirstGraphQLError(graphQLError) {
if (
graphQLError &&
graphQLError.graphQLErrors &&
graphQLError.graphQLErrors.length > 0
) {
return graphQLError.graphQLErrors[0]
}
if (graphQLError && graphQLError.message) {
return graphQLError.message
}
return graphQLError
},
getCurrentGraphQLError(graphQLError) {
const error = this.getFirstGraphQLError(graphQLError)
if (!error) {
return graphQLError
}
const currentError =
error.message && (!error.message.message || !error.message.error)
? error.message
: error.error
? error.error
: error.message && error.message.message
? error.message.message
: error.message && error.message.error
? error.message.error
: error
return currentError
}
}
export default utils
Signup
action and the error
and authError
states.
5.-Modify the store to include the - We need to modify the
index.js
store document to include theSignup
action and theerror
andauthError
states.
client\store\index.js
import { getPosts } from '~/gql/getPosts.gql'
import { getCurrentUser } from '~/gql/getCurrentUser.gql'
import { signinUser } from '~/gql/signinUser.gql'
import { signupUser } from '~/gql/signupUser.gql'
import utils from '~/helpers/utils'
export const state = () => ({
posts: [],
user: null,
loading: false,
error: null,
authError: null
})
export const mutations = {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
setError: (state, payload) => {
state.error = payload
},
clearError: state => (state.error = null),
setAuthError: (state, payload) => {
state.authError = payload
},
clearAuthError: state => (state.authError = null)
}
export const actions = {
async nuxtServerInit({ dispatch }) {
await dispatch('getCurrentUser')
await dispatch('getPosts')
},
async getPosts({ commit }) {
commit('clearError')
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getPosts
})
commit('setPosts', result.data.getPosts)
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async logOut({ commit }) {
this.app.$cookies.remove('apollo-token')
await this.app.$apolloHelpers.onLogout()
commit('clearUser')
},
async getCurrentUser({ commit, dispatch }) {
if (!utils.isJwtTokenValid(this.app.$apolloHelpers.getToken())) {
await dispatch('logOut')
return
}
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getCurrentUser
})
// Add user data to state
commit('setUser', result.data.getCurrentUser)
commit('clearError')
commit('clearAuthError')
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
if (currentError === 'Unauthorized') {
commit('setAuthError', currentError)
await dispatch('logOut')
} else {
commit('setError', currentError)
}
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async signinUser({ commit, dispatch }, payload) {
commit('clearError')
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.mutate({
mutation: signinUser,
variables: payload
})
await this.app.$apolloHelpers.onLogin(result.data.signinUser.token)
await dispatch('getCurrentUser')
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async signupUser({ commit, dispatch }, payload) {
commit('clearError')
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.mutate({
mutation: signupUser,
variables: payload
})
await this.app.$apolloHelpers.onLogin(result.data.signupUser.token)
await dispatch('getCurrentUser')
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async signoutUser({ dispatch }) {
await dispatch('logOut')
// redirect home - kick users out of private pages (i.e. profile)
this.app.router.push(this.app.localePath('index'))
}
}
export const getters = {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
localization
documents with new entries
6.-Update the client\lang\en-US.js
export default {
posts: 'Posts',
signin: 'Sign in',
signup: 'Sign up',
searchposts: 'Search Posts',
home: 'Home',
createPost: 'Create Post',
profile: 'Profile',
signout: 'Signout',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
email: 'Email',
dontHaveAnAccount: "Don't have an account?",
alreadyHaveAnAccount: 'Already have an account?',
welcomeBack: 'Welcome Back!',
getStartedHere: 'Get Started Here',
isRequired: '{name} is required',
mustBeValid: '{name} must be valid',
cannotBeMoreThanCharacters: `{name} cannot be more than {number} characters`,
mustBeAtLeast: `{name} must be at least {number} characters`,
passwordsMustMatch: 'Passwords must match',
yourAreNowSignedIn: `You are now signed in!`,
close: `Close`
}
client\lang\es-ES.js
export default {
posts: 'Entradas',
signin: 'Iniciar sesión',
signup: 'Inscripción',
searchposts: 'Buscar entradas',
home: 'Inicio',
createPost: 'Crear Entrada',
profile: 'Perfil',
signout: 'Cerrar sesión',
username: 'Usuario',
password: 'Contraseña',
confirmPassword: 'Confirmar Contraseña',
email: 'Email',
dontHaveAnAccount: '¿No tiene una cuenta?',
alreadyHaveAnAccount: '¿Ya tiene una cuenta?',
welcomeBack: '¡Bienvenido de nuevo!',
getStartedHere: 'Comience aquí',
isRequired: '{name} es requerido',
mustBeValid: '{name} debe de ser válido',
cannotBeMoreThanCharacters: `{name} no puede contener más de {number} caracteres`,
mustBeAtLeast: `{name} debe contener al menos {number} caracteres`,
passwordsMustMatch: 'Las Contraseñas deben coincidir',
yourAreNowSignedIn: `¡Ahora está registrado!`,
close: `Cerrar`
}
signin
page to include the form validation.
7.-Modify the - We need to modify the
signin\index.vue
page document to include the form validation.
client\pages\signin\index.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Signin Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1>{{ $t('welcomeBack') }}</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout v-if="error" row wrap>
<v-flex xs12 sm6 offset-sm3>
<form-alert :message="error"></form-alert>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card color="secondary" dark>
<v-container>
<v-form
ref="form"
v-model="isFormValid"
lazy-validation
@submit.prevent="handleSigninUser"
>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="username"
:rules="usernameRules"
prepend-icon="face"
:label="$t('username')"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="password"
:rules="passwordRules"
prepend-icon="extension"
:label="$t('password')"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
:disabled="!isFormValid || loading"
color="accent"
type="submit"
>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
{{ $t('signin') }}</v-btn
>
<h3>
{{ $t('dontHaveAnAccount') }}
<nuxt-link :to="localePath('signup')">{{
$t('signup')
}}</nuxt-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Signin',
data() {
return {
isFormValid: false,
username: '',
password: '',
usernameRules: [
// Check if username in input
username =>
!!username ||
this.$i18n.t('isRequired', { name: this.$i18n.t('username') }),
// Make sure username is less than 10 characters
username =>
username.length <= 10 ||
this.$i18n.t('cannotBeMoreThanCharacters', {
name: this.$i18n.t('username'),
number: 10
})
],
passwordRules: [
password =>
!!password ||
this.$i18n.t('isRequired', { name: this.$i18n.t('password') }),
// Make sure password is at least 4 characters
password =>
password.length >= 4 ||
this.$i18n.t('mustBeAtLeast', {
name: this.$i18n.t('password'),
number: 4
})
]
}
},
computed: {
...mapGetters(['loading', 'error', 'user'])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push(this.localePath('index'))
}
}
},
methods: {
handleSigninUser() {
if (this.$refs.form.validate()) {
this.$store.dispatch('signinUser', {
username: this.username,
password: this.password
})
}
}
}
}
</script>
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
signup
page to include to include all the functionality .
8.-Modify the - We need to modify the
signup\index.vue
page document to include all the functionality to manage theSignup
.
client\pages\signup\index.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Signup Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1>{{ $t('getStartedHere') }}</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout v-if="error" row wrap>
<v-flex xs12 sm6 offset-sm3>
<form-alert :message="error"></form-alert>
</v-flex>
</v-layout>
<!-- Signup Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card color="accent" dark>
<v-container>
<v-form
ref="form"
v-model="isFormValid"
lazy-validation
@submit.prevent="handleSignupUser"
>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="username"
:rules="usernameRules"
prepend-icon="face"
:label="$t('username')"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="email"
:rules="emailRules"
prepend-icon="email"
:label="$t('email')"
type="email"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="password"
:rules="passwordRules"
prepend-icon="extension"
:label="$t('password')"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="passwordConfirmation"
:rules="passwordRules"
prepend-icon="gavel"
:label="$t('confirmPassword')"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
:disabled="!isFormValid || loading"
color="info"
type="submit"
>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
{{ $t('signup') }}</v-btn
>
<h3>
{{ $t('alreadyHaveAnAccount') }}
<router-link :to="localePath('signin')">{{
$t('signin')
}}</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Signup',
data() {
return {
isFormValid: false,
username: '',
email: '',
password: '',
passwordConfirmation: '',
usernameRules: [
username =>
!!username ||
this.$i18n.t('isRequired', { name: this.$i18n.t('username') }),
username =>
username.length <= 10 ||
this.$i18n.t('cannotBeMoreThanCharacters', {
name: this.$i18n.t('username'),
number: 10
})
],
emailRules: [
email =>
!!email ||
this.$i18n.t('isRequired', { name: this.$i18n.t('email') }),
email =>
/.@+./.test(email) ||
this.$i18n.t('mustBeValid', { name: this.$i18n.t('email') })
],
passwordRules: [
password =>
!!password ||
this.$i18n.t('isRequired', { name: this.$i18n.t('password') }),
password =>
password.length >= 4 ||
this.$i18n.t('mustBeAtLeast', {
name: this.$i18n.t('password'),
number: 4
}),
confirmation =>
confirmation === this.password || this.$i18n.t('passwordsMustMatch')
]
}
},
computed: {
...mapGetters(['loading', 'error', 'user'])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push(this.localePath('index'))
}
}
},
methods: {
handleSignupUser() {
if (this.$refs.form.validate()) {
this.$store.dispatch('signupUser', {
username: this.username,
email: this.email,
password: this.password
})
}
}
}
}
</script>
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
default
layout to include the AuthSnackbar
and AuthErrorSnackbar
components
9.-Modify the main - We need to modify the
default.vue
layout document to include theAuthSnackbar
andAuthErrorSnackbar
components.
client\layouts\default.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer v-model="sideNav" fixed app temporary>
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</nuxt-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
v-for="(item, i) in sideNavItems"
:key="i"
:to="localePath(item.link)"
ripple
>
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{ item.title }}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile v-if="user" @click="handleSignoutUser">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>{{ $t('signout') }}</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link :to="localePath('index')" tag="span" style="cursor: pointer">
VueShare
</nuxt-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
color="accent"
single-line
hide-details
:placeholder="$t('searchposts')"
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
v-for="(item, i) in horizontalNavItems"
:key="i"
:to="localePath(item.link)"
flat
>
<v-icon class="hidden-sm-only" left>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
</v-toolbar-items>
<!-- Profile Button -->
<v-btn v-if="user" flat :to="localePath('profile')">
<v-icon class="hidden-sm-only" left>account_box</v-icon>
<v-badge right color="blue darken-2">
<!-- <span slot="badge"></span> -->
{{ $t('profile') }}
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn v-if="user" flat @click="handleSignoutUser">
<v-icon class="hidden-sm-only" left>exit_to_app</v-icon>
{{ $t('signout') }}
</v-btn>
<v-toolbar-title class="hidden-xs-only">
<nuxt-link
v-for="(locale, i) in showLocales"
:key="i"
tag="span"
style="cursor: pointer"
class="lang-switcher"
:to="switchLocalePath(locale.code)"
>
{{ locale.name }}
</nuxt-link>
</v-toolbar-title>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<nuxt />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout="5000"
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>{{ $t('yourAreNowSignedIn') }}</h3>
<v-btn dark flat @click="authSnackbar = false">{{
$t('close')
}}</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout="5000"
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{ authError.message }}</h3>
<v-btn dark flat :to="localePath('signin')">{{ $t('close') }}</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
head() {
return this.$nuxtI18nSeo()
},
data() {
return {
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false
}
},
computed: {
...mapGetters(['authError', 'user']),
horizontalNavItems() {
let items = [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{ icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
{ icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
]
if (this.user) {
items = [{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' }]
}
return items
},
sideNavItems() {
let items = [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{ icon: 'lock_open', title: this.$i18n.t('signin'), link: 'signin' },
{ icon: 'create', title: this.$i18n.t('signup'), link: 'signup' }
]
if (this.user) {
items = [
{ icon: 'chat', title: this.$i18n.t('posts'), link: 'posts' },
{
icon: 'stars',
title: this.$i18n.t('createPost'),
link: 'posts-add'
},
{
icon: 'account_box',
title: this.$i18n.t('profile'),
link: 'profile'
}
]
}
return items
},
showLocales() {
return this.$i18n.locales.filter(
locale => locale.code !== this.$i18n.locale
)
}
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true
}
}
},
methods: {
handleSignoutUser() {
this.$store.dispatch('signoutUser')
},
toggleSideNav() {
this.sideNav = !this.sideNav
}
}
}
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
10.-We need to test if it works.
Section 10: Add Post / Infinite Scroll Components
infiniteScrollPosts
query.
I.-Modify the server app to include the PostPage
type
1.-Create the new - We are going to create the
post-page.type.ts
document with the newPostPage
type.
server\src\posts\graphql\types\post-page.type.ts
import { Field, ObjectType } from 'type-graphql'
import { Post } from '../post.model'
import { Ref } from 'typegoose'
@ObjectType()
export class PostPage {
@Field(() => [Post], { nullable: "items" })
readonly posts: Ref<Post>[]
@Field(() => Boolean)
hasMore: boolean
}
infiniteScrollPosts
method.
2.-Modify the Posts service to include the new - We need to modify the
\posts.service.ts
service document to include the newinfiniteScrollPosts
method.
server\src\posts\posts.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from 'nestjs-typegoose';
import { Post } from './model/post.model';
import { PostPage } from './graphql/types/post-page.type';
import { CreatePostDto } from './model/dtos/create-post.dto'
import { ModelType } from 'typegoose';
@Injectable()
export class PostsService {
constructor(@InjectModel(Post) private readonly postModel: ModelType<Post>) { }
async getPosts(): Promise<Post[] | null> {
const posts = await this.postModel.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
}
async infiniteScrollPosts(pageNum: number, pageSize: number): Promise<PostPage | null> {
const skips = pageSize * (pageNum - 1)
const posts = await this.postModel.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize)
.lean()
const totalDocs = await this.postModel.countDocuments()
const hasMore = totalDocs > pageSize * pageNum
const postPage: PostPage = {
posts,
hasMore
}
return postPage
}
async addPost(createPostDto: CreatePostDto): Promise<Post> {
const newPost = new this.postModel(createPostDto);
return await newPost.save();
}
}
infiniteScrollPosts
Query.
3.-Modify the Posts resolver to include the new - We need to modify the
\posts.resolver.ts
resolver document to include the newinfiniteScrollPosts
Query.
server\src\posts\graphql\posts.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Int, ID } from 'type-graphql';
import { PostsService } from '../posts.service';
import { PostInput } from './inputs/post.input';
import { PostPage } from './types/post-page.type';
import { Post } from './types/post.type';
@Resolver()
export class PostsResolver {
constructor(private readonly postsService: PostsService) { }
@Query(() => [Post])
async getPosts() {
return await this.postsService.getPosts();
}
@Query(() => PostPage)
async infiniteScrollPosts(
@Args({ name: 'pageNum', type: () => Int}) pageNum: number,
@Args({ name: 'pageSize', type: () => Int}) pageSize: number
) {
return await this.postsService.infiniteScrollPosts(pageNum, pageSize);
}
@Mutation(() => Post)
async addPostWithInput(@Args('input') input: PostInput) {
const createdBy = input.creatorId;
const { title, imageUrl, categories, description } = input
return await this.postsService.addPost({ title, imageUrl, categories, description, createdBy });
}
@Mutation(() => Post)
async addPost(
@Args('title') title: string,
@Args('imageUrl') imageUrl: string,
@Args({ name: 'categories', type: () => [String], nullable: "items" }) categories: string[],
@Args('description') description: string,
@Args({ name: 'creatorId', type: () => ID}) creatorId: string,
) {
const createdBy = creatorId;
return await this.postsService.addPost({ title, imageUrl, categories, description, createdBy });
}
}
infiniteScrollPosts
Query works propely
4.-Test if the new query
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
variables
{
"pageNum": 1,
"pageSize": 3
}
response
{
"data": {
"infiniteScrollPosts": {
"hasMore": true,
"posts": [
{
"_id": "5d4fa77cfe7af92694bf3cae",
"title": "Abstract Painting",
"imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Vassily_Kandinsky%2C_1939_-_Composition_10.jpg/1024px-Vassily_Kandinsky%2C_1939_-_Composition_10.jpg",
"categories": [
"Art"
],
"description": "Nice painting",
"likes": 0,
"createdDate": "2019-08-11T05:28:28.697Z",
"messages": [],
"createdBy": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon"
}
},
{
"_id": "5d4fa31dcb16795b68483e10",
"title": "At the Beach",
"imageUrl": "https://images.pexels.com/photos/1139541/pexels-photo-1139541.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
"categories": [
"Photography",
"Travel"
],
"description": "A nice photo of the waves",
"likes": 0,
"createdDate": "2019-08-11T05:09:49.634Z",
"messages": [],
"createdBy": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon"
}
},
{
"_id": "5d0e10be7dd57444d0607790",
"title": "Tasty coffee",
"imageUrl": "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
"categories": [
"Art",
"Food"
],
"description": "Some nice coffee artwork",
"likes": 0,
"createdDate": "2019-06-22T11:27:58.416Z",
"messages": null,
"createdBy": {
"_id": "5d36a9e007bced421c3a2031",
"username": "John",
"avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon"
}
}
]
}
}
}
infiniteScrollPosts
Query.
II.-Modify the client app to make the Add Posts component using the new infiniteScrollPosts.gql
and addPost.gql
documents with the infiniteScrollPosts
Query and addPost
mutation.
1.-Create the - We need to create the
addPost.gql
document with theaddPost
Mutation
client\gql\addPost.gql
mutation addPost(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
- We need to create the
infiniteScrollPosts.gql
document with the newinfiniteScrollPosts
Query
client\gql\infiniteScrollPosts.gql
- We are going to modify the `` document to put only the fields that we really need.
client\gql\getPosts.gql
query getPosts {
getPosts {
_id
title
imageUrl
}
}
localization
documents with new entries
2.-Update the client\lang\en-US.js
export default {
posts: 'Posts',
signin: 'Sign in',
signup: 'Sign up',
searchposts: 'Search Posts',
home: 'Home',
createPost: 'Create Post',
profile: 'Profile',
signout: 'Signout',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
email: 'Email',
dontHaveAnAccount: "Don't have an account?",
alreadyHaveAnAccount: 'Already have an account?',
welcomeBack: 'Welcome Back!',
getStartedHere: 'Get Started Here',
isRequired: '{name} is required',
mustBeValid: '{name} must be valid',
cannotBeMoreThanCharacters: '{name} cannot be more than {number} characters',
mustBeAtLeast: '{name} must be at least {number} characters',
passwordsMustMatch: 'Passwords must match',
yourAreNowSignedIn: 'You are now signed in!',
close: 'Close',
sessionExpiredSignInAgain: 'Your session has ended. Please sign in again.',
addPost: 'Add Post',
postTitle: 'Post Title',
imageUrl: 'Image URL',
category: 'Category | Categories',
categoryItems: {
art: 'Art',
education: 'Education',
food: 'Food',
furniture: 'Furniture',
travel: 'Travel',
photography: 'Photography',
technology: 'Technology'
},
description: 'Description',
submit: 'Submit',
atLeastOne: 'At least one',
likes: 'likes',
comments: 'comments',
added: 'Added',
fetchMore: 'Fetch More'
}
client\lang\es-ES.js
export default {
posts: 'Entradas',
signin: 'Iniciar sesión',
signup: 'Inscripción',
searchposts: 'Buscar entradas',
home: 'Inicio',
createPost: 'Crear Entrada',
profile: 'Perfil',
signout: 'Cerrar sesión',
username: 'Usuario',
password: 'Contraseña',
confirmPassword: 'Confirmar Contraseña',
email: 'Email',
dontHaveAnAccount: '¿No tiene una cuenta?',
alreadyHaveAnAccount: '¿Ya tiene una cuenta?',
welcomeBack: '¡Bienvenido de nuevo!',
getStartedHere: 'Comience aquí',
isRequired: '{name} es requerido',
mustBeValid: '{name} debe de ser válido',
cannotBeMoreThanCharacters:
'{name} no puede contener más de {number} caracteres',
mustBeAtLeast: '{name} debe contener al menos {number} caracteres',
passwordsMustMatch: 'Las Contraseñas deben coincidir',
yourAreNowSignedIn: '¡Ahora está registrado!',
close: 'Cerrar',
sessionExpiredSignInAgain:
'Su sesión ha caducado. Por favor, inicie la sesión otra vez.',
addPost: 'Añadir Entrada',
postTitle: 'Título de la entrada',
imageUrl: 'URL de la imagen',
category: 'Categoría | Categorías',
categoryItems: {
art: 'Arte',
education: 'Educación',
food: 'Comida',
furniture: 'Muebles',
travel: 'Viajes',
photography: 'Fotografía',
technology: 'Tecnología'
},
description: 'Descripción',
submit: 'Enviar',
atLeastOne: 'Por lo menos una',
likes: 'me gusta',
comments: 'comentarios',
added: 'Creado',
fetchMore: 'Obtener más'
}
addPost
action
3.-Modify the store to include the - We need to modify the
store\index.js
store document to include theaddPost
action and mutation.
client\store\index.js
import { getPosts } from '~/gql/getPosts.gql'
import { getCurrentUser } from '~/gql/getCurrentUser.gql'
import { signinUser } from '~/gql/signinUser.gql'
import { signupUser } from '~/gql/signupUser.gql'
import { addPost } from '~/gql/addPost.gql'
import utils from '~/helpers/utils'
export const state = () => ({
posts: [],
user: null,
loading: false,
error: null,
authError: null
})
export const mutations = {
setPosts: (state, payload) => {
state.posts = payload
},
addPost: (state, payload) => {
const posts = state.posts
posts.unshift(payload)
state.posts = posts
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
setError: (state, payload) => {
state.error = payload
},
clearError: state => (state.error = null),
setAuthError: (state, payload) => {
state.authError = payload
},
clearAuthError: state => (state.authError = null)
}
export const actions = {
async nuxtServerInit({ dispatch }) {
await dispatch('getCurrentUser')
await dispatch('getPosts')
},
async getPosts({ commit }) {
commit('clearError')
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getPosts
})
commit('setPosts', result.data.getPosts)
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async addPost({ commit }, payload) {
commit('clearError')
commit('setLoading', true)
try {
await this.app.apolloProvider.defaultClient.mutate({
mutation: addPost,
variables: payload
})
const { _id, title, imageUrl } = payload
const newPost = {
_id,
title,
imageUrl
}
commit('addPost', newPost)
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async logOut({ commit }) {
this.app.$cookies.remove('apollo-token')
await this.app.$apolloHelpers.onLogout()
commit('clearUser')
},
async getCurrentUser({ commit, dispatch }) {
if (!utils.isJwtTokenValid(this.app.$apolloHelpers.getToken())) {
await dispatch('logOut')
return
}
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.query({
query: getCurrentUser
})
// Add user data to state
commit('setUser', result.data.getCurrentUser)
commit('clearError')
commit('clearAuthError')
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
if (currentError === 'Unauthorized') {
commit('setAuthError', this.app.i18n.t('sessionExpiredSignInAgain'))
await dispatch('logOut')
} else {
commit('setError', currentError)
}
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async signinUser({ commit, dispatch }, payload) {
commit('clearError')
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.mutate({
mutation: signinUser,
variables: payload
})
await this.app.$apolloHelpers.onLogin(result.data.signinUser.token)
await dispatch('getCurrentUser')
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async signupUser({ commit, dispatch }, payload) {
commit('clearError')
commit('setLoading', true)
try {
const result = await this.app.apolloProvider.defaultClient.mutate({
mutation: signupUser,
variables: payload
})
await this.app.$apolloHelpers.onLogin(result.data.signupUser.token)
await dispatch('getCurrentUser')
} catch (error) {
const currentError = utils.getCurrentGraphQLError(error)
commit('setError', currentError)
console.error(utils.getFirstGraphQLError(error))
}
commit('setLoading', false)
},
async signoutUser({ dispatch }) {
await dispatch('logOut')
// redirect home - kick users out of private pages (i.e. profile)
this.app.router.push(this.app.localePath('index'))
}
}
export const getters = {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
post Add
page to include all the functionality .
4.-Modify the - We need to modify the
post\add.vue
page document to include all the functionality to manage the creation of a newPost
client\pages\posts\add.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Add Post Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1 class="primary--text">{{ $t('addPost') }}</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout v-if="error" row wrap>
<v-flex xs12 sm6 offset-sm3>
<form-alert :message="error"></form-alert>
</v-flex>
</v-layout>
<!-- Add Post Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-form
ref="form"
v-model="isFormValid"
lazy-validation
@submit.prevent="handleAddPost"
>
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="title"
:rules="titleRules"
:label="$t('postTitle')"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="imageUrl"
:rules="imageRules"
:label="$t('imageUrl')"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img :src="imageUrl" height="300px" />
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select
v-model="categories"
:rules="categoriesRules"
item-text="text"
item-value="value"
:items="categoryItems"
multiple
:label="$tc('category', 2)"
></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea
v-model="description"
:rules="descRules"
:label="$t('description')"
type="text"
required
></v-textarea>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
:disabled="!isFormValid || loading"
color="info"
type="submit"
>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
{{ $t('submit') }}</v-btn
>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'AddPost',
middleware: 'auth',
data() {
return {
isFormValid: true,
title: '',
imageUrl: '',
categories: [],
categoryItems: [
{ value: 'Art', text: this.$i18n.t('categoryItems.art') },
{ value: 'Education', text: this.$i18n.t('categoryItems.education') },
{ value: 'Food', text: this.$i18n.t('categoryItems.food') },
{ value: 'Furniture', text: this.$i18n.t('categoryItems.furniture') },
{ value: 'Travel', text: this.$i18n.t('categoryItems.travel') },
{
value: 'Photography',
text: this.$i18n.t('categoryItems.photography')
},
{
value: 'Technology',
text: this.$i18n.t('categoryItems.technology')
}
],
description: '',
titleRules: [
title =>
!!title ||
this.$i18n.t('isRequired', { name: this.$i18n.t('postTitle') }),
title =>
title.length <= 20 ||
this.$i18n.t('cannotBeMoreThanCharacters', {
name: this.$i18n.t('postTitle'),
number: 20
})
],
imageRules: [
image =>
!!image ||
this.$i18n.t('isRequired', { name: this.$i18n.t('imageUrl') })
],
categoriesRules: [
categories =>
categories.length >= 1 ||
this.$i18n.t('isRequired', {
name: `${this.$i18n.t('atLeastOne')} ${this.$i18n.tc(
'category',
1
)}`
})
],
descRules: [
desc =>
!!desc ||
this.$i18n.t('isRequired', { name: this.$i18n.t('description') }),
desc =>
desc.length <= 200 ||
this.$i18n.t('cannotBeMoreThanCharacters', {
name: this.$i18n.t('description'),
number: 200
})
]
}
},
computed: {
...mapGetters(['loading', 'error', 'user'])
},
methods: {
handleAddPost() {
if (this.$refs.form.validate()) {
// add post action
this.$store.dispatch('addPost', {
title: this.title,
imageUrl: this.imageUrl,
categories: this.categories,
description: this.description,
creatorId: this.user._id
})
this.$router.push(this.localePath('index'))
}
}
}
}
</script>
posts
page to include all the functionality .
5.-Modify the - We need to modify the
post\index.vue
page document to include all the functionality to show all the posts incrementally.
client\pages\posts\index.vue
<template>
<v-container fluid grid-list-xl>
<!-- Post Cards -->
<v-layout v-if="infiniteScrollPosts" row wrap>
<v-flex
v-for="post in infiniteScrollPosts.posts"
:key="post._id"
xs12
sm6
>
<v-card hover>
<v-img :src="post.imageUrl" height="30vh" lazy></v-img>
<v-card-actions>
<v-card-title primary>
<div>
<div class="headline">{{ post.title }}</div>
<span class="grey--text"
>{{ post.likes }} {{ $t('likes') }} -
{{ post.messages ? post.messages.length : 0 }}
{{ $t('comments') }}</span
>
</div>
</v-card-title>
<v-spacer></v-spacer>
<v-btn icon @click="showPostCreator = !showPostCreator">
<v-icon>{{
`keyboard_arrow_${showPostCreator ? 'up' : 'down'}`
}}</v-icon>
</v-btn>
</v-card-actions>
<!-- Post Creator Tile -->
<v-slide-y-transition>
<v-card-text v-show="showPostCreator" class="grey lighten-4">
<v-list-tile avatar>
<v-list-tile-avatar>
<img :src="post.createdBy.avatar" />
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title class="text--primary">{{
post.createdBy.username
}}</v-list-tile-title>
<v-list-tile-sub-title class="font-weight-thin"
>{{ $t('added') }}
{{ post.createdDate }}</v-list-tile-sub-title
>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon ripple>
<v-icon color="grey lighten-1">info</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-card-text>
</v-slide-y-transition>
</v-card>
</v-flex>
</v-layout>
<!-- Fetch More Button -->
<v-layout v-if="showMoreEnabled" column>
<v-flex xs12>
<v-layout justify-center row>
<v-btn color="info" @click="showMorePosts">{{
$t('fetchMore')
}}</v-btn>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { infiniteScrollPosts } from '~/gql/infiniteScrollPosts.gql'
const pageSize = 2
export default {
name: 'Posts',
data() {
return {
pageNum: 1,
showMoreEnabled: true,
showPostCreator: false
}
},
apollo: {
infiniteScrollPosts: {
query: infiniteScrollPosts,
variables: {
pageNum: 1,
pageSize
}
}
},
methods: {
showMorePosts() {
this.pageNum += 1
// fetch more data and transform original result
this.$apollo.queries.infiniteScrollPosts.fetchMore({
variables: {
// pageNum incremented by 1
pageNum: this.pageNum,
pageSize
},
updateQuery: (prevResult, { fetchMoreResult }) => {
console.log('previous result', prevResult.infiniteScrollPosts.posts)
console.log('fetch more result', fetchMoreResult)
const newPosts = fetchMoreResult.infiniteScrollPosts.posts
const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore
this.showMoreEnabled = hasMore
return {
infiniteScrollPosts: {
__typename: prevResult.infiniteScrollPosts.__typename,
// Merge previous posts with new posts
posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
hasMore
}
}
}
})
}
}
}
</script>
6.-We need to test if it works.
Section 11: Post Component
getPost
Query and the addPostMessage
mutation.
I.-Modify the server app to include the - The folder structure for the models have been changed. Basically the
GraphQL
andDatabase(MongoDB)
models have been merged.
Old format
Level 1 | Level 2 | Level 3 | Description |
---|---|---|---|
graphql | Everthing related to GraphQL | ||
- | inputs | input objects used in GraphQL Queries and Mutations | |
- | post-input.ts | input object used when creating a post | |
- | types | object types used by GraphQL | |
- | post-page.type.ts | PostPage type | |
- | post.type.ts | Post type | |
- | posts.resolver.ts | GraphQL Post resolver | |
model | Everthing related to database model | ||
- | dtos | dtos used mainly for service input objects | |
- | create-post-message.dto.ts | CreatePostMessage dto | |
- | create-post.dto.ts | CreatePost dto | |
- | post.model.ts | Database Post model |
New format
Level 1 | Level 2 | Description |
---|---|---|
dtos | dtos used mainly for service input objects | |
- | create-post-message.dto.ts | CreatePostMessage dto |
- | create-post.dto.ts | CreatePost dto |
inputs | input objects used in GraphQL Queries and Mutations | |
- | post-input.ts | input object used when creating a post |
post.model.ts | Database and GraphQL Post model | |
posts.resolver.ts | GraphQL Post resolver |
posts
module with the new structure and add the getPost
Query and the addPostMessage
mutation.
1.-Update the server\src\posts\dtos\create-post-message.dto.ts
export class CreatePostMessageDto {
readonly messageBody: string;
readonly userId: string;
readonly postId: string;
}
server\src\posts\dtos\create-post.dto.ts
export class CreatePostDto {
readonly title: string;
readonly imageUrl: string;
readonly categories: string[];
readonly description: string;
readonly createdBy: string;
}
server\src\posts\inputs\post.input.ts
import { InputType, Field, ID } from 'type-graphql';
@InputType()
export class PostInput {
@Field()
readonly title: string;
@Field()
readonly imageUrl: string;
@Field(() => [String], { nullable: "items" })
readonly categories: string[];
@Field()
readonly description: string;
@Field(() => ID)
readonly creatorId: string;
}
server\src\posts\types\post-page.type.ts
import { Field, ObjectType } from 'type-graphql'
import { Post } from '../post.model'
import { Ref } from 'typegoose'
@ObjectType()
export class PostPage {
@Field(() => [Post], { nullable: "items" })
readonly posts: Ref<Post>[]
@Field(() => Boolean)
hasMore: boolean
}
server\src\posts\post.model.ts
import { prop, arrayProp, Typegoose, Ref } from 'typegoose';
import { IsDate, IsInt } from 'class-validator';
import { Field, ObjectType, ID, Int } from 'type-graphql';
import { User } from '../users/user.model'
@ObjectType()
export class Message extends Typegoose {
@Field(() => ID)
_id: string
@Field()
@prop({ required: true })
messageBody: string;
@IsDate()
@Field()
@prop({ default: Date.now })
messageDate: Date;
@Field(() => User)
@prop({ required: true, ref: User })
messageUser: Ref<User>
}
@ObjectType()
export class Post extends Typegoose {
@Field(() => ID)
_id: string
@Field()
@prop({ required: true })
title: string;
@Field()
@prop({ required: true })
imageUrl: string;
@Field(() => [String], { nullable: "items" })
@prop({ required: true })
categories: string[];
@Field()
@prop({ required: true })
description: string;
@IsDate()
@Field()
@prop({ default: Date.now })
createdDate: Date
@IsInt()
@Field(() => Int, { nullable: true })
@prop({ default: 0 })
likes: number;
@Field(() =>