NestJS: How to convert Mongoose document to regular class? - nestjs-mongoose

I have followed NestJS mongo docs to create a schema (without ID) from a regular class. Since this class has other general uses (unrelated to mongoose), I would like to have regular methods on that class as well:
#Schema({ _id: false })
export class Location {
#Prop()
lat: number;
#Prop()
lon: number;
regularMethod() { return something based on this.lat, this.lon }
}
export const LocationSchema = SchemaFactory.createForClass(Location);
export type CatDocument = HydratedDocument<Cat>;
#Schema()
export class Cat {
#Prop({ type: [LocationSchema] })
locations: Location[];
}
export const CatSchema = SchemaFactory.createForClass(Cat);
The problem is that if I query such an object from the db, regularMethod doesn't exist since the queried object is actually a Document based on Location, rather than Location. The document only exposes methods that were defined by the schema.methods, which is not what I need.
MyService {
constructor(#InjectModel(Cat.name) catModel: Model<CatDocument>) {}
async findLocations(catId: string) {
const cat = await catModel.findOneById(catId);
cat.location.forEach(loc => loc.regularMethod()) // no such method
}
}
Is there some obvious way to "cast" Location to the original class to have access to those methods?

Related

Prevent serialization of parent entity in #ResolveField()

When using a global interceptor like the transform interceptor in the docs, the parent entity passed to #ResolveField() is serialized, which causes properties decorated with #Exluded({ toPlainOnly: true }) to be undefined. Is there a way to always get the raw entity instance passed to #ResolveField() instead?
This is particularly annoying when entities have relation related ID fields specified:
import { Exclude } from 'class-transformer';
#ObjectType()
class Book {
#ManyToOne(() => Author)
author: Author;
#Exlude({ toPlainOnly: true })
authorId: number;
}
#ResolveField(() => Author, { name: 'author' ])
getAuthorForBook(#Parent() book: Book) {
// book.authorId does not exist
}

Node.JS: How do you validate QueryParams in routing-controllers?

lets say you have an interface like this:
import { Get, QueryParam } from 'routing-controllers';
// ...
#Get('/students')
async getStudents(
#QueryParam('count') count?: number,
): Promise<void> {
console.log(count);
}
How do you ensure count is an int and not a float, for example? Something like this is not valid:
#IsInt() #QueryParam('count') count?: number,
IsInt can only be used on a class property, eg for a body model, not for a single parameter value. But according to. this https://github.com/typestack/routing-controllers#auto-validating-action-params it is possible:
This technique works not only with #Body but also with #Param,
#QueryParam, #BodyParam and other decorators.
I had missed this in the docs: https://github.com/typestack/routing-controllers#inject-query-parameters By injecting all of the QueryParams instead of individual QueryParam, you can validate them as a class model:
enum Roles {
Admin = "admin",
User = "user",
Guest = "guest",
}
class GetUsersQuery {
#IsPositive()
limit: number;
#IsAlpha()
city: string;
#IsEnum(Roles)
role: Roles;
#IsBoolean()
isActive: boolean;
}
#Get("/users")
getUsers(#QueryParams() query: GetUsersQuery) {
// here you can access query.role, query.limit
// and others valid query parameters
}
Also, make sure you don't use barrel-imports to import Enums, or open-api-generator will produce an error that the enum is undefined and not and object; eg avoid this: import { Roles } from '../..'

Typegoose props not saving

I'm having a problem using typegoose,
I have class like:
class UserType1 extends Typegoose
implements User{
#prop({required: true, unique: true})
_username: string | undefined;
#prop({required: true})
_password: string | undefined;
constructor(username:string, password: string){
this._username = username;
this._password = password;
}
async saveToDB(): Promis<void>{ // This method is refrenced inside User Interface
const model: ModelType<UserType1> = getModelForClass(UserType1);
await model.create(this);
}
<Other methods inside class>
}
Then I use the above code sequence like this:
const u: User = new UserType1("username", "password");
u.saveToDB();
then, a new record is saved inside UserType1 collection,
but it's empty. non of the _username, _password or other props are saved inside the record. Inside of it there's only _id and a variable called "__v" (which I don't know where it's come from.
from what i got as an answer in the comments about not using an unmaintained version, i would recommend to write your example in this way:
class UserType1 implements User {
#prop({ required: true, unique: true })
public _username!: string;
#prop({ required: true })
public _password!: string;
static async default(this: ReturnModelType<typeof UserType1>, username: string, password: string): Promise<void> {
await this.create({ _username: username, _password: password });
}
// <Other methods inside class>
}
const UserModel = getModelForClass(UserType1);
// somewhere in an async code block
await UserModel.default("user1", "somePassword");
Okay, So I left my code and started a new clean one, and It was working perfectly,
so I started investigating the difference between codes until I found out this small problem with importing packages, and it was unrelated to my code:
Instead of doing
import {prop} from "#typegoose/typegoose"
I was doing:
import {prop} from "typegoose"
Since it didn't give me any warning a worked without errors, I never realized this was the problem.
Hope this he

Nestjs, How to get entity table name?

How to get entity table name ? (ex: member-pre-sale-detail)
I want to set table comment
// Seeder: Clear & set Comment
export default class ClearAllSeed implements Seeder {
public async run(factory: Factory, connection: Connection): Promise<void> {
const deleteEntities = [
{table: OrderHead, comment: '訂單/主表'},
]
for(const entity of deleteEntities){
await connection
.createQueryBuilder()
.delete()
.from(entity.table)
.execute();
await connection
// >>>> but table name is MemberPreSaleDetail not member-pre-sale-detail
.query(`alter table ${entity.table.name} comment '${entity.comment}'`);
}
}
}
// Sampel Entity
#Entity('member-pre-sale-detail')
export class MemberPreSaleDetail {
#PrimaryGeneratedColumn({unsigned: true})
id?: number;
#Column({comment: '幾批(整批)', type: 'mediumint', default: 0})
batchQty: number;
}
Expected behavior
get the 'member-pre-sale-detail' string
Environment
Nest version: 7.0.7
For Tooling issues:
- Node version: v14.5.0
- Platform: Mac
I am guessing you are using TypeORM. In that case:
You could get the entity metadata by calling connection.getMetadata(MemberPreSaleDetail).
This method returns an EntityMetadata, which has name, tableName and givenTableName properties. For your usecase I guess you could simply use givenTableName.

Nest.js + Mikro-ORM: Collection of entity not initialized when using createQueryBuilder and leftJoin

I'm using Nest.js, and considering migrating from TypeORM to Mikro-ORM. I'm using the nestjs-mikro-orm module. But I'm stuck on something that seems very simple...
I've 3 entities, AuthorEntity, BookEntity and BookMetadata. From my Author module, I try to left join the Book and BookMetadata tables with the createQueryBuilder method. But when running my query, I'm getting an error where Collection<BookEntity> of entity AuthorEntity[3390] not initialized. However columns from the Author table are well retrieved.
My 3 entities:
#Entity()
#Unique({ properties: ['key'] })
export class AuthorEntity {
#PrimaryKey()
id!: number;
#Property({ length: 255 })
key!: string;
#OneToMany('BookEntity', 'author', { orphanRemoval: true })
books? = new Collection<BookEntity>(this);
}
#Entity()
export class BookEntity {
#PrimaryKey()
id!: number;
#ManyToOne(() => AuthorEntity)
author!: AuthorEntity;
#OneToMany('BookMetadataEntity', 'book', { orphanRemoval: true })
bookMetadata? = new Collection<BookMetadataEntity>(this);
}
#Entity()
#Unique({ properties: ['book', 'localeKey'] })
export class BookMetadataEntity {
#PrimaryKey()
id!: number;
#Property({ length: 5 })
localeKey!: string;
#ManyToOne(() => BookEntity)
book!: BookEntity;
}
And the service file where I run my query:
#Injectable()
export class AuthorService {
constructor(
#InjectRepository(AuthorEntity)
private readonly authorRepository: EntityRepository<AuthorEntity>,
) {}
async findOneByKey(props: { key: string; localeKey: string; }): Promise<AuthorEntity> {
const { key, localeKey } = props;
return this.authorRepository
.createQueryBuilder('a')
.select(['a.*', 'b.*', 'c.*'])
.leftJoin('a.books', 'b')
.leftJoin('b.bookMetadata', 'c')
.where('a.key = ?', [key])
.andWhere('c.localeKey = ?', [localeKey])
.getSingleResult();
}
}
Am I missing something? Might be not related, but I also noticed that there is a special autoLoadEntities: true for TypeORM users using Nest.js. Is there something similar for Mikro-ORM? Thanks ;)
Mapping of multiple entities from single query is not yet supported, it is planned for v4. You can subscribe here: https://github.com/mikro-orm/mikro-orm/issues/440
In v3 you need to use 2 queries to load 2 entities, which for your use case is much easier without the QB involved.
return this.authorRepository.findOne({ key }, ['books']);
Or you could use qb.execute() to get the raw results and map them yourself, but you would also have to manually alias all the fields to get around duplicities (Author.name vs Book.name), as doing qb.select(['a.*', 'b.*']) will result in query select a.*, b.* ... and the duplicate columns would not be correctly mapped.
https://mikro-orm.io/docs/query-builder/#mapping-raw-results-to-entities
About the autoLoadEntities thing, never heard of that, will take a look how it works, but in general, the nestjs adapter is not developed by me, so if its something only nest related, it would be better to ask on their GH repo.
Or you could use folder based discovery (entitiesDirs).
here is the new example with 3 entities:
return this.authorRepository.findOne({
key,
books: { bookMetadata: localeKey } },
}, ['books.bookMetadata']);
This will produce 3 queries, one for each db table, but the first one will auto-join books and bookMetadata to be able to filter by them. The condition will be propagated down in the second and third query.
If you omit the populate parameter (['books.bookMetadata']), then only the first query will be fired and you will end up with books not being populated (but the Author will be queried with the joined condition).

Resources