Integration Testing with GraphQL (Nexus, Apollo), Prisma, and PostgreSQL - postgresql

I am trying to follow this tutorial to establish integration tests on our web application. Our stack currently includes Nexus, Next, Apollo, Prisma, and PostgreSQL.
I am using ApolloClient in place of GraphQLClient from graphql-request, I opted to use ApolloClient instead, especially since our web application is server less.
This is currently what I have inside the helper.ts, and the ApolloClient does work when I execute mutations. However, after executing a mutation on ApolloClient and checking if the data persists through Prisma, I get a null value.
Did I do these adjustments correctly? I am definitely missing something if Prisma is not querying correctly. Maybe there is a disconnect here between ApolloClient and Prisma or ApolloClient and the database? Any help would be much appreciated.
All of the code is below.
helper.ts
function graphqlTestContext() {
let serverInstance: ServerInfo | null = null;
return {
async before() {
const rootUrl = getRootUrl();
const httpLink = createHttpLink({
uri: rootUrl + "api/graphql",
credentials: "include",
fetch
});
const client = new ApolloClient({
// ssrMode: typeof window === "undefined",
link: httpLink,
cache: new InMemoryCache(),
});
return client;
},
async after() {
serverInstance?.server.close()
},
}
}
function prismaTestContext() {
const prismaBinary = join(__dirname, '../../', 'node_modules', '.bin', 'prisma');
let schema = '';
let databaseUrl = '';
let prismaClient: null | PrismaClient = null;
return {
async before() {
// Generate a unique schema identifier for this test context
schema = `test_${nanoid()}`;
// Generate the pg connection string for the test schema
databaseUrl = `${process.env.ROOT_DB_URL}/testing?schema=${schema}`;
// Set the required environment variable to contain the connection string
// to our database test schema
process.env.DATABASE_URL = databaseUrl;
// Run the migrations to ensure our schema has the required structure
execSync(`${prismaBinary} migrate dev`, {
env: {
...process.env,
DATABASE_URL: databaseUrl,
},
});
// Construct a new Prisma Client connected to the generated Postgres schema
prismaClient = new PrismaClient();
return prismaClient;
},
async after() {
// Drop the schema after the tests have completed
const client = new Client({
connectionString: databaseUrl,
});
await client.connect();
await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
await client.end();
// Release the Prisma Client connection
await prismaClient?.$disconnect();
},
}
User.int.test.ts
const ctx = createTestContext();
describe("User", () => {
it("creates a new user with REGISTER_MUTATION", async () => {
const userResult = await ctx.client.mutate({
mutation: gql`
mutation Register(
$firstName: String!
$lastName: String!
$email: String!
$password: String!
) {
registerUser(
firstName: $firstName
lastName: $lastName
email: $email
password: $password
) {
user {
email
firstName
}
}
}
`,
variables: {
firstName: "FirstName",
lastName: "LastName",
email: "test#email.com",
password: "password"
}
});
expect(userResult).toMatchInlineSnapshot(`
Object {
"data": Object {
"registerUser": Object {
"__typename": "UserLoginPayload",
"user": Object {
"__typename": "User",
"email": "test#email.com",
"firstName": "FirstName",
},
},
},
}
`);
});
it("verifies that user persists", async () => {
const persistedData = await ctx.prisma.user.findMany();
expect(persistedData).toMatchInlineSnapshot(`Array []`);
});
});

The reason is because graphql server is instantiated with different prisma client with its own db. And the prismaTestContext has its own prisma client with different db url.

Related

Mongoose Schema properties validation with Typescript NextJS

i am trying to save new document to mongo db, the Schema validation is not working for me, i am trying ti make required true, but i still can add new document without the required field.
this is my schema:
// lib/models/test.model.ts
import { Model, Schema } from 'mongoose';
import createModel from '../createModel';
interface ITest {
first_name: string;
last_name: string;
}
type TestModel = Model<ITest, {}>;
const testSchema = new Schema<ITest, TestModel>({
first_name: {
type: String,
required: [true, 'Required first name'],
},
last_name: {
type: String,
required: true,
},
});
const Test = createModel<ITest, TestModel>('tests', testSchema);
module.exports = Test;
this is createModel:
// lib/createModel.ts
import { Model, model, Schema } from 'mongoose';
// Simple Generic Function for reusability
// Feel free to modify however you like
export default function createModel<T, TModel = Model<T>>(
modelName: string,
schema: Schema<T>
): TModel {
let createdModel: TModel;
if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
// #ts-ignore
if (!global[modelName]) {
createdModel = model<T, TModel>(modelName, schema);
// #ts-ignore
global[modelName] = createdModel;
}
// #ts-ignore
createdModel = global[modelName];
} else {
// In production mode, it's best to not use a global variable.
createdModel = model<T, TModel>(modelName, schema);
}
return createdModel;
}
and this is my tests file:
import { connection } from 'mongoose';
import type { NextApiRequest, NextApiResponse } from 'next';
const Test = require('../../../lib/models/test.model');
import { connect } from '../../../lib/dbConnect';
const ObjectId = require('mongodb').ObjectId;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
switch (req.method) {
case 'POST': {
return addPost(req, res);
}
}
}
async function addPost(req: NextApiRequest, res: NextApiResponse) {
try {
connect();
// const { first_name, last_name } = req.body;
const test = new Test({
first_name: req.body.first_name,
last_name: req.body.last_name,
});
let post = await test.save();
// return the posts
return res.json({
message: JSON.parse(JSON.stringify(post)),
success: true,
});
// Erase test data after use
//connection.db.dropCollection(testModel.collection.collectionName);
} catch (err) {
//res.status(400).json(err);
res.status(400).json({
message: err,
success: false,
});
}
}
in the Postman, i send a request body without the required field (first_name) and i still can add it.
any help?

Mongoose getters are either not working the way I want or I'm misunderstanding what they are

I created some sample code to demonstrate my issue on a smaller scale. From my understanding, a getter function will not affect anything on my database, but when I want to make a get request to view items on my database, it will change the value to whatever is returned only when the data is displayed. However, when I make my get request to view items on my database, the item I am shown is exactly how it was saved. I'm not sure if I'm misunderstanding what a getter function is, or if my syntax is just incorrect somewhere.
Here is my main server:
const express = require('express')
const mongoose = require('mongoose')
// Linking my model
const User = require('./User')
// Initializing express
const app = express()
const PORT = 9999
app.use(express.json())
// Connecting to mongodb
const connectDB = async () => {
try {
await mongoose.connect('mongodb://localhost/testdatabase', {
useUnifiedTopology: true,
useNewUrlParser: true
})
console.log('Connected')
} catch (error) {
console.log('Failed to connect')
}
}
connectDB()
// Creates a new user
app.post('/user/create', async (req, res) => {
await User.create({
name: 'John Cena',
password: 'somepassword'
})
return res.json('User created')
})
// Allows me to view all my users
app.get('/user/view', async (req, res) => {
const findUser = await User.find()
return res.json(findUser)
})
// Running my server
app.listen(PORT, () => {
console.log(`Listening on localhost:${PORT}...`)
})
Here is my model:
const mongoose = require('mongoose')
// My setter - initialPassword is 'somepassword'
// This seems to work properly, in my database the password is changed to 'everyone has the same password here'
const autoChangePassword = (initialPassword) => {
console.log(initialPassword)
return 'everyone has the same password here'
}
// My getter - changedPassword should be 'everyone has the same password here' I think
// The console.log doesn't even run
const passwordReveal = (changedPassword) => {
console.log(changedPassword)
return 'fakehash1234'
}
// Creating my model
const UserSchema = mongoose.Schema({
name: {
type: String
},
password: {
type: String,
set: autoChangePassword,
get: passwordReveal
}
})
// Exporting my model
const model = mongoose.model('user', UserSchema)
module.exports = model
Not sure if it would help anyone since I found my answer on another StackOverflow post, but the issue was I had to set getters to true when converting back to JSON:
// Creating my model
const UserSchema = mongoose.Schema({
name: {
type: String
},
password: {
type: String,
set: autoChangePassword,
get: passwordReveal
}
}, {
toJSON: { getters: true }
})
Any similar problems can be solved by adding some combination of the following:
{
toJSON: {
getters: true,
setters: true
},
toObject: {
getters: true,
setters: true
}
}

Next JS connection with Apollo and MongoDB

I am new to Next.js and using this example from Next.js https://github.com/zeit/next.js/tree/master/examples/api-routes-apollo-server-and-client.
However, the example is silent on MongoDB integration (also I could not find any other example for the same). I have been able to make database-connection but NOT able to use it in resolvers.
My Code
pages/api/graphql.js
import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'
const MongoClient = require('mongodb').MongoClient;
let db
const apolloServer = new ApolloServer({
schema,
context: async () => {
if (!db) {
try {
const client = await MongoClient.connect(uri)
db = await client.db('dbName')
const post = await Posts.findOne()
console.log(post)
// It's working fine here
}
catch (e) {
// handle any errors
}
}
return { db }
},
})
export const config = {
api: {
bodyParser: false,
},
}
export default apolloServer.createHandler({ path: '/api/graphql' })
apollo/schema.js
import {makeExecutableSchema} from 'graphql-tools';
import {typeDefs} from './type-defs';
import {resolvers} from './resolvers';
export const schema = makeExecutableSchema({
typeDefs,
resolvers
});
apollo/resolvers.js
const Items = require('./connector').Items;
export const resolvers = {
Query: {
viewer(_parent, _args, _context, _info) {
//want to populate values here, using database connection
return { id: 1, name: 'John Smith', status: 'cached' }
},
...
}
}
I am stuck in the resolvers.js part. Don't know how to get the cached database connection inside resolvers.js. If I create a new database connection file, top-level await is not supported there, so how do I proceed?
If context is a function, whatever you return from the function will be available as the context parameter in your resolver. So if you're returning { db }, that's what your context parameter will be -- in other words, you can access it as context.db inside your resolver.

How to query nested data in mongoose model

I am attempting to build a Vue.js app with a MEVN stack backend and Vuex. I am configuring my Vuex action handler with a GET request that prompts a corresponding Express GET route to query data nested in Mongoose.
A username is passed into the handler as an argument and appended to the GET request URL as a parameter:
actions: {
loadPosts: async (context, username) => {
console.log(username)
let uri = `http://localhost:4000/posts/currentuser?username=${username}`;
const response = await axios.get(uri)
context.commit('setPosts', response.data)
}
}
The corresponding Express route queries activeUser.name, which represents the nested data in the Mongoose Model:
postRoutes.route('/currentuser').get(function (req, res) {
let params = {},
username = req.query.activeUser.name
if (username) {
params.username = username
}
Post.find(params, function(err, posts){
if(err){
res.json(err);
}
else {
res.json(posts);
}
});
});
Below is my Mongoose model, with activeUser.name representing the nested data queried by the Express route:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let Post = new Schema({
title: {
type: String
},
body: {
type: String,
},
activeUser: {
name: {
type: String
}
}
},{
collection: 'posts'
});
module.exports = mongoose.model('Post', Post);
Even with this setup, the GET route does not appear to send a response back to the action handler. I thought adding username = req.query.activeUser.name in the express route would be the right method for querying the nested data in Mongoose, but apparently not. Any recommendations on how to configure the above Express route in order to query the nested data in the Mongoose model? Thanks!
name is inside activeuser so you need to construct params object variable like this:
postRoutes.route("/currentuser").get(function(req, res) {
let params = {
activeUser: {}
};
let username = req.query.activeUserName;
if (username) {
params.activeUser.name = username;
}
Post.find(params, function(err, posts) {
if (err) {
res.json(err);
} else {
res.json(posts);
}
});
});
Note that I also used activeUserName as query param like this: /currentuser?activeUserName=JS_is_awesome18

Express Sequelize Error - Cannot read property 'findAll' of undefined

I have successfully connected Sequelize and Express using Sequelize's github example with a few changes. I am now trying to do a simple Sequelize query to test the connection, but continue to receive an error stating that the model I have queried is not defined.
// ./models/index.js
...
const sequelize = new Sequelize(process.env.DB, process.env.DB_USER, process.env.DB_PASS, {
host: 'localhost',
dialect: 'postgres'
});
// Test SEQUELIZE connection
sequelize
.authenticate()
.then(() => {
console.log('Database connection has been established successfully.');
})
.catch(err => {
console.error('Unable to connect to the database:', err);
});
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
var model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
// ./routes/index.js
const models = require('../models');
const express = require('express');
const router = express.Router();
router.get('/contacts', (req, res) => {
models.Contact.findAll().then(contacts => {
console.log("All users:", JSON.stringify(contacts, null, 4));
});
});
module.exports = router;
// ./models/contact.js
const Sequelize = require('sequelize');
var Model = Sequelize.Model;
module.exports = (sequelize, DataTypes) => {
class Contact extends Model {}
Contact.init({
// attributes
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'contact'
// options
});
return Contact;
};
The error I am getting when using postman to hit /contacts with a GET request is:
[nodemon] starting `node server.js`
The server is now running on port 3000!
Executing (default): SELECT 1+1 AS result
Database connection has been established successfully.
TypeError: Cannot read property 'findAll' of undefined
at router.get (C:\Users\username\desktop\metropolis\metropolis-backend\routes\index.js:6:20)
You are not requiring the model properly.
In ./routes/index.js add the next line:
const Contact = require('./models/contact.js');
And then call Contact.findAll()...
Second approach:
You can gather all your models by importing them into a loader.js file which you will store in the models directory. The whole job of this module is to import the modules together to the same place and then export them from a single place.
It will look something like that:
// loader.js
const modelA = require('./modelA');
const modelB = require('./modelB');
const modelC = require('./modelC');
...
module.exports = {
modelA,
modelB,
modelC,
...
}
And then you can require it in the following way:
in router/index.js:
const Models = require('./models');
const contact = Models.Contact;