Typeorm relationships - save by id - nestjs

I've been kinda confused by the relationships as I'm used to save relationship by id, while docs and examples I found suggest to get the entire object and use that instead (Isn't this strange???)
I found this on github addressing this issue ( https://github.com/typeorm/typeorm/issues/447 ) , where they suggest to use an object with just the id property, but it's from 2017. Is that a good way to do it ? And is it still the only way to do it ? (I find it pretty lame tbh)
async create( #Body() product: Product) {
product.category = <any>{ id: product.category };
return { payload: await this.repository.persist(product) };
}
Another one suggested to name the column as categoryId and it would work as expected (with id instead of object) but WHY? What does the name have to do with that ??
#Entity()
class Product {
#Column({ type: "int", nullable: true })
categoryId: number;
#ManyToOne(type => Category)
#JoinColumn({ name: "categoryId" })
category: Category;
}
I'm just confused, help ^_^

Isn't this strange???
Depends how you think about it, but yeah, I also like being able to just set the id, and not fetch the entire related entity.
Is that a good way to do it ? And is it still the only way to do it ?
I am also in the process of figuring out typeorm. What I found out is that you can do:
product.category = <any>3;
// or
product['category' as any] = 3;
repository.save(product) // I don't know how you have the persist() method.
and, in your case, the product.categoryId column will be set to 3. If categoryId is a foreign key and you set a non-existing id, you will get a foreign key error, like you should.
But this way ts will still think that product.category is of type Category. You can also specify the category property as a Category | number. But then you would have to do type checks everywhere which is annoying. I've tested this a bit, but I'm not sure if this will cause some unsuspecting bugs.
What does the name have to do with that ??
Well the option you provided is to define 2 properties: category which is a relation, and categoryId which is the column. The property categoryId should be named like the column in the table, but you can also pass the name: 'actual_name' in the #Column decorator. I don't know what happens if you set both columnId and the column properties with different ids.

According to this GitHub thread, it seems you can also do something like this:
product.category = { id: 1 }
product.save()
// Or
product.category = new Category().id = 1
product.save()

Related

How to handle many to many relationship DynamoDB

Im new to dynamoDB and im trying to build an ecommerce store. I have a table with a user, product and order.
My access patterns are:
get all products in a users order
I can then use this for a similar issue with the users cart. But im not sure how. My user to order relationship is one to many and my product to order relationship is many to many.
My data looks like this:
type Variant = {
size: Sizes;
quantity: number;
price: number;
}
type OrderProduct = {
id: string;
orderId: string;
product: Product;
status: string;
trackingId: string;
}
export type Product = {
id: string;
name: string;
description: string;
category: string;
createdAt: string;
variants: Variant[];
}
export type Order = {
id: string;
userId: string;
products: OrderProduct[];
createdAt: string;
}
export type User = {
id: string;
name: string;
address: string;
}
Ive seen this on aws for many to many relationships: aws many to many relationships
But this doesnt really explain how to do a one to many and then many to many query. Any advice and help with the query would be great!
DynamoDB only allows you to query by partition key (and ranged key), or to query by indexes.
If you have different tables, you cannot do a join query. You might need to create a global secondary index and then do a query on that.
So, for instance, if your Product had a secondary index over a field called "order_id", you coud do:
const documentClient = new AWS.DynamoDB.DocumentClient();
const orderId = 1234; // the real order id
const options = {
TableName: 'Product',
IndexName: 'OrderIdIndex',
KeyConditionExpression: 'order_id = :order_id',
ExpressionAttributeValues: {
':order_id': orderId
}
};
const response = await documentClient.query(options)
Keep in mind that this example is modifying your original structure.
You might need to add that new index and attribute
Edit
Keep in mind that there might be some delay for the index propagation. For example, if you insert a new Product, and you immediately want to search using the Index by order_id, DynamoDB might tell you that there is no product (because its propagating the data). If that small delay is not acceptable, you might prefer to first query the Order, and then query each product by Id (you could use batchGet if needed)
You do not do relationship queries in Dynamo. It is not a Relational Database, it is a document database.
This means most importantly, your normal way of storing data in multiple tables and usually by some whatever unique auto incrimented identifier in an SQL is a terrible way to do it in a dynamo
Instead, you need to store your data based on your access patterns - and this may feel very weird coming from SQL! You may even feel like you are duplicating data at times.
Since a Dynamo query requires you to know what the Partition Key is in order to query (you cannot do a search or a conditional on the PK) then the PK needs to be what you have to start your query.
so with your access pattern described, your PK must be the user. Then, a separate entry for each item in their cart would be the way to proceed - basically something like:
(EDIT: you can switch User for OrderID very easily too of course)
PK: User
SK: ITEM#123456123
PK: User
SK: ITEM#123491239
PK: User
SK: Item#113322
and maybe even a
PK: User
SK: META
with attribiutes like "total items" or "login time" or "sales offered" or whatever else needs to be tracked.
then if you query against the PK of USER, you get back a list of all their items. They remove an item, you remove the SK document associated with that item. They increase the amount, then you increase that items quantity attribute. ect.
This is in effect a One to Many relationship: One (the PK of User) and Many (SK's prefixed with ITEM#) - you can then do a query of PK=User, SK (beginsWith) ITEM# to retrieve all the items of a user.
But as you may be able to see, this can get very complex very fast if you are trying to do many different relationships - dynamo is not built for that. If you need to do anything deeper than a single relationship like this or need to be able to dynamically decide the relationships/queries at run time, then Dyanmo is not the solution, SQL is.

How can I add additional properties to an entity in NestJS?

I am using NestJS to build an api, and everything is working great so far!
I have a users table and a corresponding users.entity.ts file:
#Entity({ name: 'users' })
export class User extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string;
#Column({
unique: true,
})
email: string;
...
I am using AWS Cognito as an authentication provider - everything is working great there too. I am trying not to duplicate things, so I'd like to keep given_name and family_name values with Cognito and not have redundant fields on my postgres table.
I can get the user without problems, but I'm not sure how to "combine" them into my user entity.
For example,
// users.service.ts
const user = await this.usersRepository.findOne({
where: {
id: userId,
},
});
// id: dbc92...
// email: example#email.com
const cognitoUser = await this.cognitoService.adminGetUser(user.id);
// id: dbc92...
// email: example#email.com
// given_name: "Tony"
// family_name: "Stark"
return user;
I don't have a given_name property on my user entity—I'm not sure if I need one?
Ideally, I think what I'm trying to do is something like,
user.givenName = cognitoUser.given_name
user.familyName = cognitoUser.family_name
...
return user
But since my user.entity file doesn't have a givenName property, it's not to happy to do that.
I know I can "merge" the two objects using a spread operator, but I'd like to return the actual user entity if possible. How can I pull from two sources and return the proper entity?
As Mohammad said, if what you are trying to accomplish is:
user.givenName = cognitoUser.given_name
user.familyName = cognitoUser.family_name
then you can do this:
#Entity({ name: 'users' })
export class User extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string;
#Column({
unique: true,
})
email: string;
given_name: string;
family_name: string;
}
and be able to handle the data just from User but without replicating data in the database with unnecessary columns.
You can add your desired properties without adding #Column decorator. It will not make column in your table but still you have them in your entity.
From the official docs: https://github.com/typeorm/typeorm/blob/master/docs/listeners-and-subscribers.md#afterload
You can define a method with any name in entity and mark it with #AfterLoad and TypeORM will call it each time the entity is loaded using QueryBuilder or repository/manager find methods.
TypeORM Entity with custom properties / virtual fields:
// tested with `typeorm#0.3.7`
#Entity()
export class Post {
#AfterLoad()
updateCounters() {
if (this.likesCount === undefined) this.likesCount = 0
}
}
I don't think the chosen answer is quite correct because an exception for missing columns is thrown when an insert or update operation is attempted:
https://github.com/typeorm/typeorm/blob/cdabaa30287d357c0ae994209e573f97f92dad22/src/metadata/EntityMetadata.ts#L806-L814
If you are interested in reading more about the issue, is sounds like a feature selectAndMap is soon coming to typeorm to support this in a way that is more intuitive https://github.com/typeorm/typeorm/issues/1822#issuecomment-376069476

Typeorm: Find entity with relations and where condition

I'm working with typeorm and using repository to load entities. I want to find property and its active tenants (is_active = true)
In PropertyTenant model, I have:
#ManyToOne(type => Property)
#JoinColumn({ name: 'property_id', referencedColumnName: 'id' })
property: Property;
And in property model, I have:
#OneToMany(
() => PropertyTenant, (propertyTenant: PropertyTenant) => propertyTenant.property
)
propertyTenants: PropertyTenant[];
I tried something like this:
return await propertyRepository.findOne({
where: {
id: id,
'propertyTenants.is_active': true
},
relations: [
'location', 'location.city', 'location.city.state', 'propertyPurpose', 'propertyAmenities', 'propertyAmenities.amenity',
'propertyTenants', 'propertyTenants.tenant', 'propertyType', 'propertyDocuments', 'propertyDocuments.documentType', 'propertyImages'
]
});
It gives the error:
No entity column \"propertyTenants.is_active\" was found.
I'm not able to find anything in documentation. I don't want to use query build as it returns raw data and I need to process it.
Is there a way in which I can put condition in #OneToMany or #ManyToOne?
How to solve this?
The answer is to use QueryBuilder. You state you don't want to use it because it returns raw results, but only functions like QueryBuilder.execute() will return raw results. .getOne() and .getMany() will return results in entity format, just like you would get with Repository.findOne().

How can I get mapped type names for a graphQL type using type-graphql

Okay, so I'm starting to dig into graphql a little bit, and I've built an api using koa, type-graphql, and sequelize-typescript. Everything works pretty well.... I managed to get a query working, and even managed to optimize a little bit by using graphql-fields to filter the columns I query in the database... However when I've aliased a field name, I can't seem to get the mapped name.....
For example, given the following ObjectType/Sequelize Model....
export interface IDepartment {
departmentId: number;
name: string;
description: string;
}
#ObjectType()
#Table({ underscored: true })
export class Department extends Model<Department> implements IDepartment {
#Field({ name: 'id' })
#PrimaryKey
#Column({ field: 'department_id'})
public departmentId: number;
#Field()
#Length({ max: 100 })
#Column
name: string;
#Field()
#Length({ max: 100 })
#AllowNull
#Column
description: string;
}
and sample query....
query {
department(name: "Test Dept") {
id
name,
description
}
}
sample resolver...
async department(#Arg('name') name: string, #Info() info: GraphQLResolveInfo) {
return Department.findOne({
where: { name }
});
}
This works just fine.... but when I do
async department(#Arg('name') name: string, #Info() info: GraphQLResolveInfo) {
let fields = Object.keys(getFields(info))
return Department.findOne({
attributes: fields,
where: { name }
});
}
(getFields is graphql-fields), it fails because the query specified field name id, which is what graphql-fields returns, but the column name is department_id (sequelize model name departmentId).
I've gone through the schema with a fine tooth comb, using the introspectionFromSchema function to see a detailed copy of my schema, but nowhere is there a mention of departmentId or department_id.... However I know it's out there somewhere because when I exclude the attributes field from my sequelize query, even though sequelize returns departmentId as the property name, when I return it from my resolver and it reaches the client, the property name is id.
Any help would be appreciated.... I'm trying to optimize everything by only fetching requested properties and not the entire object. I could always store the maps as separate constants and use those in my #Field definition, but I want to do that as a last resort, however if I can I'm trying to keep the code as lean as possible....
Thank you all in advance.
Unfortunately, the name option was introduced mostly to support resolvers inheritance. Using this for mapping the schema field names is a kinda undocumented feature so it's doesn't provide any mapping or exposing mapping metadata.
Using the name option for input or args types will be even worse - it will result in no access to the fields and the properties being undefined.
For now my recommendation is to just keep it simple and don't map the field names until a proper fix arrives.

Is it possible to join a table in Sequelize without defining a relationship?

My research suggests no. Here is a quick example of what I'd like to do.
Given 3 tables: Company, Product, Lookup...
Company has many Products. Products have one Company. Lookup table serves as an enum/constant for hardcoded values. I.e. state/country names, application specific naming conventions, etc.
Here are the models in sequelize-typescript (though the question still fully relates to sequelize js):
// COMPANY
#Table
export default class Company extends Model<Company> {
#PrimaryKey
#Column
Oid:number;
#Column
Name:string;
#Column
Address:string;
#HasMany(() => Product)
products: Product[];
}
// PRODUCT
#Table
export default class Product extends Model<Product>{
#PrimaryKey
#Column
Oid: number;
#ForeignKey(() => Company)
#Column
companyOid: number;
#BelongsTo(() => Company)
company: Company;
#Column
Price: number;
#Column
Name: string;
//#ForeignKey(() => Lookup) //attempt #1
//#Column({references: {model: "Lookup", key: "Oid"}}) //attempt #2
#Column
LkpStateOid: number;
}
// LOOKUP
#Table
export default class Lookup extends Model<Lookup> {
#PrimaryKey
//#BelongsTo(() => Product) // do not want to hardcode Product, as it could be any table
#Column
Oid:number;
#Column
Value:number; // where the value represents the hardcoded state, county, etc., ie "CA"
}
The issue here is that there is no "real" relationship between Product.lkpStateOid and Lookup.oid, except that one references the other in order to obtain the Lookup.value. In any sql variant, this is not an issue- just a simple join on the tables. In sequelize, however, the relationship between the tables must be known before I can get any of the associated data in a query. This is what I'd like to do:
const companyInfo = await Db.Company.find({
include: [{
model: Product,
include: [{
model: Lookup,
attributes: ["value"]
}]
}]
})
The first include is no problem. The second, nested include is not successful. I've tried a number of different annotations on the table, but the secondary issue (even if i could successfully "fake" a relationship in order to be able to associate the data) is that I do not want to hardcode which table the Lookup table belongsTo because the Lookup table could contain values that are needed in any number of other tables. Is there a way to accomplish this goal?
This link seems close to what I'd like (and is commented as an idea in the above table): http://docs.sequelizejs.com/manual/tutorial/associations.html#enforcing-a-foreign-key-reference-without-constraints
Any help would be appreciated. Thanks!
Yes, it is possible, because Sequelize accepts literal sequel queries in the form of sequelize.query(). You can totally bypass the include[] syntax (I haven't tested this yet, but it is my understanding). Just put a raw SQL query with a JOIN statement inside sequelize.query().
http://docs.sequelizejs.com/manual/tutorial/raw-queries.html

Resources