A quick solution I made to update associated models dynamically. This can be taken a step further and made more generic so DirectorKey
and director
are based on any parent model.
I am using a transaction to rollback everything when an error occurs. Just a bit of fun and it works!
Note: createdAt
might be updated if supplied in the updates but this doesn't tend to happen in my case as Postman to controller validation do not allow default date fields.
The following works fine with the exceptions:
- it is not generic but can easily be made to be, this is from a use case I was proving.
- it will fill missing field data with null. This can be fixed by replacing bulkCreate with a custom method to generate the [key-values] of each associate for use in VALUES but using ON DUPLICATE KEY UPDATE. This works well.
- Or ensure your FE or controller validation always returns the fields with their original values and they ones that have changes - Simple! Joi scheme for each associate which the associate endpoints will have defined already, so reuse them.
// TODO: change signature to (model, id, data) so method is generic.
update = async (id: number, director: Director): Promise<Director> => {
const transaction = await this.database.sequelize.transaction();
const associateModels = Object.keys(this.database.models.director.associations);
try {
// TODO: change to AssociateKey when this method is generic
type DirectorKey = keyof typeof director;
await Promise.all(
associateModels.map(async (associate: any) => {
const directorAssociate = associate as DirectorKey;
// no need for plural reall use modelManager to refer to models consistently
const { plural, singular } = this.database.models[associate.replace(/s$/, '')].options.name;
// use data[associate] instead of hasOwnProperty
if (director.hasOwnProperty(plural)) {
const associateAttributes = Object.keys(this.database.models[singular].rawAttributes);
await this.database.models[singular].bulkCreate(
director[directorAssociate],
{
updateOnDuplicate: associateAttributes,
},
{ transaction },
);
}
// TODO: return transaction and check in then(...)
}),
).then(async (_) => {
// TODO: check each transaction state here and then call commit()
// otherwise rollback isn't possible
await this.database.models.director.update(
director,
{
where: { id },
},
{ transaction },
);
// TODO: should really happen once each transaction is valid
await transaction.commit();
});
return await this.get(id);
} catch (err) {
this.logger.error(`Director::update:: ${err}`);
await transaction.rollback();
return err;
}
};
Here I want to update a director's name and one of his existing films and add a new film titled "Don't look back!".
const director = {
id: 1,
name: 'Director X',
films: [
{
id: 1,
directorId: 1,
name: 'The Count Returns',
year: '2024',
},
{
directorId: 1,
name: 'Don\'t look back!',
year: '2023',
},
interviews: [
{
name: 'Sundance Junket',
}
] as Interview[],
] as Film[]
} as Director;
const updatedDirector = await directorService.update(id, director);
this.logger.info(updatedDirector);
{
id: 1,
name: "Director X",
films: [
{
id: 1,
directorId: 1,
name: "The Count Returns",
year: "2024",
createdAt: "...",
updatedAt: "...",
},
{
id: 2,
directorId: 1,
name: "Don't look back!",
year: "2023",
createdAt: "...",
updatedAt: "...",
},
],
interviews: [
{
id: 1,
name: "Sundance Junket",
createdAt: "...",
updatedAt: "...",
},
],
createdAt: "...",
updatedAt: "...",
};
It performs an upsert on both interview and film, as the id is missing for each. This indicates a new entity.