123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- import type { ModelStatic } from '@sequelize/core';
- import { DataTypes } from '@sequelize/core';
- import { expect } from 'chai';
- import each from 'lodash/each';
- import assert from 'node:assert';
- import sinon from 'sinon';
- import { getTestDialectTeaser, sequelize } from '../../support';
- describe(getTestDialectTeaser('hasOne'), () => {
- it('throws when invalid model is passed', () => {
- const User = sequelize.define('User');
- expect(() => {
- // @ts-expect-error -- testing that invalid input results in error
- User.hasOne();
- }).to.throw(
- `User.hasOne was called with undefined as the target model, but it is not a subclass of Sequelize's Model class`,
- );
- });
- it('warn on invalid options', () => {
- const User = sequelize.define('User', {});
- const Task = sequelize.define('Task', {});
- expect(() => {
- User.hasOne(Task, { sourceKey: 'wowow' });
- }).to.throwWithCause(
- 'Unknown attribute "wowow" passed as sourceKey, define this attribute on model "User" first',
- );
- });
- it('forbids alias inference in self-associations', () => {
- const User = sequelize.define('User');
- expect(() => {
- User.hasOne(User);
- }).to.throwWithCause(
- 'Both options "as" and "inverse.as" must be defined for hasOne self-associations, and their value must be different',
- );
- });
- it('allows self-associations with explicit alias', () => {
- const User = sequelize.define('User');
- // this would make more sense as a belongsTo(User, { as: 'mother', inverse: { type: 'many', as: 'children' } })
- User.hasOne(User, { as: 'mother', inverse: { as: 'child' } });
- });
- it('allows customizing the inverse association name (long form)', () => {
- const User = sequelize.define('User');
- const Task = sequelize.define('Task');
- User.hasMany(Task, { as: 'task', inverse: { as: 'user' } });
- expect(Task.associations.user).to.be.ok;
- expect(User.associations.task).to.be.ok;
- });
- it('allows customizing the inverse association name (shorthand)', () => {
- const User = sequelize.define('User');
- const Task = sequelize.define('Task');
- User.hasMany(Task, { as: 'task', inverse: 'user' });
- expect(Task.associations.user).to.be.ok;
- expect(User.associations.task).to.be.ok;
- });
- it('generates a default association name', () => {
- const User = sequelize.define('User', {});
- const Task = sequelize.define('Task', {});
- User.hasOne(Task);
- expect(Object.keys(Task.associations)).to.deep.eq(['user']);
- expect(Object.keys(User.associations)).to.deep.eq(['task']);
- });
- it('does not use `as` option to generate foreign key name', () => {
- // See HasOne.inferForeignKey for explanations as to why "as" is not used when inferring the foreign key.
- const User = sequelize.define('User', { username: DataTypes.STRING });
- const Task = sequelize.define('Task', { title: DataTypes.STRING });
- const association1 = User.hasOne(Task);
- expect(association1.foreignKey).to.equal('userId');
- expect(Task.getAttributes().userId).not.to.be.empty;
- const association2 = User.hasOne(Task, { as: 'Shabda' });
- expect(association2.foreignKey).to.equal('userId');
- expect(Task.getAttributes().userId).not.to.be.empty;
- });
- it('should not override custom methods with association mixin', () => {
- const methods = {
- getTask: 'get',
- setTask: 'set',
- createTask: 'create',
- };
- const User = sequelize.define('User');
- const Task = sequelize.define('Task');
- function originalFunction() {}
- each(methods, (alias, method) => {
- // TODO: remove this eslint-disable once we drop support for TypeScript <= 5.3
- // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
- // @ts-ignore -- This only became invalid starting with TS 5.4
- User.prototype[method] = originalFunction;
- });
- User.hasOne(Task, { as: 'task' });
- const user = User.build();
- each(methods, (alias, method) => {
- // @ts-expect-error -- dynamic type, not worth typing
- expect(user[method]).to.eq(originalFunction);
- });
- });
- describe('allows the user to provide an attribute definition object as foreignKey', () => {
- it(`works with a column that hasn't been defined before`, () => {
- const User = sequelize.define('user', {});
- const Profile = sequelize.define('project', {});
- User.hasOne(Profile, {
- foreignKey: {
- allowNull: false,
- name: 'uid',
- },
- });
- expect(Profile.getAttributes().uid).to.be.ok;
- const model = Profile.getAttributes().uid.references?.table;
- assert(typeof model === 'object');
- expect(model).to.deep.equal(User.table);
- expect(Profile.getAttributes().uid.references?.key).to.equal('id');
- expect(Profile.getAttributes().uid.allowNull).to.be.false;
- });
- it('works when taking a column directly from the object', () => {
- const User = sequelize.define('user', {
- uid: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- },
- });
- const Profile = sequelize.define('project', {
- user_id: {
- type: DataTypes.INTEGER,
- allowNull: false,
- },
- });
- User.hasOne(Profile, { foreignKey: Profile.getAttributes().user_id });
- expect(Profile.getAttributes().user_id).to.be.ok;
- const targetTable = Profile.getAttributes().user_id.references?.table;
- assert(typeof targetTable === 'object');
- expect(targetTable).to.deep.equal(User.table);
- expect(Profile.getAttributes().user_id.references?.key).to.equal('uid');
- expect(Profile.getAttributes().user_id.allowNull).to.be.false;
- });
- it('works when merging with an existing definition', () => {
- const User = sequelize.define('user', {
- uid: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- },
- });
- const Project = sequelize.define('project', {
- userUid: {
- type: DataTypes.INTEGER,
- defaultValue: 42,
- },
- });
- User.hasOne(Project, { foreignKey: { allowNull: false } });
- expect(Project.getAttributes().userUid).to.be.ok;
- expect(Project.getAttributes().userUid.allowNull).to.be.false;
- const targetTable = Project.getAttributes().userUid.references?.table;
- assert(typeof targetTable === 'object');
- expect(targetTable).to.deep.equal(User.table);
- expect(Project.getAttributes().userUid.references?.key).to.equal('uid');
- expect(Project.getAttributes().userUid.defaultValue).to.equal(42);
- });
- });
- it('sets the foreign key default onDelete to CASCADE if allowNull: false', async () => {
- const Task = sequelize.define('Task', { title: DataTypes.STRING });
- const User = sequelize.define('User', { username: DataTypes.STRING });
- User.hasOne(Task, { foreignKey: { allowNull: false } });
- expect(Task.getAttributes().userId.onDelete).to.eq('CASCADE');
- });
- it('should throw an error if an association clashes with the name of an already define attribute', () => {
- const User = sequelize.define('user', {
- attribute: DataTypes.STRING,
- });
- const Attribute = sequelize.define('attribute', {});
- expect(User.hasOne.bind(User, Attribute)).to.throw(
- "Naming collision between attribute 'attribute' and association 'attribute' on model user. To remedy this, change the \"as\" options in your association definition",
- );
- });
- describe('Model.associations', () => {
- it('should store all associations when associating to the same table multiple times', () => {
- const User = sequelize.define('User', {});
- const Group = sequelize.define('Group', {});
- Group.hasOne(User);
- Group.hasOne(User, {
- foreignKey: 'primaryGroupId',
- as: 'primaryUsers',
- inverse: { as: 'primaryGroup' },
- });
- Group.hasOne(User, {
- foreignKey: 'secondaryGroupId',
- as: 'secondaryUsers',
- inverse: { as: 'secondaryGroup' },
- });
- expect(Object.keys(Group.associations)).to.deep.equal([
- 'user',
- 'primaryUsers',
- 'secondaryUsers',
- ]);
- });
- });
- describe('association hooks', () => {
- let Projects: ModelStatic<any>;
- let Tasks: ModelStatic<any>;
- beforeEach(() => {
- Projects = sequelize.define('Project', { title: DataTypes.STRING });
- Tasks = sequelize.define('Task', { title: DataTypes.STRING });
- });
- describe('beforeHasOneAssociate', () => {
- it('should trigger', () => {
- const beforeAssociate = sinon.spy();
- Projects.beforeAssociate(beforeAssociate);
- Projects.hasOne(Tasks, { hooks: true });
- const beforeAssociateArgs = beforeAssociate.getCall(0).args;
- expect(beforeAssociate).to.have.been.called;
- expect(beforeAssociateArgs.length).to.equal(2);
- const firstArg = beforeAssociateArgs[0];
- expect(Object.keys(firstArg).join(',')).to.equal('source,target,type,sequelize');
- expect(firstArg.source).to.equal(Projects);
- expect(firstArg.target).to.equal(Tasks);
- expect(firstArg.type.name).to.equal('HasOne');
- expect(firstArg.sequelize.constructor.name).to.equal('Sequelize');
- });
- it('should not trigger association hooks', () => {
- const beforeAssociate = sinon.spy();
- Projects.beforeAssociate(beforeAssociate);
- Projects.hasOne(Tasks, { hooks: false });
- expect(beforeAssociate).to.not.have.been.called;
- });
- });
- describe('afterHasOneAssociate', () => {
- it('should trigger', () => {
- const afterAssociate = sinon.spy();
- Projects.afterAssociate(afterAssociate);
- Projects.hasOne(Tasks, { hooks: true });
- const afterAssociateArgs = afterAssociate.getCall(0).args;
- expect(afterAssociate).to.have.been.called;
- expect(afterAssociateArgs.length).to.equal(2);
- const firstArg = afterAssociateArgs[0];
- expect(Object.keys(firstArg).join(',')).to.equal(
- 'source,target,type,association,sequelize',
- );
- expect(firstArg.source).to.equal(Projects);
- expect(firstArg.target).to.equal(Tasks);
- expect(firstArg.type.name).to.equal('HasOne');
- expect(firstArg.association.constructor.name).to.equal('HasOne');
- expect(firstArg.sequelize.constructor.name).to.equal('Sequelize');
- });
- it('should not trigger association hooks', () => {
- const afterAssociate = sinon.spy();
- Projects.afterAssociate(afterAssociate);
- Projects.hasOne(Tasks, { hooks: false });
- expect(afterAssociate).to.not.have.been.called;
- });
- });
- });
- });
|