Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable association compatibility with Sequelize #1411

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,49 @@ explicitly:
proofedBooks: Book[];
```

### Multiple relations of same models and sequelize aliasing compatibility

When defining a relationship, _sequelize_ allows you to configure an alias, but it also allows you to configure empty aliases
as a sort of default relationship when querying. When using _sequelize-typescript_, it defaults to add an alias.

Consider the following model definition:

```typescript
@Table
class User extends Model {
@HasMany(() => Comment)
comments: Comment[];

@HasMany(() => Comment, {
as: 'archivedComments'
})
archivedComments: Comment[];
}
```

Running the following query will result in an error:

```typescript
User.findOne({ include: [Comment, 'archivedComments'] })
// Error: Alias cannot be inferred: "user" has multiple relations with "comment"
```

To revert to the original _sequelize_ behavior, you can turn off the automatic aliasing by specifying `undefined` for
the alias value:

```typescript
@Table({ modelName: 'user' })
class User extends Model {
@HasMany(() => Comment, { as: undefined })
comments: Comment[];

@HasMany(() => Comment, {
as: 'archivedComments'
})
archivedComments: Comment[];
}
```

### Type safe usage of auto generated functions

With the creation of a relation, sequelize generates some method on the corresponding
Expand Down
17 changes: 16 additions & 1 deletion src/associations/alias-inference/alias-inference-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { BaseAssociation } from '../shared/base-association';
import { getAssociationsByRelation } from '../shared/association-service';

/**
* Returns true if automatic association is possible given include parameters and list of associations
*/
function automaticAssociationInferrencePossible<TCreationAttributes, TModelAttributes>(
include: { model: any; as: any },
associations: BaseAssociation<TCreationAttributes, TModelAttributes>[]
): boolean {
const hasModelOptionWithoutAsOption = !!(include.model && !include.as);
const associationsWithoutAs = associations.filter((assoc) => !assoc.getAs());
return associationsWithoutAs.length === 1 && hasModelOptionWithoutAsOption;
}

/**
* Pre conform includes, so that "as" value can be inferred from source
*/
Expand Down Expand Up @@ -44,7 +57,9 @@ function inferAliasForInclude(include: any, source: any): any {
const relatedClass = include.model;
const associations = getAssociationsByRelation(targetPrototype, relatedClass);

if (associations.length > 0) {
if (automaticAssociationInferrencePossible(include, associations)) {
// Do nothing, this "include" is actually fine
} else if (associations.length > 0) {
if (associations.length > 1) {
throw new Error(
`Alias cannot be inferred: "${source.name}" has multiple ` +
Expand Down
2 changes: 1 addition & 1 deletion src/associations/belongs-to-many/belongs-to-many.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function BelongsToMany<
options = { ...throughOrOptions };
}

if (!options.as) options.as = propertyName;
if (!options.as && !('as' in options)) options.as = propertyName;

addAssociation(
target,
Expand Down
2 changes: 1 addition & 1 deletion src/associations/belongs-to/belongs-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function BelongsTo<TCreationAttributes extends {}, TModelAttributes exten
): Function {
return (target: any, propertyName: string) => {
const options: BelongsToOptions = getPreparedAssociationOptions(optionsOrForeignKey);
if (!options.as) options.as = propertyName;
if (!options.as && !('as' in options)) options.as = propertyName;
addAssociation(target, new BelongsToAssociation(associatedClassGetter, options));
};
}
2 changes: 1 addition & 1 deletion src/associations/has/has-many.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function HasMany<TCreationAttributes extends {}, TModelAttributes extends
): Function {
return (target: any, propertyName: string) => {
const options: HasManyOptions = getPreparedAssociationOptions(optionsOrForeignKey);
if (!options.as) options.as = propertyName;
if (!options.as && !('as' in options)) options.as = propertyName;
addAssociation(target, new HasAssociation(associatedClassGetter, options, Association.HasMany));
};
}
2 changes: 1 addition & 1 deletion src/associations/has/has-one.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function HasOne<TCreationAttributes extends {}, TModelAttributes extends
): Function {
return (target: any, propertyName: string) => {
const options: HasOneOptions = getPreparedAssociationOptions(optionsOrForeignKey);
if (!options.as) options.as = propertyName;
if (!options.as && !('as' in options)) options.as = propertyName;
addAssociation(target, new HasAssociation(associatedClassGetter, options, Association.HasOne));
};
}
17 changes: 16 additions & 1 deletion test/specs/annotations/belongs-to-many.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ describe('BelongsToMany', () => {
@Table
class Team extends Model {}

@Table
class Rank extends Model {}

@Table
class Player extends Model {
@BelongsToMany(() => Team, {
Expand All @@ -23,11 +26,23 @@ describe('BelongsToMany', () => {
otherKey: 'teamId',
})
teams: Team[];

@BelongsToMany(() => Rank, {
as: undefined,
through: 'RankPlayer',
foreignKey: 'playerId',
otherKey: 'rankId',
})
Ranks: Rank[];
}

sequelize.addModels([Team, Player]);
sequelize.addModels([Team, Player, Rank]);

it('should pass as options to sequelize association', () => {
expect(Player['associations']).to.have.property(as);
});

it('should use association model name if passed undefined for "as"', () => {
expect(Player['associations']).to.have.property('Ranks');
});
});
12 changes: 11 additions & 1 deletion test/specs/annotations/belongs-to.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,28 @@ describe('BelongsTo', () => {
@Table
class Team extends Model {}

@Table
class Rank extends Model {}

@Table
class Player extends Model {
@BelongsTo(() => Team, { as, foreignKey: 'teamId' })
team: Team;

@BelongsTo(() => Rank, { as: undefined, foreignKey: 'rankId' })
Rank: Rank;
}

sequelize.addModels([Team, Player]);
sequelize.addModels([Team, Player, Rank]);

it('should pass as options to sequelize association', () => {
expect(Player['associations']).to.have.property(as);
});

it('should use association model name if passed undefined for "as"', () => {
expect(Player['associations']).to.have.property('Rank');
});

it('should throw due to missing foreignKey', () => {
const _sequelize = createSequelize(false);

Expand Down
15 changes: 14 additions & 1 deletion test/specs/annotations/has-many.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,31 @@ describe('HasMany', () => {
@Table
class Player extends Model {}

@Table
class Rank extends Model {}

@Table
class Team extends Model {
@HasMany(() => Player, {
as,
foreignKey: 'teamId',
})
players: Player[];

@HasMany(() => Rank, {
as: undefined,
foreignKey: 'rankId',
})
Ranks: Rank[];
}

sequelize.addModels([Team, Player]);
sequelize.addModels([Team, Player, Rank]);

it('should pass as options to sequelize association', () => {
expect(Team['associations']).to.have.property(as);
});

it('should use association model name if passed undefined for "as"', () => {
expect(Team['associations']).to.have.property('Ranks');
});
});
15 changes: 14 additions & 1 deletion test/specs/annotations/has-one.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,31 @@ describe('HasOne', () => {
@Table
class Player extends Model {}

@Table
class Rank extends Model {}

@Table
class Team extends Model {
@HasOne(() => Player, {
as,
foreignKey: 'teamId',
})
player: Player;

@HasOne(() => Rank, {
as: undefined,
foreignKey: 'rankId',
})
Rank: Rank;
}

sequelize.addModels([Team, Player]);
sequelize.addModels([Team, Player, Rank]);

it('should pass as options to sequelize association', () => {
expect(Team['associations']).to.have.property(as);
});

it('should use association model name if passed undefined for "as"', () => {
expect(Player['associations']).to.have.property('Rank');
});
});