I currently have a microservice architecture powered by Apollo Federation, where a service has its own database. Consider this example, the user's service has its own database, the posts service has its own database, and the comments service has its own database. Currently, each schema sits in its own service, but each of these models references each other. For example
// posts.model
const PostSchema = new Schema({
title: String,
name: String,
user: { type: ObjectId, ref: 'User' }
});
In this case, a Post has the corresponding user attached to it. I am using typegoose alongside type-graphql in another microservice while the rest microservices are written in plain javascript. Now in my typegoose class, I am trying to achieve the same thing above. I have a class (model) that is referencing another entity in a different service/database. How do I represent it in my typegoose model? E.g
export class Post {
#GqlField(() => String, { nullable: true })
#prop()
title: string;
#GqlField(() => User)
#prop({ ref: () => User, required: true })
user: Ref<User>; // this is meant to reference user in the user-service.
#GqlField(() => String)
#prop({required: true,})
name: string;
}
Do I use an abstract class to represent the User model, if yes, how do I go about this? Or is there another method that I can use to achieve this? I tried creating a User model class in the current microservice, but that user's model is going to be registered on the current database connection and I don't want that.
I'll appreciate any advice, thank you in advance.
What you want to do is called "Cross-Database Population", where mongoose has a documentation page for it, where the TL;DR; basically is:
Either pass a Model instance to ref (which currently is not supported by typegoose) or use the model option in a populate call.
Example:
(you still require the Schema from the other services and a database connection to those databases)
#modelOptions({ existingConnection: UserDBConnection }) // this can also be set in the call for "getModelForClass"
class User {
#prop()
public name?: string;
}
const UserModel = getModelForClass(User);
// or if setting the connection here
const UserModel = getModelForClass(User, { existingConnection: UserDBConnection });
// without setting a explicit database, "mongoose.connection" will be used (which is the default connection in mongoose)
class Post {
#prop({ ref: () => User })
public user?: Ref<User>;
}
const PostModel = getModelForClass(PostModel);
// somewhere later in your code
await postdoc.populate({ path: 'user', model: UserModel }).execPopulate();
I'm trying to build a new instance with OneToOne association, according to https://www.npmjs.com/package/sequelize-typescript
models/measure.ts :
#Table({ tableName: "measure" })
export class Measure extends Model {
...
// Token
#ForeignKey(() => Token)
#Column({ type: DataType.INTEGER, allowNull: false, unique: true })
id_token!: number
#BelongsTo(() => Token)
token!: Token
}
models/token.ts :
#Table({ tableName: "token" })
export class Token extends Model {
...
// Measure
#HasOne(() => Measure)
measure: Measure
}
my sequelize instance :
export const sequelize: Sequelize = new Sequelize({
...
repositoryMode: true,
})
sequelize.addModels([Token, Measure])
export const tokenRepository = sequelize.getRepository(Token)
export const measureRepository = sequelize.getRepository(Measure)
now i want to create a new Measure instance, with an existing Token :
const token = await tokenRepository.findOne({where: {value: value}})
...
console.log(token.isNewRecord) // false
const measure = measureRepository.build({...params, token: token}, {include: [{model: tokenRepository}]})
console.log(measure.token.isNewRecord) // true
await measure.save() // it try to insert an new token here
Sequelize try to insert a new token instead of insert only the measure. Where is my mistake ?
I faced this issue some times ago and by reading the docs and trying few things on my side I conclude it was not possible.
From what I saw you can create a new object and all is nested objects in one time. But you can't create a new object and link it to other existing objects in one time.
To do so you will have to do yourself the necessary find and/or update.
I never managed to make it work in one step and the documentation never talk about this. Always says that all elements are new.
An instance can be created with nested association in one step, provided all elements are new.
For more details :
https://sequelize.org/master/manual/creating-with-associations.html
I am using typeorm with MongoDB. When I am searching for a document by Id with this syntax
const manager = getMongoManager(); const user = await manager.findOne(User, {userId}); I got undefined result and when I use const manager = getMongoManager(); const user = await manager.findOne(User, {}); I got the result with userId expected in the first syntax.
Besides the same syntax works fine with any other criteria other than Id.
The declaration of the Id in the model is: #ObjectIdColumn({ name: '_id' }) #IsNotEmpty() public userId: ObjectID;
For now, I have only one user in the database with the userId that I search for.
Where could be the problem? And how to resolve that?
When you do await manager.findOne(User, {}); You are basically selecting the first entry in your User collection, that's why you get the user you were expecting.
What you need to do is query by the _id field if you have it, or create a custom field to query by, for example:
#Entity()
export class UserEntity
{
#ObjectIdColumn()
_id: string;
#PrimaryColumn()
id: string;
#Column()
public name: string;
}
This way you can do a query like this await manager.findOne(User, { _id: <your _id>});
Or await manager.findOne(User, { id: <your id> });
I'm setting up a NestJS GraphQL API that utilizes TypeORM, and am having trouble implementing relationships between entities.
Specifically, the TypeORM relationships are working great, and the entities are linking correctly in the database. However, the problem comes in when I try to query the API to get the results.
Right now I have 2 entities, each with their own resolver: Users and Photos. Each User can have multiple Photos, while each Photo is only connected to one User (Many-to-One).
Here's how the entities are linked with TypeORM
Photo Entity, with a relationship to the User Entity
#ManyToOne(type => User, user => user.photos, {
onDelete: 'CASCADE',
})
#JoinColumn()
user: User;
User Entity, completing connection to the Photo Entity
#OneToMany(type => Photo, photo => photo.user, {
eager: true,
})
photos: Photo[];
This code works, and let's us retrieve a User's Photos
User resolver
#ResolveProperty('photos')
async photos(#Parent() user): Promise<Photo[]> {
return await this.service.readPhotos(user.id);
}
User service
async readPhotos(userId): Promise<Photo[]> {
return await this.photoRepository.find({
where: {
user: userId
}
});
}
* Note that the photoRepository is able to be filtered by the 'user' field. *
This code, however, does not work. It should let us view which User is connected to the Photo, instead it returns null.
Photo resolver
#ResolveProperty('user')
async user(#Parent() photo): Promise<User> {
console.log(photo);
return await this.service.readUser(photo.user);
}
This Photo resolver seems to contain the problem; the photo object being output by the console indicates that while the #Parent photo object has all of its static fields available (like the ID, datePublished, URL), for some reason the actual 'user' field is not accessible here. So the 'photo.user' variable is null.
* Note that this seems to indicated that the photoRepository is UNABLE to be filtered by/access the 'user' field. *
Photo service
async readUser(userId): Promise<User> {
return await this.userRepository.findOne({
where: {
user: userId
}
});
}
This returns null since the userId is blank, due to the previous Photo resolver not being able to access the 'user' field.
Conclusion
Why can't the Photo resolver access the #Parent photo 'user' field? The User service seems to be able to filter by the 'user' field just fine, yet I can't seem to be able to access the Photo 'user' field directly.
Thank you for any help on this! I've been stumped on this for the last two days...
So you'd like to access the user huh? photo.user? In FindOptions there's a key called 'relations' this will fetch and apply that for you. So in your example
async readPhotos(userId): Promise<Photo[]> {
return await this.photoRepository.find({
where: {
user: userId
},
relations: ['user'],
});
}
This should now be photo.user. Another way would be the reverse
async readUser(userId): Promise<User> {
return await this.userRepository.findOne({
where: {
user: userId
},
relations: ['photos'],
});
}
This should now return user.photos as an array of Photos
You can find more examples of FindOptions usages here http://typeorm.io/#/find-options
Hope this helps!
I was struggling with a similar problem and although #bashleigh's solution works if you want the entire entity returned I only needed the id. So if that's your case you can pass the loadRelationIds option, and set it to true.
return await this.photoRepository.find({
where: {
id: photoId
},
loadRelationIds: true
});
This will return user as just the id (string or int).
I am currently trying to add a static method to my mongoose schema but I can't find the reason why it doesn't work this way.
My model:
import * as bcrypt from 'bcryptjs';
import { Document, Schema, Model, model } from 'mongoose';
import { IUser } from '../interfaces/IUser';
export interface IUserModel extends IUser, Document {
comparePassword(password: string): boolean;
}
export const userSchema: Schema = new Schema({
email: { type: String, index: { unique: true }, required: true },
name: { type: String, index: { unique: true }, required: true },
password: { type: String, required: true }
});
userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true;
return false;
});
userSchema.static('hashPassword', (password: string): string => {
return bcrypt.hashSync(password);
});
export const User: Model<IUserModel> = model<IUserModel>('User', userSchema);
export default User;
IUser:
export interface IUser {
email: string;
name: string;
password: string;
}
If I now try to call User.hashPassword(password) I am getting the following error [ts] Property 'hashPassword' does not exist on type 'Model<IUserModel>'.
I know that I didn't define the method anywhere but I don't really know where I could put it as I can't just put a static method into an interface.
I hope you can help my find the error, thanks in advance!
I was having the same problem as you, and then finally managed to resolve it after reading the documentation in the TS mongoose typings (which I didn't know about before, and I'm not sure how long the docs have been around), specifically this section.
As for your case, you'll want to follow a similar pattern to what you currently have, although you'll need to change a few things in both files.
IUser file
Rename IUser to IUserDocument. This is to separate your schema from your instance methods.
Import Document from mongoose.
Extend the interface from Document.
Model file
Rename all instances of IUser to IUserDocument, including the module path if you rename the file.
Rename only the definition of IUserModel to IUser.
Change what IUser extends from, from IUserDocument, Document to IUserDocument.
Create a new interface called IUserModel which extends from Model<IUser>.
Declare your static methods in IUserModel.
Change the User constant type from Model<IUserModel> to IUserModel, as IUserModel now extends Model<IUser>.
Change the type argument on your model call from <IUserModel> to <IUser, IUserModel>.
Here's what your model file would look like with those changes:
import * as bcrypt from 'bcryptjs';
import { Document, Schema, Model, model } from 'mongoose';
import { IUserDocument } from '../interfaces/IUserDocument';
export interface IUser extends IUserDocument {
comparePassword(password: string): boolean;
}
export interface IUserModel extends Model<IUser> {
hashPassword(password: string): string;
}
export const userSchema: Schema = new Schema({
email: { type: String, index: { unique: true }, required: true },
name: { type: String, index: { unique: true }, required: true },
password: { type: String, required: true }
});
userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true;
return false;
});
userSchema.static('hashPassword', (password: string): string => {
return bcrypt.hashSync(password);
});
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema);
export default User;
And your (newly renamed) ../interfaces/IUserDocument module would look like this:
import { Document } from 'mongoose';
export interface IUserDocument extends Document {
email: string;
name: string;
password: string;
}
I think you are having the same issue that I just struggled with. This issue is in your call. Several tutorials have you call the .comparePassword() method from the model like this.
User.comparePassword(candidate, cb...)
This doesn't work because the method is on the schema not on the model. The only way I was able to call the method was by finding this instance of the model using the standard mongoose/mongo query methods.
Here is relevant part of my passport middleware:
passport.use(
new LocalStrategy({
usernameField: 'email'
},
function (email: string, password: string, done: any) {
User.findOne({ email: email }, function (err: Error, user: IUserModel) {
if (err) throw err;
if (!user) return done(null, false, { msg: 'unknown User' });
user.schema.methods.comparePassword(password, user.password, function (error: Error, isMatch: boolean) {
if (error) throw error;
if (!isMatch) return done(null, false, { msg: 'Invalid password' });
else {
console.log('it was a match'); // lost my $HÏT when I saw it
return done(null, user);
}
})
})
})
);
So I used findOne({}) to get the document instance and then had to access the schema methods by digging into the schema properties on the document user.schema.methods.comparePassword
A couple of differences that I have noticed:
Mine is an instance method while yours is a static method. I'm confident that there is a similar method access strategy.
I found that I had to pass the hash to the comparePassword() function. perhaps this isn't necessary on statics, but I was unable to access this.password
For future readers:
Remember that we are dealing with two different Mongo/Mongoose concepts: a Model, and Documents.
Many Documents can be created from a single Model. The Model is the blueprint, the Document is the thing created according to the Model's instructions.
Each Document contains its own data. Each also carries their own individual instance methods which are tied to its own this and only operate on that one specific instance.
The Model can have 'static' methods which are not tied to a specific Document instance, but operate over the whole collection of Documents.
How this all relates to TypeScript:
Extend Document to define types for instance properties and .method functions.
Extend the Model (of a Document) to define types for .static functions.
The other answers here have decent code, so look at them and trace through the differences between how Documents are defined and how Models are defined.
And remember when you go to use these things in your code, the Model is used to create new Documents and to call static methods like User.findOne or your custom statics (like User.hashPassword is defined above).
And Documents are what you use to access the specific data from the object, or to call instance methods like this.save and custom instance methods like this.comparePassword defined above.
I cannot see your IUser interface however I suspect that you have not included the methods in there.
EG
export interface IUser {
email: string,
hash: string,
salt: string,
setPassword(password: string): void,
validPassword(password: string): boolean,
generateJwt(): string
}
typescript will then recognize your methods and stop complaining
So the one with 70 updates I also gave an upvote. But it is not a complete solution. He uses a trivial example based on the OP. However, more often than not when we use statics and methods in order to extend the functionality of the model, we want to reference the model itself. The problem with his solution is he using a callback function which means the value of this will not refer to the class context but rather a global.
The first step is to invoke the statics property rather than pass the property as an argument to the static function:
schema.statics.hashPassword
Now we cannot assign an arrow function to this member, for this inside the arrow function will still refer to the global object! We have to use function expression syntax in order to capture this in the context of the model:
schema.statics.hashPassword = async function(password: string): Promise<string> {
console.log('the number of users: ', await this.count({}));
...
}
Refer : https://mongoosejs.com/docs/typescript.html
Just create an interface before the schema which represents a document structure.
Add the interface type to the model.
Export the model.
Quoting below from mongoose docs:
import { Schema, model, connect } from 'mongoose';
// 1. Create an interface representing a document in MongoDB.
interface User {
name: string;
email: string;
avatar?: string;
}
// 2. Create a Schema corresponding to the document interface.
const schema = new Schema<User>({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
});
// 3. Create a Model.
const UserModel = model<User>('User', schema);
If you add any method to the schema, add its definition in the interface as well.