Search code examples
typescriptwebpacknext.jstypeorm

TypeScript metadata reflection references other classes before they are defined


I have some TypeORM entities in my codebase which have relations to each other, making a circular dependency. Since decorator metadata is used on each entity class, TypeScript inserts code after each class defining metadata on it. Say that the classes are Business and Qualification. On the relating fields TypeScript will emit code that looks like this:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
const decorator = (target, thing) => {
};
class Business {
}
class Qualification {
}
__decorate([
    decorator,
    __metadata("design:type", Business)
], Qualification.prototype, "business", void 0);

This would all be fine, except the __decorate part always comes after each class, meaning that one of the classes is going to have to be used before it's defined, which causes an error. Here's a shortened version of the actual code with the actual error:

let Qualification = (_dec = Object(external_typeorm_["Entity"])(), _dec2 = Object(external_typeorm_["PrimaryGeneratedColumn"])(), _dec3 = Reflect.metadata("design:type", Number), _dec4 = Object(external_typeorm_["Column"])({
  nullable: true
}), _dec5 = Object(external_class_validator_["IsOptional"])(), _dec6 = Object(external_class_validator_["IsUrl"])(), _dec7 = Reflect.metadata("design:type", String), _dec8 = Object(external_typeorm_["ManyToOne"])(type => Business["c" /* default */], business => business.qualifications, {
  onDelete: 'CASCADE'
}), _dec9 = Reflect.metadata("design:type", typeof Business["c" /* default */] === "undefined" ?

                                                // ^ TypeError: cannot read property "c" of undefined



 Object : Business["c" /* default */]), _dec10 = Object(external_typeorm_["Column"])({
  type: 'enum',
  enum: VALIDITY_STATES,
  default: 'invalid'
}), _dec11 = Object(external_class_validator_["IsIn"])(VALIDITY_STATES), _dec12 = Reflect.metadata("design:type", Object), _dec13 = Object(external_typeorm_["Column"])('simple-json'), _dec14 = Object(external_class_validator_["ValidateNested"])(), _dec15 = Object(external_class_validator_["IsArray"])(), _dec16 = Object(external_class_validator_["IsIn"])(category["a" /* CATEGORIES */].filter(c => c.type === 'service').map(c => c.slug), {
  each: true
}), _dec17 = Reflect.metadata("design:type", Array), _dec(_class = (_class2 = (_temp = class Qualification {
  constructor() {
    _initializerDefineProperty(this, "id", _descriptor, this);

    _initializerDefineProperty(this, "imageUrl", _descriptor2, this);

    _initializerDefineProperty(this, "business", _descriptor3, this);

    _initializerDefineProperty(this, "validity", _descriptor4, this);

    _initializerDefineProperty(this, "categories", _descriptor5, this);
  }

}, _temp), (_descriptor = _applyDecoratedDescriptor(_class2.prototype, "id", [_dec2, _dec3], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor2 = _applyDecoratedDescriptor(_class2.prototype, "imageUrl", [_dec4, _dec5, _dec6, _dec7], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor3 = _applyDecoratedDescriptor(_class2.prototype, "business", [_dec8, _dec9], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor4 = _applyDecoratedDescriptor(_class2.prototype, "validity", [_dec10, _dec11, _dec12], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor5 = _applyDecoratedDescriptor(_class2.prototype, "categories", [_dec13, _dec14, _dec15, _dec16, _dec17], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
})), _class2)) || _class);

Later in the code, Business is defined, but it's too late:

let Business = (_dec6 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Entity"])(), _dec7 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])(), _dec8 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsString"])(), _dec9 = Reflect.metadata("design:type", String), _dec10 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  type: 'enum',
  enum: BUSINESS_TYPES
}), _dec11 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsIn"])(BUSINESS_TYPES), _dec12 = Reflect.metadata("design:type", Object), _dec13 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])(), _dec14 = Reflect.metadata("design:type", Boolean), _dec15 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["CreateDateColumn"])(), _dec16 = Reflect.metadata("design:type", typeof Date === "undefined" ? Object : Date), _dec17 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  nullable: true
}), _dec18 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsOptional"])(), _dec19 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsUrl"])(), _dec20 = Reflect.metadata("design:type", String), _dec21 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  nullable: true
}), _dec22 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsOptional"])(), _dec23 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsString"])(), _dec24 = Reflect.metadata("design:type", String), _dec25 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  nullable: true
}), _dec26 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsOptional"])(), _dec27 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsString"])(), _dec28 = Reflect.metadata("design:type", String), _dec29 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])(), _dec30 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsString"])(), _dec31 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["MaxLength"])(200), _dec32 = Reflect.metadata("design:type", String), _dec33 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["OneToMany"])(type => _db_all_entities__WEBPACK_IMPORTED_MODULE_3__[/* Qualification */ "i"], qualification => qualification.business, {
  cascade: true
}), _dec34 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["ValidateNested"])(), _dec35 = Reflect.metadata("design:type", Array), _dec36 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])('simple-json'), _dec37 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsArray"])(), _dec38 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["ValidateNested"])(), _dec39 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsIn"])(_misc_types_category__WEBPACK_IMPORTED_MODULE_2__[/* CATEGORIES */ "a"].filter(c => c.type === 'business').map(c => c.slug), {
  each: true
}), _dec40 = Reflect.metadata("design:type", Array), _dec41 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])('simple-json'), _dec42 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsArray"])(), _dec43 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["ValidateNested"])(), _dec44 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["ArrayMinSize"])(1), _dec45 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["ArrayMaxSize"])(5), _dec46 = Reflect.metadata("design:type", Array), _dec47 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  type: 'enum',
  enum: PRICING_PLANS
}), _dec48 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsIn"])(PRICING_PLANS), _dec49 = Reflect.metadata("design:type", String), _dec50 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["OneToMany"])(type => _db_all_entities__WEBPACK_IMPORTED_MODULE_3__[/* BaseOffer */ "b"], offer => offer.offerer), _dec51 = Reflect.metadata("design:type", Array), _dec6(_class3 = (_class4 = (_temp2 = class Business extends _db_all_entities__WEBPACK_IMPORTED_MODULE_3__[/* Account */ "a"] {
  constructor(...args) {
    super(...args);

    _initializerDefineProperty(this, "name", _descriptor3, this);

    _initializerDefineProperty(this, "type", _descriptor4, this);

    _initializerDefineProperty(this, "isApproved", _descriptor5, this);

    _initializerDefineProperty(this, "since", _descriptor6, this);

    _initializerDefineProperty(this, "logoUrl", _descriptor7, this);

    _initializerDefineProperty(this, "fein", _descriptor8, this);

    _initializerDefineProperty(this, "phoneNumber", _descriptor9, this);

    _initializerDefineProperty(this, "bio", _descriptor10, this);

    _initializerDefineProperty(this, "qualifications", _descriptor11, this);

    _initializerDefineProperty(this, "businessCategories", _descriptor12, this);

    _initializerDefineProperty(this, "geolocations", _descriptor13, this);

    _initializerDefineProperty(this, "pricingPlan", _descriptor14, this);

    _initializerDefineProperty(this, "offers", _descriptor15, this);
  }

}, _temp2), (_descriptor3 = _applyDecoratedDescriptor(_class4.prototype, "name", [_dec7, _dec8, _dec9], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor4 = _applyDecoratedDescriptor(_class4.prototype, "type", [_dec10, _dec11, _dec12], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor5 = _applyDecoratedDescriptor(_class4.prototype, "isApproved", [_dec13, _dec14], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor6 = _applyDecoratedDescriptor(_class4.prototype, "since", [_dec15, _dec16], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor7 = _applyDecoratedDescriptor(_class4.prototype, "logoUrl", [_dec17, _dec18, _dec19, _dec20], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor8 = _applyDecoratedDescriptor(_class4.prototype, "fein", [_dec21, _dec22, _dec23, _dec24], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor9 = _applyDecoratedDescriptor(_class4.prototype, "phoneNumber", [_dec25, _dec26, _dec27, _dec28], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor10 = _applyDecoratedDescriptor(_class4.prototype, "bio", [_dec29, _dec30, _dec31, _dec32], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor11 = _applyDecoratedDescriptor(_class4.prototype, "qualifications", [_dec33, _dec34, _dec35], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor12 = _applyDecoratedDescriptor(_class4.prototype, "businessCategories", [_dec36, _dec37, _dec38, _dec39, _dec40], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor13 = _applyDecoratedDescriptor(_class4.prototype, "geolocations", [_dec41, _dec42, _dec43, _dec44, _dec45, _dec46], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor14 = _applyDecoratedDescriptor(_class4.prototype, "pricingPlan", [_dec47, _dec48, _dec49], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor15 = _applyDecoratedDescriptor(_class4.prototype, "offers", [_dec50, _dec51], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
})), _class4)) || _class3);

Strangely, the code works when compiled for development mode because Business is not referred to directly but rather through a module constant. Here's how Qualification is defined in development mode:

let Qualification = (_dec = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Entity"])(), _dec2 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["PrimaryGeneratedColumn"])(), _dec3 = Reflect.metadata("design:type", Number), _dec4 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  nullable: true
}), _dec5 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsOptional"])(), _dec6 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsUrl"])(), _dec7 = Reflect.metadata("design:type", String), _dec8 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["ManyToOne"])(type => _db_all_entities__WEBPACK_IMPORTED_MODULE_2__["Business"], business => business.qualifications, {
  onDelete: 'CASCADE'
}), _dec9 = Reflect.metadata("design:type", typeof _db_all_entities__WEBPACK_IMPORTED_MODULE_2__["Business"] === "undefined" ? Object : _db_all_entities__WEBPACK_IMPORTED_MODULE_2__["Business"]), _dec10 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])({
  type: 'enum',
  enum: VALIDITY_STATES,
  default: 'invalid'
}), _dec11 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsIn"])(VALIDITY_STATES), _dec12 = Reflect.metadata("design:type", Object), _dec13 = Object(typeorm__WEBPACK_IMPORTED_MODULE_1__["Column"])('simple-json'), _dec14 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["ValidateNested"])(), _dec15 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsArray"])(), _dec16 = Object(class_validator__WEBPACK_IMPORTED_MODULE_0__["IsIn"])(_misc_types_category__WEBPACK_IMPORTED_MODULE_5__["CATEGORIES"].filter(c => c.type === 'service').map(c => c.slug), {
  each: true
}), _dec17 = Reflect.metadata("design:type", Array), _dec(_class = (_class2 = (_temp = class Qualification {
  constructor() {
    _initializerDefineProperty(this, "id", _descriptor, this);

    _initializerDefineProperty(this, "imageUrl", _descriptor2, this);

    _initializerDefineProperty(this, "business", _descriptor3, this);

    _initializerDefineProperty(this, "validity", _descriptor4, this);

    _initializerDefineProperty(this, "categories", _descriptor5, this);
  }

}, _temp), (_descriptor = _applyDecoratedDescriptor(_class2.prototype, "id", [_dec2, _dec3], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor2 = _applyDecoratedDescriptor(_class2.prototype, "imageUrl", [_dec4, _dec5, _dec6, _dec7], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor3 = _applyDecoratedDescriptor(_class2.prototype, "business", [_dec8, _dec9], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor4 = _applyDecoratedDescriptor(_class2.prototype, "validity", [_dec10, _dec11, _dec12], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
}), _descriptor5 = _applyDecoratedDescriptor(_class2.prototype, "categories", [_dec13, _dec14, _dec15, _dec16, _dec17], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: null
})), _class2)) || _class);

The actual code itself imports modules from an all-entities.ts file, which exports all the entities in the correct order so that superclasses don't accidentally get loaded after their subclasses, causing errors. That file looks like this (simplified):

export { default as Qualification } from '../entities/Qualification';
export { default as Business } from '../entities/Business';

./entities/Qualification.ts and ./entities/Business.ts are both files that contain a default export of a TypeORM entity, and I don't feel like they're worth including here but I can if anyone wants to look at them. Here's the difference between my production and development webpack configs (generated by Next.js):

diff --git a/webpack-config-dev.txt b/webpack-config-prod.txt
index f8a28c3..8e5fa4d 100644
--- a/webpack-config-dev.txt
+++ b/webpack-config-prod.txt
@@ -1,80 +1,82 @@
 {
   externals: [ [Function] ],
   optimization: {
     checkWasmTypes: false,
     nodeEnv: false,
     splitChunks: false,
     runtimeChunk: undefined,
     minimize: false,
     minimizer: [ [TerserPlugin], [CssMinimizerPlugin] ]
   },
   context: 'C:\\Users\\Robbie\\Code\\fit-society',
   node: { setImmediate: false },
   entry: [AsyncFunction: entry],
   output: {
     path: 'C:\\Users\\Robbie\\Code\\fit-society\\.next\\server',
     filename: [Function: filename],
     libraryTarget: 'commonjs2',
     hotUpdateChunkFilename: 'static/webpack/[id].[hash].hot-update.js',
     hotUpdateMainFilename: 'static/webpack/[hash].hot-update.json',
-    chunkFilename: '[name].js',
+    chunkFilename: '[name].[contenthash].js',
     strictModuleExceptionHandling: true,
     crossOriginLoading: undefined,
-    futureEmitAssets: false,
+    futureEmitAssets: true,
     webassemblyModuleFilename: 'static/wasm/[modulehash].wasm'
   },
   performance: false,
   resolve: {
     extensions: [
       '.tsx',  '.ts',
       '.js',   '.mjs',
       '.jsx',  '.json',
       '.wasm'
     ],
     modules: [ 'node_modules' ],
     alias: {
       'next/head': 'next/dist/next-server/lib/head.js',
       'next/router': 'next/dist/client/router.js',
       'next/config': 'next/dist/next-server/lib/runtime-config.js',
       'next/dynamic': 'next/dist/next-server/lib/dynamic.js',
       next: 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next',
       'private-next-pages': 'C:\\Users\\Robbie\\Code\\fit-society\\src\\pages',
       'private-dot-next': 'C:\\Users\\Robbie\\Code\\fit-society\\.next'
     },
     mainFields: [ 'main', 'module' ],
     plugins: [ [Object] ]
   },
   resolveLoader: {
     alias: {
       'emit-file-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\emit-file-loader',
       'error-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\error-loader',
       'next-babel-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\next-babel-loader',
       'next-client-pages-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\next-client-pages-loader',
       'next-data-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\next-data-loader',
       'next-serverless-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\next-serverless-loader',
       'noop-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\noop-loader',
       'next-plugin-loader': 'C:\\Users\\Robbie\\Code\\fit-society\\node_modules\\next\\dist\\build\\webpack\\loaders\\next-plugin-loader'
     },
     modules: [ 'node_modules' ],
     plugins: [ [Object] ]
   },
   module: {
     rules: [ [Object], [Object], [Object] ],
     strictExportPresence: true
   },
   plugins: [
     ChunkNamesPlugin {},
     DefinePlugin { definitions: [Object] },
-    UnlinkRemovedPagesPlugin { prevAssets: {} },
-    NoEmitOnErrorsPlugin {},
-    NextJsRequireCacheHotReloader { prevAssets: null },
+    HashedModuleIdsPlugin { options: [Object] },
+    IgnorePlugin {
+      options: [Object],
+      checkIgnore: [Function: bound checkIgnore]
+    },
     PagesManifestPlugin { serverless: false },
     NextJsSsrImportPlugin { options: [Object] },
     NextJsSsrImportPlugin {},
     FilterWarningsPlugin { exclude: [Array] }
   ],
-  mode: 'development',
+  mode: 'production',
   name: 'server',
   target: 'node',
-  devtool: 'cheap-module-source-map'
+  devtool: false
 }

Here are the classes that are causing the problem: Business.ts:

import {
  ArrayMaxSize,
  ArrayMinSize,
  IsArray,
  IsIn,
  IsOptional,
  IsString,
  IsUrl,
  MaxLength,
  ValidateNested,
  IsEmail
} from 'class-validator';
import { Column, CreateDateColumn, Entity, OneToMany } from 'typeorm';
import { CATEGORIES } from '../../misc-types/category';
import { Geolocation } from '../../misc-types/geolocation';
import { Account, BaseOffer, Qualification, ValidateableQualification } from '../db/all-entities';
import { omit } from './utils/entity-type-manipulations';
import tuple from './utils/string-enum-from-tuple';

const BUSINESS_TYPES = tuple('individual', 'company');
const PRICING_PLANS = tuple('free');

@Entity()
export default class Business extends Account {
  /**
   * Public name for the business.
   */
  @Column()
  @IsString()
  name!: string;

  @Column({ type: 'enum', enum: BUSINESS_TYPES })
  @IsIn(BUSINESS_TYPES)
  type!: typeof BUSINESS_TYPES[number];

  @Column()
  isApproved!: boolean;

  /**
   * The date the business created their account (not when it was approved)
   */
  @CreateDateColumn()
  since!: Date;

  @Column({ nullable: true })
  @IsOptional()
  @IsUrl()
  logoUrl?: string;

  @Column({ nullable: true })
  @IsOptional()
  @IsString()
  fein?: string;

  @Column({ nullable: true })
  @IsOptional()
  @IsString()
  phoneNumber?: string;

  @Column()
  @IsString()
  @MaxLength(200)
  bio!: string;

  @OneToMany(
    type => Qualification,
    qualification => qualification.business,
    { cascade: true }
  )
  @ValidateNested()
  qualifications!: Qualification[];

  @Column('simple-json')
  @IsArray()
  @ValidateNested()
  @IsIn(
    CATEGORIES.filter(c => c.type === 'business').map(c => c.slug),
    { each: true }
  )
  businessCategories!: string[];

  /**
   * Places this business is available at
   */
  @Column('simple-json')
  @IsArray()
  @ValidateNested()
  @ArrayMinSize(1)
  @ArrayMaxSize(5)
  geolocations!: Geolocation[];

  @Column({ type: 'enum', enum: PRICING_PLANS })
  @IsIn(PRICING_PLANS)
  pricingPlan!: 'free';

  @OneToMany(
    type => BaseOffer,
    offer => offer.offerer
  )
  offers!: BaseOffer[];
}

/**
 * A DTO sent to change business properties, most of which align one-to-one (excluding password/passwordHash).
 */
export class EditableBusiness extends omit(Business, [
  'id',
  'since',
  'isApproved',
  'qualifications',
  'passwordHash',
  'offers'
]) {
  @IsString()
  password!: string;
}

export class BusinessApplication extends EditableBusiness {
  @ValidateNested()
  @IsOptional()
  initialQualification!: ValidateableQualification;
}

And in Qualification.ts:

import { IsJSON, IsUrl, ValidateNested, IsOptional, IsBoolean, IsIn, IsArray } from 'class-validator';
import { Column, Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import { Business } from '../db/all-entities';
import tuple from './utils/string-enum-from-tuple';
import { omit } from './utils/entity-type-manipulations';
import { CATEGORIES } from '../../misc-types/category';

const VALIDITY_STATES = tuple('valid', 'pending-review', 'invalid');

@Entity()
export default class Qualification {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ nullable: true })
  // businesses do not need image proof
  @IsOptional()
  @IsUrl()
  imageUrl?: string;

  @ManyToOne(
    type => Business,
    business => business.qualifications,
    { onDelete: 'CASCADE' }
  )
  business!: Business;

  @Column({ type: 'enum', enum: VALIDITY_STATES, default: 'invalid' })
  @IsIn(VALIDITY_STATES)
  validity!: typeof VALIDITY_STATES[number];

  /**
   * The categories (slugs)
   */
  @Column('simple-json')
  @ValidateNested()
  @IsArray()
  @IsIn(
    CATEGORIES.filter(c => c.type === 'service').map(c => c.slug),
    { each: true }
  )
  categories!: string[];
}

/**
 * A qualification that can be sent by a business which is not necessarily verified yet.
 */
export const ValidateableQualification = omit(Qualification, ['id', 'business', 'validity']);
export type ValidateableQualification = typeof ValidateableQualification extends new () => infer U ? U : never;

Whenever either of these classes are imported, they're imported from this file to ensure the proper module loading order:

/* eslint-disable import/first */
/**
 * This file exists to solve circular dependency problems with Webpack by explicitly specifying the module loading order.
 * @see https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
 */

export { default as Qualification, ValidateableQualification } from '../entities/Qualification';

export { default as Account } from '../entities/Account';
export { default as Business, EditableBusiness, BusinessApplication } from '../entities/Business';
export { default as Customer } from '../entities/Customer';

export { default as BaseOffer } from '../entities/Offer';

import ProductOffer, { EditableProductOffer } from '../entities/ProductOffer';
import ServiceOffer, { EditableServiceOffer } from '../entities/ServiceOffer';

export { default as ProductOffer, EditableProductOffer } from '../entities/ProductOffer';
export { default as ServiceOffer, EditableServiceOffer } from '../entities/ServiceOffer';

export type Offer = ProductOffer | ServiceOffer;
export type EditableOffer = EditableProductOffer | EditableServiceOffer;

Babel is also used in this project. Here's the .babelrc:

{
  "presets": [
    [
      "next/babel",
      {
        "class-properties": {
          "loose": true
        },
        "styled-jsx": {
          "plugins": [
            "styled-jsx-plugin-postcss"
          ]
        }
      }
    ]
  ],
  "plugins": [
    "babel-plugin-transform-typescript-metadata",
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ]
  ]
}

Sorry for the huge wads of code. Could anybody help me try to solve this and figure out how to make it work in production like it works in development? Thanks.


Solution

  • EDIT: I've seen you're using Qualification as a value in the ValidateableQualification. This sounds like something you can do in JS, but I think this will mess up with TS inheritance / compilation, since using Qualification as a value and not a type force TS to import the actual code while webpack bundling is done.

    Furthermore, maybe you can do this with class-validator by itself or an extended class.

    export const ValidateableQualification = omit(Qualification, ['id', 'business', 'validity']);
    

    Can you try to remove this code, and see if circular dependency still rise?

    Furthermore, I've read you are importing all entities from a central file. Even if this sounds like a good thing to resolve circular dependencies, it may lead to bugs since there is the chance you are going to import values instead of types. I suggest you to not use something like that to import entities between them, just use a central file like RelationalEntities.ts and do:

    export const RelationalEntities = [
      Qualification,
      Business,
      // ...
    ]
    

    and use this in TypeORM database connection configuration, i.e. entities: RelationalEntities

    The old answer, before seeing updated code and config:

    Usually, this is solved in TypeORM just using type => Type in the relationship definition, instead of true type Type. i.e.:

    @OneToMany(type => Qualification)
    qualification!: Qualification;
    
    // instead of (will not work)
    @OneToMany(Qualification)
    qualification!: Qualification;
    

    This is due to TS working, so that type => Qualification is essentially used only to extract metadata about Qualification, without referencing it at runtime (or, better, without referencing it directly at first run and so since it is lazy, no circular dependency is present)