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

Code-first approach future compatibility with ESM modules #2568

Open
1 task done
timofei-iatsenko opened this issue Dec 29, 2022 · 4 comments
Open
1 task done

Code-first approach future compatibility with ESM modules #2568

timofei-iatsenko opened this issue Dec 29, 2022 · 4 comments
Labels

Comments

@timofei-iatsenko
Copy link
Contributor

timofei-iatsenko commented Dec 29, 2022

Is there an existing issue that is already proposing this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe it

This idea of this discussion appeared after trying to adopt SWC for the testing purposes. In fact SWC stated as drop-in replacement for ts-node, and as long you don't use any of cli plugins you should be good to go.
Thanks to the fact SWC the only one from many "not-official typescript transpilers" who supports emitDecoratorMetadata.

However, naive adoption by replacing ts-node with SWC showed errors "Cannot access before iniitialization" root cause of which is explained here swc-project/swc#5047

In short The code is falling on circular references in GraphQLmodels:

// author.model.ts
import { ObjectType, Field } from '@nestjs/graphql';
import { BookModel } from './book.model';

@ObjectType()
export class AuthorModel {
  @Field(() => BookModel)
  book?: BookModel;
}

// book.model.ts
import { ObjectType, Field } from '@nestjs/graphql';
import { AuthorModel } from './author.model';

@ObjectType()
export class BookModel {
  @Field(() => AuthorModel)
  author: AuthorModel;
}

Prior @swc/core@1.2.206" it would work, but after is not.

Here is what the author said about it:

Your input is wrong. It's wrong because ESM is a standard

In other words, SWC transpiles as much as closer to ESM standard and this snippet of code would not work in native ESM modules either.
So there no action points from SWC sides, and it should be responsibility of framework authors to be nake it compatible with newer standards.

Describe the solution you'd like

Other backend projects such as TypeORM , also suffer from similar problem, but they have a clear solution for that which is described in the docs.

https://typeorm.io/#relations-in-esm-projects

So the point of this issue is to provide Official, Documented Solution, on how to solve circular refences problems in ESM projects. Something similar to Relation type in TypeORM.

How the Relation type might work under the hood?

I didn't read the original Relation sourcecode, but believe it might be implemented like that:

export type Relation<T> = T; 

It's effectively just voids metadata inlining in place where it used:

type Relation<T> = T;

@ObjectType()
export class MyModel {
  public myProperty: Relation<TestClass>;
}

      
// relation is not exists in runtime, so typeof Relation would be undefined
__metadata("design:type", typeof Relation === "undefined" ? Object : Relation)

Without Relation type:

@ObjectType()
export class MyModel {
  public myProperty: TestClass;
}

      
// Tries to access TestClass and therefore trigger TestClass initialization
__metadata("design:type", typeof TestClass === "undefined" ? Object : TestClass)

Teachability, documentation, adoption, migration strategy

No response

What is the motivation / use case for changing the behavior?

Sooner or later ecosystem should migrate to native ESM modules. NestJs is not an exclusion.

@dotoleeoak
Copy link
Contributor

It seems that the solution is quite simple. The Relation type of TypeORM is simply a single-type wrapper of a class.
https://github.com/typeorm/typeorm/blob/master/src/common/RelationType.ts

export type Relation<T> = T

This comment in SWC issue also suggests the same solution.
swc-project/swc#5047 (comment)

I'm not very familiar with this library, but maybe I can try to implement this.

@CarsonF
Copy link
Contributor

CarsonF commented Jul 28, 2023

I'm not sure how related this is to NestJS. I was able to work around this by doing

  author: AuthorModel & {};

To prevent the compiler from saving a runtime reference to that class for that property type.

@nodegin
Copy link

nodegin commented Aug 20, 2024

@dotoleeoak Thank you, adding Relation<> fixes the error ReferenceError: Cannot access 'CircularEntity' before initialization for me!

  @ManyToOne(() => ProductEntity, {
    nullable: false,
    onDelete: 'CASCADE',
  })
  @JoinColumn({ name: 'product_id' })
  product: Relation<ProductEntity>;

@doanthai
Copy link

You can try implicit decorator with CLI Plugin in Graphql case. Because root problem is swc and type that circular imports, so in this way we don't declare direct @Field.

  1. Create generate-metadata.ts file at src
/**
 * This file is responsible for generating Nest.js metadata for the API.
 * Metadata generation is required when using SWC with Nest.js due to SWC
 * not natively supporting Typescript, which is required to use the `reflect-metadata`
 * API and in turn, resolve types for the OpenAPI specification.
 *
 * @see https://docs.nestjs.com/recipes/swc#monorepo-and-cli-plugins
 */
import fs from 'node:fs';
import path from 'node:path';
import { PluginMetadataGenerator } from '@nestjs/cli/lib/compiler/plugins';
import { ReadonlyVisitor } from '@nestjs/graphql/dist/plugin';

const tsconfigPath = 'tsconfig.build.json';
const srcPath = path.join(__dirname, 'src');
const metadataPath = path.join(srcPath, 'metadata.ts');

/*
 * We create an empty metadata file to ensure that files importing `metadata.ts`
 * will compile successfully before the metadata generation occurs.
 */
const defaultContent = `export default async () => { return {}; };`;

fs.writeFileSync(metadataPath, defaultContent, 'utf8');
console.log('metadata.ts file has been generated with default content.');

const generator = new PluginMetadataGenerator();
generator.generate({
  visitors: [
    new ReadonlyVisitor({
      introspectComments: true,
      pathToSource: srcPath,
      typeFileNameSuffix: ['.input.ts', '.args.ts', '.entity.ts', '.model.ts', '.schema.ts'], // update your type file name suffix
    }),
  ],
  outputDir: srcPath,
  tsconfigPath,
});

  1. Remove all @Field in field that need circular imports. Example:
// author.model.ts
import { ObjectType, Field } from '@nestjs/graphql';
import { BookModel } from './book.model';

@ObjectType()
export class AuthorModel {
  // @Field(() => BookModel) => not necessary with @graphlq/plugin auto generate metadata
  book?: BookModel;
}

// book.model.ts
import { ObjectType, Field } from '@nestjs/graphql';
import { AuthorModel } from './author.model';

@ObjectType()
export class BookModel {
//  @Field(() => AuthorModel)  => not necessary with @graphlq/plugin auto generate metadata
  author: AuthorModel;
}

4.Generate metadata.ts with command ts-node ./src/generate-metadata.ts. And new file metadata.ts will be generated at src.
5. Import metadata() to GraphQL module.

import metadata from './metadata'; // <-- file auto-generated by the "PluginMetadataGenerator"

GraphQLModule.forRoot<...>({
  ..., // other options
  metadata,
}),

In case you want to add option look like @Field(() => BookModel, { defaultValue: {} }), you can edit direct in metadata.ts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants