webpack - TypeScript metadata-reflektion refererar till andra klasser innan de är definierade


Jag har några TypeORM-enheter i min kodbas som har relationer till varandra, vilket skapar ett cirkulärt beroende. Eftersom dekoratormetadata används på varje enhetsklass, infogar TypeScript kod efter varje klass som definierar metadata på den. Antag att klasserna är Business och Qualification. På de relaterade fälten kommer TypeScript att generera kod som ser ut så här:

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);

Det här skulle allting vara bra, förutom att __decorate-delen alltid kommer efter varje klass, vilket betyder att en av klasserna kommer att behöva användas innan den är definierad, vilket orsakar ett fel. Här är en förkortad version av den faktiska koden med det faktiska felet:

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);

Senare i koden definieras Business, men det är för sent:

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);

Konstigt nog fungerar koden när den kompileras för utvecklingsläge eftersom Business inte refereras till direkt utan snarare genom en modulkonstant. Så här definieras Qualification i utvecklingsläge:

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);

Själva koden importerar moduler från en all-entities.ts-fil, som exporterar alla enheter i rätt ordning så att superklasser inte av misstag laddas efter sina underklasser, vilket skulle orsaka fel. Den filen ser ut så här (förenklat):

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

./entities/Qualification.ts och ./entities/Business.ts är båda filer som innehåller en standardexport av en TypeORM-entitet, och jag tycker inte att de är värda att inkludera här men jag kan göra det om någon vill titta på dem. Här är skillnaden mellan mina produktions- och utvecklingskonfigurationer för webpack (genererade av 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
 }

Här är klasserna som orsakar problemet: 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;
}

Och i 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;

När någon av dessa klasser importeras importeras de från denna fil för att säkerställa rätt modul-laddningsordning:

/* 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 används också i detta projekt. Här är .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
      }
    ]
  ]
}

Förlåt för de stora mängderna kod. Kan någon hjälpa mig att försöka lösa detta och lista ut hur man får det att fungera i produktion som det fungerar i utveckling? Tack.

Svar #1

REDIGERA: Jag har sett att du använder Qualification som ett värde i ValidateableQualification. Detta låter som något du kan göra i JS, men jag tror att detta kommer att ställa till problem med TS-arv/kompilering, eftersom användning av Qualification som ett värde och inte en typ tvingar TS att importera den faktiska koden medan webpack-buntningen utförs.

Dessutom kanske du kan göra detta med class-validator själv eller en utökad klass.

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

Kan du försöka ta bort den här koden och se om cirkulärt beroende fortfarande uppstår?

Dessutom har jag läst att du importerar alla enheter från en central fil. Även om detta låter som en bra metod för att lösa cirkulära beroenden, kan det leda till buggar eftersom det finns en risk att du importerar värden istället för typer. Jag föreslår att du inte använder något sådant för att importera enheter mellan dem, använd bara en central fil som RelationalEntities.ts och gör:

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

och använd detta i TypeORM-databasanslutningskonfigurationen, d.v.s. entities: RelationalEntities

Det gamla svaret, innan den uppdaterade koden och konfigurationen sågades:

Vanligtvis löses detta i TypeORM bara genom att använda type => Type i relationsdefinitionen, istället för den riktiga typen Type. dvs.:

@OneToMany(type => Qualification)
qualification!: Qualification;

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

Detta beror på att TS fungerar, så att type => Qualification i princip används endast för att extrahera metadata om Qualification, utan att referera till det vid körning (eller, bättre, utan att referera till det direkt vid första körning och eftersom det är lata, finns ingen cirkulär beroende)

Relaterade artiklar