sequelize / sequelize-typescript

Decorators and some other features for sequelize
MIT License
2.79k stars 282 forks source link

Cannot use @DefaultScope() simultaneously in two related Models #1703

Open caioalmeida12 opened 10 months ago

caioalmeida12 commented 10 months ago

Issue

Cannot use @DefaultScope() simultaneously in two related Models

Versions

Issue type

Actual behavior

I have two related models: Jogador and Responsavel, in which Jogador is the one referenced by Responsavel. I added a DefaultScope to Jogador, in which it retrieves all attributes and nested objects, including Responsavel; When it comes to Responsavel, i am trying to do the same: add a DefaultScope that retireves all attributes and the Jogador it is related to, but i cant do it because i keep getting the following error as soon as i add the DefaultScope to the ResponsavelModel (note the error only occurs once i add the DefaultScope to this one; if it is only in Jogador the error won't occur and the result will be as expected)

Expected behavior

It should let me query by default Jogador and retrieve it's Responsavel from database automatically; the same thing should happen with Responsavel, but in this case retrieving it's related Jogador.

Steps to reproduce

Start the sequelize-typescript instance and add DefaultScope to two related models

Related code

[tsconfig]
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon --exec ts-node src/index.ts --watch src"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/bcrypt": "^5.0.0",
    "@types/cors": "^2.8.14",
    "@types/express": "^4.17.17",
    "@types/jest": "^29.5.4",
    "@types/morgan": "^1.9.9",
    "@types/node": "^20.6.5",
    "@types/sequelize": "^4.28.15",
    "@types/supertest": "^2.0.12",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "jest": "^29.7.0",
    "morgan": "^1.10.0",
    "nodemon": "^3.0.1",
    "ts-jest": "^29.1.1",
    "ts-node": "^10.9.1",
    "tsc-alias": "^1.8.7",
    "typescript": "^5.1.6"
  },
  "include": [
    "src/**/*.ts"
  ],
  "dependencies": {
    "bcrypt": "^5.1.1",
    "cors": "^2.8.5",
    "helmet": "^7.1.0",
    "mysql2": "^3.6.1",
    "sequelize": "^6.33.0",
    "sequelize-typescript": "^2.1.6",
    "sqlite3": "^5.1.6",
    "tsconfig-paths": "^4.2.0",
    "utility-types": "^3.10.0",
    "zod": "^3.22.2"
  }
}

[sequelize-typescript instance]

import path from "path";
import { Dialect } from "sequelize";
import { Sequelize } from "sequelize-typescript";

if (!["sqlite", "postgres"].includes(process.env.DB_DIALECT as string)) throw new Error(`Database dialect "${process.env.DB_DIALECT}" not supported`);

let sequelizeConfig = {
    dialect: process.env.DB_DIALECT as Dialect,
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    models: [path.resolve(__dirname, '../models/')],
    storage: "" as string | undefined,
    username: "" as string | undefined,
    password: "" as string | undefined,
    database: "" as string | undefined,
    logging: Boolean(process.env.DB_LOGGING == "true") ? console.log : false,
}

switch (process.env.DB_DIALECT) {
    case "sqlite":
        sequelizeConfig = {
            ...sequelizeConfig,
            storage: process.env.DB_STORAGE,
        };
        break;
    case "postgres":
        sequelizeConfig = {
            ...sequelizeConfig,
            username: process.env.DB_USER,
            password: process.env.DB_PASS,
            database: process.env.DB_NAME,
        };
        break;
}

const sequelize = new Sequelize(sequelizeConfig);

sequelize.sync().then(() => {
    console.log(`\x1b[36m\nSuccessfully connected to database "${process.env.DB_NAME}" with "${process.env.DB_DIALECT}" dialect\n\x1b[0m`);
}).catch((error) => {
    console.log(`\x1b[31m\nError connecting to database "${process.env.DB_NAME}" with "${process.env.DB_DIALECT}" dialect\n\x1b[0m`);
    console.log(error);
});

export default sequelize;

[JogadorModel]

import { JogadorType } from '@lib/types/jogadorType';
import { AllowNull, Column, Length, Table, DataType, Model, ForeignKey, BelongsTo, HasMany, Min, Max, PrimaryKey, Unique, Scopes, DefaultScope, Default, HasOne} from "sequelize-typescript";

import ResponsavelModel from './responsavelModel';

@DefaultScope(() => ({
    include: {
        all: true,
        nested: true,
    }
}))
@Table({
    tableName: process.env.MODEL_JOGADOR_TABLE_NAME,
    paranoid: true,
})
export default class JogadorModel extends Model<JogadorType, Omit<JogadorType, "id">> {
    @PrimaryKey
    @Default(DataType.UUIDV4)
    @Column(DataType.UUIDV4)
    declare id: string;

    @Unique
    @AllowNull(false)
    @Length({ min: 11, max: 11 })
    @Column(DataType.STRING(11))
    declare cpf: string;   

    @AllowNull(false)
    @Length({ min: 1, max: 128 })
    @Column(DataType.STRING(128))
    declare nome_completo: string;

    @AllowNull(false)
    @Length({ min: 11, max: 13 })
    @Column(DataType.STRING(13))
    declare telefone: string;

    @AllowNull(false)
    @Length({ min: 1, max: 128 })
    @Column(DataType.STRING(128))
    declare email: string;

    @HasOne(() => ResponsavelModel, {
        onDelete: "CASCADE",
        onUpdate: "CASCADE",
    })
    declare responsavel: ResponsavelModel;
}

[ResponsavelModel]

import { ResponsavelType } from '@lib/types/responsavelType';
import { AllowNull, Column, Length, Table, DataType, Model, ForeignKey, BelongsTo, HasMany, Min, Max, PrimaryKey, Unique, Scopes, DefaultScope, Default, HasOne } from "sequelize-typescript";

import JogadorModel from './jogadorModel';

@DefaultScope(() => ({
    include: {
        all: true,
        nested: true,
    }
}))
@Table({
    tableName: process.env.MODEL_RESPONSAVEL_TABLE_NAME,
    paranoid: true,
})
export default class ResponsavelModel extends Model<ResponsavelType, Omit<ResponsavelType, "id">> {
    @PrimaryKey
    @Default(DataType.UUIDV4)
    @Column(DataType.UUIDV4)
    declare id: string;

    @Unique
    @AllowNull(false)
    @Length({ min: 11, max: 11 })
    @Column(DataType.STRING(11))
    declare cpf: string;

    @AllowNull(false)
    @Length({ min: 1, max: 128 })
    @Column(DataType.STRING(128))
    declare nome_completo: string;

    @AllowNull(false)
    @Length({ min: 11, max: 13 })
    @Column(DataType.STRING(13))
    declare telefone: string;

    @AllowNull(false)
    @Length({ min: 1, max: 128 })
    @Column(DataType.STRING(128))
    declare email: string;

    @ForeignKey(() => JogadorModel)
    @Column(DataType.UUIDV4)
    declare fk_jogador_id: string;

    @BelongsTo(() => JogadorModel, {
        onDelete: "CASCADE",
        onUpdate: "CASCADE",
    })
    declare jogador: JogadorModel;
}
caioalmeida12 commented 10 months ago

[Error]

TypeError: Cannot read properties of undefined (reading 'getTableName')
    at Function._validateIncludedElement (C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:611:30)     
    at C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:542:37
    at Array.map (<anonymous>)
    at Function._validateIncludedElements (C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:537:39)    
    at Function._validateIncludedElement (C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:732:38)     
    at C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:542:37
    at Array.map (<anonymous>)
    at Function._validateIncludedElements (C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:537:39)    
    at Function.findAll (C:\Users\caiod\Desktop\campeonato-municipal\server\node_modules\sequelize\src\model.js:1794:12)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
caioalmeida12 commented 10 months ago

Update: the issue seems to be related to recursive including one class onto another within the DefaultScopes, as i can workaround it by doing the following:

import { ResponsavelType } from '@lib/types/responsavelType';
import { AllowNull, Column, Length, Table, DataType, Model, ForeignKey, BelongsTo, HasMany, Min, Max, PrimaryKey, Unique, Scopes, DefaultScope, Default, HasOne } from "sequelize-typescript";

import JogadorModel from './jogadorModel';

@DefaultScope(() => ({
    include: [JogadorModel.unscoped()]
}))
@Table({
    tableName: process.env.MODEL_RESPONSAVEL_TABLE_NAME,
    paranoid: true,
})
export default class ResponsavelModel extends Model<ResponsavelType, Omit<ResponsavelType, "id">> {
    @PrimaryKey
    @Default(DataType.UUIDV4)
    @Column(DataType.UUIDV4)
    declare id: string;

    @Unique
    @AllowNull(false)
    @Length({ min: 11, max: 11 })
    @Column(DataType.STRING(11))
    declare cpf: string;

    @AllowNull(false)
    @Length({ min: 1, max: 128 })
    @Column(DataType.STRING(128))
    declare nome_completo: string;

    @AllowNull(false)
    @Length({ min: 11, max: 13 })
    @Column(DataType.STRING(13))
    declare telefone: string;

    @AllowNull(false)
    @Length({ min: 1, max: 128 })
    @Column(DataType.STRING(128))
    declare email: string;

    @ForeignKey(() => JogadorModel)
    @Column(DataType.UUIDV4)
    declare fk_jogador_id: string;

    @BelongsTo(() => JogadorModel, {
        onDelete: "CASCADE",
        onUpdate: "CASCADE",
    })
    declare jogador: JogadorModel;
}