From e601cd88d2d48697b43a82d307e5dcccbd192174 Mon Sep 17 00:00:00 2001 From: Emil Kais Date: Tue, 14 Mar 2023 14:36:30 -0400 Subject: [PATCH 1/4] Add initial tutorial to graphql training --- src/bit-u.md | 17 +- .../1-setting-up-apollo/setting-up-apollo.md | 934 ++++++++++++++++++ .../2-adding-mongodb/adding-mongodb.md | 0 .../3-porting-to-prisma/porting-to-prisma.md | 0 .../4-adding-testing/adding-testing.md | 0 src/graphql/graphql.md | 70 ++ static/img/graphql-logo.png | Bin 0 -> 81733 bytes 7 files changed, 1020 insertions(+), 1 deletion(-) create mode 100644 src/graphql/1-setting-up-apollo/setting-up-apollo.md create mode 100644 src/graphql/2-adding-mongodb/adding-mongodb.md create mode 100644 src/graphql/3-porting-to-prisma/porting-to-prisma.md create mode 100644 src/graphql/4-adding-testing/adding-testing.md create mode 100644 src/graphql/graphql.md create mode 100644 static/img/graphql-logo.png diff --git a/src/bit-u.md b/src/bit-u.md index 22ed90b99..0bb784243 100644 --- a/src/bit-u.md +++ b/src/bit-u.md @@ -502,7 +502,22 @@ a.quote-link:hover{ Take this course -
 
+
+ +

Node JS and GraphQL

+

Build a backend application for property rentals using Node JS, GraphQL and Apollo Server. + Learn about using ORMs like Mongoose and Prisma with a NoSQL Mongo database! + Write testcases testing created endpoints.

+
+

Audience: Intermediate JS developers

+

Goal: Learn the latest Node/GraphQL practices building a server as of early 2023.

+

Time: 12 hours

+
+ Take this course +
+ diff --git a/src/graphql/1-setting-up-apollo/setting-up-apollo.md b/src/graphql/1-setting-up-apollo/setting-up-apollo.md new file mode 100644 index 000000000..65304c30a --- /dev/null +++ b/src/graphql/1-setting-up-apollo/setting-up-apollo.md @@ -0,0 +1,934 @@ +@page learn-graphql/setting-up-apollo Setting Up a GraphQL Server with Apollo +@parent learn-graphql 1 + +@description This GraphQL tutorial will take you through creating different entities, how to create a server using Apollo Server 4, and some of the plugins available to use. We will go through queries, mutations, dealing with the resolver chain (A.K.A entity resolution). We’ll talk a bit about growing our graph including update mutations and introduce some error handling. Then we’ll go over directives that are available for the maintenance of your graph, and then end with adding some cache control for some fields within an entity. + +@body + +## Our Application + +We are going to create a baseline application that has three main entities: **renters, properties, and propertyOwners**. This application will create some create/read endpoints for the following entities, and establish the different relationships between renters, properties, and propertyOwners. We want to start building an application that allows renters to find roommates as well as properties that are owned by property owners. + +## Dependencies + +Open up a directory and let’s call it `node-graphql-2023`. From there run `npm init` and then run the following command to install the necessary packages: + +``` +npm i @apollo/server body-parser cors express graphql +``` + +**Note:** With the Update of Apollo Server 4, the intention is to focus on improving Apollo Server’s extensibility and making it simpler to use, maintain, and document. This means that `@apollo/server` combines numerous smaller packages, namely **startStandaloneServer** and **expressMiddleware** functions. + +## Apollo Server 4 Contents +- The `ApolloServer` class +- An Express 4 integration (similar to `apollo-server-express`) +- A standalone server (`startStandaloneServer`) +- And a set of [core plugins](https://www.apollographql.com/docs/apollo-server/migration/#plugins-are-in-deep-imports) + +## Apollo Server 4 Changes +- You can pass your **context** initialization function directly to whatever framework you are using in its integration function rather than the **ApolloServer** constructor (we will see this later in the tutorial) + +- You are responsible for setting up HTTP body parsing and CORS + +- You need to specify a path for your server to listen on explicitly, whereas before it would default to `/graphql` + +**Note:** The Apollo Server core team no longer supports the following packages: + +- `apollo-server-fastify` +- `apollo-server-hapi` +- `apollo-server-koa` +- `apollo-server-lambda` +- `apollo-server-micro` +- `apollo-server-cloud-functions` +- `apollo-server-cloudflare` +- `apollo-server-azure-functions` + +Your package.json should look something like this: + +``` +{ + "name": "node-graphql-2023", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node --watch ./index.js" + }, + "engines": { + "node": ">=18.6.0", + "npm": ">=8.13.0" + }, + "dependencies": { + "@apollo/server": "^4.3.0", + "body-parser": "^1.20.1", + "cors": "^2.8.5", + "express": "^4.18.2", + "graphql": "^16.6.0" + } +} +``` +**Note:** Notice that there is a lock on the engine for `node` and that is because we will be using some experimental features from `node@18.6.0` namely the `--watch` flag removing our need to install packages like `nodemon`. + +Next we will be creating our basic server, create an `index.js` with the following code: +``` +const { ApolloServer } = require('@apollo/server'); +const { expressMiddleware } = require('@apollo/server/express4'); +const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer'); +const express = require('express'); +const http = require('http'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { typeDefs, resolvers } = require('./src'); + +const app = express(); +const httpServer = http.createServer(app); + +const server = new ApolloServer({ + typeDefs, + resolvers, + plugins: [ + ApolloServerPluginDrainHttpServer({ httpServer }) + ] +}); + +async function main() { + await server.start(); + + app.use( + '/', + cors(), + // 50mb is the limit that `startStandaloneServer` uses, but you may configure this to suit your needs + bodyParser.json({ limit: '50mb' }), + // expressMiddleware accepts the same arguments: + // an Apollo Server instance and optional configuration options + expressMiddleware(server, { + context: async ({ req }) => ({ token: req.headers.token }), + }) + ); + await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve)); + console.log(`🚀 Server ready at http://localhost:4000/`); +} + +main(); +``` + +Currently don’t worry about the **typeDefs** and **resolvers** as we will be defining those soon within the `/src` folder. These represent our schema definitions and resolvers which is how we will resolve different fields, entities, queries and mutations. We use a plugin called **ApolloServerPluginDrainHttpServer** which is used to ensure that the node server closes down correctly. Next we create a main function so that we can call `await server.start()` and we are unable to do that outside of an async function. We will then apply our middleware, which will set up our server to have CORS enabled, the ability to pass in a json object in a body to the server, and lastly we include **expressMiddleware** to pass in the parameters we want to include in our **context** within GraphQL resolvers. Later we will include our database connection (SQL, noSQL or an ORM) in this method. + +**Note:** We don’t use `startStandaloneServer` because it does not provide out-of-the-box support for middleware. It is more used for getting simple servers up and running. + +## Schema Creation and Separation + +Next we are going to create our `/src` folder: This will include the following sub-folders: **common, properties, propertyOwners, renters**. We are creating separate folders for each of the entities so that we can introduce the idea of **schema stitching** which is essentially putting all the schemas that are separate into one big schema. By keeping these schemas separate, it makes it much easier to maintain and read as the project grows. We will add to the common sub-folder later. + +## Renter Entity + +The first schema we are going to make is for the Renter entity. We are going to create the following files: `index.js, dataSource.js, resolver.js, and schema.js` within the `/src/renters` folder. We are separating out each file so that the methods that will interact with our data source (for now it will be a local data file, but later will be a database), will be put in our `dataSource.js` file. The type definitions for the renter schema, will be put in the `schema.js` file. The object containing how we will resolve the Renter entity, queries, and mutations will be in the `resolver.js` file. **Resolvers** are a series of functions that are responsible for populating the data for a single field in your schema, while the **dataSource** should only contain methods we wish to expose on how to interact with our data. As a result of the logical separation of concerns, we want to have that code also be separate. + +### Renter Schema + +The first thing we will do is define an entity called **Renter**, and create baseline Queries and Mutations to cover our application. We will include one `input` type as the input for the `createRenter` method signature: + +``` +const typeDefs = `#graphql + type Renter { + id: ID! + name: String! + city: String! + rating: Float + roommates: [Renter] + } + + input CreateRenterInput { + name: String! + city: String! + # need ID to attach roommate to renter + roommates: [ID] + } + + type Query { + renters: [Renter] + getRenterById(renterId: ID!): Renter + } + + type Mutation { + createRenter(createRenterInput: CreateRenterInput): Renter + } +` + +module.exports = { + typeDefs +} +``` + +**Note:** By using the `#graphql` in front of a template string we are able to have syntax highlighting for GraphQL. + +Another thing to note is that our input uses `roommates` as a list of **IDs** rather than a list of **Renters**, that way we can grab the information from our dataSource and we don’t have to supply potentially invalid or out of date information for the `roommates` field. When we return the roommates field we will be covering the topic of entity resolution, but we will address this in the `resolver.js` file. + +### Renter Resolver + +We are going to write the code that will deal with the resolution process of the Queries and Mutations we have written out above: + +``` +const { + getRenterById, + createRenter, + getAllRenters +} = require('./dataSource'); + +const resolvers = { + Renter: { + roommates(parent) { + return parent.roommates.map((roommateId) => getRenterById(roommateId)); + } + }, + Query: { + getRenterById: (_parent, args) => { + return getRenterById(args.renterId); + }, + renters: () => getAllRenters() + }, + Mutation: { + createRenter: (_parent, args) => { + return createRenter(args.createRenterInput); + } + } +}; + +module.exports = { + resolvers +}; +``` + +You will see that in general when we don’t use a parameter in a method signature, we typically preface it with a `_` in order to indicate that we won’t use that parameter within the function. Another thing to notice is that our data logic is entirely separated into our `dataSource` file, so we will have to create those referenced functions above. + +### Renter entity resolution + +When we mentioned that we would have to include a method to resolve that list of **IDs** for roommates and somehow turn that into a list of `Renter` objects this concept is known as **entity resolution**. We will accomplish this by adding a specific field resolution for `roommates` within the `Renter` type in our resolver object. We will call `getRenterById` to pull the information we need from the IDs provided so that our queries don’t throw an error expecting a type `Renter` but receiving a type of `ID`. + +### Renter dataSource + +We will be using a global package called `crypto` that was added to Node that will help us to generate unique id’s for our `createRenter` mutation. Otherwise the other two Query methods are pretty self-explanatory: `getRenterById` will take an ID and return a found `Renter` entity, while `getAllRenters` will return all the renters in the system: +``` +const crypto = require('crypto'); +let { renters } = require('../../data'); + +function getRenterById(renterId) { + return renters.find((renter) => renter.id === renterId); +} + +function createRenter(renter) { + const newRenter = { + city: renter.city, + id: crypto.randomUUID(), + name: renter.name, + rating: 0, + roommates: renter.roommates || [] + }; + renters = [ + ...renters, + newRenter + ]; + return newRenter; +} + +function getAllRenters() { + return renters; +} + +module.exports = { + getRenterById, + createRenter, + getAllRenters +} +``` + +For the `createRenter` method we don’t need to pass in a `rating` as it will be a field that will get updated by another mutation down the line, but during creation the default rating should be 0. + +### Export Renter Schema and Resolver + +We will also create an `index.js` file within the `renter` folder with the following code in order to export our schema and resolver so it can be stitched together: + +``` +const { resolvers } = require('./resolver'); +const { typeDefs } = require('./schema'); + +module.exports = { + resolvers, + typeDefs +} +``` +Next we have to create an `index.js` file within `/src` so that we are able to start the schema stitching process and export one set of `resolvers` and `typeDefs` to our Apollo Server initialization: +``` +const { + mergeTypeDefs, + mergeResolvers +} = require('@graphql-tools/merge'); + +const { + resolvers: renterResolvers, + typeDefs: renterTypeDefs +} = require('./renters'); + + +const typeDefs = mergeTypeDefs([ + propertyTypeDefs +]); + +const resolvers = mergeResolvers([ + propertyResolvers +]); + +module.exports = { + resolvers, + typeDefs +} +``` +As we create resolvers for **property** and **propertyOwner** we will be adding to the lists passed to the `mergeTypeDefs` method and the `mergeResolvers` method. + +**Note:** Rather than needing the entire `@graphql-tools` library, we only need to install `@graphql-tools/merge` which reduces our overall dependency size. We still need to define some baseline data values for **renters** so we will do that next. + +### Renter Data + +Go to the top level of the project and create a file called `data.js`. We populate the object to match the schema definition we defined earlier, and it will look like this: +``` +const renters = [ + { + id: 'fdbe21a8-3eb3-4a70-a3b9-357c2af5acec', + name: 'renter 1', + city: 'Toronto', + rating: 4, + roommates: [] + }, + { + id: 'd83323ad-7dbf-4b71-8ee9-47dcf136cc18', + name: 'renter 2', + city: 'Toronto', + rating: 3.5, + roommates: [] + } +]; + +// Create renter/roommate relation +renters[0].roommates.push(renters[1].id); +renters[1].roommates.push(renters[0].id); + +module.exports = { + renters +} +``` +**Pause Step:** With this we should be able to test out our Renter endpoints in the Apollo Studio Sandbox. + +## PropertyOwner Entity + +Overall we want some baseline fields that represent what sort of information we may want to display from a property owner, but namely we want to setup a relationship between a property owner and a property. We will accomplish this via the **properties** field. Like the renter folder, we are going to create a *schema, resolver, and dataSource* as well as a file to export our **typeDefs** and **resolvers** created for schema stitching. + + +### PropertyOwner Schema +``` +const typeDefs = `#graphql + type PropertyOwner { + id: ID! + name: String! + address: String! + rating: Float + properties: [Property] + photo: String + } + + input CreatePropertyOwnerInput { + name: String! + address: String! + properties: [ID] + photo: String + } + + type Query { + propertyOwners: [PropertyOwner] + getPropertyOwnerById(propertyOwnerId: ID!): PropertyOwner + } + + type Mutation { + createPropertyOwner(createPropertyOwnerInput: CreatePropertyOwnerInput): PropertyOwner + } +` + +module.exports = { + typeDefs +} +``` + +**Note:** You’ll start to notice a pattern here with the base get/create query/mutations within the schemas. + +### PropertyOwner Resolver + +While most of the get/create methods are following the same sort of pattern, the interesting part of this is the **entity resolution** required for the `properties` field within **PropertyOwner**: +``` +const { + createPropertyOwner, + getAllPropertyOwners, + getPropertyOwnerById +} = require('./dataSource'); +const { + getPropertyById +} = require('../properties/dataSource'); + +const resolvers = { + PropertyOwner: { + properties(parent, _args) { + return parent.properties.map((propertyId) => getPropertyById(propertyId)); + } + }, + Query: { + getPropertyOwnerById: (_parent, args) => { + return getPropertyOwnerById(args.propertyOwnerId); + }, + propertyOwners: () => getAllPropertyOwners() + }, + Mutation: { + createPropertyOwner: (_parent, args) => { + return createPropertyOwner(args.createPropertyOwnerInput); + } + } +}; + +module.exports = { + resolvers +}; +``` + +### PropertyOwner properties field resolution + +Our resolver for the `properties` field will need to call the `getPropertyById` function from our **Property** dataSource in order to resolve the list of IDs and return a **Property** object (however in order for us to be able to test these mutations/queries, we will need to complete the **Property** dataSource and schema). + +### PropertyOwner dataSource + +This will follow a similar paradigm to the renter dataSource: +``` +const crypto = require('crypto'); +let { propertyOwners } = require('../../data'); + +function getPropertyOwnerById(propertyOwnerId) { + return propertyOwners.find( + (propertyOwner) => propertyOwner.id === propertyOwnerId + ); +} + +function createPropertyOwner(propertyOwner) { + const newPropertyOwner = { + id: crypto.randomUUID(), + name: propertyOwner.name, + address: propertyOwner.address, + properties: propertyOwner.properties || [], + photo: propertyOwner.photo + }; + + propertyOwners = [ + ...propertyOwners, + newPropertyOwner + ]; + return newPropertyOwner; +} + +function getAllPropertyOwners() { + return propertyOwners; +} + +module.exports = { + getPropertyOwnerById, + createPropertyOwner, + getAllPropertyOwners +} +``` + +**Note:** You can copy and paste the `index.js` from `src/renters` to `src/propertyOwners` as well as the `src/properties` folders. Since the import/export of these folders follow the same exact structure. + +### PropertyOwner Schema Stitching + +Remember to add the following lines to your `/src/index.js` file in order to export the merged typeDefs and resolvers: +``` +const { + resolvers: propertyOwnerResolvers, + typeDefs: propertyOwnerTypeDefs +} = require('./propertyOwners'); + +const typeDefs = mergeTypeDefs([ + propertyOwnerTypeDefs, + renterTypeDefs +]); + +const resolvers = mergeResolvers([ + propertyOwnerResolvers, + renterResolvers +]); +``` + +### PropertyOwner Data +Now we need to create a list of property owners within our `data.js` file, and we’ll add the following lines: + +``` +const propertyOwners = [ + { + id: 'c173abf2-648d-4df8-a839-b12c9117277e', + name: 'owner 1', + address: 'Toronto', + rating: 4.0, + properties: [], + photo: 'something' + }, + { + id: 'a09092cf-b99d-44c5-8dd6-68229d0258b5', + name: 'owner 2', + address: 'Toronto', + rating: 4.0, + properties: [], + photo: 'something' + } +] + +module.exports = { + renters, + propertyOwners +} +``` +**Pause Step:** With this we **won’t** be able to test our endpoints in Apollo Studio Sandbox because we currently have no way to resolve the properties field within the **PropertyOwner** object. + +## Property Entity + +Each property will have some basic information about it, a unique id, and will have connections to both the renter entity, and the propertyOwner entity. This will allow you to see if a property currently has tenants, and who the owner is as well as their contact information. + +### Property Schema + +``` +const typeDefs = `#graphql + type Property { + id: ID! + name: String! + city: String! + available: Boolean + description: String + photos: [String] + rating: Float + renters: [Renter] + propertyOwner: PropertyOwner! + } + + input CreatePropertyInput { + name: String! + city: String! + available: Boolean + description: String + photos: [String] + # need ID to attach renter to renters + renters: [ID] + propertyOwnerId: ID! + } + + type Query { + getPropertyById(propertyId: ID!): Property + properties: [Property] + } + + type Mutation { + createProperty(createPropertyInput: CreatePropertyInput): Property + } +`; + + +module.exports = { + typeDefs +}; +``` + +The only thing to note is that during the creation of a property entity, we are requiring the `propertyOwnerId` field to be filled. Essentially saying you need to have a person attached to the property for contact purposes. + +### Property Resolver + +This entity is interesting since we now need to define two resolver methods: one for the `renters` field and the other for the `propertyOwner` field. In this case we will have to import the methods from their respective dataSource files. It will look like the following: +``` +const { + getPropertyById, + createProperty, + getAllProperties +} = require('./dataSource'); +const { + getPropertyOwnerById +} = require('../propertyOwners/dataSource'); +const { + getRenterById +} = require('../renters/dataSource'); + +const resolvers = { + Property: { + renters(parent) { + return parent.renters.map((renterId) => getRenterById(renterId)); + }, + propertyOwner(parent) { + return getPropertyOwnerById(parent.propertyOwner); + } + }, + Query: { + getPropertyById: (_parent, args) => { + return getPropertyById(args.propertyId); + }, + properties: () => getAllProperties() + }, + Mutation: { + createProperty: (_parent, args) => { + return createProperty(args.createPropertyInput); + } + } +}; + +module.exports = { + resolvers +}; +``` + +### Property dataSource + +Creating the core methods `getEntityById, getAllEntities and createProperty` will be following the similar format to before: +``` +const crypto = require('crypto'); +let { properties } = require('../../data'); + +function getPropertyById(propertyId) { + return properties.find( + (property) => property.id === propertyId + ); +} + +function createProperty(property) { + const newProperty = { + available: property.available, + city: property.city, + description: property.description, + id: crypto.randomUUID(), + name: property.name, + photos: property.photos || [], + propertyOwner: property.propertyOwnerId, + rating: 0, + renters: property.renters || [] + }; + + properties = [ + ...properties, + newProperty + ]; + return newProperty; +} + +function getAllProperties() { + return properties; +} + +module.exports = { + getPropertyById, + createProperty, + getAllProperties +} +``` + +**Note:** Don’t forget to copy and paste the `index.js` from `src/renters` to `src/properties`! + +### Property Schema Stitching + +Remember to add the following lines to your `/src/index.js` file in order to export the merged typeDefs and resolvers: + +``` +const { + resolvers: propertyResolvers, + typeDefs: propertyTypeDefs +} = require('./properties'); + +const typeDefs = mergeTypeDefs([ + propertyTypeDefs, + propertyOwnerTypeDefs, + renterTypeDefs +]); + +const resolvers = mergeResolvers([ + propertyResolvers, + propertyOwnerResolvers, + renterResolvers +]); +``` + +### PropertyData + +Now we need to create the data that will match our properties schema, so we will add the following lines to the `data.js` file: +``` +const properties = [ + { + id: '86d401bb-cc8a-40f6-b3fc-396e6ddabb1a', + name: 'Deluxe suite 1', + city: 'Toronto', + rating: 5.0, + renters: [renters[0].id], + available: true, + description: 'amazing place 1', + photos: [], + propertyOwner: propertyOwners[0].id + }, + { + id: 'bcc2bb10-c919-42ae-8f6c-d24dba29c62f', + name: 'Deluxe suite 2', + city: 'Toronto', + rating: 5.0, + renters: [renters[1].id], + available: true, + description: 'amazing place 2', + photos: [], + propertyOwner: propertyOwners[1].id + } +]; + +// Create propertyOwner/property relation +propertyOwners[0].properties.push(properties[0].id); +propertyOwners[1].properties.push(properties[1].id); + +module.exports = { + renters, + propertyOwners, + properties +} +``` + +**Pause Step:** Now we can test our endpoints for both properties and propertyOwners now that we have data, entity resolution, and schema stitching in place. + +## Introducing Error Handling + +For all our endpoints so far we have always assumed the method would succeed, but what if it didn’t? That’s where having a generic **Error** type would help so that we can capture that error as part of its expected behavior. When throwing these errors, we want to ensure we have a human-readable error. But to start we are going to create a baseline `Error` interface in our schema. Since this is something that will be shared across schemas, we will add it to the `src/common` folder. Let’s create a `schema.js` within this folder and add the following: +``` +const typeDefs = `#graphql + interface Error { + message: String! + } +`; + +module.exports = { + typeDefs +}; +``` + +**Note:** You must make sure to add an `index.js` within `src/common` so that you can export `typeDefs` from this schema file. + +We will also need to add the typeDefs we made to be included in the schema stitching. We will add the following lines of code to our `src/index.js`: + +``` +const { + typeDefs: commonTypeDefs +} = require('./common'); + +const typeDefs = mergeTypeDefs([ + commonTypeDefs, + propertyTypeDefs, + propertyOwnerTypeDefs, + renterTypeDefs +]); +``` + +### Adding an Update mutation + +Now that we have baseline create/read methods for each of the entities we are going to go through how we will add an update mutation. First thing we will add is the **updateProperty** method to the schema and create a new input type **UpdatePropertyInput**. We will also be creating a **Union** type called **UpdatePropertyResult** which is a **Union** of either a **Property**, or a **PropertyNotFoundError**. In practice when we want to update a property, so far we will either be passed a valid id contained in our dataSource, or an invalid one, which means we will return this `PropertyNotFoundError`: + +``` +input UpdatePropertyInput { + id: ID! + name: String + city: String + available: Boolean + description: String + photos: [String] + rating: Float + # need ID to attach renter to renters + renters: [ID] + propertyOwner: ID +} + +type PropertyNotFoundError implements Error { + message: String! + propertyId: ID! +} + +union UpdatePropertyResult = Property | PropertyNotFoundError + +type Mutation { + createProperty(createPropertyInput: CreatePropertyInput): Property + updateProperty(updatePropertyInput: UpdatePropertyInput): UpdatePropertyResult +} +``` + +We have created a type **PropertyNotFoundError** which will extend our base error type. By doing this, **PropertyNotFoundError** has to have a non-nullable field `message` and then we add another field `propertyId` to return to the user the incorrect id passed to the method. We cannot have a return type of our mutation be `Property | PropertyNotFoundError` so we must create a **Union** type and set that as the return type of the mutation. + +Next we have to add the logic for the `updateProperty` within our resolver, so we will add the following lines to `src/properties/dataSource.js`: + +``` +const { + ..., + updateProperty +} = require('./dataSource'); + +Mutation: { + ..., + updateProperty: (_parent, args) => { + return updateProperty(args.updatePropertyInput.id, args.updatePropertyInput); + } +} +``` + +**Note:** The `...` signifies the code you already have in that snippet. + +Now we need to define the core logic of the `updateProperty` method within our `dataSource` file, we will add the following lines to `dataSource.js`: + +``` +const { PropertyNotFoundError } = require('../../errors'); + +function updateProperty(propertyId, updatedProperty) { + const foundPropertyIndex = properties.findIndex( + (aProperty) => aProperty.id === propertyId + ); + + if (foundPropertyIndex < 0) { + return PropertyNotFoundError(propertyId); + } + + properties[foundPropertyIndex] = { + ...properties[foundPropertyIndex], + ...updatedProperty + }; + + return { + __typename: 'Property', + ...properties[foundPropertyIndex] + }; +} +``` + +**Note:** We defined an error type called `PropertyNotFoundError` so that we can return an object with that same type. + +We’ll need to create an `/errors` folder and we’ll create a `PropertyNotFoundError.js` file in that folder as well as the `index.js` we will use to export that error (and set the stage for future errors we’ll create). This function will take a `propertyId` and return an object that has a `message` field and a `propertyId` field. You’ll also notice we have to add another field: `__typename`. This is used so that when we are resolving the return type of our update method, it will be a valid return type that we defined in our union. If we didn’t include the `__typename` field, then the resolver has no way of knowing what type it is, and will cause an error during the resolution process. + +Our `PropertyNotFoundError.js` will look like this: + +``` +function PropertyNotFoundError(propertyId) { + return { + __typename: 'PropertyNotFoundError', + message: 'Unable to find property with associated id.', + propertyId + }; +}; + +module.exports = { + PropertyNotFoundError +}; +``` + +**Pause Step:** Now we should be able to test our update method in Apollo Studio Sandbox. + +In order to test the endpoint you will need to use the `...on` for the potential return types for `updateProperty` which will look something like this: + +``` +mutation UpdateProperty($updatePropertyInput: UpdatePropertyInput) { + updateProperty(updatePropertyInput: $updatePropertyInput) { + ...on PropertyNotFoundError { + message + propertyId + } + ...on Property { + available + city + id + name + } + } +} +``` + +## Growing your schema and things to keep in mind + +As we grow our schema, especially when serving multiple customers or clients, we have to keep in mind what changes might be considered “breaking” changes. The breaking changes are as follows: + +- Removing a type or field +- Renaming a type or field +- Adding nullability to a field where the field was previously non-nullable +- Adding non-nullability to a field where the field was previously non-nullable +- Removing a field’s arguments + +One thing we can do is create a new field to an object and set the old field as deprecated, giving some time before making those breaking changes. This can be helpful to coordinate with front-end teams or other members that will use the endpoints you are creating, while keeping the endpoints stable for users that consume your API. The good news is that GraphQL has some built-in **directives** that can help us accomplish just that, in fact there is a `deprecated` directive that we can use! Let’s add the following lines to the `src/renters/schema.js` file: +``` +type Renter { + ... + deprecatedField: Boolean @deprecated(reason: "Use nonDeprecatedField.") + nonDeprecatedField: Boolean +} +input CreateRenterInput { + ... + deprecatedField: Boolean @deprecated(reason: "Use nonDeprecatedField.") + nonDeprecatedField: Boolean +} +``` + +**Note:** To use a directive you need to start with a `@` symbol followed by the name of the directive you are using. This directive is actually a function which will take a parameter `reason` which will display to the user when attempting to use the `deprecatedField`. The deprecated directive can be used with the following types: `FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE`. + +**Pause Step:** Check this out in Apollo Studio Sandbox and you will see the deprecated message! + +## Setting up a caching mechanism + +Since we are utilizing the power of directives when updating fields, we can also use it to start some ground work on caching some of our entities or fields within them. To do this we will be using a plugin offered by our `@apollo/server` library. We will add it to our `ApolloServer` initialization by passing it within the `plugins` field which you can see below (located in our top-level `index.js`): + +``` +const { ApolloServerPluginCacheControl } = require('@apollo/server/plugin/cacheControl'); + +const server = new ApolloServer({ + typeDefs, + resolvers, + plugins: [ + ApolloServerPluginDrainHttpServer({ httpServer }), + ApolloServerPluginCacheControl({ + // Cache everything for 1 second by default. + defaultMaxAge: 1, + // Don't send the `cache-control` response header. + calculateHttpHeaders: false, + }) + ] +}); +``` + +Next we will have to define the `@cacheControl` directive; and since this is something we will use across multiple graphs, we will add it to our `src/common/schema.js` file: + +``` +enum CacheControlScope { + PUBLIC + PRIVATE +} + +directive @cacheControl( + maxAge: Int + scope: CacheControlScope + inheritMaxAge: Boolean +) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION +``` + +The arguments for `@cacheControl` perform the following: +- `maxAge`: The maximum amount of time the field’s cached value is valid, in seconds. The default value is `0`, but you can set a different default. +- `scope`: If `PRIVATE`, the field’s value is specific to a single user. The default value is `PUBLIC`. +- `inheritMaxAge`: If `true`, this field inherits the `maxAge` of its parent field instead of using the default `maxAge`. Not not provide `maxAge` if you provide this argument. + +We will keep things simple for now, and have a few fields that we would like cached for 60 seconds. Let’s navigate to `src/properties/schema.js` and change the following fields under `Property`: +``` +type Property { + ... + rating: Float @cacheControl(maxAge: 60) + propertyOwner: PropertyOwner! @cacheControl(maxAge: 60) +} +``` + +These are fields that we think will not change often, and so these might be fields we would like to cache for some time. This will mean that it will only call the resolver function for `propertyOwner` or `rating` after 60 seconds for the same input, using a previously cached field value. + +## Conclusion + +During this tutorial we walked through setting up a multi-schema GraphQL server, using updated features from `Node v18, Apollo Server 4, and Graphql 16.6`. We looked into entity resolution when dealing with fields that reference other entities we have in dataSources. We looked into creating an update method for properties and starting our Error Handling processes, and introduced a use-case for Union types. We talked a little about schema maintenance, and how different directives can be used to notify deprecated fields, as well as introducing a little bit of Cache Control as well. I hope you enjoyed this tutorial, and next we will talk about introducing a live database and integrating it with our server! + +At the end of this tutorial you should have a server looking similar or identical to [this](https://github.com/bitovi/node-graphql-tutorial-2023). \ No newline at end of file diff --git a/src/graphql/2-adding-mongodb/adding-mongodb.md b/src/graphql/2-adding-mongodb/adding-mongodb.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/graphql/3-porting-to-prisma/porting-to-prisma.md b/src/graphql/3-porting-to-prisma/porting-to-prisma.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/graphql/4-adding-testing/adding-testing.md b/src/graphql/4-adding-testing/adding-testing.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/graphql/graphql.md b/src/graphql/graphql.md new file mode 100644 index 000000000..91b2a22cc --- /dev/null +++ b/src/graphql/graphql.md @@ -0,0 +1,70 @@ +@page learn-graphql Learn Graphql +@parent bit-academy 4 + +@description Build a backend application for property rentals using Node JS, GraphQL and Apollo Server. + Learn about using ORMs like Mongoose and Prisma with a NoSQL Mongo database! + Write testcases testing created endpoints. + +@body + +## Before You Begin + +

+ Click here to join the
Bitovi Community Discord

+ +
+ +Join the Bitovi Community Discord to get help on Bitovi Academy courses or other +GraphQL, Node JS, Angular, React, CanJS and JavaScript problems. + +Please ask questions related to GraphQ: in the [GraphQL chat room](https://discord.gg/Qv26e4uq5z). + +If you find bugs in this training or have suggestions, create an [issue](https://github.com/bitovi/academy/issues) or email `contact@bitovi.com`. + +## Overview + +In this guide, we will build a GraphQL server in Apollo Studio: + +You can see a finished repository of the code we are about to write [here](https://github.com/bitovi/node-graphql-tutorial-2023). + +This application (and course) assumes a fair bit of knowledge about Node JS, and some basic concepts of GraphQL. It is intended to teach developers about some updates to commonly used frameworks and to illustrate some more advanced concepts. We will be including how to perform: + +- Defining Schemas +- Schema Stitching +- Entity Resolution +- Error Handling and Unions +- Directives +- Setting Up MongoDB with Mongoose +- Converting from Mongoose to Prisma +- Adding Testing with Jest and introducing Fragments + +As for the application itself, it: + +- Is written in Node JS 18.12.1, Apollo Server 4, GraphQL 16.6 +- Has basic CRUD endpoints for three entites: Properties, PropertyOwners and Renters +- Has schema stitching and separate resolvers for each of the entities +- Has some local data that creates relationships and can illustrate entity resolution +- Transforms over time to include a database, and use different ORMs +- Illustrates an introduction to more advanced concepts and includes a baseline for testing endpoints + +## Outline + +This GraphQL tutorial will take you through creating different entities, how to create a server using Apollo Server 4, and some of the plugins available to use. We will go through queries, mutations, dealing with the resolver chain (A.K.A how to handle entity resolution). We’ll talk a bit about growing our graph including update mutations and introduce some error handling. Then we’ll go over directives that are available for the maintenance of your graph, and then end with adding some cache control for some fields within an entity. + +We are going to create a baseline application that has three main entities: renters, properties, and propertyOwners. This application will create some create/read endpoints for the following entities, and establish the different relationships between renters, properties, and propertyOwners. We want to start building an application that allows renters to find roommates as well as properties that are owned by property owners. + +Once the baseline application is created, we’ll also go over some basic methods to perform CRUD operations within Mongoose, and how we will pull related entities from our database (what is known as joins in SQL) using the `populate` function in Mongoose. We’ll also create a small seed file in order to fill our database with some baseline information. + +After that we will swap Mongoose for Prisma, and use that to perform our queries and mutations. This will encompass what porting Mongodb over to Prisma will look like. + +Finally we will add testing with Jest and include some nice abstractions to keep testing as clean as possible and introduce the idea of Fragments to do so. + +## Requirements + +In order to complete this guide, you need to have [NodeJS](https://nodejs.org/en/) version +18.12.1 or later installed. This is to use the new `--watch` feature that was added to Node JS. + +## Next Steps + +✏️ Head over to the [first lesson](learn-graphql/setting-up-apollo) and get the initial project created. \ No newline at end of file diff --git a/static/img/graphql-logo.png b/static/img/graphql-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf46ee263577d3a93cccbef248a0c2fa088570c GIT binary patch literal 81733 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

1tf+`#4r1n z#0cGNJ=}EOEI3@?Gh+$q(pWL_)$nMmw%VYqMxFMjQHcq;Cudlq`5SuWd(NHGfP1!7 z`mSx^7BSgvbjM+wM7un#*_M+~jy_|{*l6ECEVrfcp&fBwQ^4r&T|UHWQe{iK^8(A| z;4@rOnjtSMUsa+@u$ARiH5FB1>X%%dD6iu#hdii29nZZ(^&+2n`R^RfAf+xFUI&uL z^(5u(*$3S?w2#!T>+ysFRwE=cgz>trB)x_7Jt!%^x! zMW%&6R=EG!LP&HAwJZAznd|>2o1|Z{GLb)tphN$-!w0GU3;7i=d;gxM1Bp3sgl)qI zfzDqHP)GZuYxD0}Xlhm^48Q>N_XojQ^AcQY&WbVe@K7YwSR=7a* z)7AYaFRuqGzmk~EGHbuCp722RKbLxV{>W4Pxha0xi{{S_!+lSaD7FoMpsW1?AiXH} zk2IJ9^l$a$rjr1|;r$H=X4r429Ls+ZFxat*`(@0(IBY>{`CzSOcZ`W z!2Aj@2+<4`PB8G#*amc!Aq@(_l#M<3&gu}1GQ%06dLnXpl`sHz27b_`&4a&D7?PWK sAT7&))(4oI?qdF0@2~6m*M9lye)yN?OODjVflFCmPoWnNz<(S6Uo~~>&j0`b literal 0 HcmV?d00001 From e5c4f70d5aa8ff8b0f76cde6023451f78dcdb923 Mon Sep 17 00:00:00 2001 From: Emil Kais Date: Tue, 14 Mar 2023 14:59:14 -0400 Subject: [PATCH 2/4] Port over mongodb tutorial, start adding prisma tutorial --- .../2-adding-mongodb/adding-mongodb.md | 433 ++++++++++++++++++ .../3-porting-to-prisma/porting-to-prisma.md | 14 + 2 files changed, 447 insertions(+) diff --git a/src/graphql/2-adding-mongodb/adding-mongodb.md b/src/graphql/2-adding-mongodb/adding-mongodb.md index e69de29bb..1e2e5d911 100644 --- a/src/graphql/2-adding-mongodb/adding-mongodb.md +++ b/src/graphql/2-adding-mongodb/adding-mongodb.md @@ -0,0 +1,433 @@ +@page learn-graphql/adding-mongodb Connecting Apollo Server with MongoDB +@parent learn-graphql 2 + +@description In this guide we will be working off of a starting point [project](https://github.com/bitovi/node-graphql-tutorial-2023) and will be removing the need for static data, while adding a connection to a live database! + +We’ll also go over some basic methods to perform CRUD operations, and how we will pull related entities from our database (what is known as joins in SQL) using the `populate` function in Mongoose. We’ll also create a small seed file in order to fill our database with some baseline information. + +@body + +## Setting up MongoDB in the cloud + +Rather than host a local instance of MongoDB, we can instead use **MongoDB Atlas** and leverage their free-tier option in order to setup a MongoDB instance. I would recommend to follow the first 5 minutes of the video to get your account and your [cluster setup](https://www.youtube.com/watch?v=xrc7dIO_tXk&ab_channel=MongoDB). After this video I would suggest to keep your connection uri so that we can begin to use it in our project. + +### Dependencies + +First things first, we’ll need to install some packages that we’ll be going to use: + +`npm i mongodb mongoose dotenv` + +We are deciding to use `dotenv` so that we can put our connection uri inside a file .env at the highest level of the directory. And this file won’t be committed to any repository so that we don’t share our precious connection uri! We’ll add the following line to the `.env` file: + +`MONGODB_URL="mongodb+srv://"` + +## Mongoose vs MongoDB Native Driver: + +In this tutorial we’ll be going with Mongoose as our package of choice, but let’s quickly go over our options on why we might use one or the other. For us, Mongoose provides a nice way to create a Model of any entity we want to refer to, and when we want to create a new document using that Model, we get inherent model validation out of the box. Rather than having to include document validation ourselves within our resolvers or dataSource files, Mongoose will throw an error if an incorrect field is placed during a `save` operation. You can also create and manage relationships data relatively quickly by making the schema strict (in terms of validation), and using the `populate` method. Some drawbacks are that Mongoose can hide some complexity of what it is doing away from developers making it really difficult to understand complex concepts. + +The MongoDB native driver is definitely more performant than Mongoose and a lot of methods between the two are fairly similar in syntax. However you might have to create some boilerplate validation code since MongoDB will allow you store a document that doesn’t have to follow the same schema as other objects within the same collection. However depending on your use-case that may be better, for example where some sub-fields are not meant to be a consistent **type** between different documents. For this tutorial though, it keeps the GraphQL schema definition and the Mongoose definition fairly rigid to keep our entities consistent. + +## Adding the Mongoose connection to our server + +Add this line to the top of the `/index.js` file: + +``` +require('dotenv').config(); +``` + +And the following lines to setup the mongoose connection added to the database: + +``` +mongoose.set('strictQuery', false); +mongoose.connect(process.env.MONGODB_URL); +``` + +**Note:** We are adding this `strictQuery` to `false` because Mongoose has a deprecation warning around this being set to false by default in Mongoose 7. + +### Creating the Mongoose Models +In the `/src` folder we’ll create a `/models` folder and create three files: `property.js, propertyOwner.js, and renter.js`. We are keeping the model definitions separate from other parts of the code so we can keep our mongoose model definitions clean and concise. + +In our `/src/models/renter.js` file we’ll add the following lines of code: +``` +const { Schema, model } = require('mongoose'); + +const renterSchema = new Schema({ + name: { + type: String, + required: true, + maxLength: 60 + }, + city: { + type: String, + required: true + }, + rating: { + type: Number, + required: true + }, + roommates: [{ + type: Schema.Types.ObjectId, + ref: "Renter" + }], + deprecatedField: Boolean, + nonDeprecatedField: Boolean +}); + +const Renter = model("Renter", renterSchema); + +module.exports = { + Renter +}; +``` + +What is notable here is that with each field we are able to create specific types, add some level of verification and set whether the field is required or not. Another interesting portion is the **roommates** field having a `Schema.Types.ObjectId` type and a `ref: "Renter"`. This is Mongoose’s way to indicate that a field represents another object and will be referenced by the ID string we pass to that field. This is the first step in being able to take an ID, and eventually resolve a `Renter` object during the resolution process. Lastly we export our `Renter` so that we can use it in our resolvers going forward, this object will give us many helper functions around CRUD operations so that we can integrate MongoDB easily. + +In our `/src/models/propertyOwner.js` file we’ll add the following lines of code: + +``` +const { Schema, model } = require('mongoose'); + +const propertyOwnerSchema = new Schema({ + name: { + type: String, + required: true, + maxLength: 60 + }, + address: { + type: String, + required: true + }, + rating: { + type: Number, + required: true + }, + properties: [{ + type: Schema.Types.ObjectId, + ref: "Property" + }], + photo: String +}); + +const PropertyOwner = model("PropertyOwner", propertyOwnerSchema); + +module.exports = { + PropertyOwner +}; +``` + +Nothing here is notable aside from being able to add a list of `Schema.Types.ObjectId`, and remember that we are creating our Mongoose models to match the GraphQL schemas we created earlier. + +In our `/src/models/property.js` file we’ll add the following lines of code: + +``` +const { Schema, model } = require('mongoose'); + +const propertySchema = new Schema({ + name: { + type: String, + required: true, + maxLength: 60 + }, + city: { + type: String, + required: true + }, + available: Boolean, + description: { + type: String, + maxLength: 250 + }, + photos: [String], + rating: { + type: Number, + required: true + }, + renters: [{ + type: Schema.Types.ObjectId, + ref: "Renter" + }], + propertyOwner: { + type: Schema.Types.ObjectId, + ref: "PropertyOwner" + } +}); + +const Property = model("Property", propertySchema); + +module.exports = { + Property +}; +``` +In this model, the only notable fields are ensuring the relationship between `renters` and `propertyOwner` fields are created correctly and are referencing the same name that we passed into the `model` method for both renters and propertyOwners. + +## Creating a seed script + +At this point we have our models setup, but in order to start switching our resolvers to use Mongoose, we will need to create data within our database. That is where creating a seed file will come in handy, and we will learn some useful methods that we get from Mongoose. Create a `seed.js` file at the top-level of the project, we are going to use the static information we had before, and move it to the `seed.js` file with some adjustments: + +``` +require('dotenv').config(); +const mongoose = require('mongoose'); +const { Property, PropertyOwner, Renter } = require('./src/models'); + +const renters = [ + { + name: 'renter 1', + city: 'Toronto', + rating: 4, + roommates: [] + }, + { + name: 'renter 2', + city: 'Toronto', + rating: 3.5, + roommates: [] + } +]; + +const propertyOwners = [ + { + name: 'owner 1', + address: 'Toronto', + rating: 4.0, + properties: [], + photo: 'something' + }, + { + name: 'owner 2', + address: 'Toronto', + rating: 4.0, + properties: [], + photo: 'something' + } +]; + +const properties = [ + { + name: 'Deluxe suite 1', + city: 'Toronto', + rating: 5.0, + renters: [], + available: true, + description: 'amazing place 1', + photos: [], + propertyOwner: null + }, + { + name: 'Deluxe suite 2', + city: 'Toronto', + rating: 5.0, + renters: [], + available: true, + description: 'amazing place 2', + photos: [], + propertyOwner: null + } +]; +``` +**Note:** We are removing the need for `id` fields in our objects. We are also setting some initial reference values as null before we give them values later on. + +Next we are going to run an `async runSeed()` function which will connect to our database, create the initial entities and then attach the relationships before creating the fields: + +``` +async function runSeed() { + + mongoose.set('strictQuery', false); + mongoose.connect(process.env.MONGODB_URL); + try { + const newRenter = new Renter(renters[0]); + const newRenterTwo = new Renter(renters[1]); + + // establish relationship + newRenter.set('roommates', [newRenterTwo._id]); + newRenterTwo.set('roommates', [newRenter._id]); + + await newRenter.save(); + await newRenterTwo.save(); + + const newPropertyOwner = new PropertyOwner(propertyOwners[0]); + const newPropertyOwnerTwo = new PropertyOwner(propertyOwners[1]); + + const newProperty = new Property(properties[0]); + const newPropertyTwo = new Property(properties[1]); + + newProperty.set('renters', [newRenter._id]); + newPropertyTwo.set('renters', [newRenterTwo._id]); + newProperty.set('propertyOwner', [newPropertyOwner._id]); + newPropertyTwo.set('propertyOwner', [newPropertyOwnerTwo._id]); + + newPropertyOwner.set('properties', [newProperty._id]); + newPropertyOwnerTwo.set('properties', [newPropertyTwo._id]); + + await newPropertyOwner.save(); + await newPropertyOwnerTwo.save(); + + await newProperty.save(); + await newPropertyTwo.save(); + + console.log('done'); + } catch (err) { + console.error('runSeed error', err); + } finally { + mongoose.connection.close(); + } +} + +runSeed(); +``` + +We can use the `new Entity()` method which will return a MongoDB document for us to save later on. We still want to set up our relationships, our models contain a useful method called `set` that we can set any field we want in the MongoDB document. Once our objects are now in the shape we want them to be, we then save them to the database. + +We create our propertyOwners before our properties, so that we can set the `propertyOwner` field which is required by our model validation. Once we save our propertyOwners and our properties we have our console log print out `done` so we get a visual confirmation. We use the `finally` block during our `try/catch` in order to close our connection to Mongoose after our seeding is done. + +Next we go to our `package.json` and we add a `seed` field to our scripts parameter: +``` +"scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node --watch ./index.js", + "seed": "node ./seed.js" +} +``` + +Your `package.json` should look like above for the `scripts` field. + +## Adding Mongoose to our dataSources + +Now that we have created the baseline models we want that match our entities, all we need to do to add values into the database is to run `npm run seed` from a console within the project directory. If the seed successfully ran, you will know that our models are correct by viewing them within the MongoDB Atlas webpage. The next step is to go into our dataSources and begin porting over our operations, go into `src/renters/dataSource.js`: +``` +const { Renter } = require('../models'); + +async function getRenterById(renterId) { + return Renter.findById(renterId) + .populate('roommates'); +} + +async function createRenter(renter) { + const newRenter = new Renter({ + city: renter.city, + name: renter.name, + rating: 0, + roommates: renter.roommates || [] + }); + + const savedRenter = await newRenter.save(); + return savedRenter.populate('roommates'); +} + +async function getAllRenters() { + return Renter.find({}) + .populate('roommates'); +} +``` + +Mongoose provides out-of-the-box methods for most operations we want to do here. Notice that at the top of the file we import our `Renter` model from our `src/models` directory. Another important thing to notice is the `populate` function. This is Mongoose's way of performing a join operation on the id we attached to the field **roommates**. This populate method will fetch the data we need related to that id, removing the need to have a custom resolution strategy in our `src/renters/resolvers.js`. In fact we can remove the need for the entire `Renter` field in the resolvers object (we will be doing similar things for the other two entities properties, and propertyOwners). + +When creating or updating an entity, we still want to have the `roommates` field populated. In this case we have to add a step where we `save` an entity first, and then return by calling the `.populate` function with the fields we want to return. If we return without calling `.populate` then it will return only the string value of those fields which will break our GraphQL query. + +Now let’s move over to the `src/propertyOwners/dataSource.js` and add Mongoose to the methods: +``` +const { PropertyOwner } = require('../models'); + +async function getPropertyOwnerById(propertyOwnerId) { + return PropertyOwner.findById(propertyOwnerId) + .populate('properties'); +} + +async function createPropertyOwner(propertyOwner) { + const newPropertyOwner = new PropertyOwner({ + name: propertyOwner.name, + address: propertyOwner.address, + properties: propertyOwner.properties || [], + photo: propertyOwner.photo, + rating: 0 + }); + + const savedPropertyOwner = await newPropertyOwner.save(); + + return savedPropertyOwner + .populate('properties'); +} + +async function getAllPropertyOwners() { + return PropertyOwner.find({}) + .populate('properties'); +} +``` + +**Note:** Do not forget to remove the PropertyOwner field from the resolvers in `src/propertyOwners/resolver.js`. + +Finally we head to our `src/properties/dataSource.js` file and we add the following lines: +``` +const { Property } = require('../models'); +const { PropertyNotFoundError } = require('../../errors'); + +async function getPropertyById(propertyId) { + return Property.findById(propertyId) + .populate('renters propertyOwner'); +} + +async function createProperty(property) { + const newProperty = new Property({ + available: property.available, + city: property.city, + description: property.description, + name: property.name, + photos: property.photos || [], + propertyOwner: property.propertyOwnerId, + rating: 0, + renters: property.renters || [] + }); + + const savedProperty = await newProperty.save(); + + return savedProperty + .populate('renters propertyOwner'); +} + +async function updateProperty(propertyId, updatedProperty) { + const savedProperty = await Property.findByIdAndUpdate( + propertyId, + updatedProperty, + { new: true } + ).populate('renters propertyOwner'); + + if (!savedProperty) { + return PropertyNotFoundError(propertyId); + } + return { + __typename: 'Property', + id: savedProperty.id, + available: savedProperty.available, + city: savedProperty.city, + description: savedProperty.description, + name: savedProperty.name, + photos: savedProperty.photos, + propertyOwner: savedProperty.propertyOwnerId, + rating: savedProperty.rating, + renters: savedProperty.renters + } +} + +async function getAllProperties() { + return Property.find({}) + .populate('renters propertyOwner'); +} +``` + +Now an interesting part here is that if we need to `populate` multiple fields on one entity, we don’t have to chain multiple `populate` methods. We simply need to pass in one space-separated string of the fields we need to populate (see line 6 for an example). If the field we wish to populate is a field on a nested entity, then we can follow this notation `ENTITY_NAME.FIELD_NAME` and Mongoose will populate it for us. + +The `updateProperty` function is also something to note since we are doing something a bit different, we are passing in a third input to `findIdAndUpdate`. By adding `{ new: true }`, it indicates that we should return a new document on update. If there is no returned document, then we can throw our `PropertyNotFoundError`. The other difference is how we return the updated document, because we cannot destructure the fields like so: +``` +return { + __typename: 'Property', + ...savedProperty +} +``` +Doing this will make the return object have other MongoDB-related fields and will break the return type for this GraphQL mutation. + +## Conclusion + +During this tutorial we covered how to get MongoDB setup in the cloud, how to use `dotenv` in order to hide our connection string, and had walked through some of the features of Mongoose’s Schema creation methods. Next we created a `seed.js` script in order to initialize data within our database. Finally we used those models defined to upgrade our dataSource methods. We talked about some of the features of populate and how it can make entity resolution easier. + +At the end of this tutorial you should have a repository similar to [this](https://github.com/bitovi/node-graphql-tutorial-2023/tree/mongodb) + +I hope you learned something new in this tutorial, and in the next tutorial in the series will we cover porting over our endpoints from Mongoose to Prisma! \ No newline at end of file diff --git a/src/graphql/3-porting-to-prisma/porting-to-prisma.md b/src/graphql/3-porting-to-prisma/porting-to-prisma.md index e69de29bb..de3d83a88 100644 --- a/src/graphql/3-porting-to-prisma/porting-to-prisma.md +++ b/src/graphql/3-porting-to-prisma/porting-to-prisma.md @@ -0,0 +1,14 @@ +@page learn-graphql/porting-to-prisma Converting Mongoose to Prisma +@parent learn-graphql 3 + +@description In this guide we will be working off of a starting point [project](https://github.com/bitovi/node-graphql-tutorial-2023/tree/mongodb) and will be removing the need for Mongoose and instead we will be using Prisma to perform our queries and mutations! + +@body + +## What is an ORM? + +To begin we will be addressing what is an ORM: it stands for Object Relational Mapping. This is essentially a technique used in creating a bridge between object-oriented programs like Node JS and relational databases like SQL. Instead of having to build your own custom ORM tool from scratch, you can make use of an ORM (even Mongoose is an ORM). + +## What is Prisma? + +Prisma is a new type of ORM where you can define your model in the declarative Prisma schema which serves as the single source of truth for your database schema and the models in your programming language. Using the Prisma Client you get type-safe methods, and some nice autocompletion when interacting with a collection or table. Prisma also comes with **Prisma Migrate**, which is a declarative data modeling and migration tool which generates a migration file, updates the database schema, and generates the new Prisma client for use during development. \ No newline at end of file From 6f0298c43b722087d64b1e3d7ca7983f62541305 Mon Sep 17 00:00:00 2001 From: Emil Kais Date: Tue, 14 Mar 2023 15:31:31 -0400 Subject: [PATCH 3/4] Port over prisma tutorial --- .../3-porting-to-prisma/porting-to-prisma.md | 543 +++++++++++++++++- 1 file changed, 542 insertions(+), 1 deletion(-) diff --git a/src/graphql/3-porting-to-prisma/porting-to-prisma.md b/src/graphql/3-porting-to-prisma/porting-to-prisma.md index de3d83a88..5de053437 100644 --- a/src/graphql/3-porting-to-prisma/porting-to-prisma.md +++ b/src/graphql/3-porting-to-prisma/porting-to-prisma.md @@ -11,4 +11,545 @@ To begin we will be addressing what is an ORM: it stands for Object Relational M ## What is Prisma? -Prisma is a new type of ORM where you can define your model in the declarative Prisma schema which serves as the single source of truth for your database schema and the models in your programming language. Using the Prisma Client you get type-safe methods, and some nice autocompletion when interacting with a collection or table. Prisma also comes with **Prisma Migrate**, which is a declarative data modeling and migration tool which generates a migration file, updates the database schema, and generates the new Prisma client for use during development. \ No newline at end of file +Prisma is a new type of ORM where you can define your model in the declarative Prisma schema which serves as the single source of truth for your database schema and the models in your programming language. Using the Prisma Client you get type-safe methods, and some nice autocompletion when interacting with a collection or table. Prisma also comes with **Prisma Migrate**, which is a declarative data modeling and migration tool which generates a migration file, updates the database schema, and generates the new Prisma client for use during development. + +### Install the Prisma CLI + +There are a few things that we’ll need to do in order to port our Mongoose queries over to Prisma. After our last tutorial we should have a collection defined that contains entities created in the database (by running our **seed.js**). + +Let’s start by installing what packages we need: + +``` +npm i prisma -D +npx prisma init --datasource-provider mongodb +npm i @prisma/client +``` + +### Introspect the Database + +We are going to run the following command in order to update our `schema.prisma` file with the collections already created in MongoDB. One thing to note is that your `schema.prisma` file should have this value present (this will be your connection string): + +``` +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} +``` +Make sure that your `.env` file contains a value called **DATABASE_URL**. This connection string has a slight difference in that it will contain the collection name of the database so it knows where to pull from, see an example below and note the **collectionName** portion: + + +`mongodb+srv://:.mongodb.net/?retryWrites=true&w=majority` + +### Next we will run the following commands + +``` +npx prisma db pull +npx prisma generate +``` + +This will now update your `schema.prisma` file to reflect what was defined in MongoDB based on the information we put into our database. Unfortunately MongoDB doesn’t support relations between different collections. So we have to re-create the references between documents using the `ObjectId` field type from one document to another. + +### PropertyOwner → Properties relation mapping (1-many) + +In our previous definitions, every property has a propertyOwner, and a propertyOwner can have many properties. So now we need to setup that relationship in our **schema.prisma** file: + +``` +model PropertyOwners { + id String @id @default(auto()) @map("_id") @db.ObjectId + v Int @map("__v") + address String + name String + photo String + properties Properties[] + rating Float + + @@map("propertyowners") +} +``` + +This part of the relationship is fairly simple: we change the type of `properties` to be of type `Properties[]`. + +**Note:** We can use the **@@map** keyword in order to rename the model. We can also use the **@map** keyword to rename a field to another name. Prisma has the following naming conventions: PascalCase for model names and camelCase for field names. + +Next we have to change the model **Properties** to have the other portion of the relationship: +``` +model Properties { + id String @id @default(auto()) @map("_id") @db.ObjectId + v Int @map("__v") + available Boolean + city String + description String + name String + photos String[] + propertyOwner PropertyOwners @relation(fields: [propertyOwnerId], references: [id]) + propertyOwnerId String @db.ObjectId + rating Float + renters Renters[] + + @@map("properties") +} +``` +In order to define the relationship, we have to define two fields: `propertyOwner` and `propertyOwnerId`. The first one seems easy enough `propertyOwnerId` is a `String` type and we need to use the `@db.ObjectId` keyword to represent that it is an id for MongoDB. Next we change the propertyOwner field to be of type `PropertyOwners` and we need to define a `@relation`. Here we are mentioning that the fields that this references is the `propertyOwnerId` field and that is representing an `id` of another model (in this case **PropertyOwner**). We setup the first part of the properties → renters (1 → many) relationship as well, in this case the renters field represents `Renters[]`. + +## Renter Many → Many Self-Relationship + +In the Renters model we need to define the other part of the relationship for properties, and the roommate relationship as well: +``` +model Renters { + id String @id @default(auto()) @map("_id") @db.ObjectId + v Int @map("__v") + city String + name String + rating Float + roommates Renters[] @relation("RoommateRenters") + roommateRenter Renters? @relation("RoommateRenters", fields: [roommateId], references: [id], onDelete: NoAction, onUpdate: NoAction) + roommateId String? @db.ObjectId + rentedPropertyId String? @map("rentedProperty") @db.ObjectId + rentedProperty Properties? @relation(fields: [rentedPropertyId], references: [id]) + + @@map("renters") +} +``` + +To complete the Renter → Property relationship we do something similar for the Property → PropertyOwner relationship: we create `rentedPropertyId` with the `String` field being optional marked by the type ending with `?`. We then set it as a `@db.ObjectId` and use a `@relation` on the `rentedProperty` field referencing the optional id we created. + +Next is something that is a bit more complicated, we need to create three fields for the relationship between Renters and their roommates: +``` +roommates Renters[] @relation("RoommateRenters") +roommateRenter Renters? @relation("RoommateRenters", fields: [roommateId], references: [id], onDelete: NoAction, onUpdate: NoAction) +roommateId String? @db.ObjectId +``` +The `roommates` field which is the `Renters[]` type contains the `@relation("RoommateRenters")` which gives a name to the relation `RoommateRenters`. Next we define that each `roommateRenter` in this relationship called `RoommateRenters` is an optional field where it references an id we are calling `roommateId`. Then finally we create the id `roommateId` of an optional type `String`, and we use the `@db.ObjectId` to show it is a MongoDB id. We finish the `@relation` by adding `onDelete: NoAction, onUpdate: NoAction`. This is because the self-relation can cause an infinite loop if we update that field. + +## Putting Prisma Client into the project +With that, we defined all the relationships we need, the next step is to add our Prisma Client into our project. In our `src` folder we’ll create a `prisma.js` file and add the following lines: + +``` +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); + +module.exports = prisma; +``` + +Then now if our `index.js` server file we can pass it through the GraphQL context so that are resolvers only use a single instance of our Prisma Client: + +``` +const prisma = require('./src/prisma'); +... + +app.use( + ... + expressMiddleware(server, { + context: async ({ req }) => ({ + token: req.headers.token, + // passing our prisma connection for re-use + prisma + }), + }) +); +``` + +## Create the new seed file + +Though the syntax might be a bit different, the concept will still be similar to what we had to do for Mongoose. Prisma uses a `connect` option that will set up the relation between two models, and in the return portion we use the `include` option that will work similar to the way `populate` did in Mongoose. First step is to change some of the initial seed data: + +``` +const Prisma = require('./src/prisma'); + +const renters = [ + { + name: 'renter 1', + city: 'Toronto', + rating: 4.0, + v: 0 + }, + { + name: 'renter 2', + city: 'Toronto', + rating: 3.5, + v: 0 + } +]; + +const propertyOwners = [ + { + name: 'owner 1', + address: 'Toronto', + rating: 4.0, + photo: 'something', + v: 0 + }, + { + name: 'owner 2', + address: 'Toronto', + rating: 4.0, + photo: 'something', + v: 0 + } +]; + +const properties = [ + { + name: 'Deluxe suite 1', + city: 'Toronto', + rating: 5.0, + available: true, + description: 'amazing place 1', + photos: [], + v: 0 + }, + { + name: 'Deluxe suite 2', + city: 'Toronto', + rating: 5.0, + available: true, + description: 'amazing place 2', + photos: [], + v: 0 + } +]; +``` + +We’ll be needing the `v` field because it is a value present in the MongoDB store and tracks versioning. Next is we will begin to create the relationships between Renters that are roommates: +``` +// create two renters, one of which is roommates with the second nested renter +const firstRenter = await Prisma.renters.create({ + data: { + ...renters[0], + roommates: { + create: renters[1] + } + } +}); +// connect renter 2 to be a roommate of renter 1 +const secondRenter = await Prisma.renters.update({ + where: { + id: firstRenter.roommates[0].id + }, + data: { + roommates: { + connect: [{ id: firstRenter.id }] + } + } +}); +``` + +We will be using a nested `create` which allows us to create both roommates together, and putting the id of the newly created renter in the `roommates` field. Next we can update the second renter created and use the `connect` option, which can take a **list** of objects containing an **id** value of the model we are going to connect. There are other methods like `connectOrCreate` but in this case we know its data we are initializing rather than connecting to data we’re not sure if it exists. Next we create the PropertyOwners: + +``` +// Create propertyOwners with connected properties +// then connect renters to properties +await Prisma.propertyOwners.create({ + data: { + ...propertyOwners[0], + properties: { + create: { + ...properties[0], + renters: { + connect: [{ id: firstRenter.id }] + } + } + } + } +}); + +await Prisma.propertyOwners.create({ + data: { + ...propertyOwners[1], + properties: { + create: { + ...properties[1], + renters: { + connect: [{ id: secondRenter.id }] + } + } + } + } +}); +``` + +What is nice here is that we can immediately create the properties we wanted to originally link to PropertyOwners in the `create` option and simultaneously `connect` on the `renters` field and link all the models together. In fact it looks like our **seed.js** file is now a bit more clean than before! + +## Updating our Renter resolvers and dataSources + +For each of our models, we will need to update the methods to include the `prisma` instance that is now passed through the context for our dataSources to use, first up we’ll update Renters: +``` +Query: { + getRenterById: async (_parent, args, { prisma }) => { + return getRenterById(args.renterId, prisma); + }, + renters: async (_parent, _args, { prisma }) => { + return getAllRenters(prisma); + } +}, +Mutation: { + createRenter: async (_parent, args, { prisma }) => { + return createRenter(args.createRenterInput, prisma); + } +} +``` + +**Note:** The Prisma instance will be passed through the `info` variable and we’ll destructure what we need for now from it. + +Next, we need to head to the `renters/dataSource.js`: +``` +async function getRenterById(renterId, Prisma) { + return Prisma.renters.findUnique({ + where: { + id: renterId + }, + include: { + roommates: true + } + }); +} + +async function createRenter(renter, Prisma) { + return Prisma.renters.create({ + data: { + city: renter.city, + name: renter.name, + rating: 0.0, + roommates: { + connect: renter.roommates.map((renterId) => ({ id: renterId })) + }, + v: 0 + }, + include: { + roommates: true + } + }); +} + +async function getAllRenters(Prisma) { + return Prisma.renters.findMany({ + include: { + roommates: true + } + }); +} +``` + +Other than the method name changes, the most notable portion is that we have the `include` option for the `roommates` field, this ensures that when we return from this method we have a populated `roommates` object. You’ll see that we include `v: 0` in our data payload for creates, this is the same as the `seed.js` file where for mongoDB documents we need that versioning. + +**Note:** As for the Prisma naming convention, you may not end up wanting to use `Prisma` but instead use `prismaClient` or lowercase `prisma` instead. This depends on your style and feel free to adjust accordingly. + +## Updating our Property resolvers and dataSources +``` +Query: { + getPropertyById: async (_parent, args, { prisma }) => { + return getPropertyById(args.propertyId, prisma); + }, + properties: async (_parent, _args, { prisma }) => getAllProperties(prisma) +}, +Mutation: { + createProperty: async (_parent, args, { prisma }) => { + return createProperty(args.createPropertyInput, prisma); + }, + updateProperty: async (_parent, args, { prisma }) => { + return updateProperty(args.updatePropertyInput.id, args.updatePropertyInput, prisma); + } +} +``` +However things for the property will become more complicated for the update method we created before: +``` +async function getPropertyById(propertyId, Prisma) { + return Prisma.properties.findUnique({ + where: { + id: propertyId + }, + include: { + renters: true, + propertyOwner: true + } + }); +} + +async function createProperty(property, Prisma) { + return Prisma.properties.create({ + data: { + available: property.available, + city: property.city, + description: property.description, + name: property.name, + photos: property.photos || [], + propertyOwnerId: property.propertyOwnerId, + rating: 0.0, + renters: { + connect: property.renters.map((renterId) => ({ id: renterId })) + }, + v: 0 + }, + include: { + propertyOwner: true, + renters: true + } + }); +} + +async function updateProperty(propertyId, updatedProperty, Prisma) { + const nonConnectPropertyFields = omit(updatedProperty, ['id', 'renters', 'propertyOwner']); + + const renters = updatedProperty.renters && { + connect: updatedProperty?.renters.map((renterId) => ({ id: renterId })) + }; + const propertyOwner = updatedProperty.propertyOwner && { + connect: { id: updatedProperty.propertyOwner } + }; + + try { + const savedProperty = await Prisma.properties.update({ + where: { + id: propertyId + }, + data: { + ...nonConnectPropertyFields, + ...(renters), + ...(propertyOwner) + }, + include: { + renters: true, + propertyOwner: true + } + }); + return { + __typename: 'Property', + ...savedProperty + }; + + } catch (err) { + if (err?.meta?.cause === 'Record to update not found.') { + return PropertyNotFoundError(propertyId); + } + return err; + } +} + +async function getAllProperties(Prisma) { + return Prisma.properties.findMany({ + include: { + renters: true, + propertyOwner: true + } + }); +} +``` + +For creating connections, we will need a way to remove the fields we want to re-purpose or re-make using the connect syntax, in this case we will want to remove the fields we don’t want to include from our input object (this is because we still want to destructure the rest of the input fields to make the data object cleaner). In this case we will omit `id, renters, propertyOwners`. To do that we will install `lodash.omit`, and then we will form the new objects containing the ids of the renters and the propertyOwners. For the return we still have to ensure we include `__typename: 'Property'` in the object. + +## Error handling in Prisma + +In this case you will see that we handle our errors by specifically looking for error messages within the err object, this is because sometimes Prisma may not return specific errors that we can consume into our Error types we create. In this case we have to check the contents of the error object, and map that to out **PropertyNotFoundError**. Adding more edge cases and types of errors may include adding more logic to this catch block in order to interpret errors, which may not look ideal. However if we want to be returning a specific type of error every time, for each of our GraphQL Union Error types then this a good enough approach for now. + +## Updating our PropertyOwner resolvers and dataSources + +This last entity is fairly simple in both porting over the resolver and the dataSource: +``` +Query: { + getPropertyOwnerById: (_parent, args, { prisma }) => { + return getPropertyOwnerById(args.propertyOwnerId, prisma); + }, + propertyOwners: (_parent, _args, { prisma }) => getAllPropertyOwners(prisma) +}, +Mutation: { + createPropertyOwner: (_parent, args, { prisma }) => { + return createPropertyOwner(args.createPropertyOwnerInput, prisma); + } +} +``` + +``` +async function getPropertyOwnerById(propertyOwnerId, Prisma) { + return Prisma.propertyOwners.findUnique({ + where: { + id: propertyOwnerId + }, + include: { + properties: true + } + }); +} + +async function createPropertyOwner(propertyOwner, Prisma) { + return Prisma.propertyOwners.create({ + data: { + name: propertyOwner.name, + address: propertyOwner.address, + properties: { + connect: propertyOwner.properties.map((propertyId) => ({ id: propertyId })) + }, + photo: propertyOwner.photo, + rating: 0.0, + v: 0 + }, + include: { + properties: true + } + }); +} + +async function getAllPropertyOwners(Prisma) { + return Prisma.propertyOwners.findMany({ + include: { + properties: true + } + }); +} +``` + +## Transactions + +Let’s say you want to perform a query or mutation where you want to make sure that each independent query runs, but you want to ensure that you can roll it back if there is an error in between. This is where transactions come into play, and we will be creating a mutation to showcase an example, right now if you want to add a roommate to one Renter, it will not update the other Renter object saying they are both roommates. We will create a **makeRoommates** mutation in the Renter schema: +``` +type Mutation { + createRenter(createRenterInput: CreateRenterInput): Renter + makeRoommates(renterIds: [ID]): [Renter] +} +``` +Then we will add the method to the renters/resolver.js: +``` +makeRoommates: async (_parent, args, { prisma }) => { + return makeRoommates(args.renterIds, prisma); +} +``` +Finally we will add the code for the makeRoommates method: +``` +// Takes a list of renterIds and ensures that each renter +// contains a roommates field that has all the ids provided +async function makeRoommates(renterIds, Prisma) { + const connectionRenterIds = renterIds.map((renterId) => ({ id: renterId })); + const updates = renterIds.map((renterId) => Prisma.renters.update({ + where: { + id: renterId + }, + data: { + roommates: { + connect: connectionRenterIds.filter(({ id }) => id !== renterId) + } + }, + include: { + roommates: true + } + })); + return Prisma.$transaction(updates); +} +``` + +We will take the given ids, and then loop through each renterId provided in the input of the function to create an update where each Renter is a roommate of all the other Renters. Some things to point out on the syntax here for **Prisma$.transaction**: namely that I would suggest to follow the format that your transaction always includes a list of updates/mutations/queries like so: + +``` +Prisma.$transaction([update1, update2, update3]) +``` + +While Prisma does have **interactive transactions** meaning you can call a method and use its result to perform another query/mutation/update, however you can run into many problems during debugging this. Namely that the transaction may close before all the operations complete, and can require you to reconfigure your timeout for transactions (which I would not recommend in this early stage of the project). Prisma often in its documentation mentions that before using a **transaction** you can think to see if **Nested Writes** may solve your problem (see `seed.js` for an example). Likewise the **Batch/Bulk** operations like **deleteMany, updateMany, and createMany** run as transactions, so you can use those methods instead. + +**Apollo pause:** To check if you have ported over to Prisma successfully, test by running the `seed.js` and re-run the endpoints you created for each entity, that will ensure that your project is in the correct state. + +## Clean up and Conclusion + +Now that we have successfully migrated over to Prisma, we can remove all instances of the `models` folder, and any reference to Mongoose so that we can uninstall it. During this tutorial we introduced some of the features of Prisma and then ported our Models to instead use the **schema.prisma** file. We had re-created our relationships between models, and then updated our endpoints to use the Prisma CRUD methods. We then showed how to perform some rudimentary error handling, and finally showcased an example of a mutation leveraging Prisma’s Transaction API. + +At the end of this tutorial you should have a repository similar to [this](https://github.com/bitovi/node-graphql-tutorial-2023/tree/prisma) + +I hope this has given you a glimpse into the power of Prisma, and the next tutorial in the series will cover adding Jest testcases and Fragments to the project. \ No newline at end of file From 3879500c032c75ba1e5c98e6a37a40b17847d0d2 Mon Sep 17 00:00:00 2001 From: Emil Kais Date: Wed, 15 Mar 2023 13:02:21 -0400 Subject: [PATCH 4/4] Add testcase tutorial --- .../4-adding-testing/adding-testing.md | 801 ++++++++++++++++++ 1 file changed, 801 insertions(+) diff --git a/src/graphql/4-adding-testing/adding-testing.md b/src/graphql/4-adding-testing/adding-testing.md index e69de29bb..2c82e6efe 100644 --- a/src/graphql/4-adding-testing/adding-testing.md +++ b/src/graphql/4-adding-testing/adding-testing.md @@ -0,0 +1,801 @@ +@page learn-graphql/adding-testing Testing NodeJs and GraphQL with Jest +@parent learn-graphql 4 + +@description At the start of this tutorial you should have a repository similar to [this](https://github.com/bitovi/node-graphql-tutorial-2023/tree/prisma). So far we created our endpoints for our three entities, we had added Mongoose, and ported our endpoints to use Prisma instead. At each point we had to manually test the endpoints in Apollo Sandbox, and while that is a useful tool, in a real project we would be working with a testing framework in order to make sure that our endpoints still work. Today we’ll be adding testcases to our project and leveraging executeOperation to emulate requests to our Apollo Server, giving us the capability to test code ranging from our resolvers, to our dataSources. The concept here would be an **integration** test since it will be testing our GraphQL resolvers and dataSources. Without further ado, let’s start! + +@body + +## Setting up the testing Framework + +We will be using the jest library to do all of our testing: +``` +npm i jest +``` +Next we will be adding a file following the naming convention of `ENTITY_NAME.test.js` to each respective folder for each entity we have. First things first we will create our `renter.test.js` file within the renter folder. + +## Cleaning data in the database: + +It’s important to note that if you’ve been following along in these tutorials your database now contains documents that were created by Mongoose, and then by Prisma. We will now remove all that previous data in the database so that we can ensure our database only contains documents created using Prisma endpoints. We are doing this since some of the previously created data with Mongoose will not have the information required in order to resolve our relationships between entities and this will end up breaking some of our queries when testing. Before we mentioned one of the features of Prisma was **Prisma Migrate** which is something that in our tutorials we haven’t needed to use, however the first step that it does is to drop the database we have connected to. It is important to note that there are a number of ways to drop the database for MongoDB, but deleting all the entries can become messy with some of the relationships we have defined so far. + +In our terminal we will run the following: +``` +npx prisma migrate +``` +You will see an error, but when checking the collection in Mongo Atlas you will see that all the data will be deleted. Now that our database is empty, we can continue writing testcases for our entities. + +Note: To re-add data just run the `seed.js` file we created in earlier tutorials. + +## Testing Renter entities: + +To start we will be initializing our own `ApolloServer` and creating a context containing our Prisma Client in order to perform our queries. We do this because our methods that we use to call our Apollo Server uses a method called `executeOperation`, this method can take three main variables. The first is the query itself, which we can take from our testing within Apollo Sandbox. Next is `variables` for queries that require some sort of user input, but make sure that the name correlates to the one defined in your schema! Finally is the `contextValue` which in our case includes our Prisma Client; you may also end up passing your user authentication or other pieces of information here. + +You’ll note that there is a variable called `RenterFields` which are passed into the query and that the query itself has `...RenterFields`. This is an example of fragments, rather than continuously re-write each field we want to resolve from our queries, we created a `./test/helpers/fragments.js` file that contains the fragments we will re-use across different testcases and queries. A side-benefit is that we no longer have to update every query if this changes later on, we simply need to update our fragment and what are testcases expect respectively. You will need to include the `${NAME_OF_FRAGMENT}` before the query/mutation definition so that you can use it. + +``` +require('dotenv').config(); +const { ApolloServer } = require('@apollo/server'); +const omit = require('lodash.omit'); +const prisma = require('../prisma'); +const { typeDefs, resolvers } = require('../'); +const { RenterFields } = require('../../test/helpers/fragments'); + +const testServer = new ApolloServer({ + typeDefs, + resolvers +}); + +const contextValue = { prisma }; + +async function createRenter(createRenterInput) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + mutation CreateRenter($createRenterInput: CreateRenterInput) { + createRenter(createRenterInput: $createRenterInput) { + ...RenterFields + roommates { + ...RenterFields + } + } + } + `, + variables: { createRenterInput } + }, + { contextValue }); +} + +async function makeRoommates(renterIds) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + mutation Mutation($renterIds: [ID]) { + makeRoommates(renterIds: $renterIds) { + ...RenterFields + roommates { + ...RenterFields + } + } + } + `, + variables: { renterIds } + }, + { contextValue }); +} + +async function renters() { + return testServer.executeOperation({ + query: ` + ${RenterFields} + query Renters { + renters { + ...RenterFields + roommates { + ...RenterFields + } + } + } + `}, + { contextValue }); +} + +async function getRenterById(renterId) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + query GetRenterById($renterId: ID!) { + getRenterById(renterId: $renterId) { + ...RenterFields + roommates { + ...RenterFields + } + } + } + `, + variables: { + renterId + } + }, + { contextValue }); +} + +describe('Renter entity endpoints', () => { + const seedValue = Math.floor(Math.random() * 10000); + + const createRenterInput = { + city: 'Test City' + seedValue, + name: 'Test renter name' + seedValue, + roommates: [] + }; + + describe('Renter - Create', () => { + it('creates a renter and verifies it was created', async() => { + const response = await createRenter(createRenterInput); + expect(response.body.singleResult.errors).toBeUndefined(); + expect(response.body.singleResult.data.createRenter).toEqual({ + ...createRenterInput, + id: expect.any(String), + rating: 0 + }); + }); + }); + + describe('Renter - Update', () => { + it('creates two users and checks both users all roommates with one another after calling makeRoommates', async() => { + const [renter1, renter2] = await Promise.all([ + createRenter(createRenterInput), + createRenter(createRenterInput) + ]); + + const createdRenterId1 = renter1.body.singleResult.data.createRenter.id; + const createdRenterId2 = renter2.body.singleResult.data.createRenter.id; + const { body } = await makeRoommates([createdRenterId1, createdRenterId2]); + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.makeRoommates).toEqual( + expect.arrayContaining([ + { + ...renter1.body.singleResult.data.createRenter, + roommates: [{ ...omit(renter2.body.singleResult.data.createRenter, ['roommates']) }] + }, + { + ...renter2.body.singleResult.data.createRenter, + roommates: [{ ...omit(renter1.body.singleResult.data.createRenter, ['roommates']) }] + } + ]) + ) + }); + }); + + describe('Renter - Read', () => { + it('queries to retrieve all renters', async() => { + const { body } = await renters(); + expect(body.singleResult.errors).toBeUndefined(); + expect(Array.isArray(body.singleResult.data.renters)).toBe(true); + }); + + it('queries for the renter created previously using getRenterById', async() => { + const createRenterResponse = await createRenter(createRenterInput); + + const createdRenterId = createRenterResponse.body.singleResult.data.createRenter.id; + const { body } = await getRenterById(createdRenterId); + + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.getRenterById).toEqual({ + ...createRenterInput, + id: createdRenterId, + rating: 0 + }); + }); + }); +}); +``` + +## Testing Paradigms + +In this code above, we wanted to make it somewhat explicit what we are testing, in these case our core CRUD endpoints, and wanted to have different describes in order to keep the separations of what we were testing clear. Because we would be adding lots of test data into the database, we added a `seedValue` so that the names would have a lower likely-hood of clashing, making testing easier. + +When expecting values that you aren’t sure exactly what the result will be, you can use `expect.any(String)`. In our testcases we use it for an id since that will get generated by the database, but we just want to ensure that it does get returned. Another useful method with Jest is `expect.ArrayContaining([])`, this is something we use when attempting to test an array of objects but we may not be sure what order they will be returned. This will test that an object is in that list that matches what you are looking for. + +We separate the functions we call within the testcases into their own separate functions for a number of reasons: one is because we may re-use them later within different testcases, the other is so that our testcases aren’t filled with logic just to query the Apollo Server. This is so that we have our testcases only focusing on what it needs to test, nothing more. + +## Renter Fragment + +We will create the file `./test/helpers/fragments.js`: +``` +module.exports = { + RenterFields: ` + fragment RenterFields on Renter { + city + id + name + rating + } + ` +} +``` + +To define a fragment, it has to be on an existing entity, and we have to use the `fragment` keyword. In this case we define a fragment `RenterFields` on the entity `Renter` and define what fields it resolves. It does not have to include all of the fields on the entity, so you can use a fragment and still include other fields as well. As we test more entities we will add more to this file for our use. + +## Renter Deletion Problem + +While writing these testcases, an issue occurred within the codebase; there was no clear way to delete a Renter entity. Part of this problem is due to the `roommates` relationship each Renter has, and if you want to delete a Renter, you must first disconnect the relationship between it and other `roommates`. So we needed to create a new endpoint for Renters: + +First step is to add the definition to the src/renters/schema.js: +``` +type Mutation { + ... + deleteRenter(renterId: ID): Boolean +} +``` +Next is to add the resolver in `src/renters/resolvers.js`: +``` +Mutation: { + ... + deleteRenter: async (_parent, args, { prisma }) => { + return deleteRenter(args.renterId, prisma); + } +} +``` +And finally to add the code in `src/renters/dataSource.js`: +``` +async function deleteRenter(renterId, Prisma) { + // disconnect relationship between roommate and renter + await Prisma.renters.update({ + where: { + id: renterId + }, + data: { + roommates: { + set: [] + } + } + }); + // now delete renter + const deletedRenter = await Prisma.renters.delete({ + where: { + id: renterId + } + }); + + return Boolean(deletedRenter.id); +} +``` + +Now we need to actually test this new endpoint, so we will add a testcase to our `renter.test.js` file: +``` +async function deleteRenter(renterId) { + return testServer.executeOperation({ + query: ` + mutation DeleteRenter($renterId: ID) { + deleteRenter(renterId: $renterId) + } + `, + variables: { + renterId + } + }, + { contextValue }); +} +... +describe('Renter - Delete', () => { + it('creates a renter, and then deletes it', async() => { + const response = await createRenter(createRenterInput); + expect(response.body.singleResult.errors).toBeUndefined(); + const deletedRenter = await deleteRenter( + response.body.singleResult.data.createRenter.id + ); + expect(deletedRenter.body.singleResult.data.deleteRenter).toEqual(true); + }); +}); +``` + +## Testing PropertyOwner entities: + +These set of testcases are fairly simple, but follow the same idea in `./propertyOwners/propertyOwner.test.js`: + +``` +require('dotenv').config(); +const { ApolloServer } = require('@apollo/server'); +const prisma = require('../prisma'); +const { typeDefs, resolvers } = require('../'); +const { RenterFields, PropertyFields, PropertyOwnerFields } = require('../../test/helpers/fragments'); + +const testServer = new ApolloServer({ + typeDefs, + resolvers +}); + +const contextValue = { prisma }; + +async function createPropertyOwner(createPropertyOwnerInput) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyOwnerFields} + ${PropertyFields} + mutation CreatePropertyOwner($createPropertyOwnerInput: CreatePropertyOwnerInput) { + createPropertyOwner(createPropertyOwnerInput: $createPropertyOwnerInput) { + ...PropertyOwnerFields + } + } + `, + variables: { createPropertyOwnerInput } + }, + { contextValue }); +} + +async function propertyOwners() { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyOwnerFields} + ${PropertyFields} + query PropertyOwners { + propertyOwners { + ...PropertyOwnerFields + } + } + ` + }, + { contextValue }); +} + +async function getPropertyOwnerById(propertyOwnerId) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyOwnerFields} + ${PropertyFields} + query GetPropertyOwnerById($propertyOwnerId: ID!) { + getPropertyOwnerById(propertyOwnerId: $propertyOwnerId) { + ...PropertyOwnerFields + } + } + `, + variables: { + propertyOwnerId + } + }, + { contextValue }); +} + +describe('Property Owner entity endpoints', () => { + const seedValue = Math.floor(Math.random() * 10000); + + const createPropertyOwnerInput = { + address: 'Test PO Address - ' + seedValue, + name: 'Test Landlord - ' + seedValue, + photo: 'some photo' + seedValue, + properties: [] + }; + + describe('PropertyOwner - Read', () => { + it('Retrieves all propertyOwners', async() => { + const { body } = await propertyOwners(); + expect(body.singleResult.errors).toBeUndefined(); + expect(Array.isArray(body.singleResult.data.propertyOwners)).toBe(true); + }); + + it('Queries for the propertyOwner created previously using getPropertyOwnerById', async() => { + const result = await createPropertyOwner(createPropertyOwnerInput); + + expect(result.body.singleResult.errors).toBeUndefined(); + const createdPropertyOwner = result.body.singleResult.data.createPropertyOwner; + + const { body } = await getPropertyOwnerById(createdPropertyOwner.id); + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.getPropertyOwnerById).toEqual(createdPropertyOwner); + }); + }); + + describe('PropertyOwner - Create', () => { + it('Creates a propertyOwner', async() => { + const { body } = await createPropertyOwner(createPropertyOwnerInput); + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.createPropertyOwner).toEqual({ + ...createPropertyOwnerInput, + id: expect.any(String), + rating: 0 + }); + }); + }); +}); +``` + +## Property and PropertyOwner Fragment + +Now let’s add our fragments to our `./test/helpers/fragment.js` file: +``` +module.exports = { + PropertyOwnerFields: ` + fragment PropertyOwnerFields on PropertyOwner { + id + name + address + rating + photo + properties { + ...PropertyFields + } + } + `, + PropertyFields: ` + fragment PropertyFields on Property { + available + id + name + city + description + photos + rating + renters { + ...RenterFields + } + } + ` +} +``` + +There is a particular reason in this case why we left out `propertyOwner` from `PropertyFields`. This is because eventually this would lead to a very nested resolution path and would complicate our testcases. Much of the information would be redundant if we attempt to make this resolve. In practicality, if you were to query a list of `properties` from a `propertyOwner`, you would not need to know the `propertyOwner` since that information would already be available at the top level. Meanwhile if you were to query a `property` and want to get the information of the `propertyOwner` you can simply add that information to your query after the fragment. You will see an example of this when we test the Properties entity. + +## Testing PropertyOwner entities + +This entity becomes more interesting in order to test the update methods, and we even found an error in our schema when testing the endpoints in `./properties/properties.test.js`: + +``` +require('dotenv').config(); +const { ApolloServer } = require('@apollo/server'); +const omit = require('lodash.omit'); +const prisma = require('../prisma'); +const { typeDefs, resolvers } = require('../'); +const { RenterFields, PropertyFields } = require('../../test/helpers/fragments'); +const { createPropertyOwner } = require('../propertyOwners/dataSource') +const { createRenter } = require('../renters/dataSource') + + +const testServer = new ApolloServer({ + typeDefs, + resolvers +}); + +const contextValue = { prisma }; + +async function properties() { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyFields} + query Properties { + properties { + ...PropertyFields + propertyOwner { + id + name + address + rating + photo + } + } + } + ` + }, + { contextValue }); +} + +async function createProperty(createPropertyInput) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyFields} + mutation Mutation($createPropertyInput: CreatePropertyInput) { + createProperty(createPropertyInput: $createPropertyInput) { + ...PropertyFields + propertyOwner { + id + name + address + rating + photo + } + } + } + `, + variables: { createPropertyInput } + }, + { contextValue }); +} + +async function updateProperty(updatePropertyInput) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyFields} + mutation UpdateProperty($updatePropertyInput: UpdatePropertyInput) { + updateProperty(updatePropertyInput: $updatePropertyInput) { + ...on PropertyNotFoundError { + message + propertyId + } + ...on Property { + ...PropertyFields + propertyOwner { + id + name + address + rating + photo + } + } + } + } + `, + variables: { updatePropertyInput } + }, + { contextValue }); +} + +async function getPropertyById(propertyId) { + return testServer.executeOperation({ + query: ` + ${RenterFields} + ${PropertyFields} + query GetPropertyById($propertyId: ID!) { + getPropertyById(propertyId: $propertyId) { + ...PropertyFields + propertyOwner { + id + name + address + rating + photo + } + } + } + `, + variables: { + propertyId + } + }, + { contextValue }); +} + +describe('Property entity endpoints', () => { + const seedValue = Math.floor(Math.random() * 10000); + + const createPropertyInput = { + available: true, + city: 'Test Property City - ' + seedValue, + renters: [], + name: 'Test Property Name - ' + seedValue, + description: 'Test Property Decription - ' + seedValue + }; + + const createPropertyOwnerInput = { + address: 'Test PO Address - ' + seedValue, + name: 'Test Landlord - ' + seedValue, + photo: 'some photo' + seedValue, + properties: [] + }; + + const createRenterInput = { + city: 'Test City' + seedValue, + name: 'Test renter name' + seedValue, + roommates: [] + }; + + describe('Property - Read', () => { + let createdProperty; + beforeAll(async() => { + const propertyOwner = await createPropertyOwner(createPropertyOwnerInput, prisma) + + const renter = await createRenter(createRenterInput, prisma) + + const { body } = await createProperty({ + ...createPropertyInput, + propertyOwnerId: propertyOwner.id, + renters: [renter.id] + }); + + createdProperty = body.singleResult.data.createProperty; + }); + + it('Retrieves all properties', async() => { + const { body } = await properties(); + expect(body.singleResult.errors).toBeUndefined(); + expect(Array.isArray(body.singleResult.data.properties)).toBe(true); + }); + + it('Retrieves the created property with getPropertyById', async() => { + const { body } = await getPropertyById(createdProperty.id); + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.getPropertyById).toEqual(createdProperty) + }); + }); + + describe('Property - Create', () => { + it('Create a propertyOwner and a renter to create a property and tests that relationships are connected', async() => { + const propertyOwner = await createPropertyOwner(createPropertyOwnerInput, prisma) + + const renter = await createRenter(createRenterInput, prisma) + + const { body } = await createProperty({ + ...createPropertyInput, + propertyOwnerId: propertyOwner.id, + renters: [renter.id] + }); + + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.createProperty).toEqual({ + ...createPropertyInput, + rating: 0, + photos: [], + id: expect.any(String), + renters: [omit(renter, ['rentedPropertyId', 'roommateId', 'roommates', 'v'])], + propertyOwner: omit(propertyOwner, ['properties', 'v']) + }); + }); + }); + + describe('Property - Update', () => { + let createdProperty; + beforeAll(async() => { + const propertyOwner = await createPropertyOwner(createPropertyOwnerInput, prisma) + + const renter = await createRenter(createRenterInput, prisma) + + const { body } = await createProperty({ + ...createPropertyInput, + propertyOwnerId: propertyOwner.id, + renters: [renter.id] + }); + + createdProperty = body.singleResult.data.createProperty; + }); + + it('Updates a property with an incorrect ID and returns PropertyNotFoundError', async() => { + const NON_EXISTANT_UUID = '63c19d15b3db1c7857b59a7c'; + const { body } = await updateProperty({ id: NON_EXISTANT_UUID }); + + expect(body.singleResult.data.updateProperty).toEqual({ + message: 'Unable to find property with associated id.', + propertyId: NON_EXISTANT_UUID + }) + }); + + it('Updates previously created property and updates non-relational fields', async() => { + const updatesToProperty = { + id: createdProperty.id, + available: false, + city: 'New Test Property City - ' + seedValue, + name: 'New Test Property Name - ' + seedValue, + description: 'New Test Property Decription - ' + seedValue + }; + const { body } = await updateProperty(updatesToProperty); + + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.updateProperty).toEqual({ + ...updatesToProperty, + photos: [], + propertyOwner: expect.any(Object), + rating: 0, + renters: expect.any(Array) + }) + }); + + it('Updates previously created property and updates renters with newly created Renter', async() => { + const newCreatedRenterInput = { + city: 'New - Test City' + seedValue, + name: 'New - Test renter name' + seedValue, + roommates: [] + }; + const renter = await createRenter(newCreatedRenterInput, prisma) + const updatesToProperty = { + id: createdProperty.id, + renters: [renter.id] + }; + + const { body } = await updateProperty(updatesToProperty); + + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.updateProperty.renters).toEqual( + [omit(renter, ['rentedPropertyId', 'roommateId', 'roommates', 'v'])] + ) + }); + + it('Updates previously created property and updates propertyOwner with newly created propertyOwner', async() => { + const newCreatedPropertyOwnerInput = { + address: 'New - Test PO Address - ' + seedValue, + name: 'New - Test Landlord - ' + seedValue, + photo: 'New - some photo' + seedValue, + properties: [] + }; + const propertyOwner = await createPropertyOwner(newCreatedPropertyOwnerInput, prisma) + const updatesToProperty = { + id: createdProperty.id, + propertyOwnerId: propertyOwner.id + }; + const { body } = await updateProperty(updatesToProperty); + + expect(body.singleResult.errors).toBeUndefined(); + expect(body.singleResult.data.updateProperty.propertyOwner).toEqual( + omit(propertyOwner, ['v', 'properties']) + ) + }); + }); +}); +``` + +### Testing Paradigms + +In this file, we use a `beforeAll` hook to setup some data that we want to reference later, in this case we want to create a Property before the test runs so we can simply call `getPropertyById` in our Read testcase. + +In order to create a property, we need to have created a `propertyOwner` first, and to test the resolution of `renters` as well we would need to create a `renter` entity as well. Rather than test Apollo Server for those two actions (which we already covered in our other testcases), we chose to call the dataSource methods directly. This became a little tricky when expecting what the return will be since we still have extra fields that get returned, in this case we use `omit` to make the massaging of data much easier so we can test that the relationships did get created. + +## Property Update Problem + +When testing the update method, a number of situations needed to be covered: namely ensuring that our `PropertyNotFoundError` would be thrown, we could update our non-relational fields, and finally being able to ensure `renters` and `propertyOwner` could be updated with new relationships. During this process we found some errors in the schema for `property` and how we updated the relationships for both `renters` and the `propertyOwner`. + +First thing to change was the `UpdatePropertyInput`, we simply want the ID rather than an entire object of a propertyOwner, so we should be making the input say that explicitly with `propertyOwnerId`: +``` +input UpdatePropertyInput { + ... + propertyOwnerId: ID +} +``` + +Then we needed to update the object for propertyOwner since it was malformed when passed into the Prisma `update` method `properties/dataSource.js`: +``` +const propertyOwner = updatedProperty.propertyOwnerId && { + connect: { id: updatedProperty.propertyOwnerId } +}; +``` +Next as an update, we want to include a list of renters that are currently renting that property, with the previous code not only was the object not formed correctly, when it was fixed it would actually only add the new list of renters to the existing list. What we wanted to do with this update was that if you passed in a list of renters, that list became the current list of renters on the property. To do that we used the option called `set` instead of `connect` which we were using before: +``` +const renters = updatedProperty.renters && { + set: updatedProperty?.renters.map((renterId) => ({ id: renterId })) + }; +``` + +The new `updateProperty` method should contain the following new lines of code: +``` +const renters = updatedProperty.renters && { + set: updatedProperty?.renters.map((renterId) => ({ id: renterId })) +}; +const propertyOwner = updatedProperty.propertyOwnerId && { + connect: { id: updatedProperty.propertyOwnerId } +}; + +... +const savedProperty = await Prisma.properties.update({ + where: { + id: propertyId + }, + data: { + ...nonConnectPropertyFields, + renters, + propertyOwner + }, + include: { + renters: true, + propertyOwner: true + } +}); +... +``` + +This means that during the update for properties, you pass the id of the new propertyOwner you want in `propertyOwnerId`. + +## Adding a test script: + +The last step is to add a simple test script to `package.json` so that we can run our testcases from the command line: +``` +"scripts": { + ... + "test": "jest --coverage" +} +``` + +With this we can now type `npm run test` in our terminal in our project directory and have all our testcases run! With the `--coverage` flag we will see what lines of our code we haven’t tested, which is a good metric for a strong test suite. + +## Conclusion + +Testing is very useful in many projects, even pointing out flaws in our current tutorial. Now if we need to change our ORM, entities, or add new features, each existing endpoint has a testcase to ensure their stability. We leveraged the power of fragments to keep our query strings smaller and consistent between different queries. This also means that if we have to update our fragments, we only need to change it in one spot rather than across many methods. We learned about `executeOperation` and how to setup our testcases in a way to test our resolvers and dataSources. At the end of this tutorial you should have a repository similar to [this](https://github.com/bitovi/node-graphql-tutorial-2023/tree/fragments). Now that we included testcases it will become much easier to ensure we don’t make breaking changes going forward! \ No newline at end of file