Skip to content

Instantly share code, notes, and snippets.

@SippieCup
Last active September 26, 2025 22:39
Show Gist options
  • Save SippieCup/3d3827b0a1b1e7a95e1e1e64f812a4ef to your computer and use it in GitHub Desktop.
Save SippieCup/3d3827b0a1b1e7a95e1e1e64f812a4ef to your computer and use it in GitHub Desktop.
Collapse to HasOneThrough
import { Model, InferAttributes, InferCreationAttributes, DataTypes, CreationOptional, NonAttribute } from '@sequelize/core';
import { Attribute, PrimaryKey, NotNull, Table, AutoIncrement, BelongsToMany, AfterFind } from '@sequelize/core/decorators-legacy';
export class PictureSignature extends Model<InferAttributes<PictureSignature>, InferCreationAttributes<PictureSignature>> {
@Attribute(DataTypes.DATE(6))
declare createdAt: CreationOptional<Date>;
@Attribute(DataTypes.INTEGER)
@PrimaryKey
@NotNull
declare personId: number;
@Attribute(DataTypes.INTEGER)
@PrimaryKey
@NotNull
declare pictureId: number;
}
@Table({ tableName: 'Persons' })
export class Person extends Model<InferAttributes<Person>, InferCreationAttributes<Person>> {
@Attribute(DataTypes.INTEGER)
@AutoIncrement
@PrimaryKey
declare id: CreationOptional<number>;
@Attribute(DataTypes.TEXT)
declare name: string;
}
@Table({ tableName: 'Pictures' })
export class Picture extends Model<InferAttributes<Picture>, InferCreationAttributes<Picture>> {
@Attribute(DataTypes.INTEGER)
@AutoIncrement
@PrimaryKey
declare id: CreationOptional<number>;
@BelongsToMany(() => Person, {
through: () => PictureSignature,
foreignKey: 'pictureId',
otherKey: 'personId',
})
declare signers?: NonAttribute<Person[]>;
// derived single
declare firstSigner?: NonAttribute<Person>;
@AfterFind
static collapseFirstSigner(rows: Picture | Picture[] | null): void {
const pictures = Array.isArray(rows) ? rows : rows ? [rows] : [];
for (const pic of pictures) {
const signers = pic.signers ?? [];
if (!signers.length) continue;
// pick signer with smallest createdAt on the join row
const earliest = signers.reduce<Person | undefined>((min, person) => {
const curCreatedAt = (person as any).PictureSignature?.createdAt ?? Number.POSITIVE_INFINITY;
const minCreatedAt = min ? ((min as any).PictureSignature?.createdAt ?? Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY;
return curCreatedAt < minCreatedAt ? person : min;
}, undefined);
// store it as a derived single field
// @ts-expect-error setDataValue exists on Model
pic.setDataValue('firstSigner', earliest);
}
}
}
const pictures = await Picture.findAll({
include: [{
association: 'signers',
through: { attributes: ['createdAt'] }, // must include createdAt from join table
}],
});
for (const pic of pictures) {
console.log(pic.firstSigner?.name);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment