Отражение метаданных TypeScript ссылается на другие классы, прежде чем они будут определены - PullRequest
2 голосов
/ 19 апреля 2020

У меня есть несколько сущностей TypeORM в моей кодовой базе, которые имеют отношения друг к другу, образуя круговую зависимость. Поскольку метаданные декоратора используются в каждом классе сущностей, TypeScript вставляет код после каждого класса, определяющего метаданные для него. Скажите, что классы Business и Qualification. В соответствующих полях TypeScript выдаст код, который выглядит следующим образом:

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

Все это будет хорошо, за исключением того, что часть __decorate всегда идет после каждого класса, что означает, что один из классов будет иметь использоваться до его определения, что вызывает ошибку. Вот сокращенная версия фактического кода с фактической ошибкой:

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

Позже в коде определено Business, но уже слишком поздно:

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

Как ни странно, код работает при компиляции для режима разработки, поскольку на Business ссылаются не напрямую, а через константу модуля. Вот как Qualification определяется в режиме разработки:

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

Фактический код сам импортирует модули из файла all-entities.ts, который экспортирует все сущности в правильном порядке, чтобы суперклассы не могли случайно получить загружаются после их подклассов, вызывая ошибки. Этот файл выглядит следующим образом (упрощенно):

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

./entities/Qualification.ts и ./entities/Business.ts - оба файла, которые содержат экспорт по умолчанию сущности TypeORM, и я не думаю, что их стоит включать здесь, но я могу, если кто-то хочет посмотреть на них. Вот разница между моими конфигами веб-пакетов производства и разработки (сгенерированными 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
 }

Вот классы, которые вызывают проблему: 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;
}

И в 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;

Всякий раз, когда любой из этих классов импортируется, они импортируются из этого файла для обеспечения правильного порядка загрузки модуля:

/* 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 также используется в этом проекте. Вот что такое .babelr c:

{
  "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
      }
    ]
  ]
}

Извините за огромные комки кода. Может кто-нибудь помочь мне попытаться решить эту проблему и выяснить, как заставить это работать в производстве, как это работает в разработке? Спасибо.

1 Ответ

1 голос
/ 25 апреля 2020

РЕДАКТИРОВАТЬ: я видел, что вы используете Qualification в качестве значения в ValidateableQualification. Это звучит как то, что вы можете сделать в JS, но я думаю, что это испортит наследование / компиляцию TS, поскольку использование Qualification в качестве значения , а не типа force TS, чтобы импортировать реальный код во время связывания веб-пакетов.

Кроме того, возможно, вы можете сделать это с помощью class-validator отдельно или с расширенным классом.

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

Можете ли вы попытаться удалить этот код, и посмотреть, если круговая зависимость все еще растет?

Кроме того, я читал, что вы импортируете все сущности из центрального файла. Даже если это кажется хорошим решением для разрешения циклических зависимостей, это может привести к ошибкам, поскольку есть вероятность, что вы собираетесь импортировать значения вместо типов. Я предлагаю вам не использовать что-то подобное для импорта сущностей между ними, просто используйте центральный файл, такой как RelationalEntities.ts, и выполните:

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

и используйте это в конфигурации подключения базы данных TypeORM , т. е. entities: RelationalEntities

Старый ответ до просмотра обновленного кода и конфигурации:

Обычно это решается в TypeORM с использованием type => Type в определении отношения вместо истинного типа Type. то есть:

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

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

Это связано с работой TS, так что type => Qualification в основном используется только для извлечения метаданных о Qualification, без ссылки на него во время выполнения (или, что лучше без прямой ссылки на него при первом запуске и, следовательно, поскольку он ленив, циклическая зависимость отсутствует)

...