Type 'string' is not assignable to type 'Condition<UserObj>' when making mongoose query by ID - mongodb

I have the following API route in Next:
import {NextApiRequest, NextApiResponse} from "next";
import dbConnect from "../../utils/dbConnect";
import {UserModel} from "../../models/user";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") return res.status(405);
if (!req.query.id || Array.isArray(req.query.id)) return res.status(406).json({message: "No ID found in request"});
try {
await dbConnect();
const user = await UserModel.findOne({ _id: req.query.id });
if (!user) return res.status(404).json({message: "No user found"});
return res.status(200).json({data: user});
} catch (e) {
return res.status(500).json({message: e});
}
}
Typescript says that the line const user = await UserModel.findOne({ _id: req.query.id }); contains error Type 'string' is not assignable to type 'Condition<UserObj>'. Creating an ObjectId instead of a string (const user = await UserModel.findOne({ _id: mongoose.Types.ObjectId(req.query.id) });) throws the same error.
I've looked through the type files/docs but I'm struggling to figure out why this is invalid. Shouldn't querying by ID with a string or ObjectId be a valid condition object? Querying by other fields works fine.
Why is this invalid, and how should I fix it?

The proposed solution by #Tim is good and solves this punctual situation, but it doesn't get you to the root of the problem. What if you have to use the findOne method because you are going to use another field in the filter? For example:
You want to get the user with that id and that the deletedAt attribute is null.
const user = await UserModel.findOne({ _id: req.query.id, deletedAt: null});
You will get the same error cause the mistake is in the userModel definition. I guess your user class is basically as shown below:
import { ObjectId, Types } from 'mongoose';
#Schema({ versionKey: false, timestamps: true })
export class User {
#Field(() => ID, {name: 'id'})
readonly _id: ObjectId;
#Field(() => Date, {nullable: true, name: 'deleted_at'})
#Prop({type: Date, required: false, default: null})
deletedAt?: Date;
#Field()
#Prop({required: true, index: true})
name: string;
...
}
The problem is that you are directly accessing the Schema user when you should be accessing the model (repository pattern).
[SOLUTION]: Create the model or the repository for your user class, and use it to interact with your database.
In my case I just added the following lines:
import { ObjectId, Types, Document } from 'mongoose';
#Schema({ versionKey: false, timestamps: true })
export class User {
...
}
export type UserDocument = User & Document;
OR
import { ObjectId, Types, Document } from 'mongoose';
#Schema({ versionKey: false, timestamps: true })
export class User extends Document{
...
}
and in my service I instantiated an object of type model:
import { Model } from 'mongoose';
private userModel: Model<UserDocument>;
and then I was able to make the following method call:
...
await dbConnect();
const user = await UserModel.findOne({ _id: req.query.id });
if (!user) return res.status(404).json({message: "No user found"});
...

Use .findByID for id based queries.

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?

Can not Query all users because of MongoDB id

I am coding a CRUD API built in TypeScript and TypeGoose.
I get an error saying,
CannotDetermineGraphQLTypeError: Cannot determine GraphQL output type for '_id' of 'User' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper output value?
I have a User entity.
import { Field, ObjectType } from 'type-graphql';
import { ObjectId } from 'mongodb';
import { prop as Property, getModelForClass } from '#typegoose/typegoose';
#ObjectType()
export class User {
#Field()
readonly _id: ObjectId;
#Field()
#Property({ required: true })
email: string;
#Field({ nullable: true })
#Property()
nickname?: string;
#Property({ required: true })
password: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}
export const UserModel = getModelForClass(User);
And this is how my query resolver looks like.
#Query(() => [User])
async users() {
const users = await UserModel.find();
console.log(users);
return users;
}
How can I solve this? It seems to be like TypeGraphQL doesn't understand what the MongoDB ID is?
Im not sure about this, but maybe ObjectId.toString() help you.
MongoDB doc about ObjectId.toString()

The correct way to create collection during mongoose transaction

How to autocreate collection during mongoose transaction if the collection was not created yet?
I'm aware of mongoose limitation that restricts user to create (or delete) mongoose collections during open transaction session.
Also, I was able to find 3 possible solutions on how to fix that:
1. autoCreate option
2. Model.init() method
3. Model.createCollection() method
Which one to use? Without losing indexes etc.
app.models.ts
import { model, Schema } from 'mongoose';
const UserSchema = new Schema<UserDocument>({
name: {
type: Schema.Types.String,
required: true,
}
}); // { autoCreate: true } <-- ???
export const UserModel = model<UserDocument>('User', UserSchema);
app.ts
import { startSession } from 'mongoose';
import { UserModel } from './app.models.ts';
async function createUser() {
// await UserModel.createCollection(); ??
// or
// await UserModel.init(); ??
const session = await startSession();
sesssion.startTransaction();
try {
const [user] = await UserModel.create([{ name: 'John' }], { session });
await session.commitTransaction();
return user;
} catch (error) {
await session.abortTransaction();
} finally {
session.endSession()
}
}
foo();
If a collection does not exist, MongoDB creates the collection when you first store data for that collection. You can also explicitly create a collection with various options, such as setting the maximum size or the documentation validation rules.
Anyway, mongoose takes care of indexes, collection, etc...
you just need to define the collection name: https://mongoosejs.com/docs/guide.html#collection
const UserSchema = new Schema<UserDocument>({
name: {
type: Schema.Types.String,
required: true,
}
}, {collection: 'users'});
There is the answer about transactions and collection creating -https://github.com/Automattic/mongoose/issues/6699
Actually, I use https://www.npmjs.com/package/db-migrate package to create collections and indexes before starting an app.

Relationships GraphQL

The second week I try to link two collections in the apollo-server-express / MongoDB / Mongoose / GraphQL stack, but I do not understand how. I found a similar lesson with the REST API, what I need is called Relationships. I need this, but in GraphQL
watch video
How to add cars to the User?
I collected the test server, the code is here: https://github.com/gHashTag/test-graphql-server
Help
I have cloned your project and implemented some code and here what I changed to make relationship works. Note, I just did a basic code without validation or advance dataloader just to make sure non-complexity. Hope it can help.
src/graphql/resolvers/car-resolvers.js
import Car from '../../models/Car'
import User from '../../models/User'
export default {
getCar: (_, { _id }) => Car.findById(_id),
getCars: () => Car.find({}),
getCarsByUser: (user, {}) => Car.find({seller: user._id }), // for relationship
createCar: async (_, args) => {
// Create new car
return await Car.create(args)
}
}
src/graphql/resolvers/user-resolvers.js
import User from '../../models/User'
export default {
getUser: (_, { _id }) => User.findById(_id),
getUsers: () => User.find({}),
getUserByCar: (car, args) => User.findById(car.seller), // for relationship
createUser: (_, args) => {
return User.create(args)
}
}
src/graphql/resolvers/index.js
import UserResolvers from './user-resolvers'
import CarResolvers from './car-resolvers'
export default {
User:{
cars: CarResolvers.getCarsByUser // tricky part to link query relation ship between User and Car
},
Car:{
seller: UserResolvers.getUserByCar // tricky part to link query relation ship between User and Car
},
Query: {
getUser: UserResolvers.getUser,
getUsers: UserResolvers.getUsers,
getCar: CarResolvers.getCar,
getCars: CarResolvers.getCars
},
Mutation: {
createUser: UserResolvers.createUser,
createCar: CarResolvers.createCar,
}
}
src/graphql/schema.js
export default`
type Status {
message: String!
}
type User {
_id: ID!
firstName: String
lastName: String
email: String
cars: [Car]
}
type Car {
_id: ID
make: String
model: String
year: String
seller: User
}
type Query {
getUser(_id: ID!): User
getUsers: [User]
getCar(_id: ID!): Car
getCars: [Car]
}
type Mutation {
createUser(firstName: String, lastName: String, email: String): User
// change from _id to seller, due to base on logic _id conflict with CarId
createCar(seller: ID!, make: String, model: String, year: String): Car
}
schema {
query: Query
mutation: Mutation
}
`
src/middlewares.js
import bodyParser from 'body-parser'
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'
import { makeExecutableSchema } from 'graphql-tools'
import typeDefs from '../graphql/schema'
import resolvers from '../graphql/resolvers'
import constants from './constants'
export const schema = makeExecutableSchema({
typeDefs,
resolvers
})
export default app => {
app.use('/graphiql', graphiqlExpress({
endpointURL: constants.GRAPHQL_PATH
}))
app.use(
constants.GRAPHQL_PATH,
bodyParser.json(),
graphqlExpress(req => ({
schema,
context: {
event: req.event
}
}))
)
}
try to make something like this in your car resolver
export default {
getCar: ({ _id: ownId }, { _id }) =>
Car.findById(ownId || _id);
// here is the rest of your code
You need to add a resolver for the cars field on the User type.
const resolvers = {
Query: {
getUsers: ...
getCars: ...
...
},
Mutation: {
...
},
User: {
cars: ...
}
}

How should I define interfaces of documents when using Typescript and Mongodb?

Consider a simple user collection:
// db.ts
export interface User {
_id: mongodb.ObjectId;
username: string;
password: string;
somethingElse: string;
}
// user.ts
import {User} from "../db"
router.get("/:id", async (req, res) => {
const id = req.params.id;
// user._id is a mongodb.Object.
const user: User = await db.getUser(id);
res.send(user);
});
// index.ts
// code that will runs on browser
import {User} from "../../db"
$.get('/user/...').done((user: User) => {
// user._id is string.
console.log(user._id);
});
It works perfectly until I want to use this interface in client codes. Because the _id of user becomes a hex string when tranmitted as json from server. If I set _id to be mongodb.ObjectId | string, the behavior gets wierd.
You can try to separate them in a smart way :
interface User {
username: string;
password: string;
somethingElse: string;
}
export interface UserJSON extends User {
_id : string
}
export interface UserDB extends User {
_id : mongodb.ObjectId
}
and later take either UserJSON ( client ) or UserDB ( server-side ).
Thanks to #drinchev. And I have figured out a better way to do it, using generics:
interface User<IdType> {
_id: IdType;
username: string;
posts: Post<IdType>[];
}
interface Post<IdType> {
_id: IdType;
text: string;
}
export type UserDB = User<mongodb.ObjectID>;