Last active
          September 26, 2025 22:39 
        
      - 
      
- 
        Save SippieCup/3d3827b0a1b1e7a95e1e1e64f812a4ef to your computer and use it in GitHub Desktop. 
    Collapse to HasOneThrough
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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); | |
| } | |
| } | |
| } | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | 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