Sequelize query where record does not exist in related table or does exist in related table with condition - postgresql

I'm trying to write a sequelize query which includes a relation from different table with hasMany relation. I want my query to return if some column in related table has matches my condition or there is not related row in the related table with foreign key from my main table.
Think about two different models, main one, which is folders, has id, name etc. columns and related table, which is folderOwners has id, folderId and groupId columns which shows which folders is owned by which groups. So, folders can have multiple owner groups.
include: [{
association: "owners",
required: true,
where: {
groupId: {[Op.in]: userGroups.map(group => group.id)}
}
}]
I can get the folders which owned by one of the groups that user is in but I want to get all folders if there is no row in related table, which means folder is not owned by anyone.
I've tried to change required to false and use Op.or for where like below
required: true,
where: {
[Op.or]: [
{groupId: {[Op.in]: userGroups.map(group => group.id)}},
{groupId: {[Op.eq]: null}}
],
}
So, are there any ways to achieve what I want?

If I understood correctly your problem, try this code :
Folder.findAll({include: [{model: Group, required: false}])
it will get all the folders with and without Groups

You can reference to nested model's column from parent model's "where" object using syntax like this: "$AssociatedTableName.columnName$"
Folder.findAll({
where: {
[Op.or]: [
{ '$Group.id$': null },
{
'$Group.id$': {
[Op.in]: [ids],
},
},
],
},
include: [
{
model: Group,
as: 'Group',
required: false,
},
],
});

Related

Sequelize association not returning value

I am trying to understand associations in sequelize and I am able to get one association to work but when I replicate the same query nothing gets returned. I don't understand the relationships that well even after reading the user guide
I can get the data from "stages" but not able to successfully join the "status" table and retrieve the data
I was able to find a workaround too where you can create the association from within findALL()
db.leads
.findAll({
attributes: ["id", "name", "title", "name", "title", "company", "workPhone", "mobilePhone", "otherPhone", "email", "dateCreated"],
include: [
{
model: db.stages,
association: db.leads.hasMany(db.stages, { foreignKey: "id", targetKey: "id" }),
on: {
[Op.and]: [
db.sequelize.where(
db.sequelize.col("stages.id"),
Op.eq, // '=',
db.sequelize.col("leads.stageID")
),
],
},
attributes: ["name"],
},
{
model: db.status,
association: db.leads.belongsToMany(db.status, { through: "id" }),
on: {
[Op.and]: [
db.sequelize.where(
db.sequelize.col("status.id"),
Op.eq, // '=',
db.sequelize.col("leads.statusID")
),
],
},
attributes: ["name"],
},
],
where: {
ownerID: req.query.ownerID,
},
subQuery: false,
duplicating: false,
})
This is what gets returned:
{
"id": "920cc536-48ae-40ee-8c5b-e1bfedbec602",
"name": "Dummy Lead",
"title": "Dummy",
"company": "Dummy",
"workPhone": "000-000-0000",
"mobilePhone": "000-000-0000",
"otherPhone": "000-000-0000",
"email": "Dummy#Dummy.com",
"dateCreated": "2022-06-18T13:30:09.676Z",
"stages": [
{
"name": "Qualify"
}
],
"status_types": []
}
Below are my tables:
Leads:
Stages:
Status:
In general, I believe the recommended approach to associations is to set them up in your models. This will make your queries easier to write, and also help from a DRY perspective. With the associations set up in models, your include would look something like
include: [
{
model: db.stages
attributes: ['name']
}
]
This would LEFT JOIN Stages with Leads. (NB: By adding a property of required: true to the object, you can make that perform an INNER JOIN).
The associations you're trying to call in these queries also do not really seem to match your table structure. If Leads hasMany Stages, then why is there a StageId in the Leads table? Having the foreign key in Leads would suggest that this is a hasOne or belongsTo relation from the Leads side. If Leads SHOULD have many Stages, then the foreign key should exist on the Stages table, so that multiple Stages can be associated to a single Lead.
Defining Leads with a belongsToMany relationship on Status also appears to be incorrect. BelongsToMany is the association method for Many to Many relationships through a junction table. If you do want to have a Many to Many relationship here (Where a Lead can have many Statuses, and Statuses can belong to many Leads), you will need to create that belongsToMany association on each side with a junction table (LeadStatuses). If a Lead should only have one status, however, the association should be lead.belongsTo() and status.hasMany().
I would take a long read about Sequelize Associations to try to take all of this in. It's challenging stuff that you will likely need to revisit several times, but it will make your life much easier once implemented correctly.

How to target a field in Prisma and get a flat array of values rather than an array of objects

I just started using Primsa 2 so I am still a noob at this but all I am trying to do is create a flat array of strings(Array<number>) based on the values I get from a specific field. Right now when I target that field it gives me an array of objects like this: userIds: [{ issueId: 1, userId: 1 }]
All I want is the value I get from the userId key and the array to return like this userIds: [ 1 ]. I was able to fix this with some formatting code after the query which was done like so:
const issues = project.issues.map(issue => ({ ...issue, userIds: [...issue.userIds.map((id) => id.userId)] }))
const _project = { ...project, issues }
However, this doesn't seem like the most optimal solution. If this is the only way that is fine but I assume with the power that Prisma has for querying, this is something I can do just in the query alone?
For reference, my query currently looks like this:
const project = await prisma.project.findFirst({
where: { id: req.currentUser.projectId },
include: { users: true, issues: { include: { userIds: true } } },
})
Thanks in advance!
Can you show your schema? Perhaps you can model the relation differently. However, unless if you provide a field, userIds, that is a flat array and not a field of a an other relation it will be returned as a list of objects as you have already.

KeystoneJs user-defined order for Relationship

I am using KeystoneJS with PostgreSQL as my backend and Apollo on the frontend for my app.
I have a schema that has a list that is linked to another list.
I want to be able to allow users to change the order of the second list.
This is a simplified version of my schema
keystone.createList(
'forms',
{
fields: {
name: {
type: Text,
isRequired: true,
},
buttons: {
type: Relationship,
ref: 'buttons.attached_forms',
many: true,
},
},
}
);
keystone.createList(
'buttons',
{
fields: {
name: {
type: Text,
isRequired: true,
},
attached_forms: {
type: Relationship,
ref: 'forms.buttons',
many: true,
},
},
}
);
So what I would like to do, is allow users to change the order of buttons so when I fetch them in the future from forms:
const QUERY = gql`
query getForms($formId: ID!) {
allforms(where: {
id: $formId,
}) {
id
name
buttons {
id
name
}
}
}
`;
The buttons should come back from the backend in a predefined order.
{
id: 1,
name: 'Form 1',
buttons: [
{
id: 1,
name: 'Button 1',
},
{
id: 3,
name: 'Button 3',
},
{
id: 2,
name: 'Button 2',
}
]
}
Or even just have some data on that returns with the query that will allow for sorting according to the user-defined sort order on the frontend.
The catch is that this relationship is many to many.
So it wouldn't be enough to add a column to the buttons schema as the ordering needs to be relationship-specific. In other words, if a user puts a particular button last on a particular form, it shouldn't change the order of that same button on other forms.
In a backend that I was creating myself, I would add something to the joining table, like a sortOrder field or similar and then change those values to change the order, or even order them on the frontend using that information.
Something like this answer here.
The many-to-many join table would have columns like formId, buttonId, sortOrder.
I have been diving into the docs for KeystoneJS and I can't figure out a way to make this work without getting into the weeds of overriding the KnexAdapter that we are using.
I am using:
{
"#keystonejs/adapter-knex": "^11.0.7",
"#keystonejs/app-admin-ui": "^7.3.11",
"#keystonejs/app-graphql": "^6.2.1",
"#keystonejs/fields": "^20.1.2",
"#keystonejs/keystone": "^17.1.2",
"#keystonejs/server-side-graphql-client": "^1.1.2",
}
Any thoughts on how I can achieve this?
One approach would be to have two "button" lists, one with a template for a button (buttonTemplate below) with common data such as name etc, and another (button below) which references one buttonTemplate and one form. This allows you to assign a formIndex property to each button, which dictates its position on the corresponding form.
(Untested) example code:
keystone.createList(
'Form',
{
fields: {
name: {
type: Text,
isRequired: true,
},
buttons: {
type: Relationship,
ref: 'Button.form',
many: true,
},
},
}
);
keystone.createList(
'Button',
{
fields: {
buttonTemplate: {
type: Relationship,
ref: 'ButtonTemplate.buttons',
many: false,
},
form: {
type: Relationship,
ref: 'Form.buttons',
many: false,
},
formIndex: {
type: Integer,
isRequired: true,
},
},
}
);
keystone.createList(
'ButtonTemplate',
{
fields: {
name: {
type: Text,
isRequired: true,
},
buttons: {
type: Relationship,
ref: 'Button.buttonTemplate',
many: true,
},
},
}
);
I think this is less likely to cause you headaches (which I'm sure you can see coming) down the line than your buttonOrder solution, e.g. users deleting buttons that are referenced by this field.
If you do decide to go with this approach, you can guard against such issues with the hook functionality in Keystone. E.g. before a button is deleted, go through all the forms and rewrite the buttonOrder field, removing any references to the deleted button.
I had a similar challenge once, so after some research and found this answer, I implemented a solution to a project using PostgreSQL TRIGGER.
So you can add a trigger where on an update, it should shift the buttonOrder.
Here is the SQL I had on me, this was the test code, I regex replaced the terms to fit your question :)
// Assign order
await knex.raw(`
do $$
DECLARE form_id text;
begin
CREATE SEQUENCE buttons_order_seq;
CREATE VIEW buttons_view AS SELECT * FROM "buttons" ORDER BY "createdAt" ASC, "formId";
CREATE RULE buttons_rule AS ON UPDATE TO buttons_view DO INSTEAD UPDATE buttons SET order = NEW.order WHERE id = NEW.id;
FOR form_id IN SELECT id FROM form LOOP
ALTER SEQUENCE buttons_order_seq RESTART;
UPDATE buttons_view SET order = nextval('buttons_order_seq') WHERE "formId" = form_id;
END LOOP;
DROP SEQUENCE buttons_order_seq;
DROP RULE buttons_rule ON buttons_view;
DROP VIEW buttons_view;
END; $$`);
// Create function that shifts orders
await knex.raw(`
CREATE FUNCTION shift_buttons_order()
RETURNS trigger AS
$$
BEGIN
IF NEW.order < OLD.order THEN
UPDATE buttons SET order = order + 1, "shiftOrderFlag" = NOT "shiftOrderFlag"
WHERE order >= NEW.order AND order < OLD.order AND "formId" = OLD."formId";
ELSE
UPDATE buttons SET order = order - 1, "shiftOrderFlag" = NOT "shiftOrderFlag"
WHERE order <= NEW.order AND order > OLD.order AND "formId" = OLD."formId";
END IF;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql'`);
// Create trigger to shift orders on update
await knex.raw(`
CREATE TRIGGER shift_buttons_order BEFORE UPDATE OF order ON buttons FOR EACH ROW
WHEN (OLD."shiftOrderFlag" = NEW."shiftOrderFlag" AND OLD.order <> NEW.order)
EXECUTE PROCEDURE shift_buttons_order()`);
One option that we came up with is to add the order to the form table.
keystone.createList(
'forms',
{
fields: {
name: {
type: Text,
isRequired: true,
},
buttonOrder: {
type: Text,
},
buttons: {
type: Relationship,
ref: 'buttons.attached_forms',
many: true,
},
},
}
);
This new field buttonOrder could contain a string representation of the order of the button Ids, like in a JSON stringified array.
The main issue with this is that it will be difficult to keep this field in-sync with the actual linked buttons.

Sort populated record in sails waterline

I created a Sails application with two models Publication and Worksheet. They are having a one-to-one relationship. Sails-postgresql is the adapter I'm using. I'm using waterline orm to fire query to the database. I'm When I am trying to load publications data along with worksheet and then sort the records depending on a field in the Worksheet using sort() I'm getting an error.
My model is:
Publication.js
module.exports = {
attributes: {
id: {
type: 'integer'
unique: true
},
worksheetId: {
type: 'integer',
model : 'worksheet'
},
status: {
type: 'string',
defaultsTo: 'active',
in : ['active', 'disabled'],
}
}
}
Worksheet.js
module.exports = {
attributes: {
id: {
type: 'integer',
unique: true
},
name: 'string',
orderWeight: {
type: 'integer',
defaultsTo: 0
}
}
}
So now I want to load all the publication where status is "active" and populate worksheet in the data.
So I'm executing the query:
Publication.find({
where: {
status: 'active'
}
})
.populate('worksheetId').limit(1)
.exec(function (error, publications) {
})
And I'm getting a data like :
{
id : 1,
status : "active",
worksheetId : {
id : 1
name : "test",
orderWeight : 10
}
}
So till now it's all working fine. Now I want to increase the limit to 10 and want to sort the data depending on "orderWeight" which is in the populated data. Initially I sorted the whole data depending on publication id and the query worked.
Publication.find({
where: {
status: 'active'
}
})
.populate('worksheetId').sort('id ASC').limit(10)
.exec(function (error, publications) {
})
So I fired similar query to sort the data on "orderWeight"
Publication.find({
where: {
status: 'active'
}
})
.populate('worksheetId').sort('worksheetId.orderWeight ASC').limit(10)
.exec(function (error, publications) {
})
And this query is giving me error that worksheetId.orderWeight is not a column on the publication table. So I want to fire this sort query on the populated data not on the publication table.
Please let me know how I can get my expected result.
Apart from sort() method I also want to run some find command to the populated data to get those publication where the worksheet name matches with certain key as well.
Basically, what you're trying to do, is query an association's attribute. This has been in the waterline roadmap since 2014, but it's still not supported, so you'll have to figure out a workaround.
One option is to query the Worksheet model, and populate the Publication, since sails doesn't let you query across models without using raw queries (i.e. .sort('worksheetId.orderWeight ASC') doesn't work). Unfortunately, you might have to move the active flag to the Worksheet. For example:
Worksheet.find({
status: 'active'
})
.populate('publication') // you should also add publication to Worksheet.js
.sort('orderWeight ASC')
.limit(10)
Alternatively, you could combine Worksheet and Publication into one model, since they're one-to-one. Probably not ideal, but sails.js and Waterline make it very difficult to work with relational data - I'd estimate that half of the queries in the project I'm working on are raw queries due to sails' poor support of postgres. The framework is pretty biased towards using MongoDB, although it claims to "just work" with any of the "supported" DBs.

Sails.js add attribute from another table to a model

I am trying to pull an extra attribute for a model from a different table.
I have been trying with associations but it seems wrong to create an extra model and associate that to my original model.
This is my model Company.js
module.exports = {
schema: true,
autoCreatedAt: false,
autoUpdatedAt: false,
autoPK: false,
attributes: {
name: {
type: 'string'
},
url: {
type: 'string'
},
summary: {
type: 'text'
}
}
};
I have another table in MySQL with two columns, crunchbase_url and company_id, I would like to pull the crunchbase_url into the Company model, what would be the best way to do it without migrating the DB (not an option unfortunately).
Thanks,
Well, turns out that the documentation is fine and all works like a charm by simply creating another model and using one to many association with joined with via.
However, there is a bug that threw me off:
When defaultLimit is set to -1, the find action return an empty list if there are associations.