Models ref each other error: circular dependencies problem - node.js

admin.model.ts
import mongoose, { Schema, Document } from 'mongoose';
import UserRole, { IUserRole } from './user-role.model';
export interface IAdmin extends Document {
role: IUserRole;
}
let adminSchema = new Schema<IAdmin>({
role: {
type: Schema.Types.ObjectId, ref: UserRole
}
});
export default mongoose.model<IAdmin>('Admin', adminSchema);
user-role.model.ts
import { Schema, Document, model } from 'mongoose';
export interface IUserRole extends Document{
updated_by: IAdmin|string;
}
let userRoleSchema = new Schema<IUserRole>({
updated_by: {
type: Schema.Types.ObjectId, ref: Admin
}
})
export default model<IUserRole>('UserRole', userRoleSchema);
MongooseError: Invalid ref at path "updated_by". Got undefined
at validateRef (/home/ess24/ess-smartlotto/node-rest/node_modules/mongoose/lib/helpers/populate/validateRef.js:17:9)
at Schema.path (/home/ess24/ess-smartlotto/node-rest/node_modules/mongoose/lib/schema.js:655:5)
at Schema.add (/home/ess24/ess-smartlotto/node-rest/node_modules/mongoose/lib/schema.js:535:14)
at require (internal/modules/cjs/helpers.js:88:18)
[ERROR] 22:25:54 MongooseError: Invalid ref at path "updated_by". Got undefined
Here is my two model how can I solve this type of problem and how to deal with circular dependencies?

You have to put the ref values for UserRole and Admin in '' like this:
const adminSchema = new Schema<IAdmin>({
role: {
type: Schema.Types.ObjectId, ref: 'UserRole' // Name of the model you are referencing
}
});
let userRoleSchema = new Schema<IUserRole>({
updated_by: {
type: Schema.Types.ObjectId, ref: 'Admin' // <- and here
}
})

While the above answer by #gabriel-lupu is correct, in newer versions it seems another fix is recommended.
Fix:
adding quotes: ref: 'UserRole' // Recommended first fix
adding an arrow function: ref: () => UserRole
Typegoose documentation says the following:
Options ref and type can both also be defined without () =>, but is generally recommended to be used with.
If () => is not used, there can be problems when the class (/ variable) is defined after the decorator that requires it. Section in documentation for reference.
Because of the order classes are loaded and reordered at runtime, this might result in some references being null / undefined / not existing. This is why Typegoose provides the following:
class Nested {
#prop()
public someNestedProperty: string;
}
// Recommended first fix:
class Main {
#prop({ ref: () => Nested }) // since 7.1 arrow functions can be used to
defer getting the type
public nested: Ref<Nested>;
}
// Not recommended workaround (hardcoding model name):
class Main {
#prop({ ref: 'Nested' }) // since 7.0 it is recommended to use "console.log(getName(Class))" to get the generated name once and hardcode it like shown here
public nested: Ref<Nested>;
}
Section of documentation with the above example
Explanation:
The MongooseError: Invalid ref at path "updated_by". Got undefined error seems to be due to a circular dependency.
IAdmin has a role property that references IUserRole.
IUserRole has an updated_by property that references IAdmin.
When admin.model.ts loads it imports user-role.model.ts
user-role.model.ts needs IAdmin for the updated_by property from admin.model.ts
admin.model.ts hasn't finished loading yet resulting in updated_by which references it being undefined.
Relevant explanation from Mozilla about how modules deal with circular dependencies.
In a cyclic dependency, you end up having a loop in the graph.
Usually, this is a long loop. But to explain the problem, I’m going to
use a contrived example with a short loop.
A complex module graph with a 4 module cycle on the left. A simple 2
module cycle on the right.
Let’s look at how this would work with CommonJS modules. First, the
main module would execute up to the require statement. Then it would
go to load the counter module.
A commonJS module, with a variable being exported from main.js after a
require statement to counter.js, which depends on that import
The counter module would then try to access message from the export
object. But since this hasn’t been evaluated in the main module yet,
this will return undefined. The JS engine will allocate space in
memory for the local variable and set the value to undefined.
The message variable will be initialized and added to memory. But
since there’s no connection between the two, it will stay undefined in
the required module.
If the export were handled using live bindings, the counter module would see the correct value eventually. By the time the timeout runs, main.js’s evaluation would have completed and filled in the value.
how module imports work
Both? fixes seem to create a live binding.
Closures can close over imported values as well, which are regarded as live bindings, because when the original value changes, the imported one changes accordingly.
// closureCreator.js
import { x } from "./myModule.js";
export const getX = () => x; // Close over an imported live binding

Related

How to sanitize response body in Node.js and Typescript

I have a backend server written in typescript on node.js using nest.js as framework.
I want to automatically sanitize the response body of my controllers removing undeclared properties of my DTOs.
I have the following files:
class User {
_id: string
name: string
hashedPassword: string
}
class UserDTO {
_id: string
name: string
}
#ApiTags('users')
#Controller('users')
export class UsersController {
...
#Get(':id')
#UseGuards(JwtAuthGuard)
#ApiBearerAuth()
#ApiOperation({ summary: 'Find one user by id' })
#ApiResponse({
status: 200,
type: UserDto,
})
async findOne(#Param('id') id: string): Promise<UserDto> {
return await this.usersService.findById(id) as UserDto;
}
}
When I declare user as UserDTO I thought that will remove all undeclared properties as UserDTO (in this case the 'hashedPassword') that's not what happened, it still sending the 'hashedPassword', I want to remove it automatically without calling constructores or removing it manually using const { hashedPassword, ...result } = user; return result; on e each service.
It's a pain do this conversions every time I have to send a DTO. I'm looking for something that do this conversions automatically. Does exists something in typescript or in Nest.js that do this conversions automatically for me? Maybe some decorator in the DTO or calling some middleware?
In older projects I used to do this to automatically remove unwanted properties:
const UserSchema = new Schema({
hashedPassword: String,
...
}, {
timestamps: true,
toJSON: {
transform: (doc, ret, options) => {
delete ret.hashedPassword;
return ret;
},
virtuals: false,
}
});
Today I consider this a bad implementation because I adding business logic to my repository. I'm looking for something that sanitize my DTOs automatically. Is that possible?
Sounds like you might be looking for Serialization, which Nest has a setup with an interceptor and class-transformer calling classToPlain() to serialize the response. It sounds like you might be working with Mongo, so keep in mind you may need to do a bit extra in your services to get a true class instance that class-transformer can work with.
As for why your above attempt didn't work, as mentioned by jonrsharpe in the comments, type information doesn't exist at runtime. It's purely there for devs during development and compile time to help us catch bugs that could be easily avoided. Also, to take it a step further x as y tells the compiler "I know that x is of type y, whether you can read that from the code I've written or not". It's essentially telling the compiler you know more than it, so it shouldn't worry.

Typescript compiler error about incompatble types

I'm working on an API written in Typescript 3.9.7 running on Node 10. I've removed unnecessary details, but I'm basically performing the following operations:
Pulling user data from the database.
Adding a 'state' field to each user object
Sending the data to the UI.
I'm trying to use interfaces to add some type safety, but I seem to be misusing them since the TS compiler gives me some errors. Advice on how to resolve this would be helpful.
I've trimmed out other details, but here's my method where I fetch the user data and add the state field:
public async getUsers(
parameter: string
): Promise<AugmentedUser[]> {
//return an array of User objects based on some parameter
const userData = await this.userService.getAll<User>(parameter);
return userData.forEach((userRow: User) => {
userRow.state = "inactive";
});
}
and my interfaces are
export interface User {
id: number;
firstName: string;
//some other fields
}
export interface AugmentedUser extends User {
state: string;
}
I get the following error from the compiler:
error TS2322: Type 'UserData[]' is not assignable to type 'AugmentedUser[]'. Property 'state' is missing in type 'User' but required in type 'AugmentedUser'.
What am I doing wrong? I added the state field in the forEach loop so why am I getting this error? Thanks.
forEach does not return anything. Try the map function:
return userData.map((userRow: User) => {
return {...userRow, state: 'inactive'};
});
This will generate a list of objects with all the User properties plus the state present in AugmentedUser.

Typegoose enum arrayProp returns an error: Type is not a constructor

I have got a problem with the definition of my array schema. So basically, what I wanted to achieve is a single user Model with a property called games, that holds an array of games user is playing. The problem is I have got defined games as enum:
module Constants {
export enum Games {
LOL = 'League Of Legends',
}
}
export {Constants};
And now, when I try to attach it to the schema model like that:
#arrayProp({ required: true, items: Constants.Games })
games: Constants.Games[];
I receive an error (after a successful compilation, just before the server start)
^ const instance = new Type();
TypeError: Type is not a constructor
at baseProp (C:\Users\Borys\Desktop\lft\backend\node_modules\typegoose\lib\prop.js:114:22)
at C:\Users\Borys\Desktop\lft\backend\node_modules\typegoose\lib\prop.js:177:9
at DecorateProperty (C:\Users\Borys\Desktop\lft\backend\node_modules\reflect-metadata\Reflect.js:553:33)
at Object.decorate (C:\Users\Borys\Desktop\lft\backend\node_modules\reflect-metadata\Reflect.js:123:24)
at __decorate (C:\Users\Borys\Desktop\lft\backend\build\dataModel\User.js:4:92)
at Object.<anonymous> (C:\Users\Borys\Desktop\lft\backend\build\dataModel\User.js:64:1)
I have read a little bit about this error, but it relates to the required option for items/itemsRef I tried removing required, using enum, using even itemsRef and relating to the different set of documents but none of these worked for me.
Anyone could help/relate?
The problem is, you cannot use enums as an runtime mongoose type, so i would recommend using
#prop({ required: true, type: String, enum: Constants.Games })
games: Constants.Games[];
type for setting the type (which is string) (this option can be omitted - thanks to reflection)
enum for setting an enum to validate against
#arrayProp({ required: true, items: String })
games: Constants.Games[];
is a solution to this problem.
I would appreciate it if anyone could clarify and tell me more about why shouldn't I use enum in the items property.

What is a Mongoose model property that contains 'ref' but does not specify type?

Very new to Mongoose -- I'm working on an existing project and have been given the task of changing some model properties. I understand that if a model contains a property of this type
postedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
this property references another model/schema, and to get access to that linked model/schema one needs to populate it to gain access to this property.
But in the code I'm reviewing (which I didn't write) there are many properties of this type
contentTypes: [{ ref: 'ContentType' }],
source: { ref: 'Source',required: true },
where another schema is referenced, but there is no type. Is this the same sort of relationship, and the id is implied? Is this a subdocument?
As an additional question: if in a model I wanted to refer to a property of a linked model (or schema), would I need to populate first? That is, if it's a subdocument, I can just use dot notation, but if it is a "linked" document, I'm not sure.
The answer was that the model schemas do not stand on their own, but are passed to a model "factory", which gives them the property types they need.
Thus from that factory the following snippet (below). I looked into the documentation for mongoose-autopopulateand I don't see what autopopulate=truemeans.
new: function(name, properties, statics, methods, schemaMods) {
// Add default definition to properties with references and load reference schemas
Object.keys(properties).forEach(function(key) {
var modifiedProperty = (property) => {
if (property.ref) {
property.autopopulate = true;
property.type = mongoose.Schema.Types.ObjectId;
}
return property;
};
if (Array.isArray(properties[key]) && properties[key].length === 1) {
properties[key][0] = modifiedProperty(properties[key][0]);
} else {
properties[key] = modifiedProperty(properties[key]);
}
});

Static methods with typescript & mongoose does: "An interface may only extend a class or another interface."

I try to add a static method to my Model, but if I do it, I got this Error: An interface may only extend a class or another interface.
This is my code:
import * as mongoose from 'mongoose';
import {IPermission} from './IPermission';
export interface IRoleDocument extends mongoose.Document {
name: string,
inherit_from: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Role'
},
permissions: Array<IPermission>
};
export interface IRole extends mongoose.Model<IRoleDocument> {
};
Error comes from export interface IRole extends mongoose.Model<IRoleDocument>
Greetz
As far I I know it is impossible to inherit from intersection/union types in typescript. And in case of mongoose type definitions mongoose.Model<T> is declared as intersection type:
type ModelConstructor<T> = IModelConstructor<T> & events.EventEmitter;
For examples of how to use mongoose in typescript you can check this topic on SA
But you still can use intersection instead of inheritance to get your required interface, like this:
interface IRoleDefinition
{
myExtraProperty: string;
}
type IRole = mongoose.Model<IRoleDocument> & IRoleDefinition;
More info about intersection types vs inheritance: github

Resources