Mongoose Schema type as a custom interface from TypeScript? - node.js

I´d like to store a custom object that has the StationRating interface attributes, can somebody help me ?

It's possible, but it requires some boilerplate. Issues are:
Interfaces and types in TS do not exist in emitted code
While you could go in the other direction - create a schema object and make an interface/type out of it - schema object values must be constructors, eg Number, which is not the same thing as something with a number type.
But you can create a type that maps the constructor types to their primitive types (eg Number to number), then use that to turn the schema object into the type you want:
type ConstructorMapping<T> =
T extends NumberConstructor ? number :
T extends StringConstructor ? string : never; // etc: continue as needed
const schemaObj = {
score: Number,
user_id: String,
station_id: String,
description: String,
};
type SchemaObj = typeof schemaObj;
type StationRating = {
[prop in keyof SchemaObj]: ConstructorMapping<SchemaObj[prop]>
};
Then use schemaObj when calling new Schema, and you'll also have the following usable type:
type StationRating = {
score: number;
user_id: string;
station_id: string;
description: string;
}
Is it worth it? I'm not sure. For smaller objects, maybe not. For larger objects, maybe so. You might prefer just to write out the type and the schema object.

Is there a specific reason you want to store it in an object? You could use a virtual method to create the "Rating". I'm not exactly sure without seeing more, but if all you want to do is reference the score or create some calculations off it, then it seems like an easy enough solution.
https://mongoosejs.com/docs/tutorials/virtuals.html
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
});
// Create a virtual property `fullName` with a getter and setter.
userSchema.virtual('fullName').
get(function() { return `${this.firstName} ${this.lastName}`; }).
set(function(v) {
// `v` is the value being set, so use the value to set
// `firstName` and `lastName`.
const firstName = v.substring(0, v.indexOf(' '));
const lastName = v.substring(v.indexOf(' ') + 1);
this.set({ firstName, lastName });
});
const User = mongoose.model('User', userSchema);
const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';
doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'

Related

How to find out whether a nested document is an actual subschema or just an object in Mongoose

I have the following situation:
I am creating a general purpose update function for my project, which takes payload and goes through it, checking whether property in payload exists on model schema and if it does it assigns the new property value to document (updating it). It does this also in subdocuments (recursively).
I have a custom defined type Language for multi-language string fields, which is an object that contains properties in form of language ('en', 'de', etc). Now since its a custom type, Mongoose doesn't know if its contents were modified, so I have to use markModified on it. And here comes the problem: Actual subschemas behave here differently than objects. If I call markModified on subschema, it expects path within that subschema, not entire document. On the other hand, if I call markModified on an object, it expect entire path from parent. I don't know whether it is a bug or not, but if I want to support both, I need to differentiate between the two in my function. Is there a way to know whether it's a subschema made by user or just an object (that was converted to subschema by mongoose)?
Example setup model:
const TestSchema = new Schema(
{
object: {
name: {
type: Language
}
},
nestedSchema: {
type: NestedTestSchema
}
}
)
const NestedTestSchema = new Schema(
{
name: {
type: Language
}
}
)
Example code:
const testDocument = new TestModel({
object: {
name: {
en: 'NameEN',
de: 'NameDE'
}
}
nestedSchema: {
name: {
en: 'NameEN',
de: 'NameDE'
}
}
})
// We make a payload to change these values
const payload = {
object: { // Update object
name: {
en: 'Name updated',
fr: 'Something',
}
},
nestedSchema: { // Update subschema
name: {
en: 'Name updated',
fr: 'Something',
}
}
}
And now when I receive this and update the document with these values, for object I have to
const { object, nestedSchema } = document // This, of course, is useless here, I would get nestedSchema and object as argument in recursive function, its only for demonstration
nestedSchema.markModified('name.en') // Etc
and for object I have to
object.markModified('object.name.en') // Etc
Together with co-workers we found out that object is not an actual subschema, it's called a nestedPath. Only nestedPaths have property $__isNested, subschemas don't. As a result, because of this different handling of two cases, in nestedPaths we need to specify full path when using markModified while in subschemas only path within that subschema

Automatically manipulating argument for Mongoose Document constructor

Let's say I have have this model:
const employeeSchema = new Schema({
name: String,
age: Number,
employeeData: {
department: String,
position: String,
lastTraining: Date
}
});
const Employee = mongoose.model('employee', employeeSchema);
In the database, the only thing that is going to be saved is something that looks like this:
{
_id: ...
name: 'John Smith',
age: 40,
employeeCode: '.... '
}
What's going on is that by some business rules, the employeeData info, which is coming from the reqeust body, is going through some function that compiles out of it the employeeCode, and when saving to the database I just use to the employeeCode.
Right now, the way I am implementing this is using statics. So, I have in the model the follwing:
employeeSchema.statics.compileEmployeeCode = (doc) => {
if (!doc.employeeData) {
doc.employeeCode= compileCode(doc.employeeData);
delete doc.employeeData;
}
return doc;
}
And then, I need to remember, for each call that receives info from the client, to call this function before creating the document (an instance of the model):
const compiledDoc = Employee.compileEmployeeCode(req.body);
const employee = new Employee(comiledDoc);
My question is: is there a way to automatically invoke some function that compiles the code out of the data any time I create a document like that, so I won't need to remember to always call on the static method beforehand?
Middlaware is what you are looking for. You need to create a function that will set a pre-save hook on the schema (which will be triggered every time before saving a new document) and to plug this function into the schema.
function compileEmployeeCode (schema) {
schema.pre('save', next => {
if (this.employeeData) {
this.employeeCode= compileCode(this.employeeData);
delete this.employeeData;
next();
}
});
}
employeeSchema.plugin(compileEmployeeCode);
OK. It was really hard but I finally managed to find the solution. The trick is to use a setter on a specific path. Each field in the schema is of type SchemaType which can have a setter apply on it:
https://mongoosejs.com/docs/api.html#schematype_SchemaType-set
Anyway, if I want to make it possible for the request to enter an object that will be converted to some other format, say a string, I would need to define the schema like this:
const employeeSchema = new Schema({
name: String,
age: Number,
employeeCode: {
type: String,
set: setCodeFromObj,
alias: 'employeeData'
}
});
The setter function I'm using here looks something like this (I'm omitting here all the error handling and the like to keep this short:
function setCodeFromObj(v) {
const obj = {};
obj.department = v.department;
obj.position = v.position;
obj.lastTraining = v.lastTraing
// breaking the object to properties just to show that v actually includes them
return compileEmployeeCode(obj);
}
I used an alias to make the name visible to the user different from what is actually saved in the database. I could have also done that using virtuals or just design the system a bit differently to use up the same name.

NodeJs GraphQL enum type value as dynamic

https://launchpad.graphql.com/9qvqz3v5r
Here is my example graphQL Schema. i am trying to use enum type. How do i get enum values from backend and give it into schema?
// Construct a schema, using GraphQL schema language
const typeDefs = `
type User {
userId: Int
firstName: String
lastName: String
pincode:String
state:String
country:String
}
type Query {
hello: String
user: User
}
type CreateUserLoad {
user: User
}
enum Role {
writer
reader
author
admin
superAdmin
}
type Mutation{
createUser(firstName: String, lastName: String, role: Role): User
}
`;
I want to populate enum Role value from dynamic variable as
const roleData = ['writer','reader','author','admin','superAdmin'];
Can anyone help me?
You can simply use string interpolation:
// Construct a schema, using GraphQL schema language
const typeDefs = `
type User {
userId: Int
firstName: String
lastName: String
pincode:String
state:String
country:String
}
type Query {
hello: String
user: User
}
type CreateUserLoad {
user: User
}
enum Role { ${roles.join(' ')} }
type Mutation{
createUser(firstName: String, lastName: String, role: Role): User
}
`;
In fact, on every incoming grahpql query you have to pass the parsed schema to the graphql server, so you can even change it for every request. In that case, it would be better to change the object representation that the schema parsing returned.
For creating enum types directly, say you have an array of values userRoles and want a RolesEnum type, then you can create it like so:
const roleValues = {}
for (const value of userRoles) {
roleValues[value] = {value}
}
const RolesEnum = new GraphQLEnumType({
name: 'UserRoles',
values: roleValues,
})
you can then assign that directly as a type in your schema.
If your enum values are loaded from a database or any other back-end source, or if the enum list is dynamic, then you can't have a static enum definition in your schema.
The biggest problem with setting enums dynamically is that your schema is meant to be a contract between back-end and front-end and is not suppose to be altered.
If you are going to use your role as an arbitrary string then define it as such!
type Mutation {
createUser(firstName: String, lastName: String, role: String): User
}
The only difference here would be that your resolver will have to check if the role exists, which is what graphql previously did for you when you were using an enum.
Alternatively
If the role is used on a lot of queries / mutations, you can define a scalar Role and in the resolved for the scalar you can check if the role exists and simply throw an error if it doesn't.
In this case your mutation would look the same as if you had a dynamic enum Role, but you will not need to alter the schema at all.
type Mutation {
createUser(firstName: String, lastName: String, role: Role): User
}
To add dynamic enums along with documentation string using string interpolation.
Example: I have a list of countries and their ISO2 Codes
const countryData = [
{name:'India', code: 'IN'},
{name:'Afghanistan', code: 'AF'},
{name:'Algeria', code: 'DZ'},
{name: 'Ireland', code: 'IE'
];
const countryCodes = countryData.flatMap(country => [
`"${country.name}"`,
country.code
]);
Without using Array.join()
enum CountryCode { ${countryCodes} }

Using TypeScript enum with mongoose schema

I have a schema with an enum:
export interface IGameMapModel extends IGameMap, Document {}
export const gameMapSchema: Schema = new Schema({
name: { type: String, index: { unique: true }, required: true },
type: { type: String, enum: CUtility.enumToArray(GameMode) }
});
export const GameMap: Model<IGameMapModel> = model<IGameMapModel>('GameMap', gameMapSchema);
The GameMap is an enum.
First problem is already in here: I need to convert the enum to a string array in order to use it with the schema.
Secondly, I wanna use an enum value directly during the schema creation.
new GameMap({
name: 'Test',
type: GameMode.ASSAULT
});
returns ValidationError: type: '1' is not a valid enum value for path 'type'.
I am not sure whether this can actually work due to the string array I set in the model enum property.
My idea would be to create some kind of type conversion during the schema creation. Does this work with mongoose or would I have to create some kind of helper for object creation?
GameMode.ASSAULT is evaluating as it's numeric value, but GameMode is expecting the type to be a string. What are you expecting the string evaluation to be? If you need the string value of the enum, you can access it with GameMode[GameMode.ASSAULT], which would return ASSAULT as a string.
For example:
enum TEST {
test1 = 1,
test2 = 2
}
console.log(TEST[TEST.test1]);
//Prints "test1"
From the Mongoose docs on validation, in schema properties with a type of String that have enum validation, the enum that mongoose expects in an array of strings.
This means that CUtility.enumToArray(GameMode) needs to either return to you an array of the indexes as strings, or an array of the text/string values of the enum--whichever you are expecting to store in your DB.
The validation error seems to imply that 1 is not contained within the array that is being produced by CUtility.enumToArray(GameMode), or the validation is seeing GameMode.ASSAULT as a number when it is expected a string representation of 1. You might have to convert the enum value you are passing in into a string.
What is the output of CUtility.enumToArray(GameMode)? That should help you determine which of the two is your problem.
Why don't you just create custom getter/setter:
const schema = new Schema ({
enumProp: {
type: Schema.Types.String,
enum: enumKeys(EnumType),
get: (enumValue: string) => EnumType[enumValue as keyof typeof EnumType],
set: (enumValue: EnumType) => EnumType[enumValue],
},
});
EDIT:
Don't forget to explicitly enable the getter
schema.set('toJSON', { getters: true });
// and/or
schema.set('toObject', { getters: true });
This way you can fine-control how exactly you want to represent the prop in the db, backend and frontend (json response).

Handling Mongoose Populated Fields in GraphQL

How do I represent a field that could be either a simple ObjectId string or a populated Object Entity?
I have a Mongoose Schema that represents a 'Device type' as follows
// assetSchema.js
import * as mongoose from 'mongoose'
const Schema = mongoose.Schema;
var Asset = new Schema({ name : String,
linked_device: { type: Schema.Types.ObjectId,
ref: 'Asset'})
export AssetSchema = mongoose.model('Asset', Asset);
I am trying to model this as a GraphQLObjectType but I am stumped on how to allow the linked_ue field take on two types of values, one being an ObjectId and the other being a full Asset Object (when it is populated)
// graphql-asset-type.js
import { GraphQLObjectType, GraphQLString } from 'graphql'
export var GQAssetType = new GraphQLObjectType({
name: 'Asset',
fields: () => ({
name: GraphQLString,
linked_device: ____________ // stumped by this
});
I have looked into Union Types but the issue is that a Union Type expects fields to be stipulated as part of its definition, whereas in the case of the above, there are no fields beneath the linked_device field when linked_device corresponds to a simple ObjectId.
Any ideas?
As a matter of fact, you can use union or interface type for linked_device field.
Using union type, you can implement GQAssetType as follows:
// graphql-asset-type.js
import { GraphQLObjectType, GraphQLString, GraphQLUnionType } from 'graphql'
var LinkedDeviceType = new GraphQLUnionType({
name: 'Linked Device',
types: [ ObjectIdType, GQAssetType ],
resolveType(value) {
if (value instanceof ObjectId) {
return ObjectIdType;
}
if (value instanceof Asset) {
return GQAssetType;
}
}
});
export var GQAssetType = new GraphQLObjectType({
name: 'Asset',
fields: () => ({
name: { type: GraphQLString },
linked_device: { type: LinkedDeviceType },
})
});
Check out this excellent article on GraphQL union and interface.
I was trying to solve the general problem of pulling relational data when I came across this article. To be clear, the original question appears to be how to dynamically resolve data when the field may contain either the ObjectId or the Object, however I don't believe it's good design in the first place to have a field store either object or objectId. Accordingly, I was interested in solving the simplified scenario where I keep the fields separated -- one for the Id, and the other for the object. I also, thought employing Unions was overly complex unless you actually have another scenario like those described in the docs referenced above. I figured the solution below may interest others also...
Note: I'm using graphql-tools so my types are written schema language syntax. So, if you have a User Type that has fields like this:
type User {
_id: ID
firstName: String
lastName: String
companyId: ID
company: Company
}
Then in my user resolver functions code, I add this:
User: { // <-- this refers to the User Type in Graphql
company(u) { // <-- this refers to the company field
return User.findOne({ _id: u.companyId }); // <-- mongoose User type
},
}
The above works alongside the User resolver functions already in place, and allow you write GQL queries like this:
query getUserById($_id:ID!)
{ getUserById(_id:$_id) {
_id
firstName
lastName
company {
name
}
companyId
}}
Regards,
S. Arora

Resources