How to define and use a related Model in Objection.js - javascript

Using the following code (which uses ES6's "type":"module" in package.json), I can't seem to access the related Model Group:
import db from "../connection.js";
import objection from "objection";
const { Model } = objection;
Model.knex(db);
class User extends Model {
static get tableName() {
return "users";
}
static get relationMappings() {
return {
groups: {
relation: Model.ManyToManyRelation,
modelClass: Group,
join: {
from: "users.id",
through: {
from: "users_groups.user_id",
to: "users_groups.group_id",
},
to: "groups.id",
}
}
}
}
}
class Group extends Model {
static get tableName() {
return "groups";
}
}
If I run
const myUser = await User.query().findById(1)
It outputs:
User {id: 1, name: "r", email: "raj#raj.raj", username: "raj", … }
But I still can't access the Group relation:
myUser.groups
Outputs:
undefined
What am I doing wrong?

You have to use eager loading in the query to load the desired relations.
It you are using Objection.js v1:
const myUser = await User.query().eager('groups').findById(1)
And since Objection.js v2, eager was renamed as withGraphFetched:
const myUser = await User.query().withGraphFetched('groups').findById(1)
Extra: Loading relations after instantiation
You can load the relations after instantiation using $relatedQuery. Note all instance methods starts with $:
const myUser = await User.query().findById(1)
const groupsOfMyUser = await myUser.$relatedQuery('groups')

Related

How to create factories with existing relationship on model in MIKRO-ORM

Hi I am trying to figure out how to create factory and define relationship between models.
For example I have UserFactory with User entity and this entity has connection to userType table. In factory I have not access to EntityManager so I couldn´t find any existing.
export class UserFactory extends Factory<User> {
model = User
definition(faker: Faker): Partial<User> {
const user = {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
...
userType: // Here I need do something like this:
// EntityManager.findOne(UserType, {id: 1}}
// But EntityManager is private in Factory class
}
return user
}
}
Itried also something like this but this return me an error:
ValidationError: Value for User.type is required, 'undefined' found
DatabaseSeeder
export class DatabaseSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
const users: User[] = new UserFactory(em).each(async user => {
const userType : UserType| null = await em.findOne(UserType, 1)
console.log(tenant)
const userType = await em.findOne(UserType, 1)
if (userType !== null) {
user.type = userType
} else {
user.type = em.create(UserType, {
type: 'test'
})
}
}).make(10)
}
}
What is the proper way to achieve this please?
You can use the shared seeder context as describer in the docs:
https://mikro-orm.io/docs/seeding#shared-context
export class AuthorSeeder extends Seeder {
async run(em: EntityManager, context: Dictionary): Promise<void> {
// save the entity to the context
context.author = em.create(Author, {
name: '...',
email: '...',
});
}
}
export class BookSeeder extends Seeder {
async run(em: EntityManager, context: Dictionary): Promise<void> {
em.create(Book, {
title: '...',
author: context.author, // use the entity from context
});
}
}
I guess this shared context should be also available in the seeder factories, but you can always handle this yourself, as both the seeder and factory is your implementation, so you can pass any additional options in there. Its you who initializes the factory so I dont think there is a better way than doing it in your code.
I would suggest not to flush and findOne things in your seeder, you should aim for a single flush and use the shared context instead for entity look up.

How do I transform a MongoDB document into a NestJS DTO?

I have a data layer that reads and writes to a MongoDB instance. I only want to deal with MongoDB documents at that layer and not expose that implementation to my services.
Right now I am doing something like:
// users.repository.ts
...
async getUserById(id: string): Promise<UserDto> {
const user = await this.model.findOne({ _id: id }).exec();
return this.transformToDto(user);
}
private transformToDto(user: UserDocument): UserDto {
return {
id: user._id,
...etc
}
}
...
This seems overly verbose and there must be a simpler way to achieve this without adding a helper to every repository.
Is there a cleaner way to achieve this?
You can use class-transformer for that and you don't need to use extra helper methods it can be returned instantly.
import { plainToClass } from 'class-transformer';
class UserDto {
id: string;
email: string;
role: string;
}
class Service {
async getUserById(id: string): Promise<UserDto> {
const user = await this.model.findOne({ _id: id }).exec();
return plainToClass(UserDto, user);
}
}
It will return transformed value which is UserDto
UserDto { id: 'U-111', email: 'U-111#email', role: 'user' }

Extended Joi not implementing custom operators

I have issues extending joi class with custom operators. I want to validate mongodb Ids, but the extended object throws following error:
error: uncaughtException: JoiObj.string(...).objectId is not a function
TypeError: JoiObj.string(...).objectId is not a function
Code is following:
import Joi from 'joi';
import * as mongodb from 'mongodb';
interface ExtendedStringSchema extends Joi.StringSchema {
objectId(): this;
}
interface ExtendedJoi extends Joi.Root {
string(): ExtendedStringSchema;
}
const JoiObj: ExtendedJoi = Joi.extend({
base: Joi.string(),
type: 'objectId',
messages: {
'objectId.invalid': '"{{#label}}" must be a valid mongo id'
},
validate(value, helpers) {
if (!mongodb.ObjectId.isValid(value)) {
return helpers.error('objectId.invalid');
}
return value;
}
});
const objIdSchema = JoiObj.object({
id: JoiObj.string().objectId()
});
I found 2 examples:
https://github.com/sideway/joi/issues/2357
How to extend a module from npm using TypeScript?
however they use different properties than what is described in TS definition file and thus does not work.
You want to extend the Joi.string() base. Keep in mind, that you can't validate new mongodb.ObjectID() because it is of type object. You extended Joi.string() and this checks first if your value is of type string. And it will stop validating if it isn't. You can only validate new mongodb.ObjectID().toHexString() which looks like: "5f91a1449b13e3010c5548a2".
This answers is using joi 17.2.1 and mongodb 3.6.2
import Joi from 'joi';
import * as mongodb from 'mongodb';
interface ExtendedStringSchema extends Joi.StringSchema {
objectId(): this;
}
interface ExtendedJoi extends Joi.Root {
string(): ExtendedStringSchema;
}
const stringObjectExtension: Joi.Extension = {
type: 'string',
base: Joi.string(),
messages: {
'string.objectId': '{{#label}} must be a valid mongo id'
},
rules: {
objectId: {
validate: (value: any, helpers) => {
if (!mongodb.ObjectId.isValid(value)) {
return helpers.error('string.objectId')
}
return value;
}
}
}
};
// create extended Joi
const JoiObj: ExtendedJoi = Joi.extend(stringObjectExtension);
// create new mongodb id
const id = new mongodb.ObjectID();
const objIdSchema = JoiObj.object({
id: JoiObj.string().objectId()
});
// will fail because it won't pass the Joi.string() validation
const data1 = {
id: id
};
console.log(objIdSchema.validate(data1).error);
// will succeed
const data2 = {
id: id.toHexString()
};
console.log(objIdSchema.validate(data2).error);
I also had this problem. I solved it with this package.
https://www.npmjs.com/package/joi-oid
const Joi = require('joi-oid')
const schema = Joi.object({
id: Joi.objectId(),
name: Joi.string(),
age: Joi.number().min(18),
})
Good luck :)

How to automatically include `.withGraphFetched()` for all queries of model in objection.js

I have the following model defined:
import db from "../connection.js";
import objection from "objection";
const { Model } = objection;
Model.knex(db);
class User extends Model {
static get tableName() {
return "users";
}
static get relationMappings() {
return {
emails: {
relation: Model.HasManyRelation,
modelClass: Email,
join: {
from: "users.id",
to: "emails.user_id",
}
}
}
}
}
class Email extends Model {
static get tableName() {
return "emails";
}
static get relationMappings() {
return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: "emails.user_id",
to: "users.id",
},
},
};
}
}
And to query a User with their email addresses would require withGraphFetched() to be explicitly run every time as:
const myUser = await User.query().withGraphFetched("emails").findById(1)
I haven't been able to figure out what to write in the Model definition to make this possible, and I don't see any such examples online. Is it possible to ALWAYS have withGraphFetched("emails") automatically included in the query so it doesn't have to be explicitly written out every time?
Such thing doesnt exist. Maybe you could create the logic in the beforeFind hook adding the eager loader to the knex instance, but it could generate a lot of undesired and strange side-effects.
The normal practice is adding a method to that specific case:
class User extends Model {
static get tableName() {
return "users";
}
static get relationMappings() {
return {
emails: {
relation: Model.HasManyRelation,
modelClass: Email,
join: {
from: "users.id",
to: "emails.user_id",
}
}
}
}
static async getUser (id) { // Or maybe getUserWithEmails
return await this.query().withGraphFetched("emails").findById(id)
}
}
Then you can just:
const myUser = await User.getUser(id)

How to define custom query helper in mongoose model with typescript?

I want to define custom query helper using query helper api .
Here the example:
// models/article.ts
import { Document, Schema, Model, model } from 'mongoose';
interface IArticle extends Document {
name: string;
}
interface IArticleModel extends Model<IArticle> {
someStaticMethod(): Promise<any>;
}
const ArticleSchema = new Schema( { name: String } )
ArticleSchema.query.byName = function(name) {
return this.find({ name })
}
export default model<IArticle, IArticleModel>('Article', ArticleSchema);
// routes/article.ts
import ArticleModel from '../models/article.ts'
router.get('/articles, (req, res) => {
ArticleModel.find().byName('example')
})
Typescript complains about byName method when I chain it with defaults.
I can put it in IArticleModel interface but in that case I could only call it from model.
Where should I put the definition of this method to use it in chainable way?
I've drafted a new version of #types/mongoose that supports query helpers. See this answer for ways to install a modified #types package. With my version, you should be able to write the following in models/article.ts:
import { Document, Schema, Model, model, DocumentQuery } from 'mongoose';
interface IArticle extends Document {
name: string;
}
interface IArticleModel extends Model<IArticle, typeof articleQueryHelpers> {
someStaticMethod(): Promise<any>;
}
const ArticleSchema = new Schema( { name: String } )
let articleQueryHelpers = {
byName(this: DocumentQuery<any, IArticle>, name: string) {
return this.find({ name });
}
};
ArticleSchema.query = articleQueryHelpers;
export default model<IArticle, IArticleModel>('Article', ArticleSchema);
and then routes/article.ts will work. If this works for you, then I will submit a pull request to the original package on DefinitelyTyped.

Categories