How can I create a query on nested documents? - mongodb

I am trying to find a document inside nested collection, but I do not know how should I create query for this. In TypeORM documentation there is only one simple example how to use find/findOne on not nested documents.
I created this query:
const result: BlogEntity = await this.blogRepository.findOne({posts: {_id: article._id}});
but when I am trying to build my project i am getting this error:
error TS2345: Argument of type '{ posts: { _id: ObjectID; }; }' is not assignable to parameter of type 'string | number | Date | ObjectID | FindOneOptions | Partial'.
After this i also tried :
const result: BlogEntity = await this.blogRepository.findOne({posts: {_id: article._id}});
const result: BlogEntity = await this.blogRepository.findOne({'posts._id': article._id});
const result: BlogEntity = await this.blogRepository.findOne({where: {posts: { _id: article._id}}});
const result: BlogEntity = await this.blogRepository.findOne({where: {'posts._id': _id: article._id}});
const result: ArticleEntity = await this.mongoManager.findOne(ArticleEntity, {_id: article._id});
But none of them is working
So the question is how should i correctly create this find query. BlogEntity and ArticleEntity code bellow
BlogEntity
#Entity()
#ObjectType()
export class BlogEntity {
#Field(() => ID)
#ObjectIdColumn()
_id!: ObjectID;
#Field(() => [ArticleEntity])
#Column()
posts!: ArticleEntity[];
#Field(() => [ArticleEntity])
#Column()
projects!: ArticleEntity[];
#Field(() => [ArticleEntity])
#Column()
tutorials!: ArticleEntity[];
}
ArticleEntity
#Entity()
#ObjectType()
export class ArticleEntity {
#Field(() => ID)
#ObjectIdColumn()
_id!: ObjectID;
#Field()
#Column()
title!: string;
#Field(() => [String])
#Column()
tags!: string[];
#Field()
#Column()
release!: Date;
#Field(() => [SectionEntity])
#Column()
sections!: SectionEntity[];
#Field({ nullable: true })
#Column()
tutorial?: string;
#Field({ nullable: true })
#Column()
app?: string;
}
If you need anything else ping me in the comment section.

When you are working with nested object's you need to use (.) operator
await this.blogRepository.findOne({'posts._id': article._id});

So it is impossible to achive this in TypeORM. The only one solution which I see there is to swich to Typegoose or Mongoose.
If you are more interested in this issue you can read more here: Issue: Query an Array of Embedded Documents #2483

Related

How to convert this typeorm 'pg' method createQueryBuilder to 'mongodb' agregate pipeline

Unfortunately Typeorm support has no method 'createQueryBuilder' and I would like to change it some how to mongodb agregate pipeline.
Here is the existing query:
const query = this.messageRepository
.createQueryBuilder('message')
.leftJoin('message.room', 'room')
.where('room.id = :roomId', { roomId: room.id })
.leftJoinAndSelect('message.user', 'user')
.orderBy('message.created_at', 'DESC');
Here is how my model looks like:
#Entity()
export class MessageEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
text: string;
#ManyToOne(() => UserEntity, user => user.messages)
#JoinColumn()
user: UserEntity;
#ManyToOne(() => RoomEntity, room => room.messages)
#JoinTable()
room: RoomEntity;
#CreateDateColumn()
created_at: Date;
#UpdateDateColumn()
updated_at: Date;
}
I am trying to migrate from pg to mongodb by ceeping same functionality.

TypeORMError: alias was not found

I'm trying to make API pagination for GET /authors.
I have bidirectional many to many relation between authors and books table.
I found that problem is when using creatingQueryBuilder() in combination with .leftJoinAndSelect() and .skip() I get TypeORMError: ""authors"" alias was not found. Maybe you forgot to join it?. But I'm not sure how to solve it.
My database look like this:
library=# select * from authors;
id | first_name | last_name | birth_date | created_at | updated_at
----+------------+-----------+------------+----------------------------+----------------------------
library=# select * from books;
id | title | isbn | pages | created_at | updated_at
----+------------------+-----------------------+-------+----------------------------+----------------------------
library=# select * from books_authors
books_id | authors_id
----------+------------
(4 rows)
Entities look like this:
import { Exclude } from 'class-transformer';
import { BookEntity } from 'src/book/entities/book.entity';
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeInsert,
ManyToMany,
} from 'typeorm';
#Entity({ name: 'authors' })
export class AuthorEntity {
#Exclude()
#PrimaryGeneratedColumn()
id: number;
#Column()
firstName: string;
#Column()
lastName: string;
#Column({ type: 'date', nullable: true })
birthDate: Date | null;
#Exclude()
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
#Exclude()
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
#ManyToMany(() => BookEntity, (book) => book.authors)
books: BookEntity[];
#BeforeInsert()
updateTimestamp() {
this.updatedAt = new Date();
}
}
import { Exclude } from 'class-transformer';
import { AuthorEntity } from 'src/author/entities/author.entity';
import {
Entity,
PrimaryGeneratedColumn,
Column,
BeforeInsert,
ManyToMany,
JoinTable,
} from 'typeorm';
#Entity({ name: 'books' })
export class BookEntity {
#Exclude()
#PrimaryGeneratedColumn()
id: number;
#Column()
title: string;
#Column()
isbn: string;
#Column()
pages: number;
#Exclude()
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
#Exclude()
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt: Date;
#ManyToMany(() => AuthorEntity, (author) => author.books)
#JoinTable({ name: 'books_authors' })
authors: AuthorEntity[];
#BeforeInsert()
updateTimestamp() {
this.updatedAt = new Date();
}
}
Service method looks like this:
async findAll(
pageOptionsDto: PageOptionsDto,
filterAuthorDto: FilterAuthorDto,
): Promise<PageDto<AuthorEntity>> {
const builder = this.dataSource
.getRepository(AuthorEntity)
.createQueryBuilder('authors');
if (filterAuthorDto?.firstName) {
builder.where('"authors"."first_name" LIKE :firstName', {
firstName: `%${filterAuthorDto.firstName}%`,
});
}
if (filterAuthorDto?.lastName) {
builder.andWhere('"authors"."last_name" LIKE :lastName', {
lastName: `%${filterAuthorDto.lastName}%`,
});
}
// This part of code is problematic
builder
.innerJoinAndSelect('authors.books', 'books')
.orderBy('"authors"."created_at"', pageOptionsDto.order)
.skip(pageOptionsDto.skip)
.take(pageOptionsDto.perPage);
const total = await builder.getCount();
const { entities } = await builder.getRawAndEntities();
const pageMetaDto = new PageMetaDto({ total, pageOptionsDto });
return new PageDto(entities, pageMetaDto);
}
Just remove the double quotation inside the string, it's redundant and makes typeorm get confused and couldn't find related defined alias.
...
.orderBy('authors.created_at', pageOptionsDto.order)
...

How to map joined table column to an entity's field in TypeORM

There are two entities as follow:
// user.entity.ts
#Entity()
export class User extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
#RelationId((user: User) => user.group)
groupId: number;
#Column()
fullName: string;
#Column()
email: string;
#Column()
passwordHash: string;
#ManyToOne(type => Group, group => group.users)
#JoinColumn()
group: Group;
isOwner: boolean;
}
// group.entity.ts
#Entity()
export class Group extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#Column({ default: false })
isOwner: boolean;
#OneToMany(type => User, user => user.group)
users: User[];
}
I'd like to map the isOwner value of Group to isOwner of User
I tried:
async findOneById(id: number): Promise<User> {
return await User.createQueryBuilder('user')
.leftJoinAndMapOne('user.isOwner', 'user.group', 'group')
.select(['user.id', 'user.fullName', 'user.email', 'group.isOwner'])
.getOne();
}
the result was:
It is possible to achieve that by using #AfterLoad() or with JS or with raw query.
BUT
Is it possible to implement that using the orm on the query level?
Something like that could be as a solution:
findOneById(id: number): Promise<User> {
return User.createQueryBuilder('user')
.leftJoinAndMapOne('user.isOwner', 'user.group', 'group')
.select(['user.id', 'user.fullName', 'user.email', 'group.isOwner AS user.isOwner']) // or probably 'group.isOwner AS user_isOwner'
.getOne();
}
And you could look at this answer, hope it would be helpful

Cannot delete a OneToMany record in TypeORM

I have the following schemas
#Entity()
export class Question extends BaseEntity {
#PrimaryColumn()
messageId: string;
#Column()
authorId: string;
#Column()
question: string;
#Column("varchar", { array: true })
possibleAnswers: string[];
#Column()
isAnonymous: boolean;
#OneToMany(() => Answer, (answer) => answer.question, { eager: true })
answers: Answer[];
get formattedAnswers() {
return this.possibleAnswers
.map((answer, idx) => `${numericEmojis[idx]}: **${answer}**`)
.join("\n");
}
}
#Entity()
#Unique("uc_ids", ["userId", "questionMessageId"])
export class Answer extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
userId: string;
#Column()
answerIndex: number;
#ManyToOne(() => Question, (question) => question.answers)
question: Question;
#Column({ readonly: true })
// #ts-expect-error
private readonly questionMessageId: string;
}
Whenever I try to delete like
const question = await Question.findOne(message.id);
await Question.delete(question);
I get the following error:
err: query: SELECT "Question"."message_id" AS "Question_message_id", "Question"."author_id" AS "Question_author_id", "Question"."question" AS "Question_question", "Question"."possible_answers" AS "Question_possible_answers", "Question"."is_anonymous" AS "Question_is_anonymous", "Question__answers"."id" AS "Question__answers_id", "Question__answers"."user_id" AS "Question__answers_user_id", "Question__answers"."answer_index" AS "Question__answers_answer_index", "Question__answers"."question_message_id" AS "Question__answers_question_message_id" FROM "question" "Question" LEFT JOIN "answer" "Question__answers" ON "Question__answers"."question_message_id"="Question"."message_id" WHERE "Question"."message_id" IN ($1) -- PARAMETERS: ["729340583583285289"]
err: (node:19515) UnhandledPromiseRejectionWarning: EntityColumnNotFound: No entity column "answers" was found.
Originally I was trying to setup a cascade delete so that when I remove a question, the answers are removed as well, I got the same error but even after removing the cascade delete I get the same one, how can I fix this? I am using a Postgres database with the SnakeNamingStrategy
Works for me by adding onDelete:"CASCADE" or onDelete:"SET NULL".
I have the following schemas
#Entity()
export class Question extends BaseEntity {
#PrimaryColumn()
messageId: string;
#Column()
authorId: string;
#Column()
question: string;
#Column("varchar", { array: true })
possibleAnswers: string[];
#Column()
isAnonymous: boolean;
#OneToMany(() => Answer, (answer) => answer.question, { eager: true })
answers: Answer[];
get formattedAnswers() {
return this.possibleAnswers
.map((answer, idx) => `${numericEmojis[idx]}: **${answer}**`)
.join("\n");
}
}
#Entity()
#Unique("uc_ids", ["userId", "questionMessageId"])
export class Answer extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
userId: string;
#Column()
answerIndex: number;
// set onDelete as cascade for automatically delete from parent entity
#ManyToOne(() => Question, (question) => question.answers, { cascade: true, onDelete: "CASCADE" })
question: Question;
#Column({ readonly: true })
// #ts-expect-error
private readonly questionMessageId: string;
}

How do I refer to the _id field in a mongodb database via a REST API GET call?

This is hard to explain, so please bear with me.
I have a Nestjs-based server that is using MongoDB as the back end. Here are the three entries from the mongodb collection:
{"_id":{"$oid":"5e87ef7a9cf8648fac6b9f1e"},"complete":false,"editMode":false,"createdAt":{"$date":{"$numberLong":"1585966970857"}},"createdBy":"user","isDeleted":false,"title":"Make a birthday cake","note":"Make sure she poops."}
{"_id":{"$oid":"5e87f081237c70a6782d6c2a"},"createdAt":{"$date":{"$numberLong":"1585967233825"}},"createdBy":"user","isDeleted":false,"complete":false,"editMode":false,"title":"Clean the kitchen","note":"Use Lysol"}
{"_id":{"$oid":"5e87f73be81d7e0061311187"},"createdAt":{"$date":{"$numberLong":"1585968955971"}},"createdBy":"user","isDeleted":false,"complete":false,"editMode":false,"title":"Walk the dog","note":"Make sure she poops."}
Here is my model:
#Entity()
export class Todo {
#ObjectIdColumn()
#Transform(value => value.toString(), { toPlainOnly: true })
id: ObjectID;
#Column({ length: 100 })
title: string;
#Column({ length: 5000 })
note: string;
#Column()
complete: boolean;
#Column()
editMode: boolean;
#Exclude() #Column() createdAt: Date = new Date();
#Exclude() #Column() createdBy: string = 'user';
#Exclude() #Column() isDeleted: boolean = false;
}
Here is my GET-er stuff:
#Get(':id')
async getTodo(#Param('id') id: number) {
return this.todosService.getTodo(id);
}
async getTodo(id: number): Promise<Todo | undefined> {
return this.todosRepository.findOne(id, {
where: {
isDeleted: false,
},
});
}
I would like to run a REST GET call to retrieve one of the documents, say the third one. Therefore I call in my browser:
http://localhost:3000/todos/5e87f73be81d7e0061311187
Well, this returns the first document. In fact, anything I call only returns the first document.
What should my GET call be to get the third item?
I can provide any further information that might be needed.
Firstly, i think the id parameter (id: number), should be a string.
Also, findone takes all the querying parameter in first argument itself ( atleast in JS), so it should be like todoRepository.findOne({_id .$oid: string, isDeleted: false }).
Can you tell me what's $oid? ObjectId type? I haven't worked with nest.js/ typescript. Depending upon this you might want to improve the $oid in query argument.
Here's the answer: Make the id parameter an ObjectID type everywhere in the application.
For instance:
#Get(':id')
async getTodo(#Param('id') id: ObjectID) {
return this.todosService.getTodo(id);
}
async getTodo(id: ObjectID): Promise<Todo | undefined> {
return this.todosRepository.findOne(id, {
where: {
isDeleted: false,
},
});
}
Once I did that, it all worked as expected.