#nestjs/mongoose, virtual populate with 2 databases - mongodb

I'm trying to populate userId field which exists in database 1 from user document which exists in database 2.
I've already defined connectionName parameter in MongooseModule.ForRootAsync() I can't find out where the problem is. Also it works if I request info from db1 and db2 individually.
Actually on console.log(commentPopulated) userId field is just the objectId without populated fields from User schema and also sometimes with certain #Prop() and .populate() parameters app throw me this error:
MissingSchemaError: Schema hasn't been registered for model "User".
Using #nestjs/mongoose decorators how do I achieve this?
app.module.ts
MongooseModule.forRootAsync({
connectionName: 'db1',
useFactory: () => ({
uri: process.env.DB1,
connectionFactory: (connection: { plugin: (arg0: unknown) => void }) => {
connection.plugin(_)
connection.plugin(autoPopulate)
return connection
},
}),
}),
MongooseModule.forRootAsync({
connectionName: 'db2',
useFactory: () => ({
uri: process.env.DB2,
connectionFactory: (connection: { plugin: (arg0: unknown) => void }) => {
connection.plugin(_)
connection.plugin(autoPopulate)
return connection
},
}),
}),
comment.module.ts
const commentModule: DynamicModule = MongooseModule.forFeatureAsync([
{
name: Comment.name,
useFactory: () => {
return CommentSchema
}
}
], 'db1')
#Module({
imports: [commentModule],
providers: [CommentService, CommentResolver]
})
export class CommentModule { }
comment.schema.ts
#Schema({ toJSON: { virtuals: true, getters: true }, toObject: { virtuals: true, getters: true } })
#ObjectType()
export class Comment extends Document {
#Prop()
#Field(() => String)
readonly _id: MongooseSchema.Types.ObjectId
#Prop({ required: true })
#Field(() => String)
text: string
//TODO: Reference User document from DB2, Comment document exists in DB1
#Prop({ type: MongooseSchema.Types.ObjectId, ref: User.name})
#Field(() => User, { nullable: true })
userId: MongooseSchema.Types.ObjectId
#Prop({ type: String, enum: UserType, required: true, default: UserType.Regular })
#Field(() => UserType, { defaultValue: UserType.Regular })
userType: UserType
#Prop({ default: Date.now })
#Field(() => Date)
created: Date
}
export const CommentSchema = SchemaFactory.createForClass(Comment)
user.module.ts
const userModule: DynamicModule = MongooseModule.forFeatureAsync([
{
name: User.name,
useFactory: () => {
return UserSchema
},
},
], 'db2')
#Module({
imports: [userModule],
providers: [UserService, UserResolver]
})
export class UserModule { }
user.schema.ts
#Schema()
#ObjectType()
export class User extends Document {
#Prop()
#Field(() => String)
readonly _id: MongooseSchema.Types.ObjectId
#Prop({ required: true })
#Field(() => String)
firstName: string
#Prop({ required: true })
#Field(() => String)
lastName: string
#Prop({ required: true })
#Field(() => String)
email: string
}
export const UserSchema = SchemaFactory.createForClass(User)
comment.service.ts
#Injectable()
export class CommentService {
constructor(#InjectModel(Comment.name, 'db1') private readonly model: Model<Comment>) { }
async getComments() {
const commentPopulated = await this.model.findById('63b8608c7d4f880cba028bfe').populate('userId')
console.log(commentPopulated)
return commentPopulated
}
}
I have tried randomly playing with parameters on #Prop() decorator with no success, I think there is the problem, also played with .populate() function parameters.

Related

How to add timestamps fields and _id into nested schema by default

I have a correct schema User where fields _id, createdAt, updatedAt are written by default. But in schema Message it doesn't work.
export type UserDocument = HydratedDocument<User>;
#Schema({ timestamps: true, versionKey: false })
export class Message {
#Prop()
content: string;
}
#Schema({ timestamps: true, versionKey: false })
export class User implements UserInterface {
#Prop()
name: string;
#Prop()
email: string;
#Prop([{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }])
messages?: Message[];
}
export const UserSchema = SchemaFactory.createForClass(User);
Here is my function of message creating:
async createMessage() {
const user = await this.userModel.findById('63ee4fc044d93a4f6bebf934');
user.messages.push({ content: 'a' });
return await user.save();
}
And error is:
Cast to ObjectId failed for value "{ content: 'a' }" (type Object) at path "messages" because of "BSONTypeError"
But this snippet works fine:
async createUser(createUserDto: CreateUserDto): Promise<CreatedUserDto> {
return this.userModel.findOneAndUpdate(
{ name: createUserDto.name },
createUserDto,
{ upsert: true, new: true },
);
}
How to fix it?
Fixed id, correct implementation:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import { UserInterface } from '../interface/user.interface';
export type UserDocument = HydratedDocument<User>;
#Schema({ timestamps: true, versionKey: false })
export class Message {
#Prop()
content: string;
}
export const MessageSchema = SchemaFactory.createForClass(Message);
#Schema({ timestamps: true, versionKey: false })
export class User implements UserInterface {
#Prop()
name: string;
#Prop()
email: string;
#Prop({ type: [MessageSchema], default: [] })
messages?: Message[];
}
export const UserSchema = SchemaFactory.createForClass(User);

TypeORM MongoDB driver return NULL for child relations

I have a user entinty that looks as follows:
#ObjectType()
#Entity()
export class User extends BaseEntity {
#Field(() => String)
#ObjectIdColumn()
_id: ObjectID;
#Field(() => String)
#Column({ type: "varchar", unique: true, length: 25 })
username: string;
// Profile
#Field(() => Profile, { nullable: true })
#OneToOne(() => Profile, {eager: false})
#JoinColumn()
profile: Profile;
}
My profile entity looks as follows:
#ObjectType()
#Entity()
export class Profile extends BaseEntity {
#Field(() => String)
#ObjectIdColumn()
_id: ObjectID;
#Field(() => String, { nullable: false })
#Column({ nullable: false })
email: string;
#Field(() => String, { nullable: false })
#Column({ nullable: false })
username: string;
}
If i query the user as follows:
User.findOne({
where: { email },
relations: ["profile"],
});
The profile is null on the console logs and i have a profile a user who has a profile in my mongodb doccuments. What may be possibly the problem with my code?

How can I use the point type coordinate information of postgis in graphql using mutations?

I created a mutation to insert new data into the postgresql called location. The column coordinate must receive and store data, for example, ST_GeomFromGeoJSON ('{ "type": "Point", "coordinates": [-48.23456,20.12345]}').
However, graphql is not working, so I don't know where to modify it. I think it's because the scalar called GeoJSONPoint that I made is not working properly. Could you tell me how to create a scalar if graphql puts the data above?
GeoJSONPoint Scalar
import { GraphQLScalarType, Kind } from 'graphql';
export const GeoJSONPoint = new GraphQLScalarType({
name: 'GeoJSONPoint',
description: 'Geometry scalar type',
parseValue(value) {
return value;
},
serialize(value) {
return value;
},
parseLiteral(ast) {
if (ast.kind === Kind.OBJECT) {
console.log(ast);
return new Object(ast);
}
return null;
}
});
location.entity
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn
} from 'typeorm';
import { Location_Group } from './location_group.entity';
import { Geometry } from 'geojson';
import { Field, ID, Int, ObjectType } from '#nestjs/graphql';
import { GeoJSONPoint } from 'src/scalar/geoJSONPoint.scalar';
#ObjectType()
#Entity('location')
export class Location {
#Field(() => ID)
#PrimaryGeneratedColumn('increment')
id: number;
#Field(() => String)
#Column({ type: 'varchar' })
name: string;
#Field(() => GeoJSONPoint)
#Column({
type: 'geometry',
nullable: true,
spatialFeatureType: 'Point',
srid: 4326
})
coordinate: Geometry;
#Field(() => Int)
#Column({ type: 'int' })
order_number: number;
#Field()
#CreateDateColumn({ type: 'timestamptz' })
created_at: Date;
#Field(() => Location_Group)
#ManyToOne(
() => Location_Group,
(location_group) => location_group.location
)
#JoinColumn([{ name: 'location_group_id', referencedColumnName: 'id' }])
location_group: Location_Group;
}
resolver
#Mutation(() => Location)
async createLocation(
#Args('data') data: LocationDataInput
): Promise<Location> {
console.log(data);
return await this.locationService.setLocation(data);
}
I solved this problem. First of all, we divided the values entered by parseLiteral in scalar into
{type: '', coordinates: []}
and removed the foreign key column.

NestJS Insert a Comment into a user blog post

I have an app where an user can create a list of Recipes and each Recipe can have multiple comments that many users can post.
This is what im trying to do:
I have a comments Enitity:
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeUpdate,
ManyToOne,
JoinColumn,
ManyToMany,
} from 'typeorm';
import { UserEntity } from 'src/user/models/user.entity';
import { RecipeEntity } from 'src/recipe/model/recipe-entry.entity';
import { User } from 'src/user/models/user.interface';
#Entity('comments_entry')
export class CommentsEntity {
#PrimaryGeneratedColumn()
id: number;
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
#BeforeUpdate()
updateTimestamp() {
this.updatedAt = new Date();
}
#ManyToOne(
type => UserEntity,
user => user.username,
)
author: UserEntity;
#Column()
recipe_id: number;
#Column()
author_id: number;
#ManyToOne(
type => RecipeEntity,
recipe => recipe.comment,
)
#JoinColumn({ name: 'recipe_id', referencedColumnName: 'id' })
comment: RecipeEntity;
}
Linked to a Recipe entity:
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeUpdate,
ManyToOne,
JoinColumn,
OneToMany,
JoinTable,
ManyToMany,
} from 'typeorm';
import { UserEntity } from 'src/user/models/user.entity';
import { CommentsEntity } from 'src/comments/model/comments.entity';
#Entity('recipe_entry')
export class RecipeEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
title: string;
#Column()
slug: string;
#Column('text', { array: true, nullable: true })
ingr: string[];
#Column({ default: '' })
description: string;
#Column({ default: '' })
body: string;
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
#BeforeUpdate()
updateTimestamp() {
this.updatedAt = new Date();
}
#Column({ nullable: true })
headerImage: string;
#Column({ nullable: true })
publishedDate: Date;
#Column({ nullable: true })
isPublished: boolean;
#Column()
user_id: number;
#ManyToOne(
type => UserEntity,
user => user.recipeEntries,
)
#JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
author: UserEntity;
#Column({ default: 0 })
totalWeight: number;
#Column('text', { array: true, default: '{}' })
dietLabels: string[];
#Column({ default: 0 })
calorieQuantity: number;
#Column({ default: 0 })
proteinQuantity: number;
#Column({ default: 0 })
carbQuantity: number;
#Column({ default: 0 })
fatQuantity: number;
#Column({ default: 0 })
sugarQuantity: number;
#Column('text', { array: true, nullable: true })
likes: string[];
#Column({ default: false, nullable: true })
isLiked: boolean;
#OneToMany(
type => CommentsEntity,
comments => comments.comment,
)
comment: CommentsEntity[];
}
Linked to an User entity:
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeInsert,
OneToMany,
} from 'typeorm';
import { UserRole } from './user.interface';
import { RecipeEntity } from 'src/recipe/model/recipe-entry.entity';
import { CommentsEntity } from 'src/comments/model/comments.entity';
#Entity()
export class UserEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#Column({ unique: true })
username: string;
#Column({ unique: true })
email: string;
#Column({ select: false })
password: string;
#Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole;
#Column({ nullable: true })
profileImage: string;
#Column({ default: false, nullable: true })
favourite: boolean;
#OneToMany(
type => RecipeEntity,
recipeEntity => recipeEntity.author,
)
recipeEntries: RecipeEntity[];
#OneToMany(
type => CommentsEntity,
recipeEntryEntity => recipeEntryEntity.author,
)
commentEntries: CommentsEntity[];
#BeforeInsert()
emailToLowerCase() {
this.email = this.email.toLowerCase();
}
}
As an user i can post recipes. But im failing to add comments on specific recipes.
2 errors:
When i create a Recipe with some hardcoded comments, the users and recipe table gets filled but the comments_entry table is empty.
And im failing to implement the method to add comments to a specific recipe.
Controller:
#UseGuards(JwtAuthGuard)
#Post('recipe/:id')
createComment(
#Param() params,
#Body() comment: string,
#Request() req,
): Observable<RecipeEntry> {
const user = req.user;
const id = params.id;
return this.recipeService.createComment(user, id, comment);
}
createComment(id: number, commentEntry: string): Observable<RecipeEntry> {
return from(this.findOne(id)).pipe(
switchMap((recipe: RecipeEntry) => {
const newComment = recipe.comment.push(commentEntry);
return this.recipeRepository.save(newComment);
}),
);
}
Type 'Observable<DeepPartial[]>' is not assignable to type 'Observable'.
Property 'comment' is missing in type 'DeepPartial[]' but required in type 'RecipeEntry'.ts(2322)
recipe-entry.interface.ts(18, 3): 'comment' is declared here.
Any help?
Can't build working example but may be would helpful:
Redo your relations (I simplified entities for example, but you should use full:) ):
#Entity('comments_entry')
export class CommentsEntity {
#Column()
authorId: number;
#ManyToOne(type => UserEntity, user => user.id)
author: UserEntity;
#ManyToOne(type => RecipeEntity, recipe => recipe.id)
recipe: RecipeEntity;
}
#Entity('recipe_entry')
export class RecipeEntity {
#Column()
authorId: number;
#ManyToOne(type => UserEntity, user => user.id)
author: UserEntity;
#OneToMany(type => CommentsEntity, comment => comment.recipe)
comments: CommentsEntity[];
}
#Entity('user_entry')
export class UserEntity {
#OneToMany( type => RecipeEntity, recipe => recipe.author)
recipes: RecipeEntity[];
#OneToMany(type => CommentsEntity, comment => comment.author)
comments: CommentsEntity[];
}
RecipeDto something like:
RecipeDto: {
authorId: number | string,
comments: CommentDto[],
****other recipe data
}
create new Recipe:
createRecipe(recipeDto: RecipeDto): Observable<RecipeEntry> {
const { comments, authorId } = recipeDto;
if(comments) {
const commentPromises = comments.map(async comment => {
comment.authorId = authorId;
return await this.commentRepository.save(comment);
});
recipeDto.comments = await Promise.all(commentPromises);
}
return await this.recipeRepository.save(recipeDto);
}
If I understood correctly, you are trying that:
One User --> Many Recipes
One User --> Many Comments
One Recipe --> Many Comments
Your entities seems right.
Normally a typeorm repository returns a promise and not an observable.
You need to convert it to an Observable.
And at the moment you are trying to store a comment in the recipeRepo. You should save the whole recipe. And before you have to save the comment in the comment repo (if you are not working with cascades).
Something like this:
createComment(id: number, commentEntry: string): Observable<RecipeEntry> {
return from(this.findOne(id)).pipe(
switchMap((recipe: RecipeEntry) => {
return from(this.commentRepository.save(newComment)).pipe(
map(com: Comment) => {
recipe.comment.push(com);
return from(this.recipeRepository.save(recipe));
}
)
);
}
If you enable cascades, you can do this in only one call.

Is it possible to have in Mongodb (with mongoose and typegoose) an index on array of nested keys?

In mongodb (using mongoose and typegoose) is it possible to have an array index on a nested key?
export class Member extends Typegoose {
#prop({ required: true })
public email!: string;
#prop({ required: true })
private userId!: string
}
#index({ 'members.userId': 1 })
export class Group {
#arrayProp({ items: Member })
public members: Member[];
#prop()
name: string;
}
If so, how can I query this collection if I'd want to find a group by userId?
Like this?
Group.findOne({ usersIds: userId })
yes it is possible, here is how you can do it:
// NodeJS: 14.5.0
// MongoDB: 4.2-bionic (Docker)
import { getModelForClass, prop, index } from "#typegoose/typegoose"; // #typegoose/typegoose#7.3.0
import * as mongoose from "mongoose"; // mongoose#5.9.25 #types/mongoose#5.7.32
class Member {
#prop({ required: true })
public email!: string;
}
#index({ "members.email": 1 }, { unique: true }) // added unique to be easily testable
class Group {
#prop({ type: Member })
public members?: Member[];
#prop()
public name?: string;
}
const GroupModel = getModelForClass(Group);
(async () => {
await mongoose.connect(`mongodb://localhost:27017/`, { useNewUrlParser: true, dbName: "verifyMASTER", useCreateIndex: true, useUnifiedTopology: true });
await GroupModel.create({ name: "group1", members: [{ email: "h#h.h" }] });
try {
await GroupModel.create({ name: "group2", members: [{ email: "h#h.h" }] });
console.log("didnt fail");
} catch (err) {
// it should error
console.log("err", err);
}
console.log(await GroupModel.listIndexes());
await mongoose.disconnect();
})();
output of GroupModel.listIndexes:
[
{ v: 2, key: { _id: 1 }, name: '_id_', ns: 'verifyMASTER.groups' },
{
v: 2,
unique: true,
key: { 'members.email': 1 },
name: 'members.email_1',
ns: 'verifyMASTER.groups',
background: true
}
]