Sequelize BelongsToManyAddAssociationMixin performing update instead of insert in polymorphic table - javascript

I'm having a problem within my application that is related to multiple tables/models arranged in a Many:Many relationship but also leverages polymorphic columns and is using the BelongsToManyAddAssociationMixin. I am unable to insert multiple records in a M:N relationship because I'm unable to tell Sequelize to change the unique fields to include roleableId
Essentially I have a User model, a Role model, a relationship table called UserRole and other models which are "roleable" (For the sake of this example, one model is a Facility)
So essentially my UserRole model looks like
export class UserRole extends Model {
public readonly id!: UUID
public RoleId!: UUID
public UserId!: UUID
public roleableType!: string
public roleableId!: string
public readonly User!: User
public readonly Role!: Role
public getUser!: BelongsToGetAssociationMixin<User>
public getRole!: BelongsToGetAssociationMixin<Role>
}
export const initializeUserRole = (sequelize: Sequelize) => {
UserRole.init(
{
RoleId: {
type: DataTypes.UUID,
allowNull: false,
validate: {
isUUID: 4,
},
field: 'role_id',
},
UserId: {
type: DataTypes.UUID,
allowNull: false,
validate: {
isUUID: 4,
},
field: 'user_id',
},
roleableId: {
type: DataTypes.UUID,
field: 'roleable_id',
validate: {
isUUID: 4,
},
},
roleableType: {
type: DataTypes.STRING,
field: 'roleable_type',
},
},
{
timestamps: false,
sequelize,
tableName: TableNames.USER_ROLES,
}
)
}
In my scenario, I would have a role named Sales Rep and it's primary key is a UUID
In theory, I would like to create a new record for the user to have the SalesRep role twice in my UserRole table, one with values for roleableId & roleableType and one where those values are null.
Using mixins, I'm able to add these roles with something like
const randomUser = await userFactory()
const salesRepRole = await roleFactory({ roleName: 'Sales Rep' })
await randomUser.addRole(salesRepRole)
^^^ This works properly
But if I try to do something like:
const randomUser = await userFactory()
const randomFacility = await facilityFactory()
const salesRepRole = await roleFactory({ roleName: 'Sales Rep' })
await randomUser.addRole(salesRepRole)
await randomuser.addRole(salesRepRole, { through: { roleableId: randomFacility.id, roleableType: 'facility' }})
What will happen is that instead of performing a new insert into my UserRole table, instead Sequelize will identify that there is already a record based upon User.id & Role.id and will perform an UPDATE instead, resulting in only one record being returned.
I'm not sure how to indicate to Sequelize that instead of putting a unique constraint on User.id & Role.id, I really need it to be User.id, Role.id & roleableId
My work around right now has been to simply just roll my own add<Model> function which will perform an INSERT properly (adding an index on my DB to check for unique instances of user_id,role_id,roleable that throw an error if this combination already exists) but this feels a bit dirty and I'd rather try to utilize the mixin as much as possible, especially if I also have to add a BelongsToManyAddAssociationsMixin version as well for addRoles
Does anyone have any ideas? I'm happy to elaborate more if that helps.
Thanks!

Related

Prisma assigning types that I did not delcare

I have a many to many relationship between Post and Upload and prisma is claiming upload.posts is of type never. Due to this I cannot query a relationship I need. I am not sure why Prisma is assigning type never to this. See the below findFirst statement I am trying to use to connect a upload to a post.
await prisma.upload.findFirst({
where: {
id: upload.id,
},
select: {
type: true,
},
// prisma claims posts is Type never
posts: {
connect: {
id: post.id
}
}
});
Schema for relationships
model Upload {
id Int #id #default(autoincrement())
posts Post[]
}
model Post {
id Int #id #default(autoincrement())
uploads Upload[]
}
Take a look at the Prisma documentation to see how you can use findFirst:
https://www.prisma.io/docs/concepts/components/prisma-client/crud#get-the-first-record-that-matches-a-specific-criteria
You cannot pass posts as a top-level argument to findFirst. You can't make changes to data as you are querying for it, if that is what you are trying to do.
await prisma.upload.findFirst({
where: {
id: upload.id,
},
// REMOVED: "type", your model does not have a "type" field
select: {
id: true,
}
// REMOVED: "posts"
});

How to Include relationship on model via enum fields in sequelize

I have a table, table1, with a status field with type enum of values ['active', 'inactive'].
I have another table, table2, with a status field with type enum of values ['active', 'inactive'].
I tried adding a relationship between them as such:
table1.hasMany(models.table2, {
foreignKey: 'status', // enum field
sourceKey: 'status' // enum field
})
when I try to query the field including the relationship as such:
let result = await models.model1.findAll({
include: [{
model: models.model2
}]
})
I get the following error message.
"operator does not exist: \"enum_table1_status\" = \"enum_table2_status\""
What I want is for each element in result, there'd be an associated table property table2 which would be an array of the rows in table2 which have the same status with the status of the parent object from table1
I know it could probably work if both tables had the same enum type assigned to them, but I don't know how to do that.
I figured out how to go about it. When creating relationships in sequelize, the types have to be the same. string - string, integer to integer, etc. enums are a bit trickier apparently. Even if 2 enums have the same values, they are of different types like in the question above. So, to create a relationship, the the columns being related should have the same enum type.
Now, sequelize, to the best of my knowledge, doesn't have the functionality of creating enum types independent of a table/model. But, it is possible to assign type to an already created enum type. in PGadmin, the created types can be found under types, under schemas, under the db. Like this:
To assign the enum type of an already created table to another simply get the enum type name from the list of types, and assign it to the type variable when creating the migration file and model. Like this:
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('table2', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
loan_type: {
// Re-using the same ENUM than `status` so that we can create a relationship between them.
type: '"public"."enum_table1_status"',
allowNull: false,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('table2');
}
};
With this, relationships can be created on the model level between both tables.

How to group GraphQL query objects into namespaces?

How can I group my queries into namespaces in GraphQL? I have something like this right now:
const queryType = new g.GraphQLObjectType({
name: "Query",
fields: fields,
});
and in fields I have field -> object mappings and it works fine, but I'd like to group these mappings into two groups (live and historical). If I modify the above code to this however:
const queryType = new g.GraphQLObjectType({
name: "Query",
fields: {
historical: {
type: new g.GraphQLObjectType({
name: "historical",
fields: fields,
})
}
},
});
everything resolves to null. How can I write a resolver for this grouping? Is it possible at all?
so often people want namespaces for the sake of splitting up code, not sure if this is your end goal but you could achieve that this way aswell:
# in one file
type Mutation {
login(username: String, password: String): User
}
# in other file
extend type Mutation {
postX(title: String, message: String): X
}

Prisma: how to exclude properties from generated types

EDIT
there's a hidden danger in hiding fields in the TS definitions: the fields will not be accessible during development with intellisense, but the full object with "hidden" fields can be accidentally sent in a response, potentially exposing sensitive data.
I'm building my app using Prisma to connect to the DB (Next.js app). I'm having some trouble with the auto generated Typescript definitions.
I'm following the docs but I can't figure out how to select a subset of fields from Post. In their example:
import { Prisma } from '#prisma/client'
const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
include: { posts: true }, // -> how can I exclude some fields from the Post type?
})
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
Imagine Post being as follows (simplified):
model Post {
id Int #id #default(autoincrement())
createdAt DateTime #default(now())
title String
published Boolean #default(false)
author User #relation(fields: [authorId], references: [id])
authorId Int
}
I'd like to exclude some of the auto-generated fields from the Post type, for example createdAt. Basically user.posts[0] type will have all the fields except createdAt.
One solution could be:
const postData = Prisma.validator<Prisma.PostArgs>()({
select: { id: true, title: true, published: true, authorId: true }
})
type UserWithPosts = Omit<Prisma.UserGetPayload<typeof userWithPosts>, 'posts'> & {
posts: postData[]
}
But I was hoping for something a bit cleaner. Any alternatives?
I found a solution: instead of using include use select with a nested select for posts. The problem is that it becomes quite verbose and cumbersome to maintain (every time a field is added on the schema it must be added here as well...)
const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
select: {
email: true,
name: true,
posts: {
select: {
id: true,
title: true,
published: true,
authorId: true
}
}
}
})

Retrive some columns of relations in typeorm

I need to retrieve just some columns of relations in typeorm query.
I have an entity Environment that has an relation with Document, I want select environment with just url of document, how to do this in typeorm findOne/findAndCount methods?
To do that you have to use a querybuilder, here's an example:
return this.createQueryBuilder('environment') // use this if the query used inside of your entity's repository or getRepository(Environment)...
.select(["environment.id","environment.xx","environment.xx","document.url"])
.leftJoin("environment.document", "document")
.where("environment.id = :id ", { id: id })
.getOne();
Sorry I can't add comment to post above. If you by not parsed data mean something like "environment.id" instead of "id"
try this:
return this.createQueryBuilder("environment")
.getRepository(Environment)
.select([
"environment.id AS id",
"environment.xx AS xx",
"document.url AS url",
])
.leftJoin("environment.document", "document")
.where("environment.id = :id ", { id: id })
.getRawOne();
Here is the code that works for me, and it doesn't require using the QueryBuilder. I'm using the EntityManager approach, so assuming you have one of those from an existing DataSource, try this:
const environment = await this.entityManager.findOne(Environment, {
select: {
document: {
url: true,
}
},
relations: {
document: true
},
where: {
id: environmentId
},
});
Even though the Environment attributes are not specified in the select clause, my experience is that they are all returned in the results, along with document.url.
In one of the applications that I'm working on, I have the need to bring back attributes from doubled-nested relationships, and I've gotten that to work in a similar way, shown below.
Assuming an object model where an Episode has many CareTeamMembers, and each CareTeamMember has a User, something like the code below will fetch all episodes (all attributes) along with the first and last name of the associated Users:
const episodes = await this.entityManager.find(Episode, {
select: {
careTeamMembers: {
id: true, // Required for this to work
user: {
id: true,
firstName: true,
lastName: true,
},
}
},
relations: {
careTeamMembers: {
user: true,
}
},
where: {
deleted: false,
},
});
For some reason, I have to include at least one attribute from the CareTeamMembers entity itself (I'm using the id) for this approach to work.

Categories