I'm currently struggling with dynamodb-data-mapper and dynamodb-data-mapper-annotation.
I have a class corresponding to a table and some of the attributes must be recomputed every time we save an object.
I've been able to use marshall and unmarshall for the updatedAt attribute as it's completly independant, but when I need an interaction between several of them I cannot find any successful workaround.
I would like this be something totally internal and a method that I would have to call (better to avoid mistakes later).
Can you see any solution ?
#table('my-class-table')
export class MyClass {
#autoGeneratedHashKey()
readonly id: string;
#attribute({ defaultProvider: () => new Date() })
readonly createdAt: Date;
#attribute({
type: 'Custom',
defaultProvider: () => new Date(),
marshall: (): AttributeValue => ({ S: new Date().toISOString() }),
unmarshall: (persistedValue: AttributeValue): Date => new Date(persistedValue.S!),
})
readonly updatedAt: Date = new Date();
#attribute()
alerts: VehicleAlert[];
// This attribute depends on the alerts attribute
#attribute()
status: string;
}
I'm not sure of your exact use-case (and maybe you already figured it out), but I solved a similar problem by using a getter. For your updatedAt example:
class MyClass {
get updatedAt() {
return new Date().toISOString();
}
}
The data mapper will access the property every time you save the document, and it will update the record in the database with the value it gets from the getter.
Related
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
My setup
Typescript
Mongoose
Typegoose
I have a class Timecode
export class Timecode {
constructor(private timestamp: string) {}
getAsMilliseconds(): number {
throw new Error('Not implemented');
}
add(timecode: Timecode): Timecode {
throw new Error('Not implemented');
}
toString(): string {
return this.timestamp;
}
}
and an entity Clip
import { prop } from '#typegoose/typegoose';
export class Clip extends BaseEntity {
#prop({ required: true })
name: string;
#prop({ required: true })
description: string;
#prop({
required: true,
type: Timecode,
})
startTimecode: Timecode;
}
My question
Timecode can be serialised easily as a string by just saving its timestamp e.g 00:00:12:34
I would like to be able to have Timecode properties on my entities and then when I save to mongo have them automatically serialised to string. Also they should be transformed back into Timecode instances when loaded.
Here is an example
const clip = new Clip();
clip.name = "My Clip"
clip.description = "My Dessciption"
clip.startTimecode = new Timecode("00:00:12:34")
const savedClip = await ClipModel.create(clip)
const loadedClip = await ClipModel.findById(savedClip._id)
console.log(loadedClip.startTimecode.getAsMilliseconds()) //should work as startTimecode is an instance of Timecode
So I want mongoose to notice when I am saving an entity that has a property of type Typecode and convert it to a string and then reverse it to an instance of Typecode when loading.
Ideally I would be able to configure this in one place and not in every place where I declare a Typecode property.
Is this possible and how do I do it?
Thanks
The easiest way would be to use the get & set options to transform properties from / to Timecode and save it in the database as a string, but with multiple properties it can become tedious and hard to debug.
Mongoose also supports Custom Schema Types and Typegoose requires those types to have some properties (should have them by default), listed in Custom Types.
A small example can be found in typegoose's tests.
I have an interface created for my model, where I only want to return specific data from the record
// code.interface.ts
import { Document } from 'mongoose';
export interface CodeI extends Document {
readonly _id: string;
readonly logs: any;
}
But when I get the result from mongo, it completely ignores what is in my interface. (I am using NestJs as framework)
//constructor
constructor(#InjectModel(Coupon.name) private couponModel: Model<CouponDocument>) {}
// function
async findOne(codeId: string): Promise<CodeI> {
const coupon = await this.couponModel.findOne({ _id: codeId }).exec();
if (!coupon) {
throw new NotFoundException([`#${codeId} not found`]);
}
return coupon;
}
TypeScript interfaces don't work this way. They can't limit the fields of an object because they don't exist at runtime, so, we can't use them to guide any runtime behavior. TypeScript interfaces are useful for compile-time type check only.
However, in your case, there are two ways you can achieve the expected behavior.
The first one is to select only the required fields which you need to return (recommended).
In your findOne, you can do something like this
async findOne(codeId: string): Promise<CodeI> {
const coupon = await this.couponModel.findOne({ _id: codeId }, '_id logs').exec();
if (!coupon) {
throw new NotFoundException([`#${codeId} not found`]);
}
return coupon;
}
Here, as you can see, I have passed an additional string type parameter to findOne function which is projection and it will select only the specified fields from the object. This will not only solve your problem but also save query time and have increase query performance. Read more about findOne here.
The other way is to create a DTO where you can define the fields you want to return from the function.
Something like this:
// CouponDto.ts
class CouponDto {
public readonly _id: string;
public readonly logs: any;
constructor(data: CodeI) {
this._id = data._id;
this.logs = data.logs;
}
}
Then, in your service file, you can do something like
return new CouponDto(coupon);
(make sure to change the return type of the function to CouponDto as well)
You can use any of these two ways. While I would recommend going with the first one, it's up to you and how you wanna structure your project.
External Links:
Mongoose FindOne Docs
I am going to implement multi-language on a model, the structure is
const post = {
id: "123",
publishAt: "2020-09-04T00:00:00",
version: 1,
translations: [
{
locale: "en-US",
title: "hello world"
}
]
}
To store inside a relation database such as mysql, I need 2 table posts and post_translations
and thus 2 entities for typeorm
#Entity()
class Post {
#VersionColumn()
version: number;
#Column()
publishAt: Date;
#OneToMany(
(type) => PostTranslation,
(translation) => translation.post,
{ cascade: true }
)
translations: PostTranslation[];
#BeforeUpdate()
beforeUpdate() {
// do something
}
}
#Entity()
class PostTranslation {
#Column()
postId: number;
#Column()
title: string;
#ManyToOne((type) => Post, (post) => post.translations)
post: Post;
}
I am using repository.save(post) for creating/ updating records. If there are some changes inside post entity, such as publishAt, #BeforeUpdate() and #VersionColumn() will be triggered. But if there are no changes, they will not be trigger (reference).
It means that if I only modify the title from PostTranslations, and persist the changes via repository.save(post), it does not see changes and not going to trigger #BeforeUpdate() and #VersionColumn(). But the title (which lives in PostTranslations) is actually part of Post to me.
Is there any ways to ask typeorm to update the entity if some related entities changed during cascade persist?
So apparently, if there's no change in data being passed, the update method won't commit any change. The workaround I've found for this one is to update another field like a updated_at i.e from your code above:
#Entity()
class Post {
#VersionColumn()
version: number;
#Column()
publishAt: Date;
#Column() // new column/field
updatedAt: Date;
// the rest of your post entity
...
Then, whenever you pass the object to be updated, you update this date manually. It could be with a repository or something, such as:
...
post.updatedAt = new Date()
postRepository.save(post)
This will see some difference in old data, and then it would "force" the save. Hope it helps
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.