I need to select users with dogs (pets with type equal 'dog')
var User = Waterline.Collection.extend({
identity: 'user',
attributes: {
firstName: 'string',
lastName: 'string',
pets: {
collection: 'pet',
via: 'owner'
}
}
});
var Pet = Waterline.Collection.extend({
identity: 'pet',
attributes: {
type: 'string',
name: 'string',
owner: {
model: 'user'
}
}
});
I didn't find any examples, I tried like this
User.find().populate('pets', {type: 'dog'}).exec(err, users) ...
and this
User.find().where({'pets.type': 'dog'}).populate('pets').exec(err, users) ...
but that does not work
Would be greate if result users array will has no pets records
Did you try this?
User.find().populate('pets', {
where: {
type: 'dog'
}
}).exec(err, users)...
If you don't need to query users and just need the query for dogs. You could just as easily reverse the query.
Pet.find({type: 'dog'}).populate('users').exec(err, petsWithUsers)
What you are looking for hasn't been implemented in waterline (Sails ORM) yet, check issue #266 for more details.
User.find().populate('pets', {type: 'dog'}).exec(err, users) ...
This will return all users (User.find()) and only populate pets of type dog (populate('pets', {type: 'dog'})). So you'll have users without dogs in your results.
User.find().where({'pets.type': 'dog'}).populate('pets').exec(err, users) ...
Waterline does not support dot (.) notation. Sails-mongo does have some support for it due to MongoDB support.
Finally, if you are using one of the SQL adapters you may work around this by doing a raw sql query using .query() (docs).
Related
So I'm building a backend with NestJs and Typegoose, having the following models:
DEPARTMENT
#modelOptions({ schemaOptions: { collection: 'user_department', toJSON: { virtuals: true }, toObject: { virtuals: true }, id: false } })
export class Department {
#prop({ required: true })
_id: mongoose.Types.ObjectId;
#prop({ required: true })
name: string;
#prop({ ref: () => User, type: String })
public supervisors: Ref<User>[];
members: User[];
static paginate: PaginateMethod<Department>;
}
USER
#modelOptions({ schemaOptions: { collection: 'user' } })
export class User {
#prop({ required: true, type: String })
_id: string;
#prop({ required: true })
userName: string;
#prop({ required: true })
firstName: string;
#prop({ required: true })
lastName: string;
[...]
#prop({ ref: () => Department, default: [] })
memberOfDepartments?: Ref<Department>[];
static paginate: PaginateMethod<User>;
}
As you might guess, one user might be in many departments and one department can have many members(users). As the count of departments is more or less limited (compared with users), I decided to use one way embedding like described here: Two Way Embedding vs. One Way Embedding in MongoDB (Many-To-Many). That's the reason User holds the array "memberOfDepartments", but Department does not save a Member-array (as the #prop is missing).
The first question is, when I request the Department-object, how can I query members of it? The query must look for users where the department._id is in the array memberOfDepartments.
I tried multiple stuff here, like virtual populate: https://typegoose.github.io/typegoose/docs/api/virtuals/#virtual-populate like this on department.model:
#prop({
ref: () => User,
foreignField: 'memberOfDepartments',
localField: '_id', // compare this to the foreign document's value defined in "foreignField"
justOne: false
})
public members: Ref<User>[];
But it won't output that property. My guess is, that this only works for one-to-many on the one site... I also tried with set/get but I have trouble using the UserModel inside DepartmentModel.
Currently I'm "cheating" by doing this in the service:
async findDepartmentById(id: string): Promise<Department> {
const res = await this.departmentModel
.findById(id)
.populate({ path: 'supervisors', model: User })
.lean()
.exec();
res.members = await this.userModel.find({ memberOfDepartments: res._id })
.lean()
.exec()
if (!res) {
throw new HttpException(
'No Department with the id=' + id + ' found.',
HttpStatus.NOT_FOUND,
);
}
return res;
}
.. but I think this is not the proper solution to this, as my guess is it belongs in the model.
The second question is, how would I handle a delete of a department resulting in that i have to delete the references to that dep. in the user?
I know that there is documentation for mongodb and mongoose out there, but I just could not get my head arround how this would be done "the typegoose way", since the typegoose docs seem very limited to me. Any hints appreciated.
So, this was not easy to find out, hope this answer helps others. I still think there is the need to document more of the basic stuff - like deleting the references to an object when the object gets deleted. Like, anyone with references will need this, yet not in any documentation (typegoose, mongoose, mongodb) is given a complete example.
Answer 1:
#prop({
ref: () => User,
foreignField: 'memberOfDepartments',
localField: '_id', // compare this to the foreign document's value defined in "foreignField"
justOne: false
})
public members: Ref<User>[];
This is, as it is in the question, the correct way to define the virtual. But what I did wrong and I think is not so obvious: I had to call
.populate({ path: 'members', model: User })
explicitly as in
const res = await this.departmentModel
.findById(id)
.populate({ path: 'supervisors', model: User })
.populate({ path: 'members', model: User })
.lean()
.exec();
If you don't do this, you won't see the property members at all. I had problems with this because, if you do it on a reference field like supervisors, you get at least an array ob objectIds. But if you don't pupulate the virtuals, you get no members-field back at all.
Answer 2:
My research lead me to the conclusion that the best solution tho this is to use a pre-hook. Basically you can define a function, that gets called before (if you want after, use a post-hook) a specific operation gets executed. In my case, the operation is "delete", because I want to delete the references before i want to delete the document itself.
You can define a pre-hook in typegoose with this decorator, just put it in front of your model:
#pre<Department>('deleteOne', function (next) {
const depId = this.getFilter()["_id"];
getModelForClass(User).updateMany(
{ 'timeTrackingProfile.memberOfDepartments': depId },
{ $pull: { 'timeTrackingProfile.memberOfDepartments': depId } },
{ multi: true }
).exec();
next();
})
export class Department {
[...]
}
A lot of soultions found in my research used "remove", that gets called when you call f.e. departmentmodel.remove(). Do not use this, as remove() is deprecated. Use "deleteOne()" instead. With "const depId = this.getFilter()["_id"];" you are able to access the id of the document thats going to be deletet within the operation.
I'm trying to use one way association because I need only to have reference from 1 model to other model but not vice versa.
Model Arts:
module.exports = {
attributes: {
fileName: {type: 'string', required: true},
softwareUsed: {
model: 'Softwares'
}
}
}
Model Softwares:
module.exports = {
attributes: {
name: {type: 'string', required: true}
}
}
This is my api:
http://localhost:1337/api/v1/arts/create
if this is my request body, it works fine:
request body:
{
"fileName": "booking.jpeg",
"softwareUsed": "5e70309cbf12b61299d6c528",
}
but i want to store array of softwareUsed, so i tried:
request body:
{
"fileName": "booking.jpeg",
"softwareUsed": ["5e70309cbf12b61299d6c528", "5e70309cbf12b61299d6c529"],
}
but i got an error with that:
error: OperationalError [UsageError]: Invalid new record.
Details:
Could not use specified `softwareUsed`. Expecting an id representing the associated record, or `null` to indicate there will be no associated record. But the specified value is not a valid `softwareUsed`. Instead of a string (the expected pk type), the provided value is: [ '5e70309cbf12b61299d6c528', '5e70309cbf12b61299d6c529' ]
I also tried to make it array in model:
softwareUsed: [{
model: 'Softwares'
}]
but still don't work.
Is there a way to that in one way association or I need to use other association, but how can I achieve that?
Thank you.
I think you need to label the softwareUsed attribute with a collection, not a model:
module.exports = {
attributes: {
fileName: {type: 'string', required: true},
softwareUsed: {
collection: 'Softwares'
}
}
}
All the documentation on one-to-many in the sails docs involves two-way associations and adding a via attribute, but I think this way works for a one-way association.
Of course, your first api call may now longer work: you may need to wrap the single software id in an array.
Here's relevant Apollo code. (Many database fields have been omitted to make it faster/easier to read this post.)
CONNECTORS
const userAccountsDataModel = db.define('userAccountsData', {
id: {type: Sequelize.STRING, primaryKey: true},
picture_medium: {type: Sequelize.STRING}
});
const associatesDataModel = db.define('associatesData', {
_id: {type: Sequelize.STRING, primaryKey: true},
first_name: {type: Sequelize.STRING},
last_name: {type: Sequelize.STRING},
userID: {type: Sequelize.STRING},
});
associatesDataModel.belongsTo(userAccountsDataModel, {foreignKey: 'userID'});
SCHEMA
type userAccountsData {
id: String
picture_medium: String
}
type associatesData {
_id: String
first_name: String
last_name: String
userID: String
userAccountsData: [userAccountsData]
}
QUERY
const GETONEASSOCIATE_QUERY = gql`
query getOneAssociate($_id: String!) {
getOneAssociate(_id: $_id) {
_id
first_name
last_name
userID
accountsData{
id
picture_medium
}
}
} `;
RESOLVER
getOneAssociate(_, args) {
console.log('in getOneAssociate');
var ret = connectors.associatesData.findAll({
where: args,
include: [connectors.userAccountsData],
logging: console.log
}).then((res) => res.map((item) => item.dataValues));
return ret;
},
Via Sequelize, the resolver generates the following SQL:
SELECT "associatesData"."_id",
"associatesData"."first_name",
"associatesData"."last_name",
"associatesData"."userID",
"userAccountsDatum"."id" AS "userAccountsDatum.id",
"userAccountsDatum"."picture_medium" AS "userAccountsDatum.picture_medium"
FROM "associatesDatas" AS "associatesData"
LEFT OUTER JOIN "userAccountsData" AS "userAccountsDatum"
ON "associatesData"."userID" = "userAccountsDatum"."id"
WHERE "associatesData"."_id" = '35';
When I run the above SQL in my SQL client, I can see that all the data sought is being returned, including the data from the related "userAccountsData" table.
However, when the data gets to my client code running in javascript the browser, the data from the related table is missing-- the object that should contain it is null.
What am I missing?
Thanks in advance to all for any info!
This turned out to be an anomaly with Sequelize's automatic pluralizing of table names.
By default, Sequelize automatically pluralizes table names. It is supposed to be possible to turn this behavior off by using Sequelize's freezeTableName property. However, per this Sequelize issue post, there are cases in which Sequelize ignores the freezeTableName property and pluralizes (and sometimes singularizes) the table names anyway.
After discovering this I was able to resolve the anomaly by pluralizing the relevant table name and going through all my Apollo code to update the references to this table so that they would be pluralized as well.
I would like to find the parent table information of an object.
I have User hasMany Book
where Book has writer and assigned to user id.
Book has type, which is like fantasy, romance, history, scientific fiction... etc
So I want to find out the Book with type Scientific Fiction but not only for that, I also want the writer, which is User.
How can I find the book with its writer where where condition is given for books only? It seems like 'include' in Book.findAll( include: User) is not working; this tells me that include is only working for finding child tables not parent.
Here are some code for user
const User = sequelize.define('User', {
id: { type: DataTypes.STRING(6), field: 'ID', primaryKey : true }
}
associate: function(models) {
User.hasMany(models.Book, { foreignKey: 'userId' });
}
and book
const Book = sequelize.define('Book', {
id: { type: DataTypes.STRING(6), field: 'ID', primaryKey: true }, // primary key
userId: { type: DataTypes.STRING(6), field: 'USER_ID', primaryKey: true }
type: { type: DataTypes.STRING(20), field: 'TYPE'
}
Book has some more child table and I try to find those additional information in includes, so I guess I really need to find from Book.findAll(...)
Instead of User.findAll(include: Book).
Can anyone help?
I think I was making a mistake.
As soon as I changed
userId: { type: DataTypes.STRING(6), field: 'USER_ID', primaryKey: true }
to
userId: { type: DataTypes.STRING(6), field: 'USER_ID' }
include is working for belongsTo; and it finds the parent from child.
It seems like Sequelize has some problem with relation if there is more than one primary key declared in model...
If anyone does think this solves your problem, please share in reply so I can be more sure about it.
Thanks.
Hy there,
Before going to the hacky / cutom way i wanted to know if there is a built in query way to check for an empty / non empty many to many relationship as i was not successfull neither on google nor the doc.
If i take the example in the doc let's imagine i want to retrive a user only if he has a a Pet or Retrive a Pet without any Owner through a query.
// A user may have many pets
var User = Waterline.Collection.extend({
identity: 'user',
connection: 'local-postgresql',
attributes: {
firstName: 'string',
lastName: 'string',
// Add a reference to Pet
pets: {
collection: 'pet',
via: 'owners',
dominant: true
}
}
});
// A pet may have many owners
var Pet = Waterline.Collection.extend({
identity: 'pet',
connection: 'local-postgresql',
attributes: {
breed: 'string',
type: 'string',
name: 'string',
// Add a reference to User
owners: {
collection: 'user',
via: 'pets'
}
}
});
P.s. i know how to filter results after query execution that's not what i'm asking :)
There is nothing built in (aka User.hasPet() ) or something like that., so the simple answer is NO
If I know of these issues before hand I tend to write my DB in such a way that the queries are fast. IE: the User schema would have a hasPets column. Whenever a pet is added/removed a callbacks hits the user table to mark that field if it has an owner or not. So then I can query User.findAll({hasPet:true}).
Its a little much, but it depends on where you speed is needed.
This is a bit late, but I wanted to let you know it's pretty easy to do this with the Waterline ORM lifecycle functions. I've done it in a few of my projects. Basically, use the beforeCreate and beforeUpdate functions to set your flags. For your user, it might look like...
var User = Waterline.Collection.extend({
identity: 'user',
connection: 'local-postgresql',
beforeCreate: function(values, next) {
if (values.pets) {
values.has_pet = true;
} else {
values.has_pet = false;
}
next();
}
beforeUpdate: function(values, next) {
if (values.pets) {
values.has_pet = true;
} else {
values.has_pet = false;
}
next();
}
attributes: {
firstName: 'string',
lastName: 'string',
// Add a reference to Pet
pets: {
collection: 'pet',
via: 'owners',
dominant: true
},
has_pet: {
type: 'boolean'
}
}
});
Then you can query based on the has_pet attribute