123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- 'use strict';
- const chai = require('chai');
- const expect = chai.expect;
- const Support = require('../support');
- const { DataTypes, Op } = require('@sequelize/core');
- const upperFirst = require('lodash/upperFirst');
- describe(Support.getTestDialectTeaser('associations'), () => {
- describe('scope', () => {
- beforeEach(function () {
- this.Post = this.sequelize.define('post', {});
- this.Image = this.sequelize.define('image', {});
- this.Question = this.sequelize.define('question', {});
- this.Comment = this.sequelize.define('comment', {
- title: DataTypes.STRING,
- type: DataTypes.STRING,
- commentable: DataTypes.STRING,
- commentable_id: DataTypes.INTEGER,
- isMain: {
- field: 'is_main',
- type: DataTypes.BOOLEAN,
- defaultValue: false,
- },
- });
- this.Comment.prototype.getItem = function () {
- return this[`get${upperFirst(this.get('commentable'))}`]();
- };
- this.Post.addScope('withComments', {
- include: [
- {
- model: this.Comment,
- as: 'comments',
- },
- ],
- });
- this.Post.addScope('withMainComment', {
- include: [
- {
- model: this.Comment,
- as: 'mainComment',
- },
- ],
- });
- this.Post.hasMany(this.Comment, {
- foreignKey: 'commentable_id',
- scope: {
- commentable: 'post',
- },
- foreignKeyConstraints: false,
- });
- this.Post.hasMany(this.Comment, {
- foreignKey: 'commentable_id',
- as: 'coloredComments',
- scope: {
- commentable: 'post',
- type: { [Op.in]: ['blue', 'green'] },
- },
- foreignKeyConstraints: false,
- });
- this.Post.hasOne(this.Comment, {
- foreignKey: 'commentable_id',
- as: 'mainComment',
- scope: {
- commentable: 'post',
- isMain: true,
- },
- foreignKeyConstraints: false,
- });
- this.Comment.belongsTo(this.Post, {
- foreignKey: 'commentable_id',
- as: 'post',
- foreignKeyConstraints: false,
- });
- this.Image.hasMany(this.Comment, {
- foreignKey: 'commentable_id',
- scope: {
- commentable: 'image',
- },
- foreignKeyConstraints: false,
- });
- this.Comment.belongsTo(this.Image, {
- foreignKey: 'commentable_id',
- as: 'image',
- foreignKeyConstraints: false,
- });
- this.Question.hasMany(this.Comment, {
- foreignKey: 'commentable_id',
- scope: {
- commentable: 'question',
- },
- foreignKeyConstraints: false,
- inverse: {
- as: 'question',
- },
- });
- });
- describe('1:1', () => {
- it('should create, find and include associations with scope values', async function () {
- await this.sequelize.sync({ force: true });
- const [post1] = await Promise.all([
- this.Post.create(),
- this.Comment.create({
- title: 'I am a comment',
- }),
- this.Comment.create({
- title: 'I am a main comment',
- isMain: true,
- }),
- ]);
- this.post = post1;
- const comment0 = await post1.createComment({
- title: 'I am a post comment',
- });
- expect(comment0.get('commentable')).to.equal('post');
- expect(comment0.get('isMain')).to.be.false;
- const post0 = await this.Post.withScope('withMainComment').findByPk(this.post.get('id'));
- expect(post0.mainComment).to.be.null;
- const mainComment1 = await post0.createMainComment({
- title: 'I am a main post comment',
- });
- this.mainComment = mainComment1;
- expect(mainComment1.get('commentable')).to.equal('post');
- expect(mainComment1.get('isMain')).to.be.true;
- const post = await this.Post.withScope('withMainComment').findByPk(this.post.id);
- expect(post.mainComment.get('id')).to.equal(this.mainComment.get('id'));
- const mainComment0 = await post.getMainComment();
- expect(mainComment0.get('commentable')).to.equal('post');
- expect(mainComment0.get('isMain')).to.be.true;
- const comment = await this.Comment.create({
- title: 'I am a future main comment',
- });
- await this.post.setMainComment(comment);
- const mainComment = await this.post.getMainComment();
- expect(mainComment.get('commentable')).to.equal('post');
- expect(mainComment.get('isMain')).to.be.true;
- expect(mainComment.get('title')).to.equal('I am a future main comment');
- });
- it('should create included association with scope values', async function () {
- await this.sequelize.sync({ force: true });
- const post0 = await this.Post.create(
- {
- mainComment: {
- title: 'I am a main comment created with a post',
- },
- },
- {
- include: [{ model: this.Comment, as: 'mainComment' }],
- },
- );
- expect(post0.mainComment.get('commentable')).to.equal('post');
- expect(post0.mainComment.get('isMain')).to.be.true;
- const post = await this.Post.withScope('withMainComment').findByPk(post0.id);
- expect(post.mainComment.get('commentable')).to.equal('post');
- expect(post.mainComment.get('isMain')).to.be.true;
- });
- });
- describe('1:M', () => {
- it('should create, find and include associations with scope values', async function () {
- await this.sequelize.sync({ force: true });
- const [post1, image1, question1, commentA, commentB] = await Promise.all([
- this.Post.create(),
- this.Image.create(),
- this.Question.create(),
- this.Comment.create({
- title: 'I am a image comment',
- }),
- this.Comment.create({
- title: 'I am a question comment',
- }),
- ]);
- this.post = post1;
- this.image = image1;
- this.question = question1;
- await Promise.all([
- post1.createComment({
- title: 'I am a post comment',
- }),
- image1.addComment(commentA),
- question1.setComments([commentB]),
- ]);
- const comments = await this.Comment.findAll();
- for (const comment of comments) {
- expect(comment.get('commentable')).to.be.ok;
- }
- expect(
- comments
- .map(comment => {
- return comment.get('commentable');
- })
- .sort(),
- ).to.deep.equal(['image', 'post', 'question']);
- const [postComments, imageComments, questionComments] = await Promise.all([
- this.post.getComments(),
- this.image.getComments(),
- this.question.getComments(),
- ]);
- expect(postComments.length).to.equal(1);
- expect(postComments[0].get('title')).to.equal('I am a post comment');
- expect(imageComments.length).to.equal(1);
- expect(imageComments[0].get('title')).to.equal('I am a image comment');
- expect(questionComments.length).to.equal(1);
- expect(questionComments[0].get('title')).to.equal('I am a question comment');
- const [postComment, imageComment, questionComment] = [
- postComments[0],
- imageComments[0],
- questionComments[0],
- ];
- const [post0, image0, question0] = await Promise.all([
- postComment.getItem(),
- imageComment.getItem(),
- questionComment.getItem(),
- ]);
- expect(post0).to.be.instanceof(this.Post);
- expect(image0).to.be.instanceof(this.Image);
- expect(question0).to.be.instanceof(this.Question);
- const [post, image, question] = await Promise.all([
- this.Post.findOne({
- include: [
- {
- model: this.Comment,
- as: 'comments',
- },
- ],
- }),
- this.Image.findOne({
- include: [
- {
- model: this.Comment,
- as: 'comments',
- },
- ],
- }),
- this.Question.findOne({
- include: [
- {
- model: this.Comment,
- as: 'comments',
- },
- ],
- }),
- ]);
- expect(post.comments.length).to.equal(1);
- expect(post.comments[0].get('title')).to.equal('I am a post comment');
- expect(image.comments.length).to.equal(1);
- expect(image.comments[0].get('title')).to.equal('I am a image comment');
- expect(question.comments.length).to.equal(1);
- expect(question.comments[0].get('title')).to.equal('I am a question comment');
- });
- it('should make the same query if called multiple time (#4470)', async function () {
- const logs = [];
- const logging = function (log) {
- // removing 'executing(<uuid> || 'default'}) :' from logs
- logs.push(log.slice(Math.max(0, log.indexOf(':') + 1)));
- };
- await this.sequelize.sync({ force: true });
- const post = await this.Post.create();
- await post.createComment({
- title: 'I am a post comment',
- });
- await this.Post.withScope('withComments').findAll({
- logging,
- });
- await this.Post.withScope('withComments').findAll({
- logging,
- });
- expect(logs[0]).to.equal(logs[1]);
- });
- it('should created included association with scope values', async function () {
- await this.sequelize.sync({ force: true });
- let post = await this.Post.create(
- {
- comments: [
- {
- title: 'I am a comment created with a post',
- },
- {
- title: 'I am a second comment created with a post',
- },
- ],
- },
- {
- include: [{ model: this.Comment, as: 'comments' }],
- },
- );
- this.post = post;
- for (const comment of post.comments) {
- expect(comment.get('commentable')).to.equal('post');
- }
- post = await this.Post.withScope('withComments').findByPk(this.post.id);
- for (const comment of post.comments) {
- expect(comment.get('commentable')).to.equal('post');
- }
- });
- it('should include associations with operator scope values', async function () {
- await this.sequelize.sync({ force: true });
- const [post0, commentA, commentB, commentC] = await Promise.all([
- this.Post.create(),
- this.Comment.create({
- title: 'I am a blue comment',
- type: 'blue',
- }),
- this.Comment.create({
- title: 'I am a red comment',
- type: 'red',
- }),
- this.Comment.create({
- title: 'I am a green comment',
- type: 'green',
- }),
- ]);
- this.post = post0;
- await post0.addComments([commentA, commentB, commentC]);
- const post = await this.Post.findByPk(this.post.id, {
- include: [
- {
- model: this.Comment,
- as: 'coloredComments',
- },
- ],
- });
- expect(post.coloredComments.length).to.equal(2);
- for (const comment of post.coloredComments) {
- expect(comment.type).to.match(/blue|green/);
- }
- });
- it('should not mutate scope when running SELECT query (#12868)', async function () {
- await this.sequelize.sync({ force: true });
- await this.Post.findOne({
- where: {},
- include: [
- {
- association: this.Post.associations.mainComment,
- attributes: ['id'],
- required: true,
- where: {},
- },
- ],
- });
- expect(this.Post.associations.mainComment.scope.isMain).to.equal(true);
- });
- });
- if (Support.getTestDialect() !== 'sqlite3') {
- describe('N:M', () => {
- describe('on the target', () => {
- beforeEach(function () {
- this.Post = this.sequelize.define('post', {});
- this.Tag = this.sequelize.define('tag', {
- type: DataTypes.STRING,
- });
- this.PostTag = this.sequelize.define('post_tag');
- this.Post.belongsToMany(this.Tag, {
- as: 'categories',
- through: this.PostTag,
- scope: { type: 'category' },
- });
- this.Post.belongsToMany(this.Tag, {
- as: 'tags',
- through: this.PostTag,
- scope: { type: 'tag' },
- });
- return this.sequelize.sync({ force: true });
- });
- it('should create, find and include associations with scope values', async function () {
- const [postA0, postB0, postC0, categoryA, categoryB, tagA, tagB] = await Promise.all([
- this.Post.create(),
- this.Post.create(),
- this.Post.create(),
- this.Tag.create({ type: 'category' }),
- this.Tag.create({ type: 'category' }),
- this.Tag.create({ type: 'tag' }),
- this.Tag.create({ type: 'tag' }),
- ]);
- this.postA = postA0;
- this.postB = postB0;
- this.postC = postC0;
- await Promise.all([
- postA0.addCategory(categoryA),
- postA0.createTag(),
- postB0.addTag(tagA),
- postC0.createCategory(),
- ]);
- // we're calling 'setX' methods after the different 'addX' methods because
- // setCategories is not supposed to overwrite tags and vice-versa.
- // tags & categories use the same through table so this could happen is the association scope is not handled correctly.
- await postB0.setCategories([categoryB]);
- await postC0.setTags([tagB]);
- const [
- postACategories,
- postBCategories,
- postCCategories,
- postATags,
- postBTags,
- postCTags,
- ] = await Promise.all([
- this.postA.getCategories(),
- this.postB.getCategories(),
- this.postC.getCategories(),
- this.postA.getTags(),
- this.postB.getTags(),
- this.postC.getTags(),
- ]);
- expect([
- postACategories.length,
- postATags.length,
- postBCategories.length,
- postBTags.length,
- postCCategories.length,
- postCTags.length,
- ]).to.eql([1, 1, 1, 1, 1, 1]);
- expect([
- postACategories[0].get('type'),
- postATags[0].get('type'),
- postBCategories[0].get('type'),
- postBTags[0].get('type'),
- postCCategories[0].get('type'),
- postCTags[0].get('type'),
- ]).to.eql(['category', 'tag', 'category', 'tag', 'category', 'tag']);
- const [postA, postB, postC] = await Promise.all([
- this.Post.findOne({
- where: {
- id: this.postA.get('id'),
- },
- include: [
- { model: this.Tag, as: 'tags' },
- { model: this.Tag, as: 'categories' },
- ],
- }),
- this.Post.findOne({
- where: {
- id: this.postB.get('id'),
- },
- include: [
- { model: this.Tag, as: 'tags' },
- { model: this.Tag, as: 'categories' },
- ],
- }),
- this.Post.findOne({
- where: {
- id: this.postC.get('id'),
- },
- include: [
- { model: this.Tag, as: 'tags' },
- { model: this.Tag, as: 'categories' },
- ],
- }),
- ]);
- expect(postA.get('categories').length).to.equal(1);
- expect(postA.get('tags').length).to.equal(1);
- expect(postB.get('categories').length).to.equal(1);
- expect(postB.get('tags').length).to.equal(1);
- expect(postC.get('categories').length).to.equal(1);
- expect(postC.get('tags').length).to.equal(1);
- expect(postA.get('categories')[0].get('type')).to.equal('category');
- expect(postA.get('tags')[0].get('type')).to.equal('tag');
- expect(postB.get('categories')[0].get('type')).to.equal('category');
- expect(postB.get('tags')[0].get('type')).to.equal('tag');
- expect(postC.get('categories')[0].get('type')).to.equal('category');
- expect(postC.get('tags')[0].get('type')).to.equal('tag');
- });
- });
- describe('on the through model', () => {
- beforeEach(function () {
- this.Post = this.sequelize.define('post', {});
- this.Image = this.sequelize.define('image', {});
- this.Question = this.sequelize.define('question', {});
- this.ItemTag = this.sequelize.define('item_tag', {
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true,
- },
- tag_id: {
- type: DataTypes.INTEGER,
- unique: 'item_tag_taggable',
- },
- taggable: {
- type: DataTypes.STRING,
- unique: 'item_tag_taggable',
- },
- taggable_id: {
- type: DataTypes.INTEGER,
- unique: 'item_tag_taggable',
- references: null,
- },
- });
- this.Tag = this.sequelize.define('tag', {
- name: DataTypes.STRING,
- });
- this.Post.belongsToMany(this.Tag, {
- through: {
- model: this.ItemTag,
- unique: false,
- scope: {
- taggable: 'post',
- },
- },
- foreignKey: 'taggable_id',
- otherKey: 'tag_id',
- foreignKeyConstraints: false,
- inverse: {
- foreignKeyConstraints: false,
- },
- });
- this.Image.belongsToMany(this.Tag, {
- through: {
- model: this.ItemTag,
- unique: false,
- scope: {
- taggable: 'image',
- },
- },
- foreignKey: 'taggable_id',
- otherKey: 'tag_id',
- foreignKeyConstraints: false,
- inverse: {
- foreignKeyConstraints: false,
- },
- });
- this.Question.belongsToMany(this.Tag, {
- through: {
- model: this.ItemTag,
- unique: false,
- scope: {
- taggable: 'question',
- },
- },
- foreignKey: 'taggable_id',
- otherKey: 'tag_id',
- foreignKeyConstraints: false,
- inverse: {
- foreignKeyConstraints: false,
- },
- });
- });
- it('should create, find and include associations with scope values', async function () {
- await Promise.all([
- this.Post.sync({ force: true }),
- this.Image.sync({ force: true }),
- this.Question.sync({ force: true }),
- this.Tag.sync({ force: true }),
- ]);
- await this.ItemTag.sync({ force: true });
- const [post0, image0, question0, tagA, tagB, tagC] = await Promise.all([
- this.Post.create(),
- this.Image.create(),
- this.Question.create(),
- this.Tag.create({ name: 'tagA' }),
- this.Tag.create({ name: 'tagB' }),
- this.Tag.create({ name: 'tagC' }),
- ]);
- this.post = post0;
- this.image = image0;
- this.question = question0;
- await Promise.all([
- post0.setTags([tagA]).then(async () => {
- return Promise.all([post0.createTag({ name: 'postTag' }), post0.addTag(tagB)]);
- }),
- image0.setTags([tagB]).then(async () => {
- return Promise.all([image0.createTag({ name: 'imageTag' }), image0.addTag(tagC)]);
- }),
- question0.setTags([tagC]).then(async () => {
- return Promise.all([
- question0.createTag({ name: 'questionTag' }),
- question0.addTag(tagA),
- ]);
- }),
- ]);
- const [postTags, imageTags, questionTags] = await Promise.all([
- this.post.getTags(),
- this.image.getTags(),
- this.question.getTags(),
- ]);
- expect(postTags.length).to.equal(3);
- expect(imageTags.length).to.equal(3);
- expect(questionTags.length).to.equal(3);
- expect(
- postTags
- .map(tag => {
- return tag.name;
- })
- .sort(),
- ).to.deep.equal(['postTag', 'tagA', 'tagB']);
- expect(
- imageTags
- .map(tag => {
- return tag.name;
- })
- .sort(),
- ).to.deep.equal(['imageTag', 'tagB', 'tagC']);
- expect(
- questionTags
- .map(tag => {
- return tag.name;
- })
- .sort(),
- ).to.deep.equal(['questionTag', 'tagA', 'tagC']);
- const [post, image, question] = await Promise.all([
- this.Post.findOne({
- where: {},
- include: [this.Tag],
- }),
- this.Image.findOne({
- where: {},
- include: [this.Tag],
- }),
- this.Question.findOne({
- where: {},
- include: [this.Tag],
- }),
- ]);
- expect(post.tags.length).to.equal(3);
- expect(image.tags.length).to.equal(3);
- expect(question.tags.length).to.equal(3);
- expect(
- post.tags
- .map(tag => {
- return tag.name;
- })
- .sort(),
- ).to.deep.equal(['postTag', 'tagA', 'tagB']);
- expect(
- image.tags
- .map(tag => {
- return tag.name;
- })
- .sort(),
- ).to.deep.equal(['imageTag', 'tagB', 'tagC']);
- expect(
- question.tags
- .map(tag => {
- return tag.name;
- })
- .sort(),
- ).to.deep.equal(['questionTag', 'tagA', 'tagC']);
- });
- });
- });
- }
- });
- });
|