プロジェクトコピー機-nestJS


📣 このシリーズは...
これは
  • 優雅な科学技術サマーキャンプの第4期の最後のプロジェクトの完全な再記述ドキュメントシリーズです.
  • これは個人の学習記録に対するドキュメントで、私が書いていないコードが含まれています.
  • ET四大物像-GitHub Repository/リンクの配置
  • 現在の配備リンクは、内部の問題のため、APIサーバが動作しません.近いうちに解決する...
  • NestJS
    NestJSフレームワークは、
  • APIサーバを構成するために使用される.
  • DDDアーキテクチャを構成しようとしたが、ドメイン間の依存性のため、DDDは正常に動作しなかった.
  • package.json
    "dependencies": {
        "@nestjs/common": "^8.0.0",
        "@nestjs/config": "^1.0.1",
        "@nestjs/core": "^8.0.0",
        "@nestjs/elasticsearch": "^8.0.0",
        "@nestjs/jwt": "^8.0.0",
        "@nestjs/platform-express": "^8.0.0",
        "@nestjs/typeorm": "^8.0.2",
      },
      "devDependencies": {
        "@nestjs/cli": "^8.0.0",
        "@nestjs/schematics": "^8.0.0",
        "@nestjs/testing": "^8.0.0",
      },

  • ElasticSearchやTypeorm Nest内部でサポートされているものが多い

  • 設定は私がやったのではありませんが、主な議論の内容はnestの構造と方法を使うことですか?中に入るので、精霊も十分なので、このまま行きましょう.
  • ディレクトリ構造とドメイン構造
    src
    |-- cart
    |   |-- application
    |   |-- domain
    |   |-- dto
    |   |-- entity
    |   `-- presentation
    |-- config
    |   |-- filter
    |   `-- properties
    |-- destination
    |   |-- application
    |   |-- domain
    |   |-- dto
    |   |-- entity
    |   `-- presentation
    |-- infra
    |   `-- mysql
    |-- order
    |   |-- application
    |   |-- domain
    |   |-- dto
    |   |-- entity
    |   `-- presentation
    |-- payment
    |   `-- presentation
    |-- product
    |   |-- application
    |   |-- domain
    |   |-- dto
    |   |-- entity
    |   |-- infrastructure
    |   `-- presentation
    `-- user
        |-- application
        |-- domain
        |-- dto
        |-- entity
        |-- infrastructure
        `-- presentation
    core-module.ts
    jwt-middleware.ts
    main.ts

  • このプロジェクトはドメイン駆動設計モードを採用しているため、各ディレクトリは1つのドメインに分割されます.もちろん完全なDDDではありません.各ドメインは相互に接続されているので...

  • 各ドメインは5~6個のサブディレクトリで構成され、各サブディレクトリは1つ
  • core.ts
    const jwtConfig = properties.auth;
    
    @Module({
      imports: [
        MysqlConfig,
        JwtModule.register({
          secret: jwtConfig.secret,
          signOptions: { expiresIn: jwtConfig.expiresIn },
        }),
        ProductModule,
        DestinationModule,
        CartModule,
        OrderModule,
        UserModule,
        PaymentModule,
      ],
    })
    export class AppModule {
      configure(consumer: MiddlewareConsumer): any {
        consumer
          .apply(LoggerMiddleware)
          .exclude("/auth")
          .exclude("/auth/*")
          .exclude("/users")
          .exclude("/users/*")
          .forRoutes("*");
      }
    }

  • モジュールレコーダを使用してNestjsにするAPPモジュール全体を作成します.

  • 各ドメインのモジュールとMySQL、jwt関連モジュールのインポートとマージ

  • ログイン関連処理を行うjwtミドルウェアも適用される.
  • jwt-middleware.ts
    @Injectable()
    export class LoggerMiddleware implements NestMiddleware {
      constructor(private readonly jwtService: JwtService) {}
      use(@Req() req: Request, @Res() res: Response, @Next() next: NextFunction) {
        try {
          const token = req.cookies[properties.auth.tokenKey];
          if (token) {
            const result = this.jwtService.verify(token)["userId"];
            if (!result) throw Error("token expired");
            req.body.userId = this.jwtService.decode(token)["userId"];
          }
          next();
        } catch (e) {
          res.clearCookie(properties.auth.tokenKey);
          res.status(HttpStatus.PRECONDITION_FAILED);
          res.send(messages.failed.EXPIRED_TOKEN);
        }
      }
    }

  • Injectable Decoratorを使用してProviderを作成し、NestMiddlewareを実装し、NestJSで使用されるミドルウェアにする

  • 私たちのプロジェクトにはクッキーにaccessTokenが含まれているため、この中間チャネルを通過すると、このトークンの有効性をチェックし、userIdをbodyに入れるか、エラー処理を行います.

  • その後、コントローラからBody Decoratorのパラメータを使用してuserIdを抽出できます!
  • main.ts
    const serverPort = properties.server.port;
    
    const nestApplication = async () => {
      const app = await NestFactory.create(AppModule);
      app.enableCors({
        origin: [properties.client],
        methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
        preflightContinue: false,
        optionsSuccessStatus: 204,
        credentials: true,
      });
      app.useGlobalFilters(new HttpExceptionFilter());
      app.use(cookieParser());
      await app.listen(serverPort);
    };
    
    nestApplication();

  • サーバー・アプリケーションのプライマリ・ファイルを作成します.

  • corsオプション、フィルタ、各ミドルウェアなど!

  • decorator

  • ディレクトリ構造を表示する前に、Decoratorについて簡単に説明します.

  • Decoratorは,新しい関数を返すことで伝達される関数/メソッド動作を修正する関数である.
  • @接頭辞とともに使用し、飾りたいclass、関数などに適用してnestJSで使用する形にすることができます.

  • 詳細については、もう少し深く検討してみましょう.このステップでは、定義する関数/クラスをnestJSフレームワークという形式に変換するだけです.
  • entity

  • データベース・テーブル構造を定義します.

  • typeormモジュールから必要なコールバック関数をインポートします.
  • Entity:@Entity()形式でエンティティークラス宣言セクションに使用します.テーブル名に2つ以上の単語がある場合、通常のDBではSnake case()が使用されます.
  • ですがjavascriptのクラスは主にPaskel caseを使用しています.この2つに合わせるために、以下のようにDBで使用する名前です.
  • @Entity(product_option)
    class ProductOption
    -P r i maryGeneratedColumn:intタイプのPKが自動的に生成されます.
    @PrimaryGeneratedColumn()
    id: number;
    -primaryColumn:PKバー.
    @PrimaryColumn({ type: "char", length: 32 })
    id: string;
    M-anyToOne:1:n関係におけるn個の表のうち1個の表のコラムニスト
    @ManyToOne(() => User, (user) => user.wishes, { lazy: true })
    -oneToMany:1:n関係の1つのテーブルのnテーブルのコラムニスト
    @OneToMany(() => Wish, (wish) => wish.user)
    wishes: Wish[];
    -oneToOne:1:1関係
    @OneToOne(() => Review, (review) => review.order)
    review: Review;
    -JoinColumn:コラム名、参照コラム名**をマージする**コラムを作成します.私の知る限り、外部キー関係ではManyToOneToManyとともに使用されます.
    @ManyToOne(() => Product, (product) => product.images, {
      nullable: false,
      onDelete: "CASCADE",
    })
    @JoinColumn({ name: "product_id" })
    product: Product;
    -Column:通常の列を作成します.
    @Column({ type: "char", length: 32, nullable: true })
    image: string;
    -CReateDateColumn:角度の生成、更新時間などの時間列の自動生成に使用します.
    @CreateDateColumn({
      type: "timestamp",
      name: "created_at",
      default: () => "CURRENT_TIMESTAMP(6)",
    })
    createdAt: Date;
    dto
  • は、データ転送オブジェクト(DTO)の事前定義プロセス間でデータを転送するオブジェクトである.
  • は、サーバ上のデータをクライアントに転送するために使用される.
  • あるネットユーザーによると、
  • DTOの直接転送がなくても大丈夫だが、すぐに設備データに返信できないという.したがって、レプリケーションには方法
  • が必要である.
  • 人にとって、DTOの役割は応答値を決定する形式である.したがってinterface/typeで十分であるが、それ以外にも、DBクエリとしてインポートされたデータをDTO形式に変換するためにofmethodが必要である.
  • したがって、
  • は、以下のような形態を形成する.
  • export class ReviewResponse {
      averageRate: number;
      rates: ReviewRate[];
      reviews: ReviewDTO[];
    
      static of(productReviews: Review[]): ReviewResponse {
        const RATES: ReviewRate[] = [
          { rate: 1, count: 0 },
          { rate: 2, count: 0 },
          { rate: 3, count: 0 },
          { rate: 4, count: 0 },
          { rate: 5, count: 0 },
        ];
    
        const averageRate = Number(
          (
            productReviews.reduce((result, review) => {
              return result + review.rate;
            }, 0) / productReviews.length
          ).toFixed(1)
        );
    
        const rates = productReviews.reduce(
          (result: ReviewRate[], review): ReviewRate[] => {
            result[review.rate - 1].count++;
            return result;
          },
          RATES
        );
    
        const reviews: ReviewDTO[] = productReviews.map((review): ReviewDTO => {
          return {
            id: review.id,
            rate: review.rate,
            content: review.content,
            image: review.image,
            authorName: review.order.user.name,
            createdAt: review.createdAt,
          };
        });
    
        return {
          averageRate,
          rates,
          reviews,
        };
      }
    }
  • select文を使用してテーブルの特定のカラムのみをインポートするよりも、テーブル全体をインポートした後、DTOから必要なデータ/を選択して変換し、必要な応答値を生成します.
  • presentation
    @Controller("/my")
    export class MyController {
      constructor(private readonly myService: MyService) {}
    
      @Get("/info")
      async getMyInfo(@Body("userId") userId: number): Promise<MyInfoResponse> {
        return await this.myService.getMyInfo(userId);
      }
      ...
    }

  • 3階層アーキテクチャでコントローラの役割を果たします.

  • @Controller Decoratorを使用します.コントローラが使用するパスを入力します.

  • コンストラクション関数のパラメータに内部で使用されるサービスを渡します.

  • コントローラクラスのメソッドでは、HTTPメソッドに対応するデータコーディネータを貼り付け、リクエストに一致するパスを入力します!
  • application
    @Injectable()
    export class MyService {
      constructor(
        private readonly carts: Carts,
        private readonly reviews: Reviews,
        private readonly destinations: Destinations,
        private readonly questions: Questions,
        private readonly users: Users,
        private readonly orders: Orders,
        private readonly wishes: Wishes
      ) {}
    
      async getMyInfo(userId) {
        try {
          const user = await this.users.findUserById(userId);
          return MyInfoResponse.of(user);
        } catch (e) {
          throw new ETException(400, messages.failed.FAILTED_TO_FIND_MY_INFO);
        }
      }
      ...

  • 3階層アーキテクチャでサービスの役割を果たす.

  • InjectableはNest ProviderにするDecoratorです.サービスをプロバイダにする

  • Providerの核心は依存項目を注入できることです!

  • コンストラクション関数のパラメータに内部で使用されるドメインを渡します.
  • domain
    @Injectable()
    export class Wishes {
      constructor(
        @InjectRepository(Wish)
        private readonly wishRepository: Repository<Wish>,
        @InjectRepository(Product)
        private readonly productRepository: Repository<Product>
      ) {}
      ...

  • 3階層アーキテクチャでRepositoryとして機能します.

  • 同様に、ProviderとしてInjectable Decoratorを使用します.

  • 内部で使用されているエンティティRepositoryは、コンストラクション関数のパラメータでInjectRepositorDecoratorを使用して渡されます.

  • Repositoryは、エンティティに対応するDBにアクセスできるオブジェクトとして扱われます.
  • async findMyWishesByUserId(userId: number): Promise<Wish[]> {
        return await this.wishRepository.find({
          relations: ["product", "product.images", "user"],
          where: { user_id: userId },
        });
      }
  • を使用したレポート方法は、次のとおりです.
  • find:条件を満たすすべてのローを検索します.
  • Repositoty.find({ where, relations);
  • findOne:理論的には、条件に対応する最初の行を探します.しかし、条件自体は独特である.idまたは
  • Repositoty.findOne(where, relations);
    Repositoty.findOne({ id });
  • insert:row追加
  • Repositoty.insert(newRow);
  • delete:削除行
  • Repositoty.delete({ id });
  • update:1番目のパラメータ条件に一致する行を2番目の行に更新します.
  • Repositoty.update({ id }, newRow);
  • count:条件付き行数
  • Repositoty.count({ id }, newRow);
  • query:SQL query文
  • を実行
    Repositoty.query(query);
  • putObject:S 3は、
  • をキー構造で記憶する.
    s3Repository.putObject(key, value);
  • 削除対象:S 3のデータ
  • を削除する.
    s3Repository.deleteObject(id);
  • save:転送されたインスタンスを保存します.(新しく追加されたものではありません!)
  • this.productRepository.findOne(wish.productId).then((product) => {
      if (product.wishCount > 0) {
        product.wishCount++;
      }
      this.productRepository.save(product);
    });
    infrastructure
    すべてのドメインに
  • があるわけではありませんが、S 3やGitHub/GoogleOuth、bcryptなどの外部サービスやDBに直接関連しない要素を含むディレクトリです.