sequelize-typescript icon indicating copy to clipboard operation
sequelize-typescript copied to clipboard

Cannot access [Model] before initialization

Open alsonkemp opened this issue 4 years ago • 23 comments

Versions

  • sequelize: 4.44.4
  • sequelize-typescript: 1.1.0
  • typescript: 3.9.2

I'm submitting a ...

[X] bug report [ ] feature request

Actual behavior:

$ ./node_modules/.bin/ts-node test.ts ReferenceError: Cannot access 'Team' before initialization

Expected behavior:

No error.

Steps to reproduce: Use (basically) the code from the README (https://github.com/RobinBuschmann/sequelize-typescript#one-to-many) as test.ts:

import {BelongsTo, Column, ForeignKey, HasMany, Model, Table} from "sequelize-typescript";

@Table
export class Player extends Model<Player> {

  @Column
  name!: string;

  @Column
  num!: number;

  @ForeignKey(() => Team)
  @Column
  teamId!: number;

  @BelongsTo(() => Team)
  team!: Team;
}

@Table
export class Team extends Model<Team> {

  @Column
  name!: string;

  @HasMany(() => Player)
  players!: Player[];
}

tsconfig.json:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"],
    "noImplicitAny": true,
    "module": "commonjs",
    "outDir": "_dist",
    "rootDir": ".",
    "skipLibCheck": true,
    "sourceMap": false,
    "strict": true,
    "strictNullChecks": true,
    "target": "ES2019"
  }
}

alsonkemp avatar Aug 13 '20 01:08 alsonkemp

Switching to Sequelize({config: {models: ... } }) seems to work around this but I'm not sure why/how?

alsonkemp avatar Aug 13 '20 16:08 alsonkemp

I resolved that just putting each model in different files. It is not a sequelize issue, seems like the problem is on typescript compilation with emitDecoratorMetadata: true option.

yassernasc avatar Jan 11 '21 16:01 yassernasc

I have all my entities in different files but I'm getting the same issue?!

EdByrnee avatar Feb 11 '21 14:02 EdByrnee

Hi,

I have the same issue and it's really blocking me. The weird part is that it seems very inconsistent.

  • Models are all in different files
  • I use Sequelize({config: {models: ... } })

Yet I still have this error. Please help :-/

TPME avatar Mar 24 '21 13:03 TPME

Have the same problem in my code. Worked with Webpack4 and since upgrding to Webpack 5 this seems to be a problem. To what I figured out so far it seems to be related to circular dependencies due to the Foreign Key relations...

The way I solved it for now was merging all my linked model files into one - not nice - but it works

tomfree avatar May 07 '21 13:05 tomfree

The issue still happens for me when models are in different files but only when module in tsconfig.json is set to any ES version. It does not happen when targeting CommonJS. Maybe @TPME and @EdByrnee can confirm whether they're also targeting ESM. It seems like using the () => Model as a workaround for avoiding circular dependencies only works in CommonJS.

nathanlepori avatar Jul 07 '21 13:07 nathanlepori

I'm having the same issue. Trying the hardest to get it resolved but can't get it working. I'm also using @nestjs/sequelize which does not make it easier because sequelize is encapsulated in there.

Any process or new info on how to resolve this issue?

[EDIT] @nathanlepori i can confirm this only happens when targeting ESM. And yes it seems to be related to the () => Model describer.

schealex avatar Jul 29 '21 12:07 schealex

I was using @nestjs/sequelize. Got same error. Updating module:es2020 and target:es2015 to module: commonjs and target: es2017 solved the issue.

RahmatAliMalik5 avatar Aug 09 '21 15:08 RahmatAliMalik5

I was using @nestjs/sequelize. Got same error. Updating module:es2020 and target:es2015 to module: commonjs and target: es2017 solved the issue.

It does not because changes the target type to commonjs instead of ESM which is not what we want. If you want to build an ESM module that is not a solution!

schealex avatar Aug 09 '21 17:08 schealex

I am not providing a solution to this issue. I am confirming that it solves when commonjs is used instead of ESM.

RahmatAliMalik5 avatar Aug 13 '21 17:08 RahmatAliMalik5

so there is no future for this project with the now more and more coming ESM projects?

schealex avatar Aug 13 '21 19:08 schealex

TL;DR: see workaround code snippet.

Without in-depth knowledge of the code I'm not sure if this could work but I'm suggesting it anyways if someone wants to try to implement it.

One way of solving the issue might be to allow passing just the model name on one side of the association and keep passing the model getter on the other (basically a TS overload + a typeof modelGetter === 'string' check). This way the circular dependency would be avoided (no import required on one side in emitted JS code) and sequelize-typescript could resolve the model constructor from its name from the already registered models. One problem with this approach is what would happen if the model associating by name is defined before the one associating by getter: in that case sequelize-typescript wouldn't have any constructor to resolve yet... And this isn't really under the user's control since the import order is established by Node.

With that said unfortunately I don't have the time to create a PR myself at the moment but I might do it some time in the future since I will also need this feature. In the meantime maybe @RobinBuschmann has some suggestions on the feasibility of this approach/other possible solutions...

For now though I also suggest a workaround I'm planning on using myself: It's possible to associate model A with model B normally in A.ts (using the import + decorator). Then leave out the association completely in B.ts and add the association for B in A.ts using regular old sequelize.

Here's an example:

// A.ts
import B from './B';

class A extends Model {
  @HasMany(() => B)
  foo!: B[];
}
// Put association in A.ts
B.belongsTo(A);

export default A;

// B.ts
import A from './A';

class B extends Model {
  // Omit association. TS compiler should omit import for A in emitted JS since it's only used as type.
  // @BelongsTo(() => A)
  baz!: A;
}

export default B;

It's not super pretty but it should work. Let me know what do you think/any possible issue with either approach.

nathanlepori avatar Aug 15 '21 14:08 nathanlepori

Similar issue, only thing I could get to work was to add using belongsTo directly from created Sequelize instance

// instance sequelize = Sequelize({config: {models: ... } })

// create the relationship sequelize.model(Player).belongsTo(Team);

nrgxp-rbowen avatar Oct 29 '21 17:10 nrgxp-rbowen

What should be happening to avoid this is to call the @ForeignKey, @HasMany etc function with the models object after sequelize model loading so this could function after static initialization and before sync.

I don't buy the whole circular references can't be avoided or don't matter argument. They do matter and they can be avoided.

I always used this snippet before trying the typescript annotation approach, using a static associate function on each model that deals with association for that model. No circular dependencies necessary.

const sequelize = new Sequelize(dbConnectString, options);

const models = loadModels(sequelize); // dynamic model file reading and static initialization 

Object.keys(models).forEach(key => {
        // simplified version
	const model = models[key];
	if (model.associate) {
		model.associate(models);
	}
});

// Account Model snippet 

static associate = models => {
    Account.hasMany(models.AccountTenant, {
        foreignKey: 'accountId',
        onDelete: 'cascade',
        as: 'roles',
    });
}

mschipperheyn avatar Jan 24 '22 20:01 mschipperheyn

The root cause of all these issues is the requirement of recursive imports. This PR (https://github.com/RobinBuschmann/sequelize-typescript/pull/1206) solve that in part. But in order to completely solve this, I think the returned reference from an association should be a generic model based on the interface.

So, the final solution could look something like this:


@Table
export class Player extends Model<IPlayer> {

  @Column
  name!: string;

  @Column
  num!: number;

  @ForeignKey(models => models.Team)
  @Column
  teamId!: number;

  @BelongsTo(models => models.Team)
  team!: Model<ITeam>;
}

@Table
export class Team extends Model<ITeam> {

  @Column
  name!: string;

  @HasMany(models => models.Player)
  players!: Model<IPlayer>[];
}

mschipperheyn avatar Feb 20 '22 15:02 mschipperheyn

I'm trying to remove emitDecoratorMetadata option. If you set types/table names manually it can be a solution as well

gugu avatar Mar 06 '22 21:03 gugu

I've got another workaround.

I've noticed that it is possible to reference classes declared later when defining arrays

  // this works
  @HasMany(() => AuctionBid)
  public bids: AuctionBid[];

  // this does not work
  @HasOne(() => AuctionBid)
  public lastBid: AuctionBid;
                  ^ ReferenceError: Cannot access 'AuctionBid' before initialization

Which brought me to this:

  @HasOne(() => AuctionBid)
  public usersBid: ReturnType<() => AuctionBid>;

Compiles without errors.

strrife avatar Jun 06 '22 04:06 strrife

Nice find @strrife!

The following alternative works too.

  @HasOne(() => AuctionBid)
  public usersBid: Awaited<AuctionBid>;

remcohaszing avatar Jul 06 '22 13:07 remcohaszing

I've got another workaround.

I've noticed that it is possible to reference classes declared later when defining arrays

  // this works
  @HasMany(() => AuctionBid)
  public bids: AuctionBid[];

  // this does not work
  @HasOne(() => AuctionBid)
  public lastBid: AuctionBid;
                  ^ ReferenceError: Cannot access 'AuctionBid' before initialization

Which brought me to this:

  @HasOne(() => AuctionBid)
  public usersBid: ReturnType<() => AuctionBid>;

Compiles without errors.

i did change the Model to Model[] and it works.. or adding union types null also work

in my case i was trying some dirty tricks which brought upon a flawed table design that was given to me in my project while still using ORM. I get this problem when trying to re use the many-to-many intermediate table as a bridge to another table.

untimated avatar Jan 14 '23 02:01 untimated

In my case changing the order of how you import these models on the same file you use new Sequelize() solved the problem, I'd suggest having a look at this repo's example because it does the exact same thing and their version works even though it needs updating to the latest version.

jose-verissimo avatar Mar 07 '23 07:03 jose-verissimo

I don't believe in ESLINT is not screaming: "ESLint: Dependency cycle detected. (import/no-cycle)" on "import" lines on these last workarounds!

d00rsfan avatar Jul 04 '23 11:07 d00rsfan

In my case changing the order of how you import these models on the same file you use new Sequelize() solved the problem, I'd suggest having a look at this repo's example because it does the exact same thing and their version works even though it needs updating to the latest version.

I was getting the same issue, with this change worked fine to me, thanks!

Matheusleal avatar Dec 10 '23 22:12 Matheusleal

In case anyone reads this issue, I used forwardRef https://docs.nestjs.com/fundamentals/circular-dependency

Oxicode avatar Apr 01 '24 18:04 Oxicode