Sequelize - do I have to specify "as" in both model and query? - javascript

This seems redundant, and forces me to hard-code the same string in two spots - or stick it in a variable that has to be passed around. Either way, if I specify the "as" of a relationship in my model, why do I have to call it later with the same "as" property when querying?
My relationship:
organization.hasMany(client, { as: "Clients", foreignKey: "organization_id" });
client.belongsTo(organization, { as: "AuthOrganization", foreignKey: "organization_id" });
Query:
let data = await client.findOne({
include: [{ model: organization, as: "AuthOrganization" }]
}, { raw: true });
If I omit the same "as" property, an error is thrown telling me to put it in there. I'm new to Sequelize, but it appears to be this way because "as" can be used to identify relationships where it's ambiguous. However, seems like a reasonable default would be the value you set in the model, no?
What I really want is this, when I write a query:
let data = await client.findOne({
include: organization
}, { raw: true });
I'm only doing this to avoid the automatic underscore in the mixin function names. I couldn't stomach the fugly "addAuth_organization" function name, and I couldn't find another way around this issue, either.

I'll take that as a "yes". 😆

Related

Sequelize Associations: How to update parent model when creating child?

It seems i have misunderstood sequelize .hasMany() and .belongsTo() associations and how to use them in service. I have two models:
const User = db.sequelize.define("user", {
uid: { /*...*/ },
createdQuestions: {
type: db.DataTypes.ARRAY(db.DataTypes.UUID),
unique: true,
allowNull: true,
},
});
const Question = db.sequelize.define("question", {
qid: { /*...*/ },
uid: {
type: db.DataTypes.TEXT,
},
});
Given that one user can have many questions and each question belongs to only one user I have the following associatons:
User.hasMany(Question, {
sourceKey: "createdQuestions",
foreignKey: "uid",
constraints: false,
});
Question.belongsTo(User, {
foreignKey: "uid",
targetKey: "createdQuestions",
constraints: false,
});
What I want to achieve is this: After creation of a question object, the qid should reside in the user object under "createdQuestions" - just as the uid resides in the question object under uid. What I thought sequelize associations would do for me is to save individual calling and updating the user object. Is there a corresponding method? What I have so far is:
const create_question = async (question_data) => {
const question = { /*... question body containing uid and so forth*/ };
return new Promise((resolve, rejected) => {
Question.sync({ alter: true }).then(
async () =>
await db.sequelize
.transaction(async (t) => {
const created_question = await Question.create(question, {
transaction: t,
});
})
.then(() => resolve())
.catch((e) => rejected(e))
);
});
};
This however only creates a question object but does not update the user. What am I missing here?
Modelling a One-to-many relationship in SQL
SQL vs NoSQL
In SQL, contrary to how it is in NoSQL, every attribute has a fixed data type with a fixed limit of bits. That's manifested by the SQL command when creating a new table:
CREATE TABLE teachers (
name VARCHAR(32),
department VARCHAR(64),
age INTEGER
);
The reason behind this is to allow us to easily access any attribute from the database by knowing the length of each row. In our case, each row will need the space needed to store:
32 bytes (name) + 64 bytes (department) + 4 bytes (age) = 100 byes
This is a very powerful feature in Relation Databases as it minimizes the time needed to retrieve data to Constant time since we knew where each piece of data is located in the memory.
One-to-Many Relationship: Case Study
Now, let's consider we have these 3 tables
Let's say we want to create a one-to-many relation between classes and teachers where a Teacher can give many classes.
We can think of it this way. But, this model is not possible for 2 main reasons:
It will make us lose our constant-time retrieval since we don't know the size of the list anymore
We fear that the amount of space given to the list attribute won't be enough for future data. Let's say we allocate space needed for 10 classes and we end up with a teacher giving 11 classes. This will push us to recreate our database to increase the column size.
Another way would be this:
While this approach will fix the limited column size problem, we no longer have a single source of truth. The same data is duplicated and stored multiple times.
That's why for this one-to-many relationship, we'll need to store the Id of the teacher inside this class table.
This way, we still can find all the classes a teacher can teach by running
SELECT *
FROM classes
WHERE teacherID = teacher_id
And we'll avoid all the problems discussed earlier.
Your relation is a oneToMany relation. One User can have multiple Questions. In SQL, this kind of relation is modelled by adding an attribute to Question called userId or Uid as you did. In Sequelize, this would be achieved through a hasMany or BelongsTo like this:
User.hasMany(Question)
Question.belongsTo(User, {
foreignKey: 'userId',
constraints: false
})
In other words, I don't think you need the CreatedQuestions attribute under User. Only one foreign key is needed to model the oneToMany relation.
Now, when creating a new question, you just need to add the userId this way
createNewQuestion = async (userId, title, body) => {
const question = await Question.create({
userId: userId, // or just userId
title: title, // or just title
body: body // or just body
})
return question
}
Remember, we do not store arrays in SQL. Even if we can find a way to do it, it is not what we need. There must be always a better way.

GraphQL Unions and Sequelize

I'm having trouble understanding how to retrieve information from a GraphQL Union. I have something in place like this:
const Profile = StudentProfile | TeacherProfile
Then in my resolver I have:
Profile: {
__resolveType(obj, context, info) {
if (obj.studentId) {
return 'StudentProfile'
} else if (obj.salaryGrade) {
return 'TeacherProfile'
}
},
},
This doesn't throw any errors, but when I run a query like this:
query {
listUsers {
id
firstName
lastName
email
password
profile {
__typename
... on StudentProfile {
studentId
}
... on TeacherProfile {
salaryGrade
}
}
}
}
This returns everything except for profile which just returns null. I'm using Sequelize to handle my database work, but my understanding of Unions was that it would simply look up the relevant type for the ID being queried and return the appropriate details in the query.
If I'm mistaken, how can I get this query to work?
edit:
My list user resolver:
const listUsers = async (root, { filter }, { models }) => {
const Op = Sequelize.Op
return models.User.findAll(
filter
? {
where: {
[Op.or]: [
{
email: filter,
},
{
firstName: filter,
},
{
lastName: filter,
},
],
},
}
: {},
)
}
User model relations (very simple and has no relation to profiles):
User.associate = function(models) {
User.belongsTo(models.UserType)
User.belongsTo(models.UserRole)
}
and my generic user resolvers:
User: {
async type(type) {
return type.getUserType()
},
async role(role) {
return role.getUserRole()
},
},
The easiest way to go about this is to utilize a single table (i.e. single table inheritance).
Create a table that includes columns for all the types. For example, it would include both student_id and salary_grade columns, even though these will be exposed as fields on separate types in your schema.
Add a "type" column that identifies each row's actual type. In practice, it's helpful to name this column __typename (more on that later).
Create a Sequelize model for your table. Again, this model will include all attributes, even if they don't apply to a specific type.
Define your GraphQL types and your interface/union type. You can provide a __resolveType method that returns the appropriate type name based on the "type" field you added. However, if you named this field __typename and populated it with the names of the GraphQL types you are exposing, you can actually skip this step!
You can use your model like normal, utilizing find methods to query your table or creating associations with it. For example, you might add a relationship like User.belongsTo(Profile) and then lazy load it: User.findAll({ include: [Profile] }).
The biggest drawback to this approach is you lose database- and model-level validation. Maybe salary_grade should never be null for a TeacherProfile but you cannot enforce this with a constraint or set the allowNull property for the attribute to false. At best, you can only rely on GraphQL's type system to enforce validation but this is not ideal.
You can take this a step further and create additional Sequelize models for each individual "type". These models would still point to the same table, but would only include attributes specific to the fields you're exposing for each type. This way, you could at least enforce "required" attributes at the model level. Then, for example, you use your Profile model for querying all profiles, but use the TeacherProfile when inserting or updating a teacher profile. This works pretty well, just be mindful that you cannot use the sync method when structuring your models like this -- you'll need to handle migrations manually. You shouldn't use sync in production anyway, so it's not a huge deal, but definitely something to be mindful of.

How do I make a self-referential foreign key in Sequelize?

Below is my model definition for the leases table. As you can see, I'm getting an error because I can't reference a key on a model I haven't defined yet.
Line 56 works fine to set the key's relationship because I have already created the model. But how do I access the model on line 40 before I've created the model?
Here's what getForeignKey() does:
getForeignKey(name: string, foreignModel: any): any {
return {
type: Sequelize.UUID,
references: {
'model': foreignModel,
'key': name,
'deferrable': Sequelize.Deferrable.INITIALLY_DEFERRED,
'allowNull': true
}
};
}
OK, removing my entire answer because you're already associating the model to itself. That's what result.belongsTo(result, { foreignKey: 'renewed_from__lease_id' }) is doing. Does that not fulfil your needs?

What does the context:'query' option do when using mongoose?

In a failed attempt learning exercise to get validators to work with 'document.update', I came across something I don't understand.
I know now that it doesn't work, but one of the things I tried was setting my options to {runValidators:true, context:'query'}. In my validator function, I tried console.logging (this), with and without the context:"query" option.
There was no difference. I received a large object (is this called the 'query object'?) This seems to go against what I read here.
In the color validation function above, this refers to the document being validated when using document validation. However, when running update validators, the document being updated may not be in the server's memory, so by default the value of this is not defined.
It was not undefined , even without the context option.
I even tried making it an arrow function to see if the lexical this was any different. In that case, this was undefined, but again, changing the context option did not make a difference. (I'm still learning, so I don't know if that part is relevant).
in the model:
let Property = mongoose.model('Property', {
name: {type:String, required:true},
occupancy: {type:String},
maxTenants: Number,
tenants: [{ type:mongoose.Schema.Types.ObjectId, ref: 'Tenant', validate: [checkMaxTenants, "Maximum tenants exceeded for this property. Tenant not added."]}]
});
function checkMaxTenants(val){
console.log("this",this);
// return this.tenants.length <= this.maxTenants;
return true;
}
and in the route:
property.update({$set: {tenants:property.tenants}},{new:true,runValidators:true,context:'query'}, function(err,savedProperty){
Anything to help me better understand the discrepancy between what I think I'm reading and what I see would be great!
At the outset, let's be clear that validators are of two types: document validators and update validators (maybe you know this already, but the snippet you posted updates a document, whereas the issue you mention relates to document validation upon save).
There was no difference. I received a large object (is this called the 'query object'?) This seems to go against what I read here.
Document validators are run when you run save on documents as mentioned in the docs.
Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default.
Or you can call it manually with .validate()
You can manually run validation using doc.validate(callback) or doc.validateSync()
Update validators are run for update operations
In the above examples, you learned about document validation. Mongoose also supports validation for update() and findOneAndUpdate() operations.
This can be illustrated with the following snippet. For convenience I have changed the type of tenants to a simple integer array, but that shouldn't matter for the purpose of our discussion.
// "use strict";
const mongoose = require('mongoose');
const assert = require('assert');
const Schema = mongoose.Schema;
let Property = mongoose.model('Property', {
name: { type: String, required: true },
occupancy: { type:String },
maxTenants: Number,
tenants: [
{
type: Number,
ref: 'Tenant',
validate: {
validator: checkMaxTenants,
message: "Maximum tenants exceeded for this property. Tenant not added."
}
}
]
});
function checkMaxTenants (val) {
console.log("this", this);
// return this.tenants.length <= this.maxTenants;
return true;
}
mongoose.Promise = global.Promise;
mongoose.createConnection('mongodb://localhost/myapp', {
useMongoClient: true,
}).then(function(db) {
const property = new Property({ name: 'foo', occupancy: 'bar', tenants: [1] });
property.update(
{ $set: { tenants: [2, 3] } },
{
new: true,
runValidators: true,
// context: 'query'
},
function(err, savedProperty) {
}
)
// property.save();
});
Above code with trigger a update validation not document validation
To see document validation in action uncomment property.save() and comment the update operation.
You'll notice that the value of this will be the property document.
this { name: 'foo',
occupancy: 'bar',
_id: 598e9d72992907120a99a367,
tenants: [ 1 ] }
Comment the save, uncomment back the update operation and you'll see the large object you mentioned.
Now the large object you got, you may not have realised, is the global object when you didn't set context: 'query' and the query object when you set the context.
This can be explained at this line in mongoose's source. When no context was set, mongoose sets the scope to null. And then here the .call is called with the scope.
Now, in non strict mode, when .call is called with null, this is replaced with the global object. So check contents of the large object you got. When context is not set, it would be a global object and not the query object. You can add "use strict"; and see that null will be logged. (The snippet posted can verify this for you). You can verify that you got a query object by running instanceof mongoose.Query against this.
Hope this helps you understand things better.

Does it have any sense using hasOne and belongsTo at the same time?

I have a scenario like the following:
Account.belongsTo(models.Address, {
as: 'address',
foreignKey: { name: 'addressId', field: 'address_id' },
onDelete: 'RESTRICT'
});
Address.hasOne(models.Account, {
as: 'account',
foreignKey: { name: 'addressId', field: 'address_id' },
onDelete: 'RESTRICT'
});
I don't get it if I should use them both or not. BelongsTo probably yes but it's necessary to use also hasOne?
As a general rule: Yes, you should define both.
From a practical perspective, you should define the association for a model Foo if you ever need to call the fooInstance.setBar and fooInstance.getBar methods on an instance. You should also define the association if you ever need to do Foo.find(..., { include: [Bar] });.
From a conceptual perspective, you should ask if it makes sense for the relationship to be bidirectional. If a Foo belongs to a Bar, does that mean that a Bar has one (or many) Foo's? Usually the answer to that question is going to be yes.
Doing both the definitions allows you to select the address of an account and the account of an address.
models.account.findAll({
include: [{
model: models.address
}]
});
models.address.findAll({
include: [{
model: models.account
}]
});

Categories