Skip to content

Instantly share code, notes, and snippets.

@kirklewis
Created December 21, 2022 12:45
Show Gist options
  • Save kirklewis/b802841815802bcfe5292c345f2841ae to your computer and use it in GitHub Desktop.
Save kirklewis/b802841815802bcfe5292c345f2841ae to your computer and use it in GitHub Desktop.

Sequelize updateWithIncludes

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 Setup

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.

The Code

// 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;
    }
};

Usage

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment